feat(org-chart): separate fixed and auto layout previews
This commit is contained in:
1 parent
80583eb4ca
commit
a92913d6bc
18 files changed
+1211
-7
No files matched your search
@@ -0,0 +1,407 @@
|
||||
HMDesign OrgChart P5-1A Commit Preparation
|
||||
Branch: feature/orgchart-package
|
||||
HEAD / live remote main + feature: d18e3c33801e165e17b6d971e2f6198eed483c3c
|
||||
Origin: https://gitea.hmac.kr/b25027/HMDesign.git
|
||||
Stage / Commit / Push / Merge: NOT EXECUTED
|
||||
|
||||
BASELINE P0-P5-0: 163 files (2 modified tracked + 161 untracked)
|
||||
All stage labels are historical accumulated work, not Git commits. Individual P0/P1/P2 phase file ownership cannot be reconstructed from HEAD alone.
|
||||
|
||||
docs/org-chart/README.md
|
||||
docs/org-chart/architecture.md
|
||||
docs/org-chart/card-design-stage1.md
|
||||
docs/org-chart/card-design-stage2.md
|
||||
docs/org-chart/data-contract.md
|
||||
docs/org-chart/framework-integration.md
|
||||
docs/org-chart/implementation-plan.md
|
||||
docs/org-chart/implementation-report-p1.md
|
||||
docs/org-chart/implementation-report-p2.md
|
||||
docs/org-chart/implementation-report-p3.1.md
|
||||
docs/org-chart/implementation-report-p3.2.md
|
||||
docs/org-chart/implementation-report-p3.md
|
||||
docs/org-chart/implementation-report-v0.1.md
|
||||
docs/org-chart/integration.md
|
||||
docs/org-chart/quality-review-p1.md
|
||||
docs/org-chart/react-integration-report-v0.1.md
|
||||
docs/org-chart/standalone-components-p1.md
|
||||
docs/org-chart/ui-composition.md
|
||||
examples/org-chart-react/README.md
|
||||
examples/org-chart-react/app.tsx
|
||||
examples/org-chart-react/index.css
|
||||
examples/org-chart-react/index.html
|
||||
examples/org-chart-react/p1.css
|
||||
examples/org-chart-react/p1.html
|
||||
examples/org-chart-react/p1.tsx
|
||||
examples/org-chart-react/verify-boundaries.mjs
|
||||
examples/org-chart-react/verify-package.mjs
|
||||
examples/org-chart-react/verify.mjs
|
||||
examples/org-chart-react/vite.config.ts
|
||||
examples/org-chart/app.js
|
||||
examples/org-chart/audit-quality-pixels.py
|
||||
examples/org-chart/classic.html
|
||||
examples/org-chart/create-business-card-assets.py
|
||||
examples/org-chart/create-fixtures.mjs
|
||||
examples/org-chart/create-p1-fixture.mjs
|
||||
examples/org-chart/data-p1.json
|
||||
examples/org-chart/data-v1.json
|
||||
examples/org-chart/data-v2.json
|
||||
examples/org-chart/demo.css
|
||||
examples/org-chart/index.html
|
||||
examples/org-chart/p1-app.js
|
||||
examples/org-chart/p1-classic.html
|
||||
examples/org-chart/p1.css
|
||||
examples/org-chart/p1.html
|
||||
examples/org-chart/prepare.mjs
|
||||
examples/org-chart/serve.mjs
|
||||
examples/org-chart/verify-card-content.mjs
|
||||
examples/org-chart/verify-card-design.mjs
|
||||
examples/org-chart/verify-final-card-check.mjs
|
||||
examples/org-chart/verify-final-logo-check.mjs
|
||||
examples/org-chart/verify-final-status-check.mjs
|
||||
examples/org-chart/verify-guard-p1.mjs
|
||||
examples/org-chart/verify-p1.mjs
|
||||
examples/org-chart/verify-p2.mjs
|
||||
examples/org-chart/verify-p3.1.mjs
|
||||
examples/org-chart/verify-p3.2.mjs
|
||||
examples/org-chart/verify-p3.mjs
|
||||
examples/org-chart/verify-package.mjs
|
||||
examples/org-chart/verify-quality-boundaries.mjs
|
||||
examples/org-chart/verify-quality-p1.mjs
|
||||
examples/org-chart/verify.mjs
|
||||
packages/org-chart-react/README.md
|
||||
packages/org-chart-react/build.mjs
|
||||
packages/org-chart-react/package.json
|
||||
packages/org-chart-react/src/OrgChart.tsx
|
||||
packages/org-chart-react/src/index.ts
|
||||
packages/org-chart-react/src/types.ts
|
||||
packages/org-chart-react/tsconfig.build.json
|
||||
packages/org-chart-react/tsconfig.json
|
||||
packages/org-chart/README.md
|
||||
packages/org-chart/build.mjs
|
||||
packages/org-chart/package.json
|
||||
packages/org-chart/src/assets/affiliations/README.md
|
||||
packages/org-chart/src/assets/affiliations/baron.png
|
||||
packages/org-chart/src/assets/affiliations/halla.png
|
||||
packages/org-chart/src/assets/affiliations/hanmac.png
|
||||
packages/org-chart/src/assets/affiliations/jangheon-industry.png
|
||||
packages/org-chart/src/assets/affiliations/jangheon.png
|
||||
packages/org-chart/src/assets/affiliations/ptc.png
|
||||
packages/org-chart/src/assets/affiliations/saman.png
|
||||
packages/org-chart/src/assets/cards/README.md
|
||||
packages/org-chart/src/assets/cards/경영기획팀-sm.svg
|
||||
packages/org-chart/src/assets/cards/경영기획팀.svg
|
||||
packages/org-chart/src/assets/cards/대표자.svg
|
||||
packages/org-chart/src/assets/cards/디자인기획팀-sm.svg
|
||||
packages/org-chart/src/assets/cards/디자인기획팀.svg
|
||||
packages/org-chart/src/assets/cards/사업관리팀-sm.svg
|
||||
packages/org-chart/src/assets/cards/사업관리팀.svg
|
||||
packages/org-chart/src/assets/cards/솔루션통합팀-sm.svg
|
||||
packages/org-chart/src/assets/cards/솔루션통합팀.svg
|
||||
packages/org-chart/src/assets/cards/인재성장팀-sm.svg
|
||||
packages/org-chart/src/assets/cards/인재성장팀.svg
|
||||
packages/org-chart/src/assets/cards/통합시스템 디비전-sm.svg
|
||||
packages/org-chart/src/assets/cards/통합시스템 디비전.svg
|
||||
packages/org-chart/src/assets/person-placeholder.svg
|
||||
packages/org-chart/src/assets/workdesk/1-8-4x-1@2x.png
|
||||
packages/org-chart/src/assets/workdesk/Frame2@3x.png
|
||||
packages/org-chart/src/assets/workdesk/Group.svg
|
||||
packages/org-chart/src/assets/workdesk/README.md
|
||||
packages/org-chart/src/assets/workdesk/bg_main.png
|
||||
packages/org-chart/src/assets/workdesk/bg_sky.png
|
||||
packages/org-chart/src/assets/workdesk/team-arrow-blue-purple.png
|
||||
packages/org-chart/src/assets/workdesk/team-arrow-blue-short.png
|
||||
packages/org-chart/src/assets/workdesk/team-arrow-purple-teal.png
|
||||
packages/org-chart/src/assets/workdesk/team-arrow-teal-green.png
|
||||
packages/org-chart/src/company-color-presets.test.ts
|
||||
packages/org-chart/src/company-color-presets.ts
|
||||
packages/org-chart/src/components.ts
|
||||
packages/org-chart/src/core/affiliation-bar.ts
|
||||
packages/org-chart/src/core/affiliation-mark.ts
|
||||
packages/org-chart/src/core/affiliation-summary.ts
|
||||
packages/org-chart/src/core/contrast.test.ts
|
||||
packages/org-chart/src/core/contrast.ts
|
||||
packages/org-chart/src/core/department-card-assets.ts
|
||||
packages/org-chart/src/core/department-colors.ts
|
||||
packages/org-chart/src/core/employee-card.ts
|
||||
packages/org-chart/src/core/employee-info-dialog.ts
|
||||
packages/org-chart/src/core/engine.ts
|
||||
packages/org-chart/src/core/index.ts
|
||||
packages/org-chart/src/core/logo-legend.ts
|
||||
packages/org-chart/src/core/org-chart-content.ts
|
||||
packages/org-chart/src/core/org-chart-tree.ts
|
||||
packages/org-chart/src/core/organization-totals.ts
|
||||
packages/org-chart/src/core/person-name-overflow.ts
|
||||
packages/org-chart/src/core/position-summary.ts
|
||||
packages/org-chart/src/core/render.ts
|
||||
packages/org-chart/src/core/representative-card-asset.ts
|
||||
packages/org-chart/src/core/status-summary.ts
|
||||
packages/org-chart/src/core/style-presets.ts
|
||||
packages/org-chart/src/core/types.ts
|
||||
packages/org-chart/src/core/vendor.d.ts
|
||||
packages/org-chart/src/core/workdesk-artwork.ts
|
||||
packages/org-chart/src/core/workdesk-layout.ts
|
||||
packages/org-chart/src/data/content-selectors.test.ts
|
||||
packages/org-chart/src/data/content-selectors.ts
|
||||
packages/org-chart/src/data/data.test.ts
|
||||
packages/org-chart/src/data/index.ts
|
||||
packages/org-chart/src/data/normalize.ts
|
||||
packages/org-chart/src/data/snapshot.ts
|
||||
packages/org-chart/src/data/source.ts
|
||||
packages/org-chart/src/data/summary.ts
|
||||
packages/org-chart/src/data/types.ts
|
||||
packages/org-chart/src/data/validate.ts
|
||||
packages/org-chart/src/index.ts
|
||||
packages/org-chart/src/styles/design-tokens.css
|
||||
packages/org-chart/src/styles/employee-card.css
|
||||
packages/org-chart/src/styles/employee-info-dialog.css
|
||||
packages/org-chart/src/styles/org-chart-content.css
|
||||
packages/org-chart/src/styles/org-chart-p3-summary.css
|
||||
packages/org-chart/src/styles/org-chart-readonly.css
|
||||
packages/org-chart/src/styles/org-chart.css
|
||||
packages/org-chart/src/styles/summary-sections.css
|
||||
packages/org-chart/test.mjs
|
||||
packages/org-chart/tsconfig.build.json
|
||||
packages/org-chart/tsconfig.json
|
||||
src/org-chart/core-component-preview.tsx
|
||||
src/org-chart/org-chart-cards.stories.tsx
|
||||
src/org-chart/org-chart-p1.stories.tsx
|
||||
src/org-chart/org-chart-stories.css
|
||||
src/org-chart/org-chart-summary.stories.tsx
|
||||
src/org-chart/org-chart.stories.tsx
|
||||
package.json
|
||||
package-lock.json
|
||||
|
||||
P5-1A CHANGES TO EXISTING BASELINE: 9
|
||||
|
||||
packages/org-chart-react/README.md
|
||||
packages/org-chart-react/src/OrgChart.tsx
|
||||
packages/org-chart/README.md
|
||||
packages/org-chart/build.mjs
|
||||
packages/org-chart/src/core/index.ts
|
||||
packages/org-chart/src/core/render.ts
|
||||
packages/org-chart/src/core/types.ts
|
||||
packages/org-chart/src/index.ts
|
||||
packages/org-chart/src/styles/org-chart.css
|
||||
|
||||
P5-1A NEW FILES: 9
|
||||
|
||||
examples/org-chart/data-expanded.json
|
||||
examples/org-chart/verify-p5-1a.mjs
|
||||
packages/org-chart/src/core/expanded-content.ts
|
||||
packages/org-chart/src/core/expanded-layout.test.ts
|
||||
packages/org-chart/src/core/expanded-layout.ts
|
||||
packages/org-chart/src/styles/org-chart-expanded.css
|
||||
src/org-chart/org-chart-expanded.stories.tsx
|
||||
docs/org-chart/implementation-report-p5-1a.md
|
||||
docs/org-chart/commit-preparation-p5-1a.txt
|
||||
|
||||
CURRENT ENTIRE UNCOMMITTED FILE LIST: 172
|
||||
|
||||
M package-lock.json
|
||||
M package.json
|
||||
?? docs/org-chart/README.md
|
||||
?? docs/org-chart/architecture.md
|
||||
?? docs/org-chart/card-design-stage1.md
|
||||
?? docs/org-chart/card-design-stage2.md
|
||||
?? docs/org-chart/data-contract.md
|
||||
?? docs/org-chart/framework-integration.md
|
||||
?? docs/org-chart/implementation-plan.md
|
||||
?? docs/org-chart/implementation-report-p1.md
|
||||
?? docs/org-chart/implementation-report-p2.md
|
||||
?? docs/org-chart/implementation-report-p3.1.md
|
||||
?? docs/org-chart/implementation-report-p3.2.md
|
||||
?? docs/org-chart/implementation-report-p3.md
|
||||
?? docs/org-chart/implementation-report-v0.1.md
|
||||
?? docs/org-chart/integration.md
|
||||
?? docs/org-chart/quality-review-p1.md
|
||||
?? docs/org-chart/react-integration-report-v0.1.md
|
||||
?? docs/org-chart/standalone-components-p1.md
|
||||
?? docs/org-chart/ui-composition.md
|
||||
?? examples/org-chart-react/README.md
|
||||
?? examples/org-chart-react/app.tsx
|
||||
?? examples/org-chart-react/index.css
|
||||
?? examples/org-chart-react/index.html
|
||||
?? examples/org-chart-react/p1.css
|
||||
?? examples/org-chart-react/p1.html
|
||||
?? examples/org-chart-react/p1.tsx
|
||||
?? examples/org-chart-react/verify-boundaries.mjs
|
||||
?? examples/org-chart-react/verify-package.mjs
|
||||
?? examples/org-chart-react/verify.mjs
|
||||
?? examples/org-chart-react/vite.config.ts
|
||||
?? examples/org-chart/app.js
|
||||
?? examples/org-chart/audit-quality-pixels.py
|
||||
?? examples/org-chart/classic.html
|
||||
?? examples/org-chart/create-business-card-assets.py
|
||||
?? examples/org-chart/create-fixtures.mjs
|
||||
?? examples/org-chart/create-p1-fixture.mjs
|
||||
?? examples/org-chart/data-expanded.json
|
||||
?? examples/org-chart/data-p1.json
|
||||
?? examples/org-chart/data-v1.json
|
||||
?? examples/org-chart/data-v2.json
|
||||
?? examples/org-chart/demo.css
|
||||
?? examples/org-chart/index.html
|
||||
?? examples/org-chart/p1-app.js
|
||||
?? examples/org-chart/p1-classic.html
|
||||
?? examples/org-chart/p1.css
|
||||
?? examples/org-chart/p1.html
|
||||
?? examples/org-chart/prepare.mjs
|
||||
?? examples/org-chart/serve.mjs
|
||||
?? examples/org-chart/verify-card-content.mjs
|
||||
?? examples/org-chart/verify-card-design.mjs
|
||||
?? examples/org-chart/verify-final-card-check.mjs
|
||||
?? examples/org-chart/verify-final-logo-check.mjs
|
||||
?? examples/org-chart/verify-final-status-check.mjs
|
||||
?? examples/org-chart/verify-guard-p1.mjs
|
||||
?? examples/org-chart/verify-p1.mjs
|
||||
?? examples/org-chart/verify-p2.mjs
|
||||
?? examples/org-chart/verify-p3.1.mjs
|
||||
?? examples/org-chart/verify-p3.2.mjs
|
||||
?? examples/org-chart/verify-p3.mjs
|
||||
?? examples/org-chart/verify-p5-1a.mjs
|
||||
?? examples/org-chart/verify-package.mjs
|
||||
?? examples/org-chart/verify-quality-boundaries.mjs
|
||||
?? examples/org-chart/verify-quality-p1.mjs
|
||||
?? examples/org-chart/verify.mjs
|
||||
?? packages/org-chart-react/README.md
|
||||
?? packages/org-chart-react/build.mjs
|
||||
?? packages/org-chart-react/package.json
|
||||
?? packages/org-chart-react/src/OrgChart.tsx
|
||||
?? packages/org-chart-react/src/index.ts
|
||||
?? packages/org-chart-react/src/types.ts
|
||||
?? packages/org-chart-react/tsconfig.build.json
|
||||
?? packages/org-chart-react/tsconfig.json
|
||||
?? packages/org-chart/README.md
|
||||
?? packages/org-chart/build.mjs
|
||||
?? packages/org-chart/package.json
|
||||
?? packages/org-chart/src/assets/affiliations/README.md
|
||||
?? packages/org-chart/src/assets/affiliations/baron.png
|
||||
?? packages/org-chart/src/assets/affiliations/halla.png
|
||||
?? packages/org-chart/src/assets/affiliations/hanmac.png
|
||||
?? packages/org-chart/src/assets/affiliations/jangheon-industry.png
|
||||
?? packages/org-chart/src/assets/affiliations/jangheon.png
|
||||
?? packages/org-chart/src/assets/affiliations/ptc.png
|
||||
?? packages/org-chart/src/assets/affiliations/saman.png
|
||||
?? packages/org-chart/src/assets/cards/README.md
|
||||
?? packages/org-chart/src/assets/cards/경영기획팀-sm.svg
|
||||
?? packages/org-chart/src/assets/cards/경영기획팀.svg
|
||||
?? packages/org-chart/src/assets/cards/대표자.svg
|
||||
?? packages/org-chart/src/assets/cards/디자인기획팀-sm.svg
|
||||
?? packages/org-chart/src/assets/cards/디자인기획팀.svg
|
||||
?? packages/org-chart/src/assets/cards/사업관리팀-sm.svg
|
||||
?? packages/org-chart/src/assets/cards/사업관리팀.svg
|
||||
?? packages/org-chart/src/assets/cards/솔루션통합팀-sm.svg
|
||||
?? packages/org-chart/src/assets/cards/솔루션통합팀.svg
|
||||
?? packages/org-chart/src/assets/cards/인재성장팀-sm.svg
|
||||
?? packages/org-chart/src/assets/cards/인재성장팀.svg
|
||||
?? packages/org-chart/src/assets/cards/통합시스템 디비전-sm.svg
|
||||
?? packages/org-chart/src/assets/cards/통합시스템 디비전.svg
|
||||
?? packages/org-chart/src/assets/person-placeholder.svg
|
||||
?? packages/org-chart/src/assets/workdesk/1-8-4x-1@2x.png
|
||||
?? packages/org-chart/src/assets/workdesk/Frame2@3x.png
|
||||
?? packages/org-chart/src/assets/workdesk/Group.svg
|
||||
?? packages/org-chart/src/assets/workdesk/README.md
|
||||
?? packages/org-chart/src/assets/workdesk/bg_main.png
|
||||
?? packages/org-chart/src/assets/workdesk/bg_sky.png
|
||||
?? packages/org-chart/src/assets/workdesk/team-arrow-blue-purple.png
|
||||
?? packages/org-chart/src/assets/workdesk/team-arrow-blue-short.png
|
||||
?? packages/org-chart/src/assets/workdesk/team-arrow-purple-teal.png
|
||||
?? packages/org-chart/src/assets/workdesk/team-arrow-teal-green.png
|
||||
?? packages/org-chart/src/company-color-presets.test.ts
|
||||
?? packages/org-chart/src/company-color-presets.ts
|
||||
?? packages/org-chart/src/components.ts
|
||||
?? packages/org-chart/src/core/affiliation-bar.ts
|
||||
?? packages/org-chart/src/core/affiliation-mark.ts
|
||||
?? packages/org-chart/src/core/affiliation-summary.ts
|
||||
?? packages/org-chart/src/core/contrast.test.ts
|
||||
?? packages/org-chart/src/core/contrast.ts
|
||||
?? packages/org-chart/src/core/department-card-assets.ts
|
||||
?? packages/org-chart/src/core/department-colors.ts
|
||||
?? packages/org-chart/src/core/employee-card.ts
|
||||
?? packages/org-chart/src/core/employee-info-dialog.ts
|
||||
?? packages/org-chart/src/core/engine.ts
|
||||
?? packages/org-chart/src/core/expanded-content.ts
|
||||
?? packages/org-chart/src/core/expanded-layout.test.ts
|
||||
?? packages/org-chart/src/core/expanded-layout.ts
|
||||
?? packages/org-chart/src/core/index.ts
|
||||
?? packages/org-chart/src/core/logo-legend.ts
|
||||
?? packages/org-chart/src/core/org-chart-content.ts
|
||||
?? packages/org-chart/src/core/org-chart-tree.ts
|
||||
?? packages/org-chart/src/core/organization-totals.ts
|
||||
?? packages/org-chart/src/core/person-name-overflow.ts
|
||||
?? packages/org-chart/src/core/position-summary.ts
|
||||
?? packages/org-chart/src/core/render.ts
|
||||
?? packages/org-chart/src/core/representative-card-asset.ts
|
||||
?? packages/org-chart/src/core/status-summary.ts
|
||||
?? packages/org-chart/src/core/style-presets.ts
|
||||
?? packages/org-chart/src/core/types.ts
|
||||
?? packages/org-chart/src/core/vendor.d.ts
|
||||
?? packages/org-chart/src/core/workdesk-artwork.ts
|
||||
?? packages/org-chart/src/core/workdesk-layout.ts
|
||||
?? packages/org-chart/src/data/content-selectors.test.ts
|
||||
?? packages/org-chart/src/data/content-selectors.ts
|
||||
?? packages/org-chart/src/data/data.test.ts
|
||||
?? packages/org-chart/src/data/index.ts
|
||||
?? packages/org-chart/src/data/normalize.ts
|
||||
?? packages/org-chart/src/data/snapshot.ts
|
||||
?? packages/org-chart/src/data/source.ts
|
||||
?? packages/org-chart/src/data/summary.ts
|
||||
?? packages/org-chart/src/data/types.ts
|
||||
?? packages/org-chart/src/data/validate.ts
|
||||
?? packages/org-chart/src/index.ts
|
||||
?? packages/org-chart/src/styles/design-tokens.css
|
||||
?? packages/org-chart/src/styles/employee-card.css
|
||||
?? packages/org-chart/src/styles/employee-info-dialog.css
|
||||
?? packages/org-chart/src/styles/org-chart-content.css
|
||||
?? packages/org-chart/src/styles/org-chart-expanded.css
|
||||
?? packages/org-chart/src/styles/org-chart-p3-summary.css
|
||||
?? packages/org-chart/src/styles/org-chart-readonly.css
|
||||
?? packages/org-chart/src/styles/org-chart.css
|
||||
?? packages/org-chart/src/styles/summary-sections.css
|
||||
?? packages/org-chart/test.mjs
|
||||
?? packages/org-chart/tsconfig.build.json
|
||||
?? packages/org-chart/tsconfig.json
|
||||
?? src/org-chart/core-component-preview.tsx
|
||||
?? src/org-chart/org-chart-cards.stories.tsx
|
||||
?? src/org-chart/org-chart-expanded.stories.tsx
|
||||
?? src/org-chart/org-chart-p1.stories.tsx
|
||||
?? src/org-chart/org-chart-stories.css
|
||||
?? src/org-chart/org-chart-summary.stories.tsx
|
||||
?? src/org-chart/org-chart.stories.tsx
|
||||
?? docs/org-chart/implementation-report-p5-1a.md
|
||||
?? docs/org-chart/commit-preparation-p5-1a.txt
|
||||
|
||||
REQUIRED SOURCE DEPENDENCIES
|
||||
- package.json + package-lock.json: workspace package registrations and orgchart6.0.0 pinned dependency tree.
|
||||
- packages/org-chart/: manifest, build.mjs, tsconfigs, all runtime src/data/core/styles/assets; public exports refer to generated dist.
|
||||
- packages/org-chart-react/: manifest, build.mjs, tsconfigs, src; requires Core public dist declarations first.
|
||||
- src/org-chart/ and referenced examples/org-chart/data-p1.json + data-expanded.json; original CoreComponentPreview depends on Core internal baseline source.
|
||||
- Vanilla examples need prepare/serve + app/HTML/CSS/JSON; React examples need TSX/HTML/CSS/vite.config.
|
||||
- verify*.mjs, *.test.ts, audit/asset-generation scripts are authored verification/reproduction source, not generated caches.
|
||||
- Existing HM UI exports and Storybook shared preview require their existing built public dist; no source changes or package installation in this task.
|
||||
|
||||
DO NOT INCLUDE GENERATED OUTPUTS / CACHES
|
||||
packages/org-chart/dist/
|
||||
packages/org-chart-react/dist/
|
||||
examples/org-chart/dist/
|
||||
examples/org-chart-react/dist/
|
||||
storybook-static/
|
||||
test-results/ (preserve local snapshots, JSON, captures; no forced add)
|
||||
node_modules/
|
||||
_local/
|
||||
playwright-report/
|
||||
coverage/
|
||||
*.log
|
||||
*.tsbuildinfo
|
||||
*.zip
|
||||
.env and private local settings
|
||||
|
||||
RECOMMENDED COMMIT GROUPS AFTER USER APPROVAL
|
||||
1. P0-P5-0 baseline: the 163-file starting source snapshot, manifests/lock, packages/assets/styles, examples/fixtures/Storybook/docs/authored verification.
|
||||
2. P5-1A: the 9 modified baseline files + 9 new files listed above.
|
||||
Several baseline files are still untracked: staging current file paths cannot separate their old/new contents.
|
||||
Starting source snapshot: test-results/org-chart-p5-1a/baseline-source/
|
||||
Starting SHA256 inventory: test-results/org-chart-p5-1a/baseline-files.json
|
||||
Use that captured baseline and an exact reviewed P5-1A patch if two commits are required.
|
||||
If snapshot data is unavailable later, prefer one reviewed accumulated OrgChart commit over inventing historical phase boundaries.
|
||||
A P5-1A-only commit directly against current HEAD omits Core/cards/JSON/React/workspace registrations and is incomplete.
|
||||
Use explicit file selection and staged diff review. Do not use git add -A. Recheck live remote before later commit/push.
|
||||
@@ -0,0 +1,74 @@
|
||||
# HMDesign OrgChart P5-1A 구현 및 커밋 준비 결과
|
||||
|
||||
RESULT: **PASS — P5-1A 요청 범위**. Fixed/Auto 렌더링 경계를 분리하고 확장형 초기 Storybook 화면을 추가했다. 자동 재귀 배치, 접기·펼치기, Zoom/Pan은 구현하지 않았다.
|
||||
|
||||
## 작업 환경과 시간
|
||||
|
||||
- 경로: `D:\디자인시스템\HMDesign-OrgChart`
|
||||
- 범위: Core·React 패키지와 요청된 Storybook 초기 화면. 운영 main 수정 없음.
|
||||
- 브랜치: `feature/orgchart-package`
|
||||
- HEAD: `d18e3c33801e165e17b6d971e2f6198eed483c3c`
|
||||
- Origin: `https://gitea.hmac.kr/b25027/HMDesign.git`
|
||||
- 실제 원격 `main`·`feature/orgchart-package` SHA 조회 결과: 위 HEAD와 동일.
|
||||
- Started At: 2026-10-12 17:23:00 KST
|
||||
- Completed At: 2026-10-12 17:42:41 KST
|
||||
- Actual Duration: 19분 41초
|
||||
- Estimated Duration: 약 45분 (분석 10분, 구현 20분, 검증·커밋 준비 15분)
|
||||
- Variance: 예상보다 25분 19초 짧음
|
||||
|
||||
## 구현 결과
|
||||
|
||||
공개 `OrgChartOptions.layoutMode?: 'fixed' | 'auto'`를 추가했다. 생략 시 `fixed`이며 기존 `displayMode`와 독립적이다. Fixed는 기존 `createOrgChartContent`, Workdesk 좌표·아트워크·크기 맞춤 경로를 그대로 사용한다. 기존 세 사용자 Story 파일도 시작 스냅샷과 바이트 단위로 동일하다.
|
||||
|
||||
Auto는 `expandedPreviewRoots` → 공유 `engineTree`/dabeng engine → `createExpandedOrgChartContent` 경계로 초기 그룹을 표시한다. 정규화된 실제 hierarchy의 직계 그룹과 원본 `parentId`를 보존한다. 하위 데이터는 삭제하지 않고 이번 표시만 보류하며 집계에는 남긴다. 새로운 presenter는 `.hm-orgchart-expanded` 아래에서 일반 흐름·줄바꿈·스크롤을 사용한다. Fixed Stage, scale observer, 고정 좌표는 사용하지 않는다.
|
||||
|
||||
직원·대표자 카드는 기존 `createEmployeeCard`, 승인 SVG 대형/소형/대표자 자산, 사진 없음 아이콘, Noto Sans KR 및 Typography를 재사용한다. 초기 fixture는 대표자와 기존 부서 ID의 1Depth 그룹 3개, 조직장 3명·직원 6명을 표시한다. 대표자 146×74, `대표이사` / `홍 길 동`, 뱃지·상태·속성 wrapper 0개를 확인했다. 대형 카드 146×68, 소형 카드 108×52도 확인했다. 소형 카드 재사용은 2Depth 자동 계층 배치 완료를 뜻하지 않는다.
|
||||
|
||||
회사 설정 `companyId`는 Auto에서만 `getCompanyColorPreset`을 조회한다. 기본값 `hanmac`; 미등록 ID는 TypeError로 거부한다. Fixed는 회사 옵션을 무시하고 조직별 Gradient를 유지한다. 실제 ID와 Main은 `hanmac` #CB2E28, `saman` #F08300, `jangheon-industry` #4580A6, `jangheon` #1D418D, `ptc` #84338E, `halla` #0068B7, `baron` #0C367F다. 보조 컬러 전체는 기존 프리셋에서 조회하며 노드의 indexed secondary 변수로 연결한다. 기존 프리셋 파일은 변경하지 않았다.
|
||||
|
||||
React는 `layoutMode`·`companyId`를 동일 Core에 전달하고 설정 변경 시 기존 lifecycle 계약으로 인스턴스를 재생성한다. 데이터 검증·JSON Source·엔진·카드·팝업·update/reload/destroy는 Vanilla와 공유한다. React나 Story에 레이아웃 알고리즘을 구현하지 않았다. Auto 초기 preview의 카드 표시 정책과 후속 기능 보류는 명시적이며 `displayMode` 공개 값은 변경하지 않는다.
|
||||
|
||||
## Storybook과 재사용 판단
|
||||
|
||||
- [Expanded OrgChart](http://127.0.0.1:6052/?path=/story/orgchart--expanded-org-chart)
|
||||
- [직접 화면](http://127.0.0.1:6052/iframe.html?id=orgchart--expanded-org-chart&viewMode=story)
|
||||
- 기존 `Logo · Personnel Summary`, `Department Colors`, `Full Composition`을 보존하고 네 번째 공개 화면을 추가했다. 내부 Story ID·태그는 유지했다.
|
||||
- 회사 선택에는 실제 공개 HM `Label`, `Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`을 렌더링한다. Label ID와 control의 접근성 이름을 연결했다.
|
||||
- FilterBar 미사용: 목록 검색·조회 조건 요구가 없고 단일 preset 선택만 필요하므로 기존 Select composition을 사용했다.
|
||||
- 페이지 Navigation 미적용: 요청된 Component Storybook preview이며 신규 업무 페이지·GNB/LNB/Dashboard를 만들지 않았다.
|
||||
- 팝업은 기존 Core native dialog를 재사용하며 Enter·Tab·Escape 및 카드 focus 복귀를 확인했다.
|
||||
|
||||
## 변경 범위 통합 검증
|
||||
|
||||
- Core Typecheck·Build: PASS (`npm.cmd run typecheck:org-chart`, `npm.cmd run build:org-chart`)
|
||||
- React Typecheck·Build: PASS (`npm.cmd run typecheck:org-chart-react`, `npm.cmd run build:org-chart-react`)
|
||||
- Storybook 포함 앱 Typecheck: PASS (`node node_modules/typescript/bin/tsc -p tsconfig.app.json --incremental`)
|
||||
- 집중 단위 테스트: **4/4 PASS**, hierarchy 보존·독립 root·기본 호환성·옵션 오류 경계.
|
||||
- 집중 브라우저 검증: **89/89 PASS**, 콘솔 오류·경고 0. [원본 JSON](../../test-results/org-chart-p5-1a/browser.json), [검증 소스](../../examples/org-chart/verify-p5-1a.mjs).
|
||||
- 기존 빌드를 `baseline-dist`에 보존해 Fixed 기본값 및 명시적 Fixed의 카드 좌표·자산·내용·Typography·헤더 Gradient·집계와 팝업을 비교했다. 이번 최소 회귀 범위에서 변화 없음.
|
||||
- 실제 React/Vanilla의 카드·자산·폰트가 일치했다. 7개 회사 선택 후 Main·보조 컬러 연결을 확인했다. Auto update/reload/destroy의 stale DOM·중복 없음, 후속 3Depth 데이터는 보존되며 아직 배치하지 않음을 확인했다.
|
||||
- 실제 신규 Storybook을 390/768/1280/1440px에서 검사했다. 페이지 가로 넘침·카드 겹침 없음; 작은 폭에서 그룹이 일반 흐름으로 줄바꿈된다. 문서·Core viewport의 client/scroll 크기를 원본 JSON에 기록했다.
|
||||
- `git diff --check`: PASS. P5-1A 대부분이 untracked이므로 시작 스냅샷 대비 별도 변경 목록과 공백 검사를 함께 기록한다.
|
||||
- 최종 소스 리뷰: Fixed 분기·lifecycle·CSS 격리·요구 범위에서 수정이 필요한 버그 없음.
|
||||
|
||||
Guard는 최초 통합 Story 검사에서 기존 private Core 참조의 동적 토큰을 해석하지 못해 TOKEN-001 FAIL을 기록했다. 진단을 [보존](../../test-results/org-chart-p5-1a/guard-initial-truncated.txt)했고 기존 코드를 임의 수정하지 않았다. 신규 화면을 독립 public-API Story로 분리한 후 해당 변경 파일만 PARTIAL 검사했다. [최종 Guard](../../test-results/org-chart-p5-1a/guard-expanded.json)는 원본 status **PASS**, staticStatus **PASS**, runtimeStatus **NOT VERIFIED**다.
|
||||
|
||||
CSS styleEvidence: STATIC **PASS**, RUNTIME **NOT VERIFIED**, DESIGN_INTENT **NOT VERIFIED**, FINAL **NOT VERIFIED**. [결합 증거](../../test-results/org-chart-p5-1a/guard-combined.json)는 해시 연결 `linked=true`, 정적 PASS이지만 Storybook profile의 Production `--result` bootstrap 증명 및 위치별 CSS 승인 증거가 없어 **NOT VERIFIED**다. styleVerification도 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT NOT VERIFIED / FINAL NOT VERIFIED다. 실제 브라우저 89 PASS로 원본 Guard나 디자인 의도 승인을 덮어쓰지 않는다. 초기 디자인의 사용자 최종 승인은 미검증이다.
|
||||
|
||||
## 변경 파일과 커밋 준비
|
||||
|
||||
시작 기존 P0~P5-0 변경은 **163개**(tracked 수정 2개 + untracked 161개)였다. 현재 전체 미커밋 파일은 **172개**다. P5-1A는 기존 파일 수정 9개와 신규 9개이며 삭제된 baseline 파일은 없다. [전체·단계별 파일 목록, 의존성, 제외 대상과 추천 커밋 분리](commit-preparation-p5-1a.txt)를 제공한다.
|
||||
|
||||
기존 파일 수정: Core `README.md`, `build.mjs`, `src/index.ts`, `src/core/index.ts`, `src/core/render.ts`, `src/core/types.ts`, `src/styles/org-chart.css`; React `README.md`, `src/OrgChart.tsx`.
|
||||
|
||||
신규: Core `src/core/expanded-content.ts`, `src/core/expanded-layout.ts`, `src/core/expanded-layout.test.ts`, `src/styles/org-chart-expanded.css`; `src/org-chart/org-chart-expanded.stories.tsx`; `examples/org-chart/data-expanded.json`, `examples/org-chart/verify-p5-1a.mjs`; 이 보고서와 `commit-preparation-p5-1a.txt`.
|
||||
|
||||
HEAD에는 두 OrgChart 패키지 자체가 없으므로 P5-1A 파일만 단독 커밋하면 카드·프리셋·Core·wrapper·workspace 의존성이 누락된다. 권장 방식은 저장한 **163개 시작 소스 스냅샷으로 P0~P5-0 baseline 커밋**을 구성하고, 이후 **P5-1A 변경 커밋**을 분리하는 것이다. 현재 untracked 파일을 단순히 경로별 Stage하면 과거 내용과 P5-1A를 분리할 수 없다. 시작 스냅샷·SHA-256 목록은 `test-results/org-chart-p5-1a/baseline-source/`, `baseline-files.json`에 보존했다. 이후 승인 단계에서 정확한 patch·staged diff를 검토해야 한다. 과거 P0/P1별 소유 파일은 Git만으로 확정할 수 없다.
|
||||
|
||||
`dist`, `storybook-static`, `test-results`, `node_modules`, `_local`, log/cache/tsbuildinfo는 커밋에서 제외한다. authored test·verify·asset 생성 소스는 임시 출력과 구분한다. 기존 미커밋 작업 및 과거 증거는 보존했다. **Stage·Commit·Push·Merge는 실행하지 않았다.**
|
||||
|
||||
## 남은 항목과 NOT VERIFIED
|
||||
|
||||
P5-1B: 동일 Depth 가로 배치, 부모 아래 하위 그룹 배치, 재귀 3Depth 이상, 조직 증가 시 재계산, 카드 겹침 방지 알고리즘. 접기·펼치기·Zoom/Pan은 후속 범위다. P5-1A의 일반 그룹 표시를 자동 배치 완성으로 판정하지 않았다.
|
||||
|
||||
전체 과거 P2/P3 suite, Storybook 전체 production build, 임의 조직 데이터·모든 테마·다른 브라우저, 실기기 touch·스크린리더 발화·SSR/운영 연동, 최종 디자인 승인·CSS site별 결합 검증은 NOT VERIFIED다. P5-1B를 자동으로 시작하지 않았으며 사용자 검토를 기다린다.
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"schemaVersion": "0.1-draft",
|
||||
"organizations": [
|
||||
{ "id": "general-planning", "parentId": null, "name": "총괄기획실", "type": "root", "leaderId": "expanded-representative", "displayOrder": 0 },
|
||||
{ "id": "business-management", "parentId": "general-planning", "name": "사업관리", "type": "office", "leaderId": "expanded-leader-01", "displayOrder": 0 },
|
||||
{ "id": "business-planning", "parentId": "general-planning", "name": "경영기획", "type": "office", "leaderId": "expanded-leader-02", "displayOrder": 1 },
|
||||
{ "id": "talent-growth", "parentId": "general-planning", "name": "인재성장", "type": "office", "leaderId": "expanded-leader-03", "displayOrder": 2 }
|
||||
],
|
||||
"employees": [
|
||||
{ "id": "expanded-representative", "organizationId": "general-planning", "affiliationId": "hanmac", "name": "홍길동", "position": "대표이사", "jobTitle": "대표이사", "profileImage": null, "workStatus": "active" },
|
||||
{ "id": "expanded-leader-01", "organizationId": "business-management", "affiliationId": "hanmac", "name": "이도윤", "position": "수석", "jobTitle": "팀장", "profileImage": null, "workStatus": "active" },
|
||||
{ "id": "expanded-employee-01", "organizationId": "business-management", "affiliationId": "saman", "name": "김하린", "position": "책임", "jobTitle": null, "profileImage": null, "workStatus": "active" },
|
||||
{ "id": "expanded-employee-02", "organizationId": "business-management", "affiliationId": "jangheon-industry", "name": "박서준", "position": "선임", "jobTitle": null, "profileImage": null, "workStatus": "active" },
|
||||
{ "id": "expanded-leader-02", "organizationId": "business-planning", "affiliationId": "jangheon", "name": "정지우", "position": "수석", "jobTitle": "팀장", "profileImage": null, "workStatus": "active" },
|
||||
{ "id": "expanded-employee-03", "organizationId": "business-planning", "affiliationId": "ptc", "name": "최유진", "position": "책임", "jobTitle": null, "profileImage": null, "workStatus": "active" },
|
||||
{ "id": "expanded-employee-04", "organizationId": "business-planning", "affiliationId": "hanmac", "name": "윤서아", "position": "연구", "jobTitle": null, "profileImage": null, "workStatus": "active" },
|
||||
{ "id": "expanded-leader-03", "organizationId": "talent-growth", "affiliationId": "halla", "name": "강민준", "position": "수석", "jobTitle": "팀장", "profileImage": null, "workStatus": "active" },
|
||||
{ "id": "expanded-employee-05", "organizationId": "talent-growth", "affiliationId": "baron", "name": "한예린", "position": "책임", "jobTitle": null, "profileImage": null, "workStatus": "active" },
|
||||
{ "id": "expanded-employee-06", "organizationId": "talent-growth", "affiliationId": "saman", "name": "오하준", "position": "선임", "jobTitle": null, "profileImage": null, "workStatus": "active" }
|
||||
],
|
||||
"affiliations": [
|
||||
{ "id": "hanmac", "name": "(주)한맥기술" },
|
||||
{ "id": "saman", "name": "(주)삼안" },
|
||||
{ "id": "jangheon", "name": "(주)장헌" },
|
||||
{ "id": "jangheon-industry", "name": "장헌산업" },
|
||||
{ "id": "ptc", "name": "PTC" },
|
||||
{ "id": "halla", "name": "한라산업개발" },
|
||||
{ "id": "baron", "name": "바론컨설턴트" }
|
||||
],
|
||||
"relations": [],
|
||||
"presentation": {
|
||||
"title": "확장형 조직도 · P5-1A",
|
||||
"description": "대표자와 1Depth 그룹 초기 표시 · 자동 계층 배치 미구현 · 가상 직원 정보",
|
||||
"hubText": null,
|
||||
"groups": { "showEmployees": true, "showSummary": false, "showDispatched": false, "showOnLeave": false, "showAffiliations": false },
|
||||
"organizationOrder": ["general-planning", "business-management", "business-planning", "talent-growth"]
|
||||
},
|
||||
"metadata": {
|
||||
"asOfDate": "2026-10-12",
|
||||
"dataVersion": "p5-1a-expanded-initial-v1",
|
||||
"updateNote": "P5-1A 초기 1Depth 구성 · 운영 HR 정보와 무관한 가상 데이터"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
import http from 'node:http';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
// P5-1A only: preserve previous suites and the baseline-files.json snapshot.
|
||||
// Copy the pre-change public Core dist to baseline-dist before rebuilding.
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url));
|
||||
const output = path.join(root, 'test-results/org-chart-p5-1a');
|
||||
const storyOrigin = process.env.ORGCHART_STORY_ORIGIN ?? 'http://127.0.0.1:6052';
|
||||
const { chromium } = await import(pathToFileURL(process.env.ORGCHART_PLAYWRIGHT_MODULE
|
||||
?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright/index.mjs').href);
|
||||
const fixedFixture = JSON.parse(await fs.readFile(path.join(root, 'examples/org-chart/data-p1.json'), 'utf8'));
|
||||
const expandedFixture = JSON.parse(await fs.readFile(path.join(root, 'examples/org-chart/data-expanded.json'), 'utf8'));
|
||||
const presets = [
|
||||
{ id: 'hanmac', name: '한맥기술', primary: '#CB2E28', secondary: ['#1D4289'] },
|
||||
{ id: 'saman', name: '삼안', primary: '#F08300', secondary: ['#6D6C6C'] },
|
||||
{ id: 'jangheon-industry', name: '장헌산업', primary: '#4580A6', secondary: ['#92BED4'] },
|
||||
{ id: 'jangheon', name: '장헌', primary: '#1D418D', secondary: ['#AE232A'] },
|
||||
{ id: 'ptc', name: '피티씨', primary: '#84338E', secondary: ['#F7AF00', '#0066B5'] },
|
||||
{ id: 'halla', name: '한라산업개발', primary: '#0068B7', secondary: ['#231815'] },
|
||||
{ id: 'baron', name: '바론컨설턴트', primary: '#0C367F', secondary: [] },
|
||||
];
|
||||
const results = [], measurements = [], errors = [], warnings = [];
|
||||
const check = (pass, name, detail) => results.push({ name, status: pass ? 'PASS' : 'FAIL', ...(detail === undefined ? {} : { detail }) });
|
||||
const near = (a, b) => Number.isFinite(a) && Number.isFinite(b) && Math.abs(a - b) <= 0.35;
|
||||
const rgb = hex => `rgb(${[1, 3, 5].map(offset => parseInt(hex.slice(offset, offset + 2), 16)).join(', ')})`;
|
||||
const expectedCompanyIds = presets.map(preset => preset.id);
|
||||
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.ttf': 'font/ttf', '.svg': 'image/svg+xml', '.png': 'image/png' };
|
||||
const shellHtml = '<!doctype html><html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="data:,"><style>@font-face{font-family:"Noto Sans KR";src:url("/fonts/NotoSansKR-variable.ttf");font-weight:100 900;font-style:normal}*{box-sizing:border-box}html,body{margin:0;width:100%;min-height:100%;font-family:"Noto Sans KR",sans-serif}#chart{width:100%;height:100vh;min-width:0}</style></head><body><main id="chart"></main></body></html>';
|
||||
await fs.mkdir(output, { recursive: true });
|
||||
const server = http.createServer(async (request, response) => {
|
||||
try {
|
||||
const url = new URL(request.url, 'http://127.0.0.1');
|
||||
if (url.pathname === '/__p51a') return response.writeHead(200, { 'Content-Type': 'text/html' }).end(shellHtml);
|
||||
const pathname = decodeURIComponent(url.pathname);
|
||||
const file = path.resolve(root, '.' + (pathname.startsWith('/fonts/') ? '/public' + pathname : pathname));
|
||||
const relative = path.relative(root, file);
|
||||
if (relative.startsWith('..') || path.isAbsolute(relative)) return response.writeHead(403).end();
|
||||
const bytes = await fs.readFile(file);
|
||||
response.writeHead(200, { 'Content-Type': mime[path.extname(file)] ?? 'application/octet-stream' });
|
||||
response.end(bytes);
|
||||
} catch { response.writeHead(404).end(); }
|
||||
});
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
const origin = `http://127.0.0.1:${server.address().port}`;
|
||||
const browser = await chromium.launch({ executablePath: process.env.ORGCHART_CHROME ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: true });
|
||||
|
||||
async function newPage(name, viewport = { width: 1440, height: 900 }) {
|
||||
const page = await browser.newPage({ viewport });
|
||||
page.on('pageerror', error => errors.push({ name, type: 'pageerror', message: error.message }));
|
||||
page.on('console', message => {
|
||||
if (message.type() === 'error') errors.push({ name, type: 'console', message: message.text() });
|
||||
if (message.type() === 'warning') warnings.push({ name, message: message.text() });
|
||||
});
|
||||
return page;
|
||||
}
|
||||
async function ready(page, mode) {
|
||||
await page.waitForFunction(expected => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host')?.shadowRoot;
|
||||
const content = shadow?.querySelector(expected === 'auto' ? '.hm-orgchart-expanded' : '.hm-orgchart-composition');
|
||||
return content?.querySelector('.hm-orgchart-employee') && [...shadow.querySelectorAll('link[rel=stylesheet]')].every(link => link.sheet);
|
||||
}, mode, { timeout: 25000 });
|
||||
await page.evaluate(async () => {
|
||||
await document.fonts.load('600 24px "Noto Sans KR"', '홍 길 동');
|
||||
await document.fonts.ready;
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
const images = [...shadow.querySelectorAll('.hm-orgchart-employee img')];
|
||||
for (const image of images) image.loading = 'eager';
|
||||
await Promise.all(images.map(image => image.decode().catch(() => {})));
|
||||
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
});
|
||||
}
|
||||
async function mount(page, { baseline = false, fixture = expandedFixture, options = {} } = {}) {
|
||||
await page.goto(`${origin}/__p51a`);
|
||||
await page.evaluate(async ({ base, fixture, options }) => {
|
||||
window.hmModule = await import(`${base}/index.js`);
|
||||
window.fixture = structuredClone(fixture);
|
||||
window.handle = window.hmModule.createOrgChart({
|
||||
container: document.getElementById('chart'),
|
||||
dataSource: new window.hmModule.JsonOrgChartDataSource(fixture),
|
||||
stylesheetUrls: { engine: `${base}/engine.css`, core: `${base}/style.css` },
|
||||
...options,
|
||||
});
|
||||
await window.handle.mount();
|
||||
}, { base: `${origin}/${baseline ? 'test-results/org-chart-p5-1a/baseline-dist' : 'packages/org-chart/dist'}`, fixture, options });
|
||||
await ready(page, options.layoutMode === 'auto' ? 'auto' : 'fixed');
|
||||
return page.evaluate(() => window.handle.getState());
|
||||
}
|
||||
async function measure(page) {
|
||||
return page.evaluate(() => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
const stage = shadow.querySelector('.hm-orgchart-stage');
|
||||
const base = stage?.getBoundingClientRect() ?? shadow.querySelector('.hm-orgchart-expanded').getBoundingClientRect();
|
||||
const scale = stage ? base.width / stage.offsetWidth : 1;
|
||||
const box = element => {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
return { x: (bounds.x - base.x) / scale, y: (bounds.y - base.y) / scale, width: bounds.width / scale, height: bounds.height / scale };
|
||||
};
|
||||
const cards = [...shadow.querySelectorAll('.hm-orgchart-employee')].map(card => ({
|
||||
key: `${card.closest('.hm-orgchart-status-group')?.dataset.workStatus ?? 'tree'}:${card.dataset.employeeId}`,
|
||||
id: card.dataset.employeeId, kind: card.dataset.cardKind, size: card.dataset.cardAssetSize ?? null,
|
||||
box: box(card), asset: card.querySelector('.hm-orgchart-card-background')?.src ?? null,
|
||||
logo: card.querySelector('.hm-orgchart-affiliation-mark img')?.src ?? null,
|
||||
name: card.querySelector('.hm-orgchart-person-name')?.textContent,
|
||||
role: card.querySelector('.hm-orgchart-person-role')?.textContent,
|
||||
font: getComputedStyle(card.querySelector('.hm-orgchart-person-name')).fontFamily,
|
||||
badgeCount: card.querySelectorAll('.hm-orgchart-job-title,.hm-orgchart-status-label,.hm-orgchart-person-attributes').length,
|
||||
placeholder: card.dataset.photoState,
|
||||
}));
|
||||
const nodes = [...shadow.querySelectorAll('.node[data-organization-id]')].map(node => {
|
||||
const heading = node.querySelector('.hm-orgchart-organization-name');
|
||||
const style = getComputedStyle(heading);
|
||||
return { id: node.dataset.organizationId, companyId: node.dataset.companyId ?? null,
|
||||
depth: Number(node.dataset.organizationDepth), text: heading.textContent, box: box(heading),
|
||||
backgroundColor: style.backgroundColor, backgroundImage: style.backgroundImage,
|
||||
primary: node.style.getPropertyValue('--hm-orgchart-expanded-primary'),
|
||||
secondary: [...node.style].filter(property => property.startsWith('--hm-orgchart-expanded-secondary-')).map(property => node.style.getPropertyValue(property)),
|
||||
};
|
||||
});
|
||||
const collisions = [];
|
||||
for (let index = 0; index < cards.length; index++) for (const other of cards.slice(index + 1)) {
|
||||
const a = cards[index].box, b = other.box;
|
||||
if (Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x) > 0.5
|
||||
&& Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y) > 0.5) collisions.push([cards[index].key, other.key]);
|
||||
}
|
||||
const viewport = shadow.querySelector('.hm-orgchart-viewport');
|
||||
const expanded = shadow.querySelector('.hm-orgchart-expanded');
|
||||
const summaries = [...shadow.querySelectorAll('[data-component="PositionSummary"],[data-component="AffiliationSummary"],[data-component="StatusSummary"]')].map(element => ({ kind: element.dataset.component, text: element.textContent }));
|
||||
return { scale, cards, nodes, summaries, collisions,
|
||||
hostCount: document.querySelectorAll('.hm-orgchart-host').length,
|
||||
compositionCount: shadow.querySelectorAll('.hm-orgchart-composition,.hm-orgchart-expanded').length,
|
||||
controls: shadow.querySelectorAll('.hm-orgchart-toggle,.zoom-controls,.pan-controls,.minimap').length,
|
||||
layoutStatus: expanded?.dataset.layoutStatus ?? null,
|
||||
phaseText: expanded?.textContent ?? '',
|
||||
viewport: viewport ? { clientWidth: viewport.clientWidth, scrollWidth: viewport.scrollWidth, clientHeight: viewport.clientHeight, scrollHeight: viewport.scrollHeight, overflowX: getComputedStyle(viewport).overflowX } : null,
|
||||
document: { clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth, clientHeight: document.documentElement.clientHeight, scrollHeight: document.documentElement.scrollHeight },
|
||||
};
|
||||
});
|
||||
}
|
||||
function compareFixed(before, after, name) {
|
||||
check(JSON.stringify(before.cards.map(({ box, ...rest }) => rest)) === JSON.stringify(after.cards.map(({ box, ...rest }) => rest)), `${name}: card assets/content/typography/placeholder unchanged`);
|
||||
check(before.cards.length === after.cards.length && before.cards.every((card, index) => ['x', 'y', 'width', 'height'].every(key => near(card.box[key], after.cards[index].box[key]))), `${name}: fixed card coordinates and dimensions unchanged`);
|
||||
check(before.nodes.length === after.nodes.length && before.nodes.every((node, index) => {
|
||||
const next = after.nodes[index];
|
||||
return node.id === next.id && node.depth === next.depth && node.text === next.text
|
||||
&& node.backgroundColor === next.backgroundColor && node.backgroundImage === next.backgroundImage
|
||||
&& ['x', 'y', 'width', 'height'].every(key => near(node.box[key], next.box[key]));
|
||||
}), `${name}: hierarchy headers and Workdesk gradients unchanged`);
|
||||
check(JSON.stringify(before.summaries) === JSON.stringify(after.summaries), `${name}: summary counts unchanged`);
|
||||
check(after.controls === 0 && after.compositionCount === 1 && after.layoutStatus === null, `${name}: fixed read-only renderer retained`);
|
||||
}
|
||||
function assertExpanded(value, name) {
|
||||
const representative = value.cards.filter(card => card.kind === 'topLeader');
|
||||
check(value.layoutStatus === 'preview' && value.phaseText.includes('P5-1A') && value.phaseText.includes('후속'), `${name}: deferred work is explicitly marked preview`);
|
||||
check(representative.length === 1 && near(representative[0].box.width, 146) && near(representative[0].box.height, 74)
|
||||
&& representative[0].name === '홍 길 동' && representative[0].role === '대표이사' && representative[0].badgeCount === 0, `${name}: representative 146x74/name/position/zero badges`);
|
||||
check(value.cards.filter(card => card.kind === 'leader').length === 3
|
||||
&& value.cards.filter(card => card.kind === 'leader').every(card => near(card.box.width, 146) && near(card.box.height, 68)), `${name}: reused 1Depth leader 146x68 cards`);
|
||||
check(value.cards.filter(card => card.kind === 'employee').length === 6
|
||||
&& value.cards.filter(card => card.kind === 'employee').every(card => near(card.box.width, 108) && near(card.box.height, 52)), `${name}: reused small 108x52 employee cards`);
|
||||
check(value.cards.every(card => card.font.includes('Noto Sans KR') && card.placeholder === 'placeholder' && card.asset), `${name}: approved assets/common photo fallback/Noto font reused`);
|
||||
check(value.nodes.length === 3 && value.nodes.every(node => node.depth === 1) && value.controls === 0, `${name}: only initial 1Depth groups without collapse/zoom/pan controls`);
|
||||
check(value.cards.length === 10 && value.collisions.length === 0 && value.hostCount === 1 && value.compositionCount === 1, `${name}: no duplicate or overlapping cards`, value.collisions);
|
||||
}
|
||||
async function popup(page, name, id) {
|
||||
const card = page.locator(`.hm-orgchart-employee[data-employee-id="${id}"]`).first();
|
||||
await card.focus();
|
||||
await card.press('Enter');
|
||||
await page.locator('dialog[open]').waitFor();
|
||||
const data = await page.evaluate(() => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
const dialog = shadow.querySelector('dialog[open]');
|
||||
return { employeeId: dialog.dataset.employeeId, text: dialog.textContent, count: shadow.querySelectorAll('dialog[open]').length, activeInside: dialog.contains(shadow.activeElement) };
|
||||
});
|
||||
check(data.employeeId === id && data.count === 1 && data.activeInside, `${name}: keyboard opens shared employee popup`, data);
|
||||
await page.keyboard.press('Tab');
|
||||
check(await page.evaluate(() => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
return shadow.querySelector('dialog[open]').contains(shadow.activeElement);
|
||||
}), `${name}: popup Tab focus remains inside`);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.locator('dialog[open]').waitFor({ state: 'detached' });
|
||||
check(await page.evaluate(id => document.querySelector('.hm-orgchart-host').shadowRoot.activeElement?.dataset.employeeId === id, id), `${name}: Escape restores initiating card focus`);
|
||||
return data.text;
|
||||
}
|
||||
async function scenario(name, run) {
|
||||
try { await run(); }
|
||||
catch (error) { check(false, `${name}: verification completed`, { message: error.message, stack: error.stack }); }
|
||||
}
|
||||
|
||||
try {
|
||||
let baselineFixed, defaultFixed, autoVanilla;
|
||||
let baselinePopup;
|
||||
await scenario('Fixed baseline', async () => {
|
||||
const page = await newPage('baseline-fixed');
|
||||
try {
|
||||
const state = await mount(page, { baseline: true, fixture: fixedFixture, options: { displayMode: 'readOnly' } });
|
||||
check(state.status === 'ready', 'baseline Fixed: existing public Core ready');
|
||||
baselineFixed = await measure(page);
|
||||
measurements.push({ name: 'baseline-fixed', ...baselineFixed });
|
||||
baselinePopup = await popup(page, 'baseline Fixed', 'mock-leader-00');
|
||||
await page.screenshot({ path: path.join(output, 'baseline-fixed-1440.png'), fullPage: true });
|
||||
} finally { await page.close(); }
|
||||
});
|
||||
for (const explicit of [false, true]) await scenario(`Fixed ${explicit ? 'explicit' : 'default'}`, async () => {
|
||||
const name = explicit ? 'explicit-fixed' : 'default-fixed';
|
||||
const page = await newPage(name);
|
||||
try {
|
||||
const state = await mount(page, { fixture: fixedFixture, options: { displayMode: 'readOnly', ...(explicit ? { layoutMode: 'fixed', companyId: 'unknown-is-ignored-for-fixed' } : {}) } });
|
||||
const value = await measure(page);
|
||||
measurements.push({ name, ...value });
|
||||
check(state.status === 'ready' && state.summary.raw.total === fixedFixture.employees.length, `${name}: ready with preserved employee count`);
|
||||
if (baselineFixed) compareFixed(baselineFixed, value, name);
|
||||
else check(false, `${name}: baseline required for regression comparison`);
|
||||
if (!explicit) {
|
||||
defaultFixed = value;
|
||||
check(await popup(page, name, 'mock-leader-00') === baselinePopup, `${name}: existing popup field content unchanged`);
|
||||
await page.screenshot({ path: path.join(output, 'current-fixed-1440.png'), fullPage: true });
|
||||
}
|
||||
} finally { await page.close(); }
|
||||
});
|
||||
await scenario('Auto public Core', async () => {
|
||||
const page = await newPage('vanilla-auto');
|
||||
try {
|
||||
const state = await mount(page, { options: { layoutMode: 'auto', displayMode: 'readOnly' } });
|
||||
check(state.status === 'ready' && state.summary.raw.total === expandedFixture.employees.length, 'Auto: JsonOrgChartDataSource and full data summary retained');
|
||||
autoVanilla = await measure(page);
|
||||
measurements.push({ name: 'vanilla-auto', ...autoVanilla });
|
||||
assertExpanded(autoVanilla, 'Vanilla Auto');
|
||||
const popupText = await popup(page, 'Auto representative', 'expanded-representative');
|
||||
check(popupText.includes('대표이사') && popupText.includes('(주)한맥기술'), 'Auto popup: original employee/affiliation information');
|
||||
await popup(page, 'Auto employee', 'expanded-employee-01');
|
||||
const presetData = await page.evaluate(ids => ids.map(id => window.hmModule.getCompanyColorPreset(id)), expectedCompanyIds);
|
||||
check(presets.every((preset, index) => presetData[index]?.companyId === preset.id && presetData[index].primary === preset.primary && JSON.stringify(presetData[index].secondary) === JSON.stringify(preset.secondary)), 'Public lookup: all 7 P5-0 primary/secondary presets unchanged', presetData);
|
||||
const invalid = await page.evaluate(() => {
|
||||
const base = { dataSource: new window.hmModule.JsonOrgChartDataSource(window.fixture), stylesheetUrls: { core: 'core.css', engine: 'engine.css' } };
|
||||
return [{ layoutMode: 'invalid' }, { layoutMode: 'auto', companyId: 'unknown-company' }].map(options => {
|
||||
try { window.hmModule.createOrgChart({ ...base, ...options }); return false; }
|
||||
catch (error) { return error instanceof TypeError; }
|
||||
});
|
||||
});
|
||||
check(invalid.every(Boolean), 'New options: invalid layout mode and unknown Auto preset rejected', invalid);
|
||||
const lifecycle = await page.evaluate(async () => {
|
||||
const changed = structuredClone(window.fixture);
|
||||
changed.metadata.dataVersion = 'p5-1a-update';
|
||||
const removed = 'talent-growth';
|
||||
changed.organizations = changed.organizations.filter(organization => organization.id !== removed);
|
||||
changed.employees = changed.employees.filter(employee => employee.organizationId !== removed);
|
||||
changed.presentation.organizationOrder = changed.presentation.organizationOrder.filter(id => id !== removed);
|
||||
changed.organizations.find(organization => organization.id === 'business-management').name = '갱신된 사업관리';
|
||||
await window.handle.update(changed);
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
const updated = { state: window.handle.getState().status, version: window.handle.getState().dataVersion,
|
||||
compositions: shadow.querySelectorAll('.hm-orgchart-expanded').length, nodes: shadow.querySelectorAll('.node[data-organization-id]').length,
|
||||
cards: shadow.querySelectorAll('.hm-orgchart-employee').length, stale: !!shadow.querySelector('[data-organization-id="talent-growth"]'), text: shadow.textContent.includes('갱신된 사업관리') };
|
||||
await window.handle.reload();
|
||||
const reloaded = { state: window.handle.getState().status, version: window.handle.getState().dataVersion,
|
||||
compositions: shadow.querySelectorAll('.hm-orgchart-expanded').length, nodes: shadow.querySelectorAll('.node[data-organization-id]').length,
|
||||
cards: shadow.querySelectorAll('.hm-orgchart-employee').length, stale: shadow.textContent.includes('갱신된 사업관리') };
|
||||
return { updated, reloaded };
|
||||
});
|
||||
check(lifecycle.updated.state === 'ready' && lifecycle.updated.version === 'p5-1a-update' && lifecycle.updated.compositions === 1
|
||||
&& lifecycle.updated.nodes === 2 && lifecycle.updated.cards === 7 && !lifecycle.updated.stale && lifecycle.updated.text, 'Auto update: replaces groups/cards without duplicates or stale DOM', lifecycle.updated);
|
||||
check(lifecycle.reloaded.state === 'ready' && lifecycle.reloaded.version === expandedFixture.metadata.dataVersion && lifecycle.reloaded.compositions === 1
|
||||
&& lifecycle.reloaded.nodes === 3 && lifecycle.reloaded.cards === 10 && !lifecycle.reloaded.stale, 'Auto reload: restores JSON source without stale DOM', lifecycle.reloaded);
|
||||
await ready(page, 'auto');
|
||||
await page.screenshot({ path: path.join(output, 'vanilla-auto-1440.png'), fullPage: true });
|
||||
const deep = await page.evaluate(async () => {
|
||||
const deep = structuredClone(window.fixture);
|
||||
deep.organizations.push({ id: 'deferred-depth-2', parentId: 'business-management', name: '후속 2Depth', type: 'team', leaderId: null, displayOrder: 0 },
|
||||
{ id: 'deferred-depth-3', parentId: 'deferred-depth-2', name: '후속 3Depth', type: 'cell', leaderId: null, displayOrder: 0 });
|
||||
for (const [index, id] of ['deferred-depth-2', 'deferred-depth-3'].entries()) deep.employees.push({ id: `deferred-employee-${index}`, organizationId: id, affiliationId: 'hanmac', name: '가상 직원', position: '연구', jobTitle: null, profileImage: null, workStatus: 'active' });
|
||||
deep.presentation.organizationOrder.push('deferred-depth-2', 'deferred-depth-3');
|
||||
deep.metadata.dataVersion = 'p5-1b-deferred-fixture';
|
||||
const validated = window.hmModule.validateOrgChartDataset(deep);
|
||||
const normalized = validated.valid ? window.hmModule.normalizeOrgChartDataset(validated.data) : null;
|
||||
await window.handle.update(deep);
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
return { valid: validated.valid, state: window.handle.getState().status, total: window.handle.getState().summary?.raw.total,
|
||||
deepest: normalized?.roots[0].children[0].children[0].children[0].organization.id,
|
||||
rendered: [...shadow.querySelectorAll('.node[data-organization-id]')].map(node => node.dataset.organizationId),
|
||||
deferredCards: shadow.querySelectorAll('[data-employee-id^="deferred-employee-"]').length,
|
||||
retained: deep.organizations.length === window.fixture.organizations.length + 2 };
|
||||
});
|
||||
check(deep.valid && deep.state === 'ready' && deep.deepest === 'deferred-depth-3' && deep.total === expandedFixture.employees.length + 2
|
||||
&& deep.rendered.length === 3 && !deep.rendered.some(id => id.startsWith('deferred-')) && deep.deferredCards === 0 && deep.retained,
|
||||
'P5-1B boundary: 3Depth data retained and validated; descendants intentionally unplaced', deep);
|
||||
const destroyed = await page.evaluate(() => {
|
||||
window.handle.destroy(); window.handle.destroy();
|
||||
return { status: window.handle.getState().status, children: document.getElementById('chart').children.length };
|
||||
});
|
||||
check(destroyed.status === 'destroyed' && destroyed.children === 0, 'Auto destroy: terminal/idempotent cleanup', destroyed);
|
||||
} finally { await page.close(); }
|
||||
});
|
||||
await scenario('Auto default displayMode', async () => {
|
||||
const page = await newPage('auto-default-display');
|
||||
try {
|
||||
await mount(page, { options: { layoutMode: 'auto' } });
|
||||
const initial = await measure(page);
|
||||
check(initial.controls === 0 && initial.nodes.length === 3 && initial.layoutStatus === 'preview', 'Auto without displayMode: initial preview, no collapse controls');
|
||||
const transforms = await page.evaluate(() => [...document.querySelector('.hm-orgchart-host').shadowRoot.querySelectorAll('.hm-orgchart-engine')].map(element => element.style.transform));
|
||||
const viewport = page.locator('.hm-orgchart-viewport');
|
||||
await viewport.hover(); await page.mouse.wheel(0, -100);
|
||||
const bounds = await viewport.boundingBox();
|
||||
await page.mouse.move(bounds.x + bounds.width / 2, bounds.y + 10); await page.mouse.down();
|
||||
await page.mouse.move(bounds.x + bounds.width / 2 + 40, bounds.y + 10, { steps: 4 }); await page.mouse.up();
|
||||
const after = await page.evaluate(() => [...document.querySelector('.hm-orgchart-host').shadowRoot.querySelectorAll('.hm-orgchart-engine')].map(element => element.style.transform));
|
||||
check(JSON.stringify(transforms) === JSON.stringify(after), 'Auto: wheel/drag do not activate engine zoom or pan');
|
||||
} finally { await page.close(); }
|
||||
});
|
||||
await scenario('Actual Storybook', async () => {
|
||||
const page = await newPage('storybook-auto');
|
||||
try {
|
||||
const response = await page.request.get(`${storyOrigin}/index.json`);
|
||||
const index = await response.json();
|
||||
const visible = Object.values(index.entries).filter(entry => entry.type === 'story' && entry.title === 'OrgChart' && !entry.tags?.includes('internal')).map(entry => entry.id).sort();
|
||||
const expected = ['orgchart--logo-personnel-summary', 'orgchart--department-colors', 'orgchart--full-composition', 'orgchart--expanded-org-chart'].sort();
|
||||
check(response.ok() && JSON.stringify(visible) === JSON.stringify(expected), 'Storybook: existing 3 public menus plus Expanded OrgChart only', visible);
|
||||
await page.goto(`${storyOrigin}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`);
|
||||
await ready(page, 'auto');
|
||||
const vanillaStyles = autoVanilla?.cards.map(card => ({ id: card.id, kind: card.kind, width: card.box.width, height: card.box.height, asset: card.asset, font: card.font }));
|
||||
const reactValue = await measure(page);
|
||||
check(Boolean(vanillaStyles) && reactValue.cards.length === vanillaStyles.length && reactValue.cards.every((card, index) => {
|
||||
const vanilla = vanillaStyles[index];
|
||||
return card.id === vanilla.id && card.kind === vanilla.kind && near(card.box.width, vanilla.width) && near(card.box.height, vanilla.height) && card.asset === vanilla.asset && card.font === vanilla.font;
|
||||
}), 'React Story / Vanilla: same Core fixture/cards/assets/font');
|
||||
for (const preset of presets) {
|
||||
const select = page.getByRole('combobox', { name: '회사별 컬러 테마' });
|
||||
await select.click();
|
||||
await page.getByRole('option', { name: preset.name, exact: true }).click();
|
||||
await page.waitForFunction(companyId => {
|
||||
const nodes = [...document.querySelector('.hm-orgchart-host')?.shadowRoot.querySelectorAll('.node[data-organization-id]') ?? []];
|
||||
return nodes.length === 3 && nodes.every(node => node.dataset.companyId === companyId);
|
||||
}, preset.id);
|
||||
await ready(page, 'auto');
|
||||
const value = await measure(page);
|
||||
measurements.push({ name: `storybook-company-${preset.id}`, nodes: value.nodes });
|
||||
check(value.nodes.every(node => node.companyId === preset.id && node.primary === preset.primary && node.backgroundColor === rgb(preset.primary)
|
||||
&& JSON.stringify(node.secondary) === JSON.stringify(preset.secondary)), `HM Select: ${preset.name} primary and all secondary colors connected`, value.nodes);
|
||||
}
|
||||
await page.getByRole('combobox', { name: '회사별 컬러 테마' }).click();
|
||||
await page.getByRole('option', { name: '한맥기술', exact: true }).click();
|
||||
await page.waitForFunction(() => document.querySelector('.hm-orgchart-host')?.shadowRoot.querySelector('.node[data-company-id="hanmac"]'));
|
||||
for (const width of [390, 768, 1280, 1440]) {
|
||||
await page.setViewportSize({ width, height: width < 1000 ? 1024 : 900 });
|
||||
await ready(page, 'auto');
|
||||
const value = await measure(page);
|
||||
measurements.push({ name: `storybook-${width}`, ...value });
|
||||
assertExpanded(value, `Storybook ${width}`);
|
||||
check(value.document.scrollWidth <= value.document.clientWidth + 1
|
||||
&& value.viewport && ['auto', 'scroll'].includes(value.viewport.overflowX), `Storybook ${width}: horizontal overflow contained by Core viewport`, { document: value.document, viewport: value.viewport });
|
||||
await page.screenshot({ path: path.join(output, `storybook-expanded-${width}.png`), fullPage: true });
|
||||
}
|
||||
await popup(page, 'React Auto employee', 'expanded-employee-01');
|
||||
await page.goto(`${storyOrigin}/iframe.html?id=orgchart--full-composition&viewMode=story`);
|
||||
await ready(page, 'fixed');
|
||||
const value = await measure(page);
|
||||
check(value.cards.length === defaultFixed?.cards.length && value.controls === 0 && value.layoutStatus === null && value.compositionCount === 1,
|
||||
'Existing Full Composition Story: fixed read-only route retained');
|
||||
} finally { await page.close(); }
|
||||
});
|
||||
} finally {
|
||||
await browser.close();
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
check(errors.length === 0, 'Focused browser scenarios: no new console/runtime errors', errors);
|
||||
const summary = { total: results.length, pass: results.filter(result => result.status === 'PASS').length, fail: results.filter(result => result.status === 'FAIL').length };
|
||||
await fs.writeFile(path.join(output, 'browser.json'), JSON.stringify({ scope: 'P5-1A only', browser: chromium.name(), summary, results, measurements, errors, warnings,
|
||||
notVerified: ['P5-1B recursive automatic layout/recalculation/collision solver', 'Collapse/expand and Zoom/Pan implementation', 'Arbitrary data/themes/browsers and real touch/screen reader speech', 'Entire historical P2/P3 suites and complete Storybook production build'] }, null, 2));
|
||||
console.log(JSON.stringify(summary));
|
||||
if (summary.fail) process.exitCode = 1;
|
||||
}
|
||||
@@ -44,6 +44,8 @@ Named runtime export: `OrgChart`. Type exports: `OrgChartProps` and the Core's p
|
||||
- Required `dataSource`: the same Core `OrgChartDataSource` interface, `load(options?: { signal?: AbortSignal }): Promise<unknown>`.
|
||||
- Required `stylesheetUrls`: `{ engine: string; core: string }`, the exact Core asset contract.
|
||||
- Optional `displayMode`: `'interactive'` (default) or `'readOnly'`, passed to the Core composition. Changing it recreates the Core handle.
|
||||
- Optional `layoutMode`: `'fixed'` (default) or `'auto'`, independent of `displayMode`. Auto is the P5-1A initial-group preview; automatic recursive layout, collapse and Zoom/Pan remain deferred. Changing it recreates the same Core handle.
|
||||
- Optional `companyId`: existing Expanded company preset ID (default `hanmac`). Changing it recreates the handle; Fixed ignores company colors. Unknown Auto IDs report a Core construction lifecycle error. All seven IDs and preview limits are documented in the [Core README](../org-chart/README.md#layout-boundary-p5-1a).
|
||||
- Optional `organizationLinks`: existing organization-ID-to-URL map, applied only to original 1Depth titles. Missing/blank values retain plain titles. Core validates HTTP(S)/relative URLs and rejects invalid URLs, other schemes and credentials. Treat the map as immutable and memoize it; a new map recreates the handle.
|
||||
- Optional `onEmployeeClick(employee, context)`: host-owned information UI, replacing the Core's built-in popup. `context` contains the existing `organization`, `affiliation`, focusable `trigger` card and `originalEvent`. It works in both modes and receives untouched Employee data.
|
||||
- Optional `aggregationPolicy`: the Core's `ApprovedAggregationPolicy`.
|
||||
|
||||
@@ -18,6 +18,8 @@ export function OrgChart({
|
||||
aggregationPolicy,
|
||||
stylesheetUrls,
|
||||
displayMode,
|
||||
layoutMode,
|
||||
companyId,
|
||||
organizationLinks,
|
||||
onEmployeeClick,
|
||||
className,
|
||||
@@ -62,6 +64,8 @@ export function OrgChart({
|
||||
aggregationPolicy,
|
||||
stylesheetUrls: { engine: engineStylesheetUrl, core: coreStylesheetUrl },
|
||||
displayMode,
|
||||
layoutMode,
|
||||
companyId,
|
||||
organizationLinks,
|
||||
onEmployeeClick: hasEmployeeCallback ? (employee, context) => {
|
||||
if (generationRef.current === generation) committedRef.current.onEmployeeClick?.(employee, context);
|
||||
@@ -101,7 +105,7 @@ export function OrgChart({
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [aggregationPolicy, engineStylesheetUrl, coreStylesheetUrl, displayMode, organizationLinks, hasEmployeeCallback]);
|
||||
}, [aggregationPolicy, engineStylesheetUrl, coreStylesheetUrl, displayMode, layoutMode, companyId, organizationLinks, hasEmployeeCallback]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const instance = instanceRef.current;
|
||||
|
||||
@@ -24,6 +24,39 @@ chart.destroy();
|
||||
|
||||
## Organization links and employee information (P3)
|
||||
|
||||
### Layout boundary (P5-1A)
|
||||
|
||||
`layoutMode?: 'fixed' | 'auto'` is independent of `displayMode`. The default
|
||||
`'fixed'` keeps the existing Workdesk composition and its display-mode behavior.
|
||||
`'auto'` currently selects the **P5-1A initial preview**, showing the representative
|
||||
and immediate organization groups from the validated hierarchy. Descendants stay
|
||||
in the dataset and statistics but are not placed in this preview. Automatic
|
||||
recursive placement/recalculation/collision prevention belongs to P5-1B;
|
||||
collapse controls and Zoom/Pan remain deferred. No preview readiness state claims
|
||||
those later features are complete.
|
||||
|
||||
Use `companyId` with Auto to select an existing company preset (default `hanmac`).
|
||||
The seven IDs are `hanmac`, `saman`, `jangheon-industry`, `jangheon`, `ptc`, `halla`,
|
||||
and `baron`. Main colors style only Expanded group headers; indexed secondary
|
||||
variables come from that same preset. Fixed ignores this option and preserves
|
||||
its organization gradients. Unknown Auto IDs reject construction with `TypeError`.
|
||||
The preview reuses approved Workdesk card presentation, the same data/engine and
|
||||
employee dialog; it has an independent wrapper without Fixed coordinates or scaling.
|
||||
Choose `displayMode: 'readOnly'` for the matching read-only popup name policy.
|
||||
|
||||
```js
|
||||
const chart = createOrgChart({
|
||||
...configuredBaseOptions,
|
||||
layoutMode: 'auto',
|
||||
displayMode: 'readOnly',
|
||||
companyId: 'hanmac',
|
||||
});
|
||||
```
|
||||
|
||||
Options are captured at construction: create a new handle to change the layout or
|
||||
company. Standalone Tree/Summary factories keep their existing rendering paths.
|
||||
|
||||
|
||||
### P3.2 follow-up: representative badges
|
||||
|
||||
Representative cards never render job-title, role or status badges, empty attribute wrappers or a badge row, including standalone specimens. They display `대표이사` / `홍 길 동`, retaining the approved 146×74 asset and existing typography, with no gap between position and name. The fixture's raw representative position (`경영진`), employee data and dialog fields remain unchanged. Other 1Depth/2Depth badge policies are preserved. This display policy takes precedence over the historical P3.2/P3.1 notes below.
|
||||
|
||||
@@ -30,7 +30,7 @@ execFileSync(process.execPath, [path.resolve(path.dirname(typescriptManifest), t
|
||||
// Ship one self-contained public CSS asset. ?url and file:// consumers cannot
|
||||
// rely on source-relative imports or a bundler to discover private style files.
|
||||
const coreStyles = await readFile(path.join(root, 'src/styles/org-chart.css'), 'utf8');
|
||||
const inlinedStyles = await Promise.all(['design-tokens.css', 'employee-card.css', 'summary-sections.css', 'org-chart-content.css', 'org-chart-readonly.css', 'org-chart-p3-summary.css', 'employee-info-dialog.css'].map(async file => [file, await readFile(path.join(root, 'src/styles', file), 'utf8')]));
|
||||
const inlinedStyles = await Promise.all(['design-tokens.css', 'employee-card.css', 'summary-sections.css', 'org-chart-content.css', 'org-chart-readonly.css', 'org-chart-p3-summary.css', 'employee-info-dialog.css', 'org-chart-expanded.css'].map(async file => [file, await readFile(path.join(root, 'src/styles', file), 'utf8')]));
|
||||
let publicStyles = coreStyles;
|
||||
for (const [file, css] of inlinedStyles) publicStyles = publicStyles.replace(`@import './${file}';`, css);
|
||||
await writeFile(path.join(root, 'dist/style.css'), publicStyles);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { createEmployeeCard } from './employee-card.js';
|
||||
import { createHeading, type RenderContext } from './render.js';
|
||||
import { representativeCardAsset } from './representative-card-asset.js';
|
||||
|
||||
/** P5-1A presenter boundary. The shared Core supplies the initial engine groups;
|
||||
* this presenter never assigns coordinates or interprets parentId. A later
|
||||
* layout adapter can replace group placement without duplicating card or data
|
||||
* rendering in React and Vanilla consumers. */
|
||||
export function createExpandedOrgChartContent(
|
||||
document: Document, context: RenderContext, tree: HTMLElement, signal: AbortSignal,
|
||||
): HTMLElement {
|
||||
const content = document.createElement('div');
|
||||
content.className = 'hm-orgchart-expanded';
|
||||
content.dataset.component = 'ExpandedOrgChartContent';
|
||||
content.dataset.layoutMode = 'auto';
|
||||
content.dataset.layoutStatus = 'preview';
|
||||
|
||||
const heading = createHeading(document, context);
|
||||
const status = document.createElement('p');
|
||||
status.className = 'hm-orgchart-expanded-stage-note';
|
||||
status.textContent = 'P5-1A · 확장형 준비 화면';
|
||||
const description = document.createElement('p');
|
||||
description.className = 'hm-orgchart-expanded-description';
|
||||
description.textContent = '대표자와 1Depth 예시 그룹을 표시합니다. 하위 계층 자동 배치와 접기·펼치기, Zoom/Pan은 후속 단계에서 구현합니다.';
|
||||
content.append(heading, status, description);
|
||||
|
||||
const leaders = document.createElement('section');
|
||||
leaders.className = 'hm-orgchart-expanded-top-leaders';
|
||||
leaders.setAttribute('aria-label', '최상위 조직 리더');
|
||||
for (const id of context.topLeaderIds) {
|
||||
const employee = context.data.employees.find(item => item.id === id);
|
||||
if (!employee) continue;
|
||||
leaders.append(createEmployeeCard(document, employee, {
|
||||
kind: 'topLeader',
|
||||
workdeskPresentation: true,
|
||||
cardAsset: representativeCardAsset,
|
||||
affiliationName: context.data.affiliations.find(item => item.id === employee.affiliationId)?.name,
|
||||
}, signal));
|
||||
}
|
||||
if (leaders.childElementCount) content.append(leaders);
|
||||
|
||||
// Keep the stable Core viewport for keyboard access and ordinary scrolling.
|
||||
// This is a flow preview, with no fixed-stage scale observer or artwork.
|
||||
tree.setAttribute('aria-label', '확장형 조직도 초기 그룹 영역');
|
||||
content.append(tree);
|
||||
return content;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { expandedPreviewRoots } from './expanded-layout.js';
|
||||
import { createOrgChart } from './index.js';
|
||||
import type { OrganizationTreeNode } from '../data/index.js';
|
||||
|
||||
const node = (id: string, parentId: string | null, type: 'root' | 'office' = 'office', children: OrganizationTreeNode[] = []): OrganizationTreeNode => ({
|
||||
organization: { id, parentId, name: id, type, leaderId: null, displayOrder: 0 }, children,
|
||||
});
|
||||
|
||||
test('initial preview keeps actual first-depth ancestry and defers descendants without mutating hierarchy', () => {
|
||||
const grandchild = node('depth-2', 'depth-1');
|
||||
const child = node('depth-1', 'root', 'office', [grandchild]);
|
||||
const roots = [node('root', null, 'root', [child])];
|
||||
const before = structuredClone(roots);
|
||||
const projected = expandedPreviewRoots(roots);
|
||||
assert.deepEqual(projected.map(item => item.organization.id), ['depth-1']);
|
||||
assert.equal(projected[0]!.organization.parentId, 'root');
|
||||
assert.deepEqual(projected[0]!.children, []);
|
||||
assert.deepEqual(roots, before);
|
||||
});
|
||||
|
||||
test('independent roots and normalized order stay independent in the initial preview', () => {
|
||||
const roots = [node('first', null), node('root', null, 'root', [node('second', 'root'), node('third', 'root')])];
|
||||
assert.deepEqual(expandedPreviewRoots(roots).map(item => item.organization.id), ['first', 'second', 'third']);
|
||||
assert.deepEqual(expandedPreviewRoots([]), []);
|
||||
});
|
||||
|
||||
const options = { dataSource: { load: async () => ({}) }, stylesheetUrls: { engine: '/engine.css', core: '/style.css' } };
|
||||
|
||||
test('layout defaults remain compatible and displayMode is an independent public option', () => {
|
||||
for (const displayMode of ['interactive', 'readOnly'] as const) {
|
||||
for (const layoutMode of [undefined, 'fixed', 'auto'] as const) {
|
||||
const handle = createOrgChart({ ...options, displayMode, layoutMode });
|
||||
assert.equal(handle.getState().status, 'idle');
|
||||
handle.destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('invalid layout and unknown Auto company fail explicitly, while Fixed ignores company configuration', () => {
|
||||
assert.throws(() => createOrgChart({ ...options, layoutMode: 'invalid' as 'auto' }), /layoutMode/);
|
||||
assert.throws(() => createOrgChart({ ...options, layoutMode: 'auto', companyId: 'unknown' }), /companyId/);
|
||||
const fixed = createOrgChart({ ...options, layoutMode: 'fixed', companyId: 'unknown' });
|
||||
assert.equal(fixed.getState().status, 'idle');
|
||||
fixed.destroy();
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { OrganizationTreeNode } from '../data/index.js';
|
||||
|
||||
/** P5-1A preview projection only. Keep the validated hierarchy intact and
|
||||
* expose the root's immediate groups without pretending to place descendants.
|
||||
* P5-1B can replace this boundary with a recursive layout result shared by
|
||||
* every runtime; neither React nor the Story owns a layout algorithm. */
|
||||
export function expandedPreviewRoots(roots: readonly OrganizationTreeNode[]): OrganizationTreeNode[] {
|
||||
return roots.flatMap(root => root.organization.type === 'root' ? root.children : [root])
|
||||
.map(node => ({ organization: node.organization, children: [] }));
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import type { EngineAdapter, EngineConstructor } from './engine.js';
|
||||
import { decorateNode, engineTree } from './render.js';
|
||||
import { createOrgChartTree } from './org-chart-tree.js';
|
||||
import { createOrgChartContent } from './org-chart-content.js';
|
||||
import { createExpandedOrgChartContent } from './expanded-content.js';
|
||||
import { expandedPreviewRoots } from './expanded-layout.js';
|
||||
import { getCompanyColorPreset } from '../company-color-presets.js';
|
||||
import { createPositionSummary } from './position-summary.js';
|
||||
import { createAffiliationSummary } from './affiliation-summary.js';
|
||||
import { createStatusSummary } from './status-summary.js';
|
||||
@@ -13,7 +16,7 @@ import type { EmployeeInfoDialog } from './employee-info-dialog.js';
|
||||
import type { RenderContext } from './render.js';
|
||||
import type { OrgChartEmployeeClickContext, OrgChartHandle, OrgChartOptions, OrgChartState } from './types.js';
|
||||
|
||||
export type { OrgChartEmployeeClickContext, OrgChartHandle, OrgChartOptions, OrgChartOrganizationLinks, OrgChartState } from './types.js';
|
||||
export type { OrgChartLayoutMode, OrgChartEmployeeClickContext, OrgChartHandle, OrgChartOptions, OrgChartOrganizationLinks, OrgChartState } from './types.js';
|
||||
|
||||
const owners = new WeakMap<HTMLElement, symbol>();
|
||||
let instanceSequence = 0;
|
||||
@@ -47,6 +50,11 @@ export function createOrgChartView(options: OrgChartOptions, view: CoreView): Or
|
||||
if (!options.stylesheetUrls?.core || (needsEngine && !options.stylesheetUrls.engine)) throw new TypeError('Provide the required OrgChart stylesheet URLs');
|
||||
const stylesheetUrls = { ...options.stylesheetUrls };
|
||||
if (options.displayMode !== undefined && options.displayMode !== 'interactive' && options.displayMode !== 'readOnly') throw new TypeError('displayMode must be "interactive" or "readOnly"');
|
||||
if (options.layoutMode !== undefined && options.layoutMode !== 'fixed' && options.layoutMode !== 'auto') throw new TypeError('layoutMode must be "fixed" or "auto"');
|
||||
const layoutMode = options.layoutMode ?? 'fixed';
|
||||
const expanded = view === 'composition' && layoutMode === 'auto';
|
||||
const expandedCompanyPreset = expanded ? getCompanyColorPreset(options.companyId ?? 'hanmac') : undefined;
|
||||
if (expanded && !expandedCompanyPreset) throw new TypeError('companyId must be a registered company preset in Auto mode');
|
||||
const readOnly = view === 'composition' && options.displayMode === 'readOnly';
|
||||
const organizationLinks: Record<string, string> = Object.create(null);
|
||||
if (options.onEmployeeClick !== undefined && typeof options.onEmployeeClick !== 'function') throw new TypeError('onEmployeeClick must be a function');
|
||||
@@ -200,14 +208,16 @@ export function createOrgChartView(options: OrgChartOptions, view: CoreView): Or
|
||||
const normalized = normalizeOrgChartDataset(data);
|
||||
const summary = freeze(calculateSummary(data, policy));
|
||||
const topLeaderIds = new Set(view === 'composition' ? normalized.roots.filter(node => node.organization.type === 'root' && node.organization.leaderId).map(node => node.organization.leaderId!) : []);
|
||||
const nextContext: RenderContext = { data, summary, orderedOrganizations: [], employeesByOrganization: normalized.employeesByOrganization, topLeaderIds, readOnly, organizationLinks };
|
||||
const nextContext: RenderContext = { data, summary, orderedOrganizations: [], employeesByOrganization: normalized.employeesByOrganization, topLeaderIds, readOnly: readOnly || expanded, organizationLinks, expandedCompanyPreset };
|
||||
// Composition shows the root leader as the standalone top card, so the root's direct
|
||||
// organizations become the horizontal engine roots (data and parentId stay untouched).
|
||||
// A root with other direct employees keeps its node so those employees stay visible.
|
||||
const flattenRoot = (node: typeof normalized.roots[number]) => view === 'composition' && node.organization.type === 'root' && !!node.organization.leaderId
|
||||
&& (normalized.employeesByOrganization.get(node.organization.id) ?? []).every(employee => employee.id === node.organization.leaderId);
|
||||
const flattened = normalized.roots.some(flattenRoot);
|
||||
const tree = engineTree(flattened ? normalized.roots.flatMap(node => flattenRoot(node) ? node.children : [node]) : normalized.roots, prefix, nextContext, flattened ? 1 : 0);
|
||||
const roots = expanded ? expandedPreviewRoots(normalized.roots)
|
||||
: flattened ? normalized.roots.flatMap(node => flattenRoot(node) ? node.children : [node]) : normalized.roots;
|
||||
const tree = engineTree(roots, prefix, nextContext, expanded || flattened ? 1 : 0);
|
||||
const styleIssues = await styleReady;
|
||||
if (!active(version)) return;
|
||||
if (styleIssues) { fail(version, styleIssues); return; }
|
||||
@@ -233,7 +243,9 @@ export function createOrgChartView(options: OrgChartOptions, view: CoreView): Or
|
||||
});
|
||||
}
|
||||
// If regeneration removed keyboard focus, offer the stable viewport next.
|
||||
const nextContent = view === 'composition' ? createOrgChartContent(document, nextContext, viewport!, contentGeneration.signal)
|
||||
const nextContent = view === 'composition' ? expanded
|
||||
? createExpandedOrgChartContent(document, nextContext, viewport!, contentGeneration.signal)
|
||||
: createOrgChartContent(document, nextContext, viewport!, contentGeneration.signal)
|
||||
: view === 'tree' ? viewport!
|
||||
: view === 'positionSummary' ? createPositionSummary(document, nextContext)
|
||||
: view === 'affiliationSummary' ? createAffiliationSummary(document, nextContext)
|
||||
@@ -306,6 +318,7 @@ export function createOrgChartView(options: OrgChartOptions, view: CoreView): Or
|
||||
cancelStyles = () => { for (const dispose of styleDisposers) dispose(); };
|
||||
shell = document.createElement('section');
|
||||
shell.className = 'hm-orgchart-shell';
|
||||
shell.dataset.layoutMode = view === 'composition' ? layoutMode : 'fixed';
|
||||
// The Core's Korean labels must shape identically in ko HTML and en Storybook hosts.
|
||||
shell.lang = 'ko';
|
||||
shell.tabIndex = -1;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createEmployeeCard } from './employee-card.js';
|
||||
import { applyOrganizationStyle, organizationStylePresets } from './style-presets.js';
|
||||
import { getDepartmentCardAsset } from './department-card-assets.js';
|
||||
import type { OrgChartOrganizationLinks } from './types.js';
|
||||
import type { CompanyColorPreset } from '../company-color-presets.js';
|
||||
|
||||
const organizationNames = { root: '최상위 조직', division: '부문', office: '실', div: 'Div', cell: '셀', team: '팀' } as const;
|
||||
|
||||
@@ -16,6 +17,8 @@ export interface RenderContext {
|
||||
/** View-only composition: no collapse controls or diagnostic text are created. */
|
||||
readOnly?: boolean;
|
||||
organizationLinks?: OrgChartOrganizationLinks;
|
||||
/** Only the Expanded presenter supplies company header colors. */
|
||||
expandedCompanyPreset?: CompanyColorPreset;
|
||||
}
|
||||
|
||||
export function engineTree(roots: OrganizationTreeNode[], prefix: string, context: RenderContext, startDepth = 0): RenderEngineNode[] {
|
||||
@@ -61,6 +64,14 @@ export function decorateNode(
|
||||
}
|
||||
node.dataset.organizationDepth = String(organizationDepth);
|
||||
applyOrganizationStyle(node, Number(value.groupVariant));
|
||||
if (context.expandedCompanyPreset) {
|
||||
const preset = context.expandedCompanyPreset;
|
||||
node.dataset.companyId = preset.companyId;
|
||||
node.style.setProperty('--hm-orgchart-current-gradient', preset.primary);
|
||||
node.style.setProperty('--hm-orgchart-expanded-primary', preset.primary);
|
||||
// Preserve every existing secondary value for the later Expanded theme.
|
||||
preset.secondary.forEach((color, index) => node.style.setProperty(`--hm-orgchart-expanded-secondary-${index + 1}`, color));
|
||||
}
|
||||
node.dataset.organizationType = organization.type;
|
||||
// Header role is styling-only: depth 0-1 are organization titles, deeper nodes are cell/team headers.
|
||||
node.dataset.headerRole = (context.readOnly ? organizationDepth : Number(value.depth)) <= 1 ? 'organization' : 'group';
|
||||
|
||||
@@ -10,6 +10,10 @@ export type OrgChartState =
|
||||
* controls or development text, compact summaries on a fitted design canvas. */
|
||||
export type OrgChartDisplayMode = 'interactive' | 'readOnly';
|
||||
|
||||
/** Layout is independent of displayMode. Auto currently exposes only the
|
||||
* P5-1A initial groups; recursive placement is reserved for P5-1B. */
|
||||
export type OrgChartLayoutMode = 'fixed' | 'auto';
|
||||
|
||||
/** Links are keyed by the existing organization ID. Only 1Depth titles use them. */
|
||||
export type OrgChartOrganizationLinks = Readonly<Record<string, string | undefined>>;
|
||||
|
||||
@@ -29,6 +33,11 @@ export interface OrgChartOptions {
|
||||
stylesheetUrls: { engine: string; core: string };
|
||||
/** Full composition presentation only; standalone views ignore it. */
|
||||
displayMode?: OrgChartDisplayMode;
|
||||
/** Full composition only. Omission preserves the existing Fixed renderer. */
|
||||
layoutMode?: OrgChartLayoutMode;
|
||||
/** Expanded-only company preset ID. Defaults to hanmac; ignored by Fixed.
|
||||
* Unknown IDs are rejected in Auto rather than silently replacing colors. */
|
||||
companyId?: string;
|
||||
/** No entry (or an empty URL) keeps an ordinary title. HTTP(S) and relative URLs are supported. */
|
||||
organizationLinks?: OrgChartOrganizationLinks;
|
||||
/** Optional host-owned information UI. When supplied, it replaces the default read-only dialog.
|
||||
|
||||
@@ -2,6 +2,6 @@ export * from './data/index.js';
|
||||
export { companyColorPresets, getCompanyColorPreset } from './company-color-presets.js';
|
||||
export type { CompanyColorPreset, CompanyId } from './company-color-presets.js';
|
||||
export { createOrgChart } from './core/index.js';
|
||||
export type { OrgChartDisplayMode, OrgChartEmployeeClickContext, OrgChartHandle, OrgChartOptions, OrgChartOrganizationLinks, OrgChartState } from './core/types.js';
|
||||
export type { OrgChartDisplayMode, OrgChartLayoutMode, OrgChartEmployeeClickContext, OrgChartHandle, OrgChartOptions, OrgChartOrganizationLinks, OrgChartState } from './core/types.js';
|
||||
export { createOrgChartTree, createPositionSummary, createAffiliationSummary, createStatusSummary } from './components.js';
|
||||
export type { OrgChartSummaryComponentOptions } from './components.js';
|
||||
@@ -0,0 +1,92 @@
|
||||
/* P5-1A flow preview only. Every rule is under the new presenter namespace;
|
||||
the fixed Workdesk composition and approved card internals stay unchanged.
|
||||
Ordinary flex wrapping is not the P5-1B automatic hierarchy layout. */
|
||||
.hm-orgchart-expanded {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--hm-orgchart-resolved-spacing-sm);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: var(--hm-orgchart-resolved-spacing-lg);
|
||||
background: var(--hm-orgchart-resolved-bg-content);
|
||||
}
|
||||
.hm-orgchart-expanded .hm-orgchart-heading { margin: 0; }
|
||||
.hm-orgchart-expanded .hm-orgchart-expanded-stage-note {
|
||||
margin: 0;
|
||||
color: var(--hm-orgchart-resolved-fg-primary);
|
||||
font-weight: var(--hm-orgchart-resolved-font-weight-bold);
|
||||
}
|
||||
.hm-orgchart-expanded .hm-orgchart-expanded-description {
|
||||
margin: 0;
|
||||
color: var(--hm-orgchart-resolved-fg-secondary);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.hm-orgchart-expanded .hm-orgchart-expanded-top-leaders {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: var(--hm-orgchart-resolved-spacing-lg);
|
||||
padding: var(--hm-orgchart-resolved-spacing-lg) 0;
|
||||
}
|
||||
.hm-orgchart-expanded .hm-orgchart-viewport {
|
||||
padding: var(--hm-orgchart-resolved-spacing-lg);
|
||||
overflow: auto;
|
||||
background: var(--hm-orgchart-resolved-bg-surface);
|
||||
}
|
||||
.hm-orgchart-expanded .hm-orgchart-forest {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: var(--hm-orgchart-resolved-spacing-xxl);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.hm-orgchart-expanded .hm-orgchart-tree { flex: 0 0 auto; }
|
||||
.hm-orgchart-expanded .hm-orgchart-engine.orgchart {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] {
|
||||
width: max-content;
|
||||
min-width: 146px;
|
||||
margin: 0;
|
||||
}
|
||||
.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-organization {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
/* Same approved 1Depth title geometry; color resolves from the auto-only
|
||||
company variables supplied by the shared node decorator. */
|
||||
.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-organization-name {
|
||||
align-self: stretch;
|
||||
min-width: 146px;
|
||||
height: 36px;
|
||||
min-height: 36px;
|
||||
margin: 0;
|
||||
padding: 0 12px;
|
||||
background: var(--hm-orgchart-current-gradient);
|
||||
color: var(--hm-orgchart-resolved-on-action-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 36px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee { margin: 0; }
|
||||
.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee.hm-orgchart-leader { margin: 8px 0 0; }
|
||||
.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employees {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employees:empty { display: none; }
|
||||
@@ -5,6 +5,7 @@
|
||||
@import './org-chart-readonly.css';
|
||||
@import './org-chart-p3-summary.css';
|
||||
@import './employee-info-dialog.css';
|
||||
@import './org-chart-expanded.css';
|
||||
/* Standalone defaults are scoped to the shell; only OrgChart public variables
|
||||
inherit across the Shadow DOM boundary. No host HM Theme is required. */
|
||||
:host { display: block; min-width: 0; max-width: 100%; font: inherit; }
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { StrictMode, useId, useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@hm-design/ui';
|
||||
import { OrgChart } from '@hm-design/org-chart-react';
|
||||
import { companyColorPresets, getCompanyColorPreset, JsonOrgChartDataSource } from '@hm-design/org-chart';
|
||||
import engineUrl from '@hm-design/org-chart/engine.css?url';
|
||||
import coreUrl from '@hm-design/org-chart/style.css?url';
|
||||
import expandedFixture from '../../examples/org-chart/data-expanded.json';
|
||||
import './org-chart-stories.css';
|
||||
|
||||
const expandedSource = new JsonOrgChartDataSource(expandedFixture);
|
||||
const compositionStylesheets = { engine: engineUrl, core: coreUrl };
|
||||
|
||||
/** P5-1A establishes the Core rendering boundary and initial 1Depth groups.
|
||||
* Automatic hierarchy placement belongs to P5-1B; this host owns no layout. */
|
||||
function ExpandedPreview() {
|
||||
const id = useId();
|
||||
const [companyId, setCompanyId] = useState('hanmac');
|
||||
const preset = getCompanyColorPreset(companyId);
|
||||
return <section className="hm-orgchart-story" aria-label="Expanded OrgChart · P5-1A">
|
||||
<p className="hm-orgchart-story-note"><strong>P5-1A · 확장형 조직도 초기 화면</strong> — 대표자와 예시 1Depth 그룹, 기존 카드 및 회사 컬러 프리셋을 표시합니다. 자동 계층 배치·재계산은 P5-1B, 접기·펼치기와 Zoom/Pan은 후속 작업입니다. 모든 직원 정보는 가상 데이터입니다.</p>
|
||||
<div className="hm-orgchart-story-controls">
|
||||
<div className="hm-orgchart-story-instance">
|
||||
<Label id={`${id}-company-label`} htmlFor={`${id}-company`}>회사별 컬러 테마</Label>
|
||||
<Select value={companyId} onValueChange={setCompanyId}>
|
||||
<SelectTrigger id={`${id}-company`} aria-labelledby={`${id}-company-label`} size="medium"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{Object.values(companyColorPresets).map(company => <SelectItem key={company.companyId} value={company.companyId}>{company.companyName}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{preset && <p className="hm-orgchart-story-note" aria-live="polite">{preset.companyName} · Main {preset.primary} · 보조 컬러 {preset.secondary.length ? preset.secondary.join(', ') : '등록 없음'}</p>}
|
||||
<OrgChart dataSource={expandedSource} stylesheetUrls={compositionStylesheets} displayMode="readOnly" layoutMode="auto" companyId={companyId} className="hm-orgchart-story-chart" />
|
||||
</section>;
|
||||
}
|
||||
|
||||
const meta = { title: 'OrgChart', component: ExpandedPreview, parameters: { layout: 'padded', controls: { disable: true } }, decorators: [(Story) => <StrictMode><Story /></StrictMode>] } satisfies Meta<typeof ExpandedPreview>;
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
export const ExpandedOrgChart: Story = { name: 'Expanded OrgChart' };
|
||||
Reference in new issue
Block a user