From 346c648c093ae3b0dedc1fc754d8ffa52f00ddd0 Mon Sep 17 00:00:00 2001 From: b25027 Date: Tue, 13 Oct 2026 22:43:21 +0900 Subject: [PATCH] feat(org-chart): support external CSS themes and align documentation --- docs/guidelines/ai-generation-guard.md | 8 + docs/org-chart/consistency-review-p11-1.md | 270 ++++++++++++++++++ docs/org-chart/implementation-report-p11.md | 139 +++++++++ docs/org-chart/org-chart-usage-guide.md | 133 ++++++++- .../org-chart-react/external-group-theme.css | 2 + examples/org-chart-react/external-theme.css | 44 +++ examples/org-chart-react/host-theme.css | 7 + examples/org-chart-react/p11.html | 2 + examples/org-chart-react/p11.tsx | 34 +++ .../org-chart-react/verify-p11-stories.mjs | 35 +++ examples/org-chart-react/verify-p11.mjs | 184 ++++++++++++ examples/org-chart-react/vite.config.ts | 2 +- examples/org-chart/verify-p11-standalone.mjs | 62 ++++ packages/org-chart/build.mjs | 2 +- .../org-chart/src/core/department-colors.ts | 4 +- .../org-chart/src/core/expanded-content.ts | 7 - packages/org-chart/src/core/style-presets.ts | 15 +- .../org-chart/src/styles/color-presets.css | 106 +++++++ .../org-chart/src/styles/employee-card.css | 10 +- .../src/styles/employee-info-dialog.css | 14 +- .../src/styles/org-chart-content.css | 13 +- .../src/styles/org-chart-expanded.css | 58 ++-- .../src/styles/org-chart-readonly.css | 10 +- packages/org-chart/src/styles/org-chart.css | 7 +- .../org-chart/src/styles/summary-sections.css | 25 +- scripts/guard-orgchart-theme.test.mjs | 62 ++++ scripts/guard-theme-analysis.mjs | 9 +- 27 files changed, 1189 insertions(+), 75 deletions(-) create mode 100644 docs/org-chart/consistency-review-p11-1.md create mode 100644 docs/org-chart/implementation-report-p11.md create mode 100644 examples/org-chart-react/external-group-theme.css create mode 100644 examples/org-chart-react/external-theme.css create mode 100644 examples/org-chart-react/host-theme.css create mode 100644 examples/org-chart-react/p11.html create mode 100644 examples/org-chart-react/p11.tsx create mode 100644 examples/org-chart-react/verify-p11-stories.mjs create mode 100644 examples/org-chart-react/verify-p11.mjs create mode 100644 examples/org-chart/verify-p11-standalone.mjs create mode 100644 packages/org-chart/src/styles/color-presets.css create mode 100644 scripts/guard-orgchart-theme.test.mjs diff --git a/docs/guidelines/ai-generation-guard.md b/docs/guidelines/ai-generation-guard.md index 68b95b4..5808868 100644 --- a/docs/guidelines/ai-generation-guard.md +++ b/docs/guidelines/ai-generation-guard.md @@ -1,5 +1,13 @@ # AI Generation Guard — 경량 검증 +## OrgChart 공개 CSS 선언 수집 (P11, 2026-10-13) + +`guard-theme-analysis.mjs`는 연결된 `@hm-design/org-chart/style.css` 및 `style.css?url`의 공개 source CSS와 상대 import에서 선언명을 수집한다. OrgChart는 URL 값을 `stylesheetUrls.core`로 Shadow DOM에 넣으므로 일반 CSS entry와 구분한다. 이 수집은 **공개 변수 선언의 존재**를 확인하며, 실제 link 로딩·selector 우선순위·computed style을 증명하지 않는다. 실제 적용은 별도 브라우저 검증이 필요하다. + +임의 `?url`/`?raw`/`?inline` CSS, 연결되지 않은 OrgChart 패키지, 다른 Story의 CSS를 선언 공급자로 사용하지 않는다. `setProperty`를 무조건 선언으로 인정하거나 미선언 HM 변수를 fallback으로 PASS 처리하지 않는다. OrgChart `current-*` 회사/부서 계산 변수는 실제 사용 범위의 CSS에 명시적 기본값을 둔다. 외부 입력 변수는 조상 상속을 막지 않도록 내부 요소에 기본 선언하지 않고 기존 `resolved-*` fallback 경로를 유지한다. + +이 변경은 UI/Status CSS 매핑과 기존 검사 판정·exit 규칙을 유지한다. P11 검증은 기존 Guard 94개 + 별도 `guard-orgchart-theme.test.mjs` 5개 PASS이며 다른 UI/CENTER/Foundation/Pattern의 67개 기존 이슈가 동일함을 확인했다. 과거 검증 기록과 미검증 항목은 보존한다. [P11 결과](../org-chart/implementation-report-p11.md)에서 기존 TOKEN-001 5건의 원본 재현과 FAIL 0 결과를 구분한다. + ## 별도 요구사항 액션 검사 `node scripts/guard-requirements.mjs `는 원본 Guard의 status/issues를 보존하고 별도 판정을 출력한다. 정책 입력 예시는 `{"schemaVersion":1,"actionPolicy":{"allowedLabels":["조회","초기화"]}}`다. 실제 요구사항에서 허용한 소비 화면의 Button/button/link 이름만 작성하며, Component 기본 동작을 검사하는 경우 그 이름도 명시한다. 허용하지 않은 정적 이름(예: 미요청 CSV 다운로드)은 REQ-003 FAIL, 동적/복합 이름·spread props·오래된 source/contract는 NOT VERIFIED다. diff --git a/docs/org-chart/consistency-review-p11-1.md b/docs/org-chart/consistency-review-p11-1.md new file mode 100644 index 0000000..a9884a2 --- /dev/null +++ b/docs/org-chart/consistency-review-p11-1.md @@ -0,0 +1,270 @@ +# HMDesign OrgChart P11.1 — 최종 정합성 검증 + +**RESULT: PASS — 요청 범위의 문서·공개 API·CSS·Storybook 정합성 및 Feature Commit 조건 충족.** 이 보고서는 Commit 전 검증 기록이며 최종 Commit/Push SHA·원격 일치·Working Tree 결과는 작업 최종 응답에 기록한다. main 병합·배포·최종 디자인 승인 완료를 뜻하지 않는다. + +## 1. 검사 기준과 범위 + +| 항목 | 확인값 | +|---|---| +| 작업 경로 | `D:\디자인시스템\HMDesign-OrgChart-Recovered` | +| 브랜치 | `feature/orgchart-package` | +| 검사 기준 HEAD / 시작 원격 Feature | `dcfa4e0895eecc97ce516cbf3d4424bbaadfddeb` | +| 시작 원격 main | `c45d63a03b84eaac43f01bd400a16488a80e4229` | +| origin | `https://gitea.hmac.kr/b25027/HMDesign.git` | +| 작성자 | `b25027 ` | +| 시작 변경 | P11 정상 산출물 26개, staged 0. [P11 보고서](implementation-report-p11.md)의 목록과 일치 | +| P11.1 추가 변경 | 사용 가이드의 문서 오류 보완 및 이 보고서 1개. 제품·공개 타입·Story 소스 변경 0 | +| 최종 Stage 후보 | P11 26개 + 이 보고서 = 27개. 임시 로그·캡처·dist·node_modules는 Git ignored이며 제외 | + +이번 작업의 명시적 경로·Feature Commit/Push 지시를 공통 지침의 공식 경로/main 지시보다 우선했다. main Checkout/Merge/Push, PR, Registry Publish, Docker, 배포, 다른 Worktree 정리, Reset/Rebase/Force Push를 수행하지 않는다. + +## 2. 정합성 결과표 + +| 검사 대상 | 문서 | 실제 코드 | Storybook | 결과 | +|---|---|---|---|---| +| Core 공개 API | 가이드 import·옵션·Handle 대조 | manifest/공개 export/구현/`.d.ts` 확인 | Full/Expanded 렌더·대표 동작 | PASS | +| React Wrapper | 기본/HM Entry·Props·Adapter 대조 | optional peer, 타입 상속·설정 전달 확인 | Full/Expanded React 19 렌더 | PASS | +| Chart Only | 기본 fit/direct·Fixed 거부·UI 제외 대조 | Core 기본 분기·viewport·wrapper 전달 확인 | 별도 React/Vanilla 화면 | PASS | +| 외부 CSS 테마 | 이름·적용 범위·우선순위 수정 | CSS 선언/사용 추출, 예제 입력 39개 연결 | React/Vanilla 테마 대표 화면 | PASS | +| HMDesign Dialog | 별도 HM Theme/Portal 범위 대조 | `/hm-dialog` 및 HMEmployeeDialog 확인 | Expanded Dialog·ESC·focus 복귀 | PASS | +| 회사별 컬러 | 7개 회사색/secondary 입력 대조 | TS 프리셋과 CSS fallback 일치 | Expanded 실제 computed 7색 | PASS | +| Fixed 디자인 | 6개 부서색·SVG/PNG 한계 대조 | 기존 자산·구성 보존 | Full/Department/Overview | PASS | +| AI 생성 정책 | 사전 선택·가변 구조·원본 보존 | enum/parentId 트리·회사 경계 대조 | 기존 Story 비소급 범위 확인 | PASS(정적 정책) | +| Guard 토큰 | P10 원본/P11 현재 결과 구분 | 실제 재검사 FAIL 0, 규칙 비활성화 없음 | 실제 theme 렌더는 별도 증거 | PASS(정적), Runtime NOT VERIFIED | + +Core 공개 export는 `createOrgChart`, DataSource/Validator/집계/이동 함수와 독립 presenter이며 새 API를 만들지 않았다. `layoutMode`는 Fixed/Auto, `autoLayout.type`은 TB/LR/Hybrid다. Auto의 Zoom/Pan/Fit·접기·편집·직원 팝업은 기존 구현을 사용하며 CSV/XLSX parser, 공개 collapse setter, React imperative ref, 서버 저장 API는 없다. JSON 최소/깊은 예제 및 필수·nullable 필드는 현재 `data/types.ts`·Validator와 일치한다. + +React 기본 Entry는 Core lifecycle·Props만 사용하고 HM UI를 import하지 않는다. `@hm-design/ui` peer는 optional, React peer는 `^19.0.0`이며 현재 19.3.0에서 렌더링했다. 선택 Entry `/hm-dialog`는 기존 HM UI Dialog를 제공하며 `employeePopup="hm-dialog"`를 명시해야 한다. `employeePopupAdapter`도 모드를 함께 지정한다. Fixed는 기존 Core 팝업/`onEmployeeClick` 경계를 유지한다. HM UI 없는 fresh install은 [기존 최소 설치 증거](minimal-install-report-2026-10-13.md)를 재사용했고 이번에 다시 설치하지 않았다. + +## 3. 공개 화면과 Chart Only + +공개 index에는 `OrgChart/Overview`, `Department Colors`, `Full Composition`, `Expanded OrgChart` 4개가 그대로 있다. ID는 각각 `orgchart--logo-personnel-summary`, `orgchart--department-colors`, `orgchart--full-composition`, `orgchart--expanded-org-chart`다. OrgChart는 최상위 그룹이며 Components/Patterns와 같은 Depth다. 신규 Story 등록·기존 ID/URL 교체 없음. + +| 화면 | 실제 확인 | +|---|---| +| Overview | 로고 legend 7개, 중복 내부 h2 없음, 동일 `data-p1.json` Core 집계/표·하단 관계사 총계 41명. 하단 집계에 반복된 회사 로고를 legend 수에 더하지 않음 | +| Department Colors | 대표자·그룹장·직원, 6개 서로 다른 기존 gradient, 모든 직원 이름 `홍 길 동`, 파견/휴직 preview. 기존 카드·Typography 캡처 확인, Cell Border Only 미적용 | +| Full Composition | 대표자·화살촉 1개·협업 PNG 화살표 5개·중앙 구성·상단 제목/인원표·하단 파견/휴직·관계사 현황·팝업 X/카드 focus 복귀 확인. 390px 반응형/내부 스크롤 판정은 [P10.10](visual-review-p10-10.md) 재사용, 새 수정 사유 없음 | +| Expanded OrgChart | 최초 7회사×20=140명, 회사 선택, 한맥기술 단독 20/100/300명 실제 카드 수, 300명 TB/LR/Hybrid 유지, 7회사 기본 computed 색, 기본 D&D OFF, ON의 직원/그룹 소속 이동(키보드 편집 경로)·300명 보존, OFF Pan, HM Dialog ESC/focus 복귀, 기존 Zoom/Fit 컨트롤 유지 | +| React/Vanilla Chart Only | 기본 흰 캔버스, 외부 heading/selection/toolbar 없음, 전체 실제 Bounds 초기 Fit/가로·세로 중앙, 직접 wheel·커서 기준 anchor, 직원 Core 팝업, 접기/재확장 확인. Fit은 빈 engine wrapper 여백 대신 실제 조직/조직 연결 path bounds로 측정 | + +Chart Only는 `layoutMode='auto'` + `presentationMode='chart-only'`, 기본 `initialView='fit'` / `wheelZoom='direct'`다. 기본 Expanded에는 이 모드를 전달하지 않아 기존 제목·요약·툴바 및 modifier wheel 정책을 유지한다. Fixed Chart Only는 Core `TypeError` 경로로 명시적 미지원이다. React와 Vanilla는 같은 Core 설정을 사용하며 호스트가 캔버스 크기를 제공한다. P11 React fixture의 host 높이 미지정 fallback 320px와 Vanilla의 850px 컨테이너를 동일 크기라고 보고하지 않는다. + +신규 대표 검사: 최초 16개 중 10 PASS/6 FAIL. 소스 대조 후 실패 6개만 보정해 6/6 PASS + 재검사 오류 0. 최초 결과를 보존한다. 원인은 logo legend와 하단 반복 로고 합산, 3종류 composition region의 6개 slot을 3개 element로 예상, Fit의 engine wrapper 여백 포함, 상태 gradient 카드에 일반 active 카드 backgroundColor 기대였다. 제품 버그나 신규 제품 변경은 없었다. + +현재 캡처를 확인한 공개 Story 4개는 렌더 PASS다. 기존 Story 전체의 변경 전후 픽셀 동일성이나 사용자의 최종 디자인 승인을 뜻하지 않는다. 순수 pointer D&D의 모든 조합은 이번에 재실행하지 않았으며 키보드 편집은 실제 ON의 공통 validation/engine drop/move 경로를 사용했다. + +## 4. CSS 변수·연결·기본값 + +실제 PostCSS AST에서 532개 변수 선언/사용 위치를 추출했다. P11 tracked CSS 변경에 나타난 66개 이름과 신규 `color-presets.css`를 합치면 116개이며, 관련 기존 공통 컬러 입력 12개를 더해 부록에 128개 이름을 기록했다. **128개가 모두 신규 공개 변수라는 뜻은 아니다.** 공개 입력, 기존 입력, 내부 resolved/current alias·geometry를 구분했다. 예제 CSS 3개의 서로 다른 공개 입력 39개는 모두 실제 `var(...)` 사용 경로가 있으며 가상의 이름은 없다. + +- 공개 입력은 상속을 위해 내부에 고정 선언하지 않는다. 기본값은 해당 scope의 소비 표현식 fallback에 있다. 내부 선언·default·selector/적용 속성은 부록에 기록한다. React/Vanilla는 같은 Core CSS를 사용하므로 모든 행의 CSS 적용 경로가 공통이다. +- 회사 ID primary 7개 기본값은 변경하지 않았다. 보조 입력은 6회사 secondary-1과 ptc secondary-2만 연결되고 baron 보조 입력은 없다. 보조 alias를 주요 카드/제목의 추가 컬러 역할로 오인하지 않는다. +- Fixed 6부서의 gradient/Border/default는 기존 TS 프리셋과 일치한다. 공통 그룹 배경은 회사/부서 제목 배경보다 우선한다. 부서별 배경·fallback 텍스트와 공통 Border·compact HTML 이름의 우선순위 차이를 문서에 보완했다. +- 파견/휴직 Fixed 요약 gradient·Border·텍스트와 Auto 상태 카드 gradient·Border·텍스트를 구분한다. Fixed `*-gradient`는 `background-image`에 연결하므로 CSS image 표현식이 필요하다. 단색 CSS color를 그 입력에 넣는다고 일반 배경값과 동일하게 적용되지 않는다. +- Core dialog 배경/글자/Border, 닫기 Hover/Focus는 Core 내부 입력이다. `/hm-dialog`는 Shadow 밖 HM Theme/Portal이며 자동 동기화하지 않는다. + +`stylesheetUrls.core`는 기본 CSS 전체를 포함한 URL 하나다. 기본 CSS `@import` 뒤에 `:host` override를 두며 engine→core 순서로 Shadow link를 로드한다. Vite는 공개 CSS/외부 CSS의 `?url`, HTTP ESM 소비자는 실제 서빙 URL을 사용한다. 변수 전용 host CSS는 document에 연결하고 class로 상속한다. 일반 host selector가 Shadow 내부를 통과하지 않는다. + +Shadow `:host`의 직접 지정값이 조상 상속보다 우선하며 소비자가 같은 입력을 inline 지정하면 해당 inline이 우선한다. P11 Core의 회사·부서 기본색 inline `setProperty`는 제거됐고 dataset에 따른 CSS 해석으로 바뀌었다. `setProperty` 자체를 새 테마 API로 문서화하거나 Guard 자동 승인에 사용하지 않았다. 호스트 class 제거의 기본값 복원과 배율 보존, React/Vanilla 외부 테마·팝업·Hover/Focus·4폭은 P11 104/104 PASS 증거를 재사용했다. 이번 대표 테마 검사는 두 runtime의 회사 제목·active 카드·연결선·상태 gradient·Core 팝업을 추가 확인했다. + +Fixed SVG/PNG 내부 색상은 CSS 변수로 바뀌지 않는다. 실제 inline SVG overlay 연결선은 CSS로 변경되지만 이미지 화살표·로고·배경·카드 SVG 불투명 영역은 그대로다. 외부 Theme 기능은 삼안/장헌산업 흰 제목 대비나 보안 Advisory를 자동 해결하지 않는다. + +## 5. 문서 보완과 예제 검증 + +| 발견된 문서 불일치 | 최소 수정 | +|---|---| +| 하단 제한에 잔여 Guard FAIL 5건이 현재 상태처럼 기재 | P10 당시 기록과 P11 실제 FAIL 0건을 분리, 원본 보고서 링크 보존 | +| 외부 CSS 테마 표준화가 현재 미포함처럼 기재 | P11 지원과 Fixed 이미지/Chart Only 미지원을 분리 | +| Fixed 카드 변수 우선순위 전체 일반화 | 부서 배경/fallback 텍스트, 공통 Border/accent, SVG/compact HTML 이름을 구분 | +| 모든 gradient 입력에 background 표현식을 허용한 설명 | Fixed `background-image`용 CSS image 표현식 필요 명시 | +| company secondary의 임의 n처럼 읽히는 표 | 실제 ID/n 조합 명시 | +| HTTP Vanilla snippet의 Element/null 컨테이너 | `getElementById` + 존재 확인으로 HTMLElement 계약 명시 | + +가이드의 TS/TSX 13개와 HTTP Vanilla JS 1개를 추출해 strict·ES2022·Bundler·react-jsx·skipLibCheck=false로 **14/14 타입 검사 PASS**. 공개 package `.d.ts`와 Vite client CSS URL 타입을 사용했다. 짧은 theme 예제의 앞 문맥 `dataSource`만 harness 선언으로 제공하고, HTTP ESM 경로는 실제 공개 Core 타입으로 해석했다. API·Props를 임의 생성하거나 잘못된 옵션을 mock으로 감추지 않았다. P11 TSX fixture도 함께 검사했다. + +전체 문서 예제의 개별 browser 실행은 NOT VERIFIED다. HMChart/CustomChart 자체의 browser 시연 대신 실제 HM Entry Story 및 Core/React fixture를 확인했다. P10.7·UsageGuide 당시 검증 원본을 지우거나 후속 결과로 덮어쓰지 않았다. + +AI 정책의 사전 질문, 이미 명시된 방식 재질문 금지, Chart Only 최소 UI, Full Page HM UI 우선 재사용, 정확한 계층/인원/제공 데이터 보존, 기존 화면 비소급은 현재 API와 일치한다. parentId 기반 가변 계층과 division/office/div/team/cell enum을 소스에서 확인했으며 유형별 새 디자인 자동 선택으로 설명하지 않는다. Cell Border Only 제외 및 Fixed Chart Only 제약을 보존했다. 새 조직도 생성 요청이 아니므로 이번 감사에 생성 방식 질문을 추가하지 않았다. + +## 6. 검증·Guard·보존 증거 + +| 확인 | 결과 / 근거 | +|---|---| +| Core/React 변경 범위 Typecheck | 신규 실행 PASS (`typecheck:org-chart`, `typecheck:org-chart-react`) | +| Core/React/소비 예제 Build | 신규 실행 PASS, CSS URL production assets 생성. 기존 host-context/minifier·큰 chunk 경고 유지 | +| 문서 예제/fixture Typecheck | 신규 실행 PASS, `test-results/org-chart-p11-1/tsconfig.json` | +| Storybook 4개 화면 렌더 | 신규 4/4 PASS, 오류 0. 공개 index/데이터/대표 동작은 신규 browser/recheck 결과 | +| React/Vanilla Chart Only·Theme 대표 | 신규 browser + 실패 한정 recheck PASS, console/page 오류 0 | +| 실제 Guard | 신규 재검사 `status PASS`, `staticStatus PASS`, issues 0 / FAIL 0 | +| Guard CSS phases | STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED. 결합 styleVerification 미생성 유지 | +| Guard 회귀 | **기존 P11 94/94 및 CSS 회귀 5/5 PASS 재사용**. 검사기 P11.1 변경 0이라 반복하지 않음. 실제 Guard 결과와 별개 | +| 다른 HM 패키지 Guard 영향 | 기존 P11 실제 비교: 이슈 67개 동일, FAIL 0, 원본 NOT VERIFIED 보존. 변경 없는 동일 분석기/제품 소스 범위 재사용 | +| 기본 디자인 6화면 | P11 before/after PNG 12파일을 읽어 기록 SHA-256을 재확인, 6/6 동일. 신규 baseline 캡처·승인 아님 | +| Full 390px/4폭/P11 HTTP Vanilla | P10.10 Full 390px·P11 4폭 104개·순수 HTTP Core 8개 PASS 기존 유효 결과 재사용 | +| Markdown 링크/경로·diff | 변경 문서 4개 로컬 링크/앵커 236개 PASS, `git diff --check` PASS. Staged 검사는 Commit 직전 수행 | + +Guard는 다음 한 번의 실제 검사로 확인했다. 규칙 우회·예외 목록·미선언 token 자동 승인·Runtime PASS 승격 없음. + +```powershell +node scripts/ai-generation-guard.mjs --target . --scope PARTIAL --profile STORYBOOK --files packages/org-chart/src/core/department-colors.ts,packages/org-chart/src/core/style-presets.ts,packages/org-chart/src/core/expanded-content.ts --json +``` + +증거는 ignored 로컬 폴더에 보존하며 새 Clone에 포함되지 않는다. `verify-p11.mjs`의 기본값 비교는 변경 전 산출물에서 `--baseline`으로 생성한 기록을 필요로 한다. 새 Clone에서 baseline이 없는 실행을 역사적 기본 디자인 보존 검사로 해석하지 않는다: + +- [현재 검사 원본](../../test-results/org-chart-p11-1/browser-result.json), [실패 한정 재검사](../../test-results/org-chart-p11-1/recheck-result.json), [실제 Guard](../../test-results/org-chart-p11-1/guard-actual.json), [CSS 전체 추출](../../test-results/org-chart-p11-1/css-inventory.json), [예제 manifest](../../test-results/org-chart-p11-1/snippet-manifest.json). +- [Chart Only React 최초 화면](../../test-results/org-chart-p11-1/initial-react.png), [Vanilla 최초 화면](../../test-results/org-chart-p11-1/initial-vanilla.png), [HM Dialog](../../test-results/org-chart-p11-1/hm-dialog.png). +- [기본 6화면 해시](../../test-results/org-chart-p11/default-image-comparison.json), [P11 공용 비교](../../test-results/org-chart-p11/guard-other-comparison.json), [P11 구현·검증](implementation-report-p11.md). + +실행 URL: [Storybook](http://127.0.0.1:6034/), [React Chart Only](http://127.0.0.1:6026/p11.html?runtime=react&mode=auto), [Vanilla Chart Only](http://127.0.0.1:6026/p11.html?runtime=vanilla&mode=auto), [React 외부 테마](http://127.0.0.1:6026/p11.html?runtime=react&mode=auto&theme=1&statuses=1). 서버는 이 작업에서 시작했고 검토용으로 유지했다. sandbox 로컬 연결 제한을 해소하기 위해 서버/Chrome을 같은 실행 환경에서 실행했다. 연결 실패를 제품 오류로 판정하지 않았다. + +## 7. 미검증과 Commit 판정 + +**Commit 가능: PASS.** 공개 API 일치·테마/Chart Only/4 Story 유지·필수 타입/빌드·실제 Guard FAIL 0·기본 6화면 보존·정상 변경 27개·작성자 일치를 확인했다. 관련 파일만 Stage하고 Staged Diff·원격 Feature/main SHA를 재확인한 뒤 일반 Feature Fast-forward Push한다. 원격이 기준에서 바뀌면 중단하며 main은 건드리지 않는다. + +NOT VERIFIED: 모든 pointer D&D 조합·회사×100/300명×모든 layout의 전체 회귀, HM Dialog 별도 팔레트 변경 비교, HMChart/CustomChart 자체 browser 시연, 전체 Story before/after 픽셀 비교, Firefox/Safari/실제 모바일/touch/스크린리더, 전체 성능·SSR·새 fresh install·신규 npm audit·배포. 사용자 최종 디자인 승인 PENDING. Core 86개·Chart Only 51개 및 직접 영향 없는 P7~P10 전체 회귀는 반복하지 않았다. + +기존 개발 High Advisory 7건, 삼안 약 2.64:1·장헌산업 약 4.29:1 제목 대비 미해결 기록을 보존한다. P11 Guard 개선/테마 기능을 보안 해결·WCAG 자동 충족으로 표현하지 않는다. Fixed Chart Only와 Fixed 이미지 내부 테마화는 미검증 후보가 아닌 미지원이다. + +## 8. 작업 시간 + +- Started At: 2026-10-13 22:26:04 KST +- Estimated Duration: 최초 약 60분 (분석 20 / 검증 30 / 보고·Git 10) +- 검증 완료·최종 Git 완료의 Completed At / Actual Duration / Variance는 작업 최종 응답에 기록한다. 이 파일은 Commit 전 기록이므로 미래 완료 시간을 추정하지 않는다. + +## 9. P11 변수 소스 대조 부록 + +각 이름의 실제 선언 또는 첫 사용 표현식·selector/속성을 기록한다. 공개 입력은 내부 선언 없음(상속용)이며 표현식의 fallback이 기본값이다. 공개 행은 호스트 상속 또는 Shadow 외부 CSS override 가능, 내부 행은 계산 구현이며 직접 override를 권장하지 않는다. 모든 행은 같은 Core CSS를 쓰는 React/Vanilla 공통 경로다. 여러 scope에서 fallback이 다르면 해당 소스 전체가 최종 기준이며 첫 표현식만 유일한 기본값으로 해석하지 않는다. 카드 SVG/PNG 영역·HM Dialog는 앞의 지원 경계를 따른다. + + + +| 실제 이름 | 구분 | 선언/사용 위치 | scope / 적용 속성 | 선언값 또는 소비 표현식(default fallback 포함) | +|---|---|---|---|---| +| `--hm-orgchart-accent` | 내부/기존 선언 | [color-presets.css:11](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-accent` | `var(--hm-orgchart-current-border)` | +| `--hm-orgchart-accent-color` | 공개 입력(상속) | [design-tokens.css:17](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-accent` | `var(--hm-orgchart-accent-color, #eef2fa)` | +| `--hm-orgchart-action-primary` | 공개 입력(상속) | [design-tokens.css:19](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-action-primary` | `var(--hm-orgchart-action-primary, #45639f)` | +| `--hm-orgchart-bg-brand` | 공개 입력(상속) | [design-tokens.css:11](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-bg-brand` | `var(--hm-orgchart-bg-brand, linear-gradient(180deg, #45639f 0%, #809dd7 100%))` | +| `--hm-orgchart-bg-content` | 공개 입력(상속) | [design-tokens.css:9](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-bg-content` | `var(--hm-orgchart-bg-content, #f4f7fb)` | +| `--hm-orgchart-bg-subtle` | 공개 입력(상속) | [design-tokens.css:10](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-bg-subtle` | `var(--hm-orgchart-bg-subtle, #e9edf3)` | +| `--hm-orgchart-bg-surface` | 공개 입력(상속) | [design-tokens.css:8](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-bg-surface` | `var(--hm-orgchart-bg-surface, #ffffff)` | +| `--hm-orgchart-border` | 공개 입력(상속) | [color-presets.css:9](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-business-management-border, var(--hm-orgchart-border, #194d82))` | +| `--hm-orgchart-business-management-border` | 공개 입력(상속) | [color-presets.css:9](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-business-management-border, var(--hm-orgchart-border, #194d82))` | +| `--hm-orgchart-business-management-card-background` | 공개 입력(상속) | [color-presets.css:6](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-business-management-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` | +| `--hm-orgchart-business-management-card-color` | 공개 입력(상속) | [color-presets.css:7](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-business-management-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` | +| `--hm-orgchart-business-management-department-color` | 공개 입력(상속) | [color-presets.css:8](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-business-management-department-color, var(--hm-orgchart-department-color, #ffffff))` | +| `--hm-orgchart-business-management-gradient` | 공개 입력(상속) | [color-presets.css:10](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-business-management-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #194d82 0%, #1d395c 100%)))` | +| `--hm-orgchart-business-planning-border` | 공개 입력(상속) | [color-presets.css:19](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="1"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-business-planning-border, var(--hm-orgchart-border, #2164a8))` | +| `--hm-orgchart-business-planning-card-background` | 공개 입력(상속) | [color-presets.css:16](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="1"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-business-planning-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` | +| `--hm-orgchart-business-planning-card-color` | 공개 입력(상속) | [color-presets.css:17](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="1"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-business-planning-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` | +| `--hm-orgchart-business-planning-department-color` | 공개 입력(상속) | [color-presets.css:18](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="1"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-business-planning-department-color, var(--hm-orgchart-department-color, #ffffff))` | +| `--hm-orgchart-business-planning-gradient` | 공개 입력(상속) | [color-presets.css:20](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="1"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-business-planning-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #2164a8 0%, #254975 100%)))` | +| `--hm-orgchart-card-accent` | 공개 입력(상속) | [employee-card.css:40](../../packages/org-chart/src/styles/employee-card.css) | `.hm-orgchart-employee[data-card-kind="leader"], .hm-orgchart-employee[data-card-kind="topLeader"]` → `border-top` | `var(--hm-orgchart-resolved-border-top-width) solid var(--hm-orgchart-card-accent, var(--hm-orgchart-current-border, var(--hm-orgchart-resolved-action-primary)))` | +| `--hm-orgchart-card-background` | 공개 입력(상속) | [color-presets.css:6](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-business-management-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` | +| `--hm-orgchart-card-border` | 공개 입력(상속) | [employee-card.css:14](../../packages/org-chart/src/styles/employee-card.css) | `.hm-orgchart-employee[data-card-kind]` → `border` | `var(--hm-orgchart-resolved-border-width) solid var(--hm-orgchart-card-border, var(--hm-orgchart-current-border, var(--hm-orgchart-resolved-stroke-default)))` | +| `--hm-orgchart-card-color` | 공개 입력(상속) | [color-presets.css:7](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-business-management-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` | +| `--hm-orgchart-card-focus-color` | 공개 입력(상속) | [employee-info-dialog.css:10](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-organization-link:focus-visible, .hm-orgchart-employee[role="button"]:focus-visible` → `outline` | `2px solid var(--hm-orgchart-card-focus-color, var(--hm-orgchart-resolved-stroke-input-focus))` | +| `--hm-orgchart-card-hover-background` | 공개 입력(상속) | [org-chart-expanded.css:117](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-presentation="expanded"]:hover` → `background` | `var(--hm-orgchart-card-hover-background, var(--hm-orgchart-resolved-status-card-background, var(--hm-orgchart-resolved-role-card-background)))` | +| `--hm-orgchart-card-position-color` | 공개 입력(상속) | [employee-card.css:242](../../packages/org-chart/src/styles/employee-card.css) | `.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]) .hm-orgchart-person-role` → `color` | `var(--hm-orgchart-card-position-color, var(--hm-orgchart-person-position-color))` | +| `--hm-orgchart-company-baron` | 공개 입력(상속) | [color-presets.css:103](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="baron"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-baron, #0C367F)` | +| `--hm-orgchart-company-halla` | 공개 입력(상속) | [color-presets.css:97](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="halla"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-halla, #0068B7)` | +| `--hm-orgchart-company-halla-secondary-1` | 공개 입력(상속) | [color-presets.css:99](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="halla"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-halla-secondary-1, #231815)` | +| `--hm-orgchart-company-hanmac` | 공개 입력(상속) | [color-presets.css:66](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="hanmac"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-hanmac, #CB2E28)` | +| `--hm-orgchart-company-hanmac-secondary-1` | 공개 입력(상속) | [color-presets.css:68](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="hanmac"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-hanmac-secondary-1, #1D4289)` | +| `--hm-orgchart-company-jangheon` | 공개 입력(상속) | [color-presets.css:84](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="jangheon"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-jangheon, #1D418D)` | +| `--hm-orgchart-company-jangheon-industry` | 공개 입력(상속) | [color-presets.css:78](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="jangheon-industry"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-jangheon-industry, #4580A6)` | +| `--hm-orgchart-company-jangheon-industry-secondary-1` | 공개 입력(상속) | [color-presets.css:80](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="jangheon-industry"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-jangheon-industry-secondary-1, #92BED4)` | +| `--hm-orgchart-company-jangheon-secondary-1` | 공개 입력(상속) | [color-presets.css:86](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="jangheon"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-jangheon-secondary-1, #AE232A)` | +| `--hm-orgchart-company-ptc` | 공개 입력(상속) | [color-presets.css:90](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="ptc"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-ptc, #84338E)` | +| `--hm-orgchart-company-ptc-secondary-1` | 공개 입력(상속) | [color-presets.css:92](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="ptc"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-ptc-secondary-1, #F7AF00)` | +| `--hm-orgchart-company-ptc-secondary-2` | 공개 입력(상속) | [color-presets.css:93](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="ptc"]` → `--hm-orgchart-expanded-secondary-2` | `var(--hm-orgchart-company-ptc-secondary-2, #0066B5)` | +| `--hm-orgchart-company-saman` | 공개 입력(상속) | [color-presets.css:72](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="saman"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-saman, #F08300)` | +| `--hm-orgchart-company-saman-secondary-1` | 공개 입력(상속) | [color-presets.css:74](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="saman"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-saman-secondary-1, #6D6C6C)` | +| `--hm-orgchart-current-border` | 내부/기존 선언 | [color-presets.css:9](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-business-management-border, var(--hm-orgchart-border, #194d82))` | +| `--hm-orgchart-current-card-background` | 내부/기존 선언 | [color-presets.css:6](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-business-management-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` | +| `--hm-orgchart-current-card-color` | 내부/기존 선언 | [color-presets.css:7](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-business-management-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` | +| `--hm-orgchart-current-department-color` | 내부/기존 선언 | [color-presets.css:8](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-business-management-department-color, var(--hm-orgchart-department-color, #ffffff))` | +| `--hm-orgchart-current-gradient` | 내부/기존 선언 | [color-presets.css:10](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-business-management-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #194d82 0%, #1d395c 100%)))` | +| `--hm-orgchart-department-color` | 공개 입력(상속) | [color-presets.css:8](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-business-management-department-color, var(--hm-orgchart-department-color, #ffffff))` | +| `--hm-orgchart-design-planning-border` | 공개 입력(상속) | [color-presets.css:49](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="4"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-design-planning-border, var(--hm-orgchart-border, #09a7a2))` | +| `--hm-orgchart-design-planning-card-background` | 공개 입력(상속) | [color-presets.css:46](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="4"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-design-planning-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` | +| `--hm-orgchart-design-planning-card-color` | 공개 입력(상속) | [color-presets.css:47](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="4"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-design-planning-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` | +| `--hm-orgchart-design-planning-department-color` | 공개 입력(상속) | [color-presets.css:48](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="4"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-design-planning-department-color, var(--hm-orgchart-department-color, #ffffff))` | +| `--hm-orgchart-design-planning-gradient` | 공개 입력(상속) | [color-presets.css:50](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="4"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-design-planning-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #09a7a2 0%, #00847f 100%)))` | +| `--hm-orgchart-dialog-background` | 공개 입력(상속) | [employee-info-dialog.css:21](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog` → `background` | `var(--hm-orgchart-dialog-background, var(--hm-orgchart-resolved-bg-surface))` | +| `--hm-orgchart-dialog-border` | 공개 입력(상속) | [employee-info-dialog.css:19](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog` → `border` | `var(--hm-orgchart-resolved-border-width) solid var(--hm-orgchart-dialog-border, var(--hm-orgchart-resolved-stroke-default))` | +| `--hm-orgchart-dialog-close-color` | 공개 입력(상속) | [employee-info-dialog.css:38](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog-close` → `--hm-orgchart-dialog-close-ink` | `var(--hm-orgchart-dialog-close-color, var(--foreground, var(--hm-sem-fg-primary, #0a221d)))` | +| `--hm-orgchart-dialog-close-hover` | 내부/기존 선언 | [employee-info-dialog.css:39](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog-close` → `--hm-orgchart-dialog-close-hover` | `var(--hm-orgchart-dialog-close-hover-background, var(--muted, var(--hm-sem-bg-subtle, #f4f1ea)))` | +| `--hm-orgchart-dialog-close-hover-background` | 공개 입력(상속) | [employee-info-dialog.css:39](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog-close` → `--hm-orgchart-dialog-close-hover` | `var(--hm-orgchart-dialog-close-hover-background, var(--muted, var(--hm-sem-bg-subtle, #f4f1ea)))` | +| `--hm-orgchart-dialog-close-ink` | 내부/기존 선언 | [employee-info-dialog.css:38](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog-close` → `--hm-orgchart-dialog-close-ink` | `var(--hm-orgchart-dialog-close-color, var(--foreground, var(--hm-sem-fg-primary, #0a221d)))` | +| `--hm-orgchart-dialog-close-ring` | 내부/기존 선언 | [employee-info-dialog.css:40](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog-close` → `--hm-orgchart-dialog-close-ring` | `var(--hm-orgchart-dialog-focus-color, var(--ring, var(--hm-sem-stroke-input-focus, #266651)))` | +| `--hm-orgchart-dialog-color` | 공개 입력(상속) | [employee-info-dialog.css:22](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog` → `color` | `var(--hm-orgchart-dialog-color, var(--hm-orgchart-resolved-fg-primary))` | +| `--hm-orgchart-dialog-focus-color` | 공개 입력(상속) | [employee-info-dialog.css:40](../../packages/org-chart/src/styles/employee-info-dialog.css) | `.hm-orgchart-employee-dialog-close` → `--hm-orgchart-dialog-close-ring` | `var(--hm-orgchart-dialog-focus-color, var(--ring, var(--hm-sem-stroke-input-focus, #266651)))` | +| `--hm-orgchart-dispatched-card-border` | 공개 입력(상속) | [org-chart-expanded.css:107](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="dispatched"]` → `border-color` | `var(--hm-orgchart-dispatched-card-border, var(--hm-orgchart-card-border, var(--hm-orgchart-resolved-stroke-default)))` | +| `--hm-orgchart-dispatched-card-color` | 공개 입력(상속) | [org-chart-expanded.css:108](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="dispatched"]` → `color` | `var(--hm-orgchart-dispatched-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` | +| `--hm-orgchart-dispatched-card-gradient` | 공개 입력(상속) | [org-chart-expanded.css:105](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-background` | `var(--hm-orgchart-dispatched-card-gradient, var(--hm-orgchart-resolved-role-card-background))` | +| `--hm-orgchart-dispatched-title-background` | 공개 입력(상속) | [summary-sections.css:142](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-title-background` | `var(--hm-orgchart-dispatched-title-background, var(--hm-orgchart-status-title-background, linear-gradient(180deg, #87909c 0%, #68727e 100%)))` | +| `--hm-orgchart-expanded-card-accent-width` | 내부/기존 선언 | [org-chart-expanded.css:154](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded` → `--hm-orgchart-expanded-card-accent-width` | `3px` | +| `--hm-orgchart-expanded-card-border-width` | 내부/기존 선언 | [org-chart-expanded.css:153](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded` → `--hm-orgchart-expanded-card-border-width` | `1px` | +| `--hm-orgchart-expanded-card-primary` | 내부/기존 선언 | [color-presets.css:67](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="hanmac"]` → `--hm-orgchart-expanded-card-primary` | `var(--hm-orgchart-company-hanmac, #CB2E28)` | +| `--hm-orgchart-expanded-line-color` | 공개 입력(상속) | [org-chart-expanded.css:10](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded` → `--hm-orgchart-resolved-expanded-line-color` | `var(--hm-orgchart-expanded-line-color, #738399)` | +| `--hm-orgchart-expanded-line-width` | 내부/기존 선언 | [org-chart-expanded.css:8](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded` → `--hm-orgchart-expanded-line-width` | `var(--hm-orgchart-resolved-border-width)` | +| `--hm-orgchart-expanded-primary` | 내부/기존 선언 | [color-presets.css:66](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="hanmac"]` → `--hm-orgchart-expanded-primary` | `var(--hm-orgchart-company-hanmac, #CB2E28)` | +| `--hm-orgchart-expanded-secondary-1` | 내부/기존 선언 | [color-presets.css:68](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="hanmac"]` → `--hm-orgchart-expanded-secondary-1` | `var(--hm-orgchart-company-hanmac-secondary-1, #1D4289)` | +| `--hm-orgchart-expanded-secondary-2` | 내부/기존 선언 | [color-presets.css:93](../../packages/org-chart/src/styles/color-presets.css) | `.hm-orgchart-expanded [data-company-id="ptc"]` → `--hm-orgchart-expanded-secondary-2` | `var(--hm-orgchart-company-ptc-secondary-2, #0066B5)` | +| `--hm-orgchart-fg-primary` | 공개 입력(상속) | [design-tokens.css:12](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-fg-primary` | `var(--hm-orgchart-fg-primary, #10243b)` | +| `--hm-orgchart-fg-secondary` | 공개 입력(상속) | [design-tokens.css:13](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-fg-secondary` | `var(--hm-orgchart-fg-secondary, #596575)` | +| `--hm-orgchart-fixed-line-color` | 공개 입력(상속) | [org-chart-readonly.css:204](../../packages/org-chart/src/styles/org-chart-readonly.css) | `.hm-orgchart-workdesk-overlay path[data-connector]` → `stroke` | `var(--hm-orgchart-fixed-line-color, #45639F)` | +| `--hm-orgchart-gradient` | 공개 입력(상속) | [color-presets.css:10](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-business-management-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #194d82 0%, #1d395c 100%)))` | +| `--hm-orgchart-group-background` | 내부/기존 선언 | [color-presets.css:12](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="0"]` → `--hm-orgchart-group-background` | `var(--hm-orgchart-current-gradient)` | +| `--hm-orgchart-group-border` | 공개 입력(상속) | [org-chart-content.css:73](../../packages/org-chart/src/styles/org-chart-content.css) | `.hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-organization` → `border-top` | `var(--hm-orgchart-resolved-border-top-width) solid var(--hm-orgchart-group-border, var(--hm-orgchart-current-border))` | +| `--hm-orgchart-group-title-background` | 공개 입력(상속) | [org-chart-content.css:100](../../packages/org-chart/src/styles/org-chart-content.css) | `.hm-orgchart-engine.orgchart .node[data-header-role="organization"] .hm-orgchart-organization-name` → `background` | `var(--hm-orgchart-group-title-background, var(--hm-orgchart-group-background))` | +| `--hm-orgchart-group-title-color` | 공개 입력(상속) | [org-chart-content.css:95](../../packages/org-chart/src/styles/org-chart-content.css) | `.hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-organization-name` → `color` | `var(--hm-orgchart-group-title-color, var(--hm-orgchart-current-department-color))` | +| `--hm-orgchart-integrated-system-border` | 공개 입력(상속) | [color-presets.css:39](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="3"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-integrated-system-border, var(--hm-orgchart-border, #8c67b6))` | +| `--hm-orgchart-integrated-system-card-background` | 공개 입력(상속) | [color-presets.css:36](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="3"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-integrated-system-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` | +| `--hm-orgchart-integrated-system-card-color` | 공개 입력(상속) | [color-presets.css:37](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="3"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-integrated-system-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` | +| `--hm-orgchart-integrated-system-department-color` | 공개 입력(상속) | [color-presets.css:38](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="3"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-integrated-system-department-color, var(--hm-orgchart-department-color, #ffffff))` | +| `--hm-orgchart-integrated-system-gradient` | 공개 입력(상속) | [color-presets.css:40](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="3"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-integrated-system-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #8c67b6 0%, #5d3494 100%)))` | +| `--hm-orgchart-leader-card-background` | 공개 입력(상속) | [org-chart-expanded.css:99](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-kind="leader"]` → `background` | `var(--hm-orgchart-leader-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` | +| `--hm-orgchart-on-accent` | 공개 입력(상속) | [design-tokens.css:18](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-on-accent` | `var(--hm-orgchart-on-accent, #224387)` | +| `--hm-orgchart-on-action-primary` | 공개 입력(상속) | [design-tokens.css:20](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-on-action-primary` | `var(--hm-orgchart-on-action-primary, #ffffff)` | +| `--hm-orgchart-on-leave-card-border` | 공개 입력(상속) | [org-chart-expanded.css:113](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="onLeave"]` → `border-color` | `var(--hm-orgchart-on-leave-card-border, var(--hm-orgchart-card-border, var(--hm-orgchart-resolved-stroke-default)))` | +| `--hm-orgchart-on-leave-card-color` | 공개 입력(상속) | [org-chart-expanded.css:114](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="onLeave"]` → `color` | `var(--hm-orgchart-on-leave-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` | +| `--hm-orgchart-on-leave-card-gradient` | 공개 입력(상속) | [org-chart-expanded.css:111](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="onLeave"]` → `--hm-orgchart-resolved-status-card-background` | `var(--hm-orgchart-on-leave-card-gradient, var(--hm-orgchart-resolved-role-card-background))` | +| `--hm-orgchart-on-leave-title-background` | 공개 입력(상속) | [summary-sections.css:148](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="onLeave"]` → `--hm-orgchart-resolved-status-title-background` | `var(--hm-orgchart-on-leave-title-background, var(--hm-orgchart-status-title-background, linear-gradient(180deg, #87909c 0%, #68727e 100%)))` | +| `--hm-orgchart-person-position-color` | 내부/기존 선언 | [employee-card.css:206](../../packages/org-chart/src/styles/employee-card.css) | `.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"])` → `--hm-orgchart-person-position-color` | `#797979` | +| `--hm-orgchart-resolved-action-primary` | 내부/기존 선언 | [design-tokens.css:19](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-action-primary` | `var(--hm-orgchart-action-primary, #45639f)` | +| `--hm-orgchart-resolved-bg-surface` | 내부/기존 선언 | [design-tokens.css:8](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-bg-surface` | `var(--hm-orgchart-bg-surface, #ffffff)` | +| `--hm-orgchart-resolved-border-top-width` | 내부/기존 선언 | [design-tokens.css:35](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-border-top-width` | `var(--hm-orgchart-border-top-width, 3px)` | +| `--hm-orgchart-resolved-border-width` | 내부/기존 선언 | [design-tokens.css:34](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-border-width` | `var(--hm-orgchart-border-width, 1px)` | +| `--hm-orgchart-resolved-expanded-line-color` | 내부/기존 선언 | [org-chart-expanded.css:10](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded` → `--hm-orgchart-resolved-expanded-line-color` | `var(--hm-orgchart-expanded-line-color, #738399)` | +| `--hm-orgchart-resolved-fg-primary` | 내부/기존 선언 | [design-tokens.css:12](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-fg-primary` | `var(--hm-orgchart-fg-primary, #10243b)` | +| `--hm-orgchart-resolved-on-action-primary` | 내부/기존 선언 | [design-tokens.css:20](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-on-action-primary` | `var(--hm-orgchart-on-action-primary, #ffffff)` | +| `--hm-orgchart-resolved-role-card-background` | 내부/기존 선언 | [org-chart-expanded.css:119](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-employee[data-card-kind="employee"]` → `--hm-orgchart-resolved-role-card-background` | `var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface))` | +| `--hm-orgchart-resolved-status-card-background` | 내부/기존 선언 | [org-chart-expanded.css:105](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-background` | `var(--hm-orgchart-dispatched-card-gradient, var(--hm-orgchart-resolved-role-card-background))` | +| `--hm-orgchart-resolved-status-card-border` | 내부/기존 선언 | [summary-sections.css:144](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-border` | `var(--hm-orgchart-dispatched-card-border, var(--hm-orgchart-status-card-border, rgba(120, 130, 142, 0.35)))` | +| `--hm-orgchart-resolved-status-card-color` | 내부/기존 선언 | [summary-sections.css:145](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-color` | `var(--hm-orgchart-dispatched-card-color, var(--hm-orgchart-status-card-color, #3e4650))` | +| `--hm-orgchart-resolved-status-card-gradient` | 내부/기존 선언 | [summary-sections.css:143](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-gradient` | `var(--hm-orgchart-dispatched-card-gradient, var(--hm-orgchart-status-card-gradient, linear-gradient(180deg, #f4f5f6 0%, #dfe3e7 100%)))` | +| `--hm-orgchart-resolved-status-title-background` | 내부/기존 선언 | [summary-sections.css:142](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-title-background` | `var(--hm-orgchart-dispatched-title-background, var(--hm-orgchart-status-title-background, linear-gradient(180deg, #87909c 0%, #68727e 100%)))` | +| `--hm-orgchart-resolved-stroke-default` | 내부/기존 선언 | [design-tokens.css:14](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-stroke-default` | `var(--hm-orgchart-stroke-default, #bac7d7)` | +| `--hm-orgchart-resolved-stroke-input-focus` | 내부/기존 선언 | [design-tokens.css:16](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-stroke-input-focus` | `var(--hm-orgchart-stroke-input-focus, #45639f)` | +| `--hm-orgchart-solution-sales-border` | 공개 입력(상속) | [color-presets.css:59](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="5"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-solution-sales-border, var(--hm-orgchart-border, #5a963c))` | +| `--hm-orgchart-solution-sales-card-background` | 공개 입력(상속) | [color-presets.css:56](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="5"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-solution-sales-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` | +| `--hm-orgchart-solution-sales-card-color` | 공개 입력(상속) | [color-presets.css:57](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="5"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-solution-sales-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` | +| `--hm-orgchart-solution-sales-department-color` | 공개 입력(상속) | [color-presets.css:58](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="5"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-solution-sales-department-color, var(--hm-orgchart-department-color, #ffffff))` | +| `--hm-orgchart-solution-sales-gradient` | 공개 입력(상속) | [color-presets.css:60](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="5"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-solution-sales-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #5a963c 0%, #3f7629 100%)))` | +| `--hm-orgchart-status-card-border` | 공개 입력(상속) | [summary-sections.css:144](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-border` | `var(--hm-orgchart-dispatched-card-border, var(--hm-orgchart-status-card-border, rgba(120, 130, 142, 0.35)))` | +| `--hm-orgchart-status-card-color` | 공개 입력(상속) | [summary-sections.css:145](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-color` | `var(--hm-orgchart-dispatched-card-color, var(--hm-orgchart-status-card-color, #3e4650))` | +| `--hm-orgchart-status-card-gradient` | 공개 입력(상속) | [summary-sections.css:143](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-card-gradient` | `var(--hm-orgchart-dispatched-card-gradient, var(--hm-orgchart-status-card-gradient, linear-gradient(180deg, #f4f5f6 0%, #dfe3e7 100%)))` | +| `--hm-orgchart-status-position-color` | 공개 입력(상속) | [summary-sections.css:139](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group .hm-orgchart-employee[data-card-kind] .hm-orgchart-person-role` → `color` | `var(--hm-orgchart-status-position-color, #555f6a)` | +| `--hm-orgchart-status-title-background` | 공개 입력(상속) | [summary-sections.css:142](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group[data-work-status="dispatched"]` → `--hm-orgchart-resolved-status-title-background` | `var(--hm-orgchart-dispatched-title-background, var(--hm-orgchart-status-title-background, linear-gradient(180deg, #87909c 0%, #68727e 100%)))` | +| `--hm-orgchart-status-title-border` | 공개 입력(상속) | [summary-sections.css:129](../../packages/org-chart/src/styles/summary-sections.css) | `.hm-orgchart-status-group > .hm-orgchart-summary-title` → `border` | `1px solid var(--hm-orgchart-status-title-border, rgba(92, 101, 112, 0.52))` | +| `--hm-orgchart-status-title-color` | 공개 입력(상속) | [org-chart-readonly.css:275](../../packages/org-chart/src/styles/org-chart-readonly.css) | `.hm-orgchart-composition[data-display-mode="readOnly"] .hm-orgchart-status-group > .hm-orgchart-summary-title` → `color` | `var(--hm-orgchart-status-title-color, var(--hm-orgchart-resolved-on-action-primary))` | +| `--hm-orgchart-stroke-default` | 공개 입력(상속) | [design-tokens.css:14](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-stroke-default` | `var(--hm-orgchart-stroke-default, #bac7d7)` | +| `--hm-orgchart-stroke-input-default` | 공개 입력(상속) | [design-tokens.css:15](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-stroke-input-default` | `var(--hm-orgchart-stroke-input-default, #d3dce8)` | +| `--hm-orgchart-stroke-input-focus` | 공개 입력(상속) | [design-tokens.css:16](../../packages/org-chart/src/styles/design-tokens.css) | `.hm-orgchart-shell` → `--hm-orgchart-resolved-stroke-input-focus` | `var(--hm-orgchart-stroke-input-focus, #45639f)` | +| `--hm-orgchart-talent-growth-border` | 공개 입력(상속) | [color-presets.css:29](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="2"]` → `--hm-orgchart-current-border` | `var(--hm-orgchart-talent-growth-border, var(--hm-orgchart-border, #0f6ace))` | +| `--hm-orgchart-talent-growth-card-background` | 공개 입력(상속) | [color-presets.css:26](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="2"]` → `--hm-orgchart-current-card-background` | `var(--hm-orgchart-talent-growth-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` | +| `--hm-orgchart-talent-growth-card-color` | 공개 입력(상속) | [color-presets.css:27](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="2"]` → `--hm-orgchart-current-card-color` | `var(--hm-orgchart-talent-growth-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))` | +| `--hm-orgchart-talent-growth-department-color` | 공개 입력(상속) | [color-presets.css:28](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="2"]` → `--hm-orgchart-current-department-color` | `var(--hm-orgchart-talent-growth-department-color, var(--hm-orgchart-department-color, #ffffff))` | +| `--hm-orgchart-talent-growth-gradient` | 공개 입력(상속) | [color-presets.css:30](../../packages/org-chart/src/styles/color-presets.css) | `[data-group-variant="2"]` → `--hm-orgchart-current-gradient` | `var(--hm-orgchart-talent-growth-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #0f6ace 0%, #044a94 100%)))` | +| `--hm-orgchart-top-leader-card-background` | 공개 입력(상속) | [org-chart-expanded.css:102](../../packages/org-chart/src/styles/org-chart-expanded.css) | `.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-kind="topLeader"]` → `background` | `var(--hm-orgchart-top-leader-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)))` | diff --git a/docs/org-chart/implementation-report-p11.md b/docs/org-chart/implementation-report-p11.md new file mode 100644 index 0000000..018bde7 --- /dev/null +++ b/docs/org-chart/implementation-report-p11.md @@ -0,0 +1,139 @@ +# OrgChart P11 — 외부 CSS 테마 및 Guard 결과 + +RESULT: **PASS** (아래 명시된 구현·검증 범위) + +| 구분 | 결과 | +|---|---| +| 외부 CSS 테마 실제 적용 | **PASS** — React / Vanilla / 순수 HTTP Core 소비자 | +| 기존 TOKEN-001 5건 Guard 실제 재검사 | **PASS** — 원본 `status PASS`, `staticStatus PASS`, FAIL **5 → 0**, issues 0 | +| 다른 공용 Guard 검사 결과 보존 | **PASS** — UI/CENTER/Foundation/Pattern의 기존 67개 이슈 동일, FAIL 0 유지 | +| 기본 디자인 보존 | **PASS** — 6개 기본 화면의 변경 전후 PNG SHA-256 동일 | + +Guard의 자체 `runtimeStatus NOT VERIFIED`는 그대로 보존했다. 정적 Guard PASS를 브라우저 결과로 덮어쓰지 않았다. 외부 테마 브라우저 검증과 원본 Guard 판정은 각각 보고한다. 전체 제품·전체 접근성·모든 브라우저의 PASS를 뜻하지 않는다. + +## 작업 기준 및 Git + +- 폴더: `D:\디자인시스템\HMDesign-OrgChart-Recovered` +- 브랜치: `feature/orgchart-package` +- 시작/완료 HEAD: `dcfa4e0895eecc97ce516cbf3d4424bbaadfddeb` +- origin: `https://gitea.hmac.kr/b25027/HMDesign.git` +- Git 작성자 설정: `b25027 ` +- 시작 Working Tree: clean. 완료 Working Tree: 아래 P11 변경만 unstaged. Staged 파일 0. +- 이번 첨부 요청의 경로·Feature 브랜치·Commit/Push 보류 지시를 적용했다. Stage·Commit·Push·Merge·운영 서버·Docker·Registry Publish 미진행. +- 범위: 기존 OrgChart Core의 컬러 구현 및 소비 검증. 공개 API·데이터 계약·엔진·회사 프리셋·기존 4개 공개 Story 소스·HM UI Theme를 변경하지 않았다. 신규 업무 화면/Navigation/FilterBar를 추가하는 작업이 아니다. + +## 구현과 CSS 변수 + +회사색과 Fixed 6개 부서색의 JS inline 주입을 제거하고 `data-company-id`, `data-group-variant`에 따른 CSS 계산으로 옮겼다. `color-presets.css`에 실제 사용 범위의 `current-card-background`, `current-card-color`, `current-department-color`, `current-border`, `current-gradient`와 compatibility alias 기본 선언을 추가했다. Auto의 `expanded-primary`, `expanded-card-primary`, 기존 secondary alias도 CSS로 계산한다. + +외부 공개 입력은 조상 CSS가 Shadow DOM을 통과해 상속되도록 내부에 고정 기본 선언하지 않는다. 내부 `resolved-*`/`current-*`가 해당 공개 입력과 기존 색상 fallback을 해석한다. 외부 테마를 JS inline 기본색으로 막지 않는다. + +주요 추가/연결 입력은 다음과 같다. 전체 이름·기본 정책·회사/부서 ID 목록은 [사용 가이드](org-chart-usage-guide.md#p11-외부-css-테마-2026-10-13)에 있다. 접두사는 모두 `--hm-orgchart-`다. + +| 영역 | 입력 | +|---|---| +| 회사 | 기존 `company-{id}` 경로를 공개 입력으로 연결, 7개 기본색 유지 | +| 그룹 | `group-title-background`, `group-title-color`, `group-border` | +| 카드 | `card-background`, `card-color`, `card-position-color`, `card-border`, `card-accent` | +| 역할 | `leader-card-background`, `top-leader-card-background` (Auto) | +| 상태/Focus | `card-hover-background`, `card-focus-color` | +| 연결선 | 기존 `expanded-line-color`를 상속 입력으로 전환, `fixed-line-color` 추가 | +| 파견/휴직 | `status-*`, `dispatched-*`, `on-leave-*`의 제목·카드 gradient/Border/글자 입력 | +| Core 팝업 | `dialog-background`, `dialog-color`, `dialog-border`, `dialog-close-color`, `dialog-close-hover-background`, `dialog-focus-color` | +| 기존 공통 | `bg-surface`, `bg-content`, `fg-primary`, `stroke-default` 등 재사용 | +| Fixed | 기존 `{department}-gradient/border/department-color/card-background/card-color` 입력 유지 | + +상태 요약 카드의 고정 gradient/Border를 변수로 바꾸고 기존 `!important`를 제거했다. 미지정 Hover는 원래 카드/상태 gradient를 유지한다. SVG/PNG 자산을 수정하거나 Cell Border Only를 재적용하지 않았다. + +## 외부 파일 연결 및 실제 결과 + +- [external-theme.css](../../examples/org-chart-react/external-theme.css): 기본 공개 CSS를 먼저 import하고 `:host`의 컬러 입력을 뒤에 선언한다. Vite에서는 `?url`로 import해 기존 `stylesheetUrls.core`에 전달한다. +- 일반 HTTP Vanilla에서는 기본 `style.css`를 상대/절대 HTTP 경로로 import하는 같은 파일을 사용한다. 검증 fixture는 공개 Core ESM만 로드했으며 React 또는 node_modules 요청 0건이었다. +- [host-theme.css](../../examples/org-chart-react/host-theme.css): 호스트 HTML에 연결하는 변수 전용 예제. 호스트 class 적용/제거만으로 회사색·배경이 변경/복원됐다. +- [external-group-theme.css](../../examples/org-chart-react/external-group-theme.css): 회사색과 별도로 공통 제목 배경을 바꾸는 CSS 파일. 실제 `rgb(97, 62, 139)` 적용 확인. +- Shadow 파일의 `:host` 선언이 조상 상속보다 우선한다. 호스트 selector는 Shadow 내부 카드에 접근하지 못하므로 내부 selector 수정은 `stylesheetUrls.core`에 포함한다. 기본 CSS 전체를 포함하지 않은 변수 전용 파일로 core URL을 대체하면 안 된다. + +| 확인 대상 | 실제 적용 결과 | +|---|---| +| 한맥 / 삼안 | 제목·소속 카드 강조선이 `#2457a7` / `#385ca8`로 변경 | +| 나머지 5개 회사 | 각각 개별 CSS 색상 변경. 7개 승인 기본색도 브라우저 확인 | +| 그룹 | 제목 글자 `#fff4d6`, 공통 배경 별도 파일 override 확인 | +| 직원·대표자·그룹장 | Auto 배경 `#f4f8ff` / `#d2e3ff` / `#e3eeff`, Border `#527aa9` | +| Hover / Focus | 실제 pointer Hover `#c1d8f8`, 키보드 Focus `#7650a5` | +| Auto 연결선 | 직원 SVG 경로 및 조직 pseudo 연결선 `#249a79` | +| Fixed 연결선 | 실제 overlay path `#297ab0`. 이미지 화살표 내부 색은 그대로 | +| 조직도 배경 | Auto canvas `#edf4ff`, Fixed readOnly 내용 `#dce8f8` | +| 파견·휴직 | 서로 다른 녹색/보라색 gradient·Border, Fixed 및 Auto 상태 카드 확인 | +| Core 팝업 | 배경 `#eef2ff`, 글자 `#283568`, Border `#6879ba`, 실제 닫기 Hover/Focus 확인 | +| 런타임 복원 | mount 상태에서 호스트 class 제거 시 기본 색상·크기 즉시 복원 | + +React HM Dialog는 Shadow 밖의 별도 HM Theme/Portal 계약을 계속 따른다. `dialog-*`는 Core 팝업에만 적용된다. `@hm-design/ui/theme` 연계 및 Portal 주의사항을 사용 가이드에 추가했으며 HM Dialog 구현 자체는 바꾸지 않았다. + +## 기본 디자인 및 Fixed 한계 + +React/Vanilla 각각 Auto Chart Only, Fixed readOnly Full Composition, Fixed Interactive를 변경 전 Core/Wrapper dist로 캡처했다. 변경 후 빌드에서 모든 카드·제목·캔버스·팝업의 주요 computed 색상/Border/크기를 비교했다. 화면 6개의 PNG SHA-256이 정확히 동일했다([이미지 비교 결과](../../test-results/org-chart-p11/default-image-comparison.json)). + +기존 공개 Story 4개(Full Composition, Expanded OrgChart, Department Colors, Overview)를 실제 브라우저에서 열었다. 4/4 렌더 확인, 오류 0, Department Colors의 서로 다른 기본 gradient 6개 유지. Story 소스·ID·등록은 변경하지 않았다. Story 화면은 현재 렌더 확인이며 변경 전 Story 전체의 픽셀 비교를 수행한 것은 아니다. + +Fixed의 CSS 제목·상태 카드·Focus·배경·실제 SVG overlay path는 변경할 수 있다. `cards/*.svg` 카드 이미지, 회사 로고 PNG, Workdesk `Group.svg`, `bg_sky.png`, `bg_main.png`, `Frame2@3x.png`, `1-8-4x-1@2x.png`, `team-arrow-*.png` 내부 색상은 CSS 변수로 바꿀 수 없다. 대표자/부서 SVG의 불투명 영역은 카드 CSS 배경을 바꿔도 유지된다. 자산 전면 재작성·필터 착색 없음. + +## Guard 5건 해결과 공용 영향 + +기존 분석기에서 다음 명령으로 TOKEN-001 5건을 먼저 재현했다. [원본 재현 JSON](../../test-results/org-chart-p11/guard-before.json)은 보존한다. + +```powershell +node scripts/ai-generation-guard.mjs --target . --scope PARTIAL --profile STORYBOOK --files packages/org-chart/src/core/department-colors.ts,packages/org-chart/src/core/style-presets.ts,packages/org-chart/src/core/expanded-content.ts --json +``` + +| 기존 FAIL | 해결 | +|---|---| +| Department Colors의 `current-gradient`, `current-card-background` (2건) | 실제 `data-group-variant` 범위의 CSS 기본 선언 및 공개 CSS 연결 | +| Style presets의 `resolved-bg-surface` (1건) | 기존 공개 design-tokens.css 선언 정상 수집 | +| Style presets의 `current-border`, `current-gradient` (2건) | 실제 범위 기본 선언. JS 색상/alias 주입 제거 | + +CSS 선언만으로는 `?url`로 연결된 공개 CSS를 기존 분석기가 볼 수 없어 5건을 해결할 수 없었다. 필요한 분석기 변경은 `guard-theme-analysis.mjs`의 **공개 OrgChart CSS 두 import 형태와 상대 CSS import 선언 수집**으로 한정했다. 임의 CSS URL 전체를 적용 스타일로 인정하지 않고, setProperty 정의 자동 승인·예외 목록·규칙 비활성화를 하지 않았다. 미선언 HM 토큰은 fallback이 있어도 FAIL이다. + +동일 명령의 [최종 JSON](../../test-results/org-chart-p11/guard-final.json): `status PASS`, `staticStatus PASS`, `issues []`, FAIL 0. 원본 CSS phases는 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED다. 실제 테마 적용은 별도 브라우저 결과로 확인했다. Guard에 그 Runtime 증거를 결합하여 원본 결과를 변경하지 않았다. + +기존 Guard 94/94 및 신규 공개 CSS 회귀 5/5 PASS. 이전 분석기를 임시 복사해 UI/CENTER/Foundation/Pattern의 실제 현재 소스를 검사한 뒤 수정 분석기와 비교했다. 기존 67개 이슈·status·staticStatus가 동일했다([비교 JSON](../../test-results/org-chart-p11/guard-other-comparison.json)). 해당 다른 범위의 원본 status는 NOT VERIFIED, FAIL 0이며 이를 PASS로 올리지 않았다. 비교용 임시 분석기 파일은 제거했다. + +P11 React 소비 fixture는 `--result p11.html`도 별도 실행했다([소비 Guard](../../test-results/org-chart-p11/guard-consumer.json)). 기존 공용 Guard는 OrgChart를 HM UI 화면 재사용 계약 대상으로 모델링하지 않아 `staticStatus NOT APPLICABLE`, 원본 NOT VERIFIED다. 이를 기존 5건의 PASS와 혼동하지 않는다. 패키지 CSS 구현 자체는 해당 소비 graph 밖의 범위이며 빌드·타입·실제 브라우저로 검증했다. + +## 기술 검증 + +| 검사 | 결과 / 증거 | +|---|---| +| 최종 빌드 React/Vanilla 테마·기본값·동작·4개 폭 | **104/104 PASS**, 콘솔/page 오류 0 — [browser-result.json](../../test-results/org-chart-p11/browser-result.json) | +| 순수 HTTP Vanilla Core | **8/8 PASS**, 오류 0. 외부 CSS @import, 팝업, 공개 zoomTo/panBy/fitToView/destroy — [standalone 결과](../../test-results/org-chart-p11/standalone-vanilla-result.json) | +| 기존 공개 Story | **4/4 PASS**, 오류 0 — [Storybook 결과](../../test-results/org-chart-p11/storybook-result.json) | +| 회사 프리셋·Expanded company/layout 관련 테스트 | **17/17 PASS** (bundled Node 24.19.0 사용) | +| 공용 Guard 회귀 | **94/94 PASS** — [로그](../../test-results/org-chart-p11/guard-tests.txt) | +| 신규 Guard/CSS 기본값 회귀 | **5/5 PASS** — [로그](../../test-results/org-chart-p11/guard-orgchart-tests.txt) | +| Core / React Typecheck | PASS. P11 TSX fixture 별도 Typecheck도 PASS | +| Core / React Wrapper / 소비 예제 Build | PASS. 외부 CSS URL이 실제 배포 assets에 산출됨 | +| 390/768/1280/1440px | Auto canvas 폭 일치, 문서 가로 overflow 없음. Fixed 문서 overflow 없음 | +| 상호작용 | 직접 Wheel, Pan, 접기/펼치기, Enter 팝업, Escape 닫기/Focus 복귀, Hover/Focus, Drag & Drop ON/OFF 속성 확인 | +| git diff --check | PASS, Staged 파일 0, HEAD 유지 | + +빌드의 기존 `:host-context(.dark)` minifier 경고와 큰 bundle 경고는 남아 있다. 최초 시스템 Node 20에서 기존 unit runner의 `node:fs globSync` import가 실패해, 해당 runner는 제공된 Node 24로 실행하여 관련 17개를 통과했다. 의존성/패키지 버전 업그레이드 없음. + +NOT VERIFIED: Firefox/Safari/Edge, screen reader 음성, 물리 터치, 실제 Drag & Drop 데이터 이동 전체 회귀, `/hm-dialog` 별도 Theme를 변경한 새 브라우저 비교, 기존 Story 4개 전체의 변경 전 픽셀 비교, 전체 제품 테스트·배포. Fixed 이미지 내부 색상 테마화는 이번 범위에서 지원하지 않는다. 기본 렌더·팝업의 픽셀/computed 비교와 실제 테마 결과의 검사 범위는 위 증거로 한정한다. + +## 변경 파일 + +- Core: `packages/org-chart/src/core/style-presets.ts`, `expanded-content.ts`, `department-colors.ts` +- CSS: `packages/org-chart/src/styles/color-presets.css`(신규), `org-chart.css`, `employee-card.css`, `employee-info-dialog.css`, `org-chart-content.css`, `org-chart-expanded.css`, `org-chart-readonly.css`, `summary-sections.css` +- CSS packaging: `packages/org-chart/build.mjs` +- Guard: `scripts/guard-theme-analysis.mjs`, `scripts/guard-orgchart-theme.test.mjs`(신규) +- 소비/검증: `examples/org-chart-react/p11.html`, `p11.tsx`, `external-theme.css`, `external-group-theme.css`, `host-theme.css`, `verify-p11.mjs`, `verify-p11-stories.mjs`(모두 신규), `vite.config.ts` +- 순수 Vanilla 검증: `examples/org-chart/verify-p11-standalone.mjs`(신규) +- 문서: `docs/org-chart/org-chart-usage-guide.md`, `docs/guidelines/ai-generation-guard.md`, 이 보고서 +- ignored 산출물: Core/Wrapper/소비 dist, `test-results/org-chart-p11/*`. Stage하지 않았다. + +## 시간 (Asia/Seoul) + +- Started At: 2026-10-13 21:43:34 KST +- Completed At: 2026-10-13 22:20:11 KST +- Actual Duration: 36분 37초 +- Estimated Duration: 최초 약 60분 (분석 10 / 구현·가이드 25 / 검증 25) +- Variance: 예상보다 23분 23초 단축 diff --git a/docs/org-chart/org-chart-usage-guide.md b/docs/org-chart/org-chart-usage-guide.md index a2f12f0..1031aed 100644 --- a/docs/org-chart/org-chart-usage-guide.md +++ b/docs/org-chart/org-chart-usage-guide.md @@ -4,6 +4,133 @@ 빠른 시작은 아래 데이터 파일과 React 또는 Vanilla 진입점을 함께 복사한다. 예제는 Vite의 TypeScript 환경을 전제로 한다. 기본 예제는 제목·요약·툴바가 있는 Auto 화면이다. **Auto/Expanded Chart Only는 구현 및 기존 브라우저 검증이 완료됐으며**, 외부 UI 없이 삽입할 때는 [Chart Only 공개 API](#chart-only-공개-api-auto)를 사용한다. **Fixed Full Composition은 지원하지만 Fixed Chart Only는 미지원**이다. +## P11 외부 CSS 테마 (2026-10-13) + +Core와 React는 같은 Shadow DOM CSS 정책을 사용한다. 공개 JS API·데이터 계약은 바뀌지 않는다. 외부 입력 `--hm-orgchart-*`는 호스트에서 상속되며, 내부 `resolved-*`/`current-*`는 실제 사용 요소에서 기본값을 계산한다. 회사색과 Fixed 부서색을 JS inline으로 덮어쓰지 않는다. 기본 파일만 연결하면 기존 회사색·카드·디자인을 유지한다. + +### Shadow DOM 안에 외부 파일 연결 + +`stylesheetUrls.core`는 **기본 스타일 전체를 포함하는 한 개의 CSS URL**이다. 테마 변수만 있는 파일로 교체하면 기본 레이아웃이 누락된다. `engine`에는 기존 엔진 CSS URL을 유지한다. 일반 HTTP 서버에서는 공개 `style.css`를 복사한 경로로 import한다. + +```css +/* /themes/external-theme.css — 기본 CSS를 먼저, 컬러 override를 뒤에 */ +@import url('/assets/org-chart/style.css'); +:host { + --hm-orgchart-bg-surface: #edf4ff; + --hm-orgchart-bg-content: #dce8f8; + --hm-orgchart-company-hanmac: #2457a7; + --hm-orgchart-company-saman: #385ca8; + --hm-orgchart-group-title-color: #fff4d6; + --hm-orgchart-card-background: #f4f8ff; + --hm-orgchart-card-border: #527aa9; + --hm-orgchart-expanded-line-color: #249a79; +} +``` + +Vite에서는 `@import '@hm-design/org-chart/style.css';`를 사용하고 CSS URL로 import한다. 실행 가능한 전체 예제는 [external-theme.css](../../examples/org-chart-react/external-theme.css), [React·Vanilla 소비 fixture](../../examples/org-chart-react/p11.tsx)다. `!important`가 필요하지 않다. `:host`에 선언한 값은 내부 기본값의 fallback보다 우선한다. 동일 속성의 CSS 규칙을 직접 재작성할 때는 보통의 specificity·source order가 적용된다. + +React: + +```tsx +import { OrgChart } from '@hm-design/org-chart-react'; +import engineUrl from '@hm-design/org-chart/engine.css?url'; +import externalThemeUrl from './external-theme.css?url'; + + +``` + +Vanilla (공개 ESM을 서빙하는 환경): + +```js +import { createOrgChart } from '/assets/org-chart/index.js'; +const container = document.getElementById('chart'); +if (!container) throw new Error('chart 요소가 없습니다.'); +const chart = createOrgChart({ + container, dataSource, + layoutMode: 'auto', + stylesheetUrls: { + engine: '/assets/org-chart/engine.css', + core: '/themes/external-theme.css', + }, +}); +await chart.mount(); +``` + +### 호스트 HTML CSS 상속 방식 + +호스트에서 변수만 설정할 때는 기본 `stylesheetUrls.core`를 유지한다. 다음 파일을 호스트 HTML의 ``로 연결하고, React의 `className="orgchart-custom-theme"` 또는 Vanilla 컨테이너의 같은 class를 사용한다. + +```css +.orgchart-custom-theme { + --hm-orgchart-company-hanmac: #2457a7; + --hm-orgchart-company-saman: #385ca8; + --hm-orgchart-expanded-line-color: #249a79; +} +``` + +이 방식은 **Custom Property 상속만** Shadow DOM을 통과한다. 호스트의 `.hm-orgchart-employee { ... }` 같은 selector는 Shadow 내부 카드에 적용되지 않는다. 내부 selector를 수정해야 하면 `stylesheetUrls.core` 파일 안에 작성한다. 호스트 class를 제거하면 기본값이 즉시 복원된다. 외부 테마를 동시에 쓸 경우 Shadow 파일의 `:host`에 같은 입력을 선언하면 조상에서 상속된 값보다 우선한다. 소비자가 직접 inline으로 입력 변수를 설정하면 소비자 inline이 우선하므로 CSS 파일 운영 시 이를 제거한다. + +### 공식 컬러 변수 + +아래 이름은 모두 `--hm-orgchart-` 접두사를 사용한다. `resolved-*`, `current-*`, `expanded-primary`, `expanded-card-primary`는 내부 계산 결과이므로 테마는 아래 공개 입력을 사용한다. 표에 없는 geometry API와 기존 table 변수는 그대로 유지한다. + +| 입력 이름 (접두사 생략) | 적용 범위 / 기본값 정책 | +|---|---| +| `bg-surface`, `bg-content`, `bg-subtle`, `bg-brand` | 캔버스·내용·보조 배경·브랜드 배경. Fixed readOnly의 `bg-content` 미지정 기본값은 기존 `#F5F8FB` | +| `fg-primary`, `fg-secondary` | 일반 텍스트 / 보조 텍스트 | +| `stroke-default`, `stroke-input-default`, `stroke-input-focus` | 일반 Border / 입력 Border / Focus | +| `accent-color`, `on-accent`, `action-primary`, `on-action-primary` | 기존 공통 강조·액션 색 | +| `company-{id}` | Auto 회사 제목과 해당 직원의 소속 회사 강조선. 소속이 부모와 다르면 직원 소속색을 사용 | +| `company-{id}-secondary-{n}` | 기존 보조색 alias 입력. `{n}=1`은 hanmac/saman/jangheon-industry/jangheon/ptc/halla, `{n}=2`는 ptc만 있다. baron에는 보조색 입력이 없다. 현재 주요 제목·카드에는 primary만 사용 | +| `group-title-background`, `group-title-color` | Fixed·Auto 그룹 제목 배경(단색/gradient), 글자. 배경 미지정 시 부서/회사색 | +| `group-border` | Fixed 일반 조직 카드의 상단 Border. Auto에는 기본 그룹 외곽 Border가 없음 | +| `card-background`, `card-color`, `card-position-color`, `card-border`, `card-accent` | CSS 카드 배경·이름/텍스트·Fixed 직위·Border·강조선. Fixed 부서별 배경과 일반 fallback 카드의 텍스트 입력은 공통 값보다 우선한다. SVG/compact 카드의 HTML 이름은 공통 `card-color`를 사용한다. 직원 카드 Border는 공통 `card-border`가 부서 Border보다 우선하며 상단 강조선은 `card-accent`가 우선한다. SVG 이미지 내부 색은 바뀌지 않는다 | +| `leader-card-background`, `top-leader-card-background` | Auto 그룹장 / 대표자 카드의 별도 배경. 미지정 시 공통 카드 배경 | +| `card-hover-background`, `card-focus-color` | Auto 카드 Hover 배경 / Core 직원 버튼 Focus 색(Fixed에도 Focus 적용) | +| `expanded-line-color` | Auto CSS pseudo선과 SVG 조직·직원 연결선. 기존 기본 `#738399` | +| `fixed-line-color` | Fixed 엔진 연결선, Workdesk overlay의 실제 SVG path/arrowhead. PNG 화살표 제외 | +| `gradient`, `border`, `department-color` | 기존 Fixed 공통 부서 gradient·Border·제목 글자색 | +| `{department}-gradient`, `{department}-border`, `{department}-department-color`, `{department}-card-background`, `{department}-card-color` | Fixed 6개 부서별 override. 기존 측정 gradient·기본값 유지 | +| `status-title-background`, `status-title-color`, `status-title-border` | 파견·휴직 요약 제목의 공통 색. 기존 회색 gradient, readOnly 흰 글자 유지 | +| `status-card-gradient`, `status-card-border`, `status-card-color`, `status-position-color` | Fixed 파견·휴직 요약 카드의 공통 gradient·Border·이름·직위. 기존 회색 기본값 | +| `dispatched-title-background`, `on-leave-title-background` | 파견 / 휴직 제목의 독립 gradient, 공통보다 우선 | +| `dispatched-card-gradient`, `dispatched-card-border`, `dispatched-card-color` | 파견 카드 독립 입력. Fixed 요약·Auto 상태 직원 카드에 적용 | +| `on-leave-card-gradient`, `on-leave-card-border`, `on-leave-card-color` | 휴직 카드 독립 입력. Fixed 요약·Auto 상태 직원 카드에 적용 | +| `dialog-background`, `dialog-color`, `dialog-border` | Core native 팝업 배경·글자·Border | +| `dialog-close-color`, `dialog-close-hover-background`, `dialog-focus-color` | Core 닫기 버튼 기본·Hover·Focus. 미지정 시 기존 HM 호스트 값/기본 fallback | + +회사 `{id}`와 기본색은 `hanmac #CB2E28`, `saman #F08300`, `jangheon-industry #4580A6`, `jangheon #1D418D`, `ptc #84338E`, `halla #0068B7`, `baron #0C367F`다. 등록된 ID와 기존 프리셋은 그대로다. Fixed `{department}`는 `business-management`, `business-planning`, `talent-growth`, `integrated-system`, `design-planning`, `solution-sales`다. 회사색 override로 Fixed 부서색을 바꾸지 않는다. + +```css +:host { + --hm-orgchart-company-saman: #385ca8; /* Auto 회사 제목·소속 카드 강조선 */ + --hm-orgchart-business-planning-gradient: linear-gradient(180deg, #385ca8, #23417f); + --hm-orgchart-business-planning-border: #385ca8; /* Fixed 부서: 별도 설정 */ + --hm-orgchart-dispatched-card-gradient: linear-gradient(180deg, #e9f9f0, #a9d8bd); + --hm-orgchart-dispatched-card-border: #278857; + --hm-orgchart-on-leave-card-gradient: linear-gradient(180deg, #f7efff, #d6bbeb); + --hm-orgchart-dialog-background: #eef2ff; + --hm-orgchart-dialog-focus-color: #8958ba; +} +``` + +색상 입력에는 유효한 CSS color를 사용한다. `*-background`는 적용되는 CSS 속성에 맞는 배경 표현식을, `*-gradient`는 `linear-gradient(...)` 같은 CSS image 표현식을 사용한다(Fixed 요약 카드는 `background-image`에 적용). CSS 파일만으로 동작하며 소스를 수정하거나 JS 색상 설정을 추가할 필요가 없다. 그룹 공통 배경을 설정하면 회사별 제목 배경보다 우선하므로 회사별 배경을 유지하려면 공통 배경을 생략한다. 상태 카드도 Auto의 회사 강조선을 유지한다. + +### 런타임 변경과 기본 테마 복원 + +호스트 변수 class/값을 바꾸면 이미 mount된 차트에 반영되며 데이터·Zoom·Pan을 재생성하지 않는다. 서버의 CSS 파일 값을 바꾼 뒤 기존 link는 브라우저 캐시에 따라 다시 요청되지 않을 수 있으므로 CSS link를 갱신하거나 새 URL로 mount한다. React는 `stylesheetUrls.core` URL 변경 시 기존 lifecycle로 remount한다. Vanilla는 기존 handle을 `destroy()`한 뒤 새 CSS URL로 `createOrgChart()`·`mount()`한다. 새 테마 변경 API는 추가하지 않았다. + +복원은 호스트 테마 class/link/소비자 inline 입력을 제거하고 Core URL을 기본 `@hm-design/org-chart/style.css?url` 또는 `/assets/org-chart/style.css`로 되돌린다. 일부 값만 복원하려면 외부 파일의 해당 선언을 삭제한다. `resolved-*`에 `unset`을 주는 방식은 사용하지 않는다. + +### HM Dialog와 Fixed 이미지 한계 + +React `/hm-dialog`의 `employeePopup="hm-dialog"`는 Shadow 밖의 HM React Dialog다. 위 `dialog-*` 변수는 Core 팝업에만 적용된다. HM Dialog에는 기존 `@hm-design/ui/theme.css`와 `@hm-design/ui/theme`의 `applyRuntimeTheme`/`resetRuntimeTheme`를 사용한다([Runtime Theme 가이드](../guidelines/runtime-theme.md)). Portal은 DOM 경계를 따르므로 차트 컨테이너에만 적용한 Scoped Theme는 body Portal로 전달되지 않는다. 같은 팔레트를 원하면 HM Global Runtime Theme 입력과 OrgChart CSS 입력에 각각 연결한다. 자동 동기화나 HM Dialog 디자인 변경은 하지 않는다. + +Fixed에서 회사 로고 PNG, `cards/*.svg`의 대표자·6개 부서 직원/그룹장 카드 이미지, `workdesk/Group.svg`, `bg_sky.png`, `bg_main.png`, `Frame2@3x.png`, `1-8-4x-1@2x.png`, `team-arrow-*.png`는 이미지 내부 픽셀 색상을 CSS 변수로 바꿀 수 없다. 카드 바깥의 Focus, CSS 제목·파견/휴직·배경과 실제 inline SVG 연결 path는 변경할 수 있다. 카드 CSS 배경을 바꿔도 SVG 이미지의 불투명 영역은 그대로다. 자산 재작성·필터 착색·이미지 교체는 하지 않았다. + +검증 범위·Guard 원본 상태는 [P11 결과 보고서](implementation-report-p11.md)를 참고한다. + ## 1. 설치·패키지·실행 환경 - `@hm-design/org-chart@0.1.0`: 데이터 타입·Validator·JSON DataSource·집계·이동 함수·DOM 렌더러. React 없이 사용한다. 내부 엔진 의존성은 `orgchart@6.0.0`이며 소비자가 엔진을 직접 조작하지 않는다. @@ -698,8 +825,8 @@ React 없이 생성해 달라는 명시적 요청은 이 가이드의 Vanilla Co - 자연어→데이터 자동 생성 UI, CSV/XLSX 직접 import, 서버 저장·외부 API·CSV/PDF/이미지 export는 공개 사용 기능이 아니다. - 임의 회사 프리셋 등록 setter·카드 교체·Compact·공개 collapse setter·React imperative ref는 없다. Chart Only는 Auto 전용 `presentationMode`로만 제공하며 Fixed Chart Only는 없다. - 제공 데이터의 최대 인원/Depth·모든 브라우저·실제 모바일 터치/스크린리더·외부 fresh install을 이번 소스 분석으로 보장하지 않는다. -- 기존 P10 PARTIAL/Guard FAIL 10건의 원본 기록은 보존한다. 2026-10-13 Feature 통합 대상은 Chart Only·초기 Fit·직접 Wheel Zoom, GuardFix의 F3–F7 해결 5건, AI 생성 방식 사전 선택 정책이다. Cell 디자인·미리보기는 제외한다. 잔여 Guard FAIL 5건·NOT VERIFIED 9건을 유지하며 원본 `status/staticStatus FAIL`, `runtimeStatus NOT VERIFIED`를 제품 전체 PASS로 표현하지 않는다. 범위와 새 결과는 [최종 Feature 통합 보고서](final-integration-2026-10-13.md)를 따른다. -- [P10.3 GuardReview](guard-review-p10-3.md)는 별도 Worktree에서 작성한 원본 분석이다. 당시 FAIL 10건을 재현했고 제품·Guard 수정 없이 공유했다. 현재 Auto Chart Only는 위 공개 API로 지원하며 Fixed Chart Only, 외부 CSS 테마 표준화, Fixed SVG/PNG 테마화, 최종 디자인 승인, main Merge와 Docker 배포는 이번 통합 대상이 아니다. +- 기존 P10 PARTIAL/Guard FAIL 10건의 원본 기록은 보존한다. [P10 Feature 통합](final-integration-2026-10-13.md)은 Chart Only·초기 Fit·직접 Wheel Zoom, GuardFix의 F3–F7 해결 5건, AI 생성 방식 사전 선택 정책을 포함했고 당시 잔여 FAIL 5건·NOT VERIFIED 9건이었다. 이후 [P11](implementation-report-p11.md)의 동일 대상 실제 Guard는 `status/staticStatus PASS`, FAIL 0건이다. Guard `runtimeStatus NOT VERIFIED`와 제품 전체 미검증을 계속 구분한다. Cell 디자인·미리보기는 제외한다. +- [P10.3 GuardReview](guard-review-p10-3.md)는 별도 Worktree에서 당시 FAIL 10건을 재현한 원본 분석이다. 현재 Auto Chart Only와 P11 외부 CSS 테마는 지원한다. Fixed Chart Only·Fixed SVG/PNG 이미지 내부 테마화는 미지원이며, 최종 디자인 승인·main Merge·Docker 배포는 완료하지 않았다. ### 이 가이드 예제 검증 범위 @@ -725,4 +852,4 @@ React 없이 생성해 달라는 명시적 요청은 이 가이드의 Vanilla Co - [데이터 타입](../../packages/org-chart/src/data/types.ts), [검증](../../packages/org-chart/src/data/validate.ts), [이동](../../packages/org-chart/src/data/moves.ts): 실제 Contract와 정책. - [P10 보고서](implementation-report-p10.md): 기존 PARTIAL/Guard FAIL·별도 미검증 기록. 현재 기능의 근거는 보고서보다 이 가이드 기준 소스다. -원본 UsageGuide/P10.2의 문서 예제 검증 기록은 위에 보존했다. P10.7에서는 검증 기준 SHA·Gitea Commit 고정 절차·현재 Chart Only 지원 상태와 옵션 설명을 공개 소스와 대조했다. 새 전체 타입 검사·제품 테스트·빌드·브라우저 검사는 수행하지 않았다. 제품 코드·기존 디자인·Core 공개 API 변경 없음. +원본 UsageGuide/P10.2의 문서 예제 검증 기록은 위에 보존했다. P10.7에서는 검증 기준 SHA·Gitea Commit 고정 절차·현재 Chart Only 지원 상태와 옵션 설명을 공개 소스와 대조했으며 새 전체 타입 검사·제품 테스트·빌드·브라우저 검사는 수행하지 않았다. P11/P11.1의 후속 검증은 [P11 보고서](implementation-report-p11.md)와 [P11.1 정합성 검증](consistency-review-p11-1.md)을 따른다. P11.1은 공개 API·제품 스타일·Story 소스를 변경하지 않는다. diff --git a/examples/org-chart-react/external-group-theme.css b/examples/org-chart-react/external-group-theme.css new file mode 100644 index 0000000..460408e --- /dev/null +++ b/examples/org-chart-react/external-group-theme.css @@ -0,0 +1,2 @@ +@import './external-theme.css'; +:host { --hm-orgchart-group-title-background: #613e8b; } diff --git a/examples/org-chart-react/external-theme.css b/examples/org-chart-react/external-theme.css new file mode 100644 index 0000000..2fec44d --- /dev/null +++ b/examples/org-chart-react/external-theme.css @@ -0,0 +1,44 @@ +/* Served to stylesheetUrls.core: imports the complete default package stylesheet. + Vite resolves this public export; plain servers use their copied /assets/style.css. + To change a theme, edit only values in this file. No !important or inline colors. */ +@import '@hm-design/org-chart/style.css'; +:host { + --hm-orgchart-bg-surface: #edf4ff; + --hm-orgchart-bg-content: #dce8f8; + --hm-orgchart-fg-primary: #172b4d; + --hm-orgchart-stroke-default: #8b9bb0; + --hm-orgchart-company-hanmac: #2457a7; + --hm-orgchart-company-saman: #385ca8; + --hm-orgchart-company-jangheon-industry: #14786b; + --hm-orgchart-company-jangheon: #503a91; + --hm-orgchart-company-ptc: #755024; + --hm-orgchart-company-halla: #207879; + --hm-orgchart-company-baron: #4d6295; + --hm-orgchart-group-title-color: #fff4d6; + --hm-orgchart-card-background: #f4f8ff; + --hm-orgchart-leader-card-background: #e3eeff; + --hm-orgchart-top-leader-card-background: #d2e3ff; + --hm-orgchart-card-border: #527aa9; + --hm-orgchart-card-hover-background: #c1d8f8; + --hm-orgchart-card-focus-color: #7650a5; + --hm-orgchart-expanded-line-color: #249a79; + --hm-orgchart-fixed-line-color: #297ab0; + --hm-orgchart-business-management-gradient: linear-gradient(180deg, #2457a7, #123568); + --hm-orgchart-business-management-border: #2457a7; + --hm-orgchart-business-planning-gradient: linear-gradient(180deg, #385ca8, #23417f); + --hm-orgchart-business-planning-border: #385ca8; + --hm-orgchart-dispatched-card-gradient: linear-gradient(180deg, #e9f9f0, #a9d8bd); + --hm-orgchart-dispatched-card-border: #278857; + --hm-orgchart-dispatched-card-color: #145d39; + --hm-orgchart-dispatched-title-background: linear-gradient(180deg, #328c61, #1e5b3d); + --hm-orgchart-on-leave-card-gradient: linear-gradient(180deg, #f7efff, #d6bbeb); + --hm-orgchart-on-leave-card-border: #8050a1; + --hm-orgchart-on-leave-card-color: #603080; + --hm-orgchart-on-leave-title-background: linear-gradient(180deg, #8d66b3, #573771); + --hm-orgchart-dialog-background: #eef2ff; + --hm-orgchart-dialog-color: #283568; + --hm-orgchart-dialog-border: #6879ba; + --hm-orgchart-dialog-close-color: #36478a; + --hm-orgchart-dialog-close-hover-background: #dce2f9; + --hm-orgchart-dialog-focus-color: #8958ba; +} diff --git a/examples/org-chart-react/host-theme.css b/examples/org-chart-react/host-theme.css new file mode 100644 index 0000000..84687c0 --- /dev/null +++ b/examples/org-chart-react/host-theme.css @@ -0,0 +1,7 @@ +/* Loaded by the host document. Colors inherit; Shadow selectors stay private. */ +.orgchart-custom-theme { + --hm-orgchart-company-hanmac: #2457a7; + --hm-orgchart-company-saman: #385ca8; + --hm-orgchart-expanded-line-color: #249a79; + --hm-orgchart-bg-surface: #edf4ff; +} diff --git a/examples/org-chart-react/p11.html b/examples/org-chart-react/p11.html new file mode 100644 index 0000000..9780d27 --- /dev/null +++ b/examples/org-chart-react/p11.html @@ -0,0 +1,2 @@ + +OrgChart P11 theme verification
diff --git a/examples/org-chart-react/p11.tsx b/examples/org-chart-react/p11.tsx new file mode 100644 index 0000000..e7a3c18 --- /dev/null +++ b/examples/org-chart-react/p11.tsx @@ -0,0 +1,34 @@ +// Isolated consumer fixture; no Storybook registration or production controls. +import { createRoot } from 'react-dom/client'; +import { OrgChart } from '@hm-design/org-chart-react'; +import { createOrgChart, JsonOrgChartDataSource } from '@hm-design/org-chart'; +import coreUrl from '@hm-design/org-chart/style.css?url'; +import engineUrl from '@hm-design/org-chart/engine.css?url'; +import externalThemeUrl from './external-theme.css?url'; +import hostThemeUrl from './host-theme.css?url'; +import groupThemeUrl from './external-group-theme.css?url'; +import autoData from '../org-chart/data-expanded-small.json'; +import allCompanies from '../org-chart/data-expanded-large.json'; +import fixedData from '../org-chart/data-p1.json'; + +const query = new URLSearchParams(location.search); +const mode = query.get('mode') ?? 'auto'; +const data = structuredClone(mode === 'auto' ? query.has('all') ? allCompanies : autoData : fixedData); +if (mode === 'auto' && query.has('statuses')) { + data.employees.filter(employee => !data.organizations.some(org => org.leaderId === employee.id)).slice(0, 2).forEach((employee, index) => { employee.workStatus = index ? 'onLeave' : 'dispatched'; }); +} +const options = { + dataSource: new JsonOrgChartDataSource(data), + dragDropEnabled: query.has('drag'), + stylesheetUrls: { core: query.has('groups') ? groupThemeUrl : query.has('theme') ? externalThemeUrl : coreUrl, engine: engineUrl }, + ...(mode === 'auto' ? { layoutMode: 'auto' as const, presentationMode: 'chart-only' as const, initialView: 'fit' as const, wheelZoom: 'direct' as const } : { displayMode: mode === 'fixed' ? 'readOnly' as const : 'interactive' as const }), +}; +document.body.style.margin = '0'; +Object.assign(window, { p11HostThemeUrl: hostThemeUrl }); +const container = document.getElementById('root')!; +container.style.height = '850px'; +if (query.get('runtime') === 'vanilla') { + const chart = createOrgChart({ container, ...options }); + void chart.mount(); + Object.assign(window, { p11Chart: chart }); +} else createRoot(container).render(); diff --git a/examples/org-chart-react/verify-p11-stories.mjs b/examples/org-chart-react/verify-p11-stories.mjs new file mode 100644 index 0000000..a790f30 --- /dev/null +++ b/examples/org-chart-react/verify-p11-stories.mjs @@ -0,0 +1,35 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import assert from 'node:assert/strict'; +import { launchChromium } from '../../scripts/orgchart-browser-runtime.mjs'; +const output = path.resolve('test-results/org-chart-p11'); +const browser = await launchChromium({ headless: true }); +const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); +const errors = [], checks = []; +page.on('pageerror', error => errors.push(error.message)); +page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); }); +try { + for (const [id, selector] of [ + ['full-composition', '.hm-orgchart-composition[data-display-mode="readOnly"]'], + ['expanded-org-chart', '.hm-orgchart-expanded'], + ['department-colors', '.hm-orgchart-department-colors'], + ['logo-personnel-summary', '.hm-orgchart-affiliation-mark'], + ]) { + await page.goto(`http://127.0.0.1:6034/iframe.html?id=orgchart--${id}&viewMode=story`); + await page.locator(selector).first().waitFor(); + await page.evaluate(async () => { await document.fonts.ready; await new Promise(resolve => setTimeout(resolve, 800)); }); + if (id === 'department-colors') { + const gradients = await page.locator('.hm-orgchart-color-preview .hm-orgchart-color-department-name').evaluateAll(headings => headings.map(heading => getComputedStyle(heading).backgroundImage)); + assert.equal(gradients.length, 6); + assert.ok(gradients.every(gradient => gradient !== 'none')); + assert.equal(new Set(gradients).size, 6); + } + await page.screenshot({ path: path.join(output, `storybook-${id}.png`) }); + checks.push({ name: id, status: 'PASS' }); + } + assert.deepEqual(errors, []); +} finally { + await fs.writeFile(path.join(output, 'storybook-result.json'), JSON.stringify({ checks, errors }, null, 2)); + await browser.close(); +} +console.log(`Storybook: ${checks.length} checks, ${errors.length} errors`); diff --git a/examples/org-chart-react/verify-p11.mjs b/examples/org-chart-react/verify-p11.mjs new file mode 100644 index 0000000..59a1a0b --- /dev/null +++ b/examples/org-chart-react/verify-p11.mjs @@ -0,0 +1,184 @@ +import fs from 'node:fs/promises'; +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { launchChromium } from '../../scripts/orgchart-browser-runtime.mjs'; + +const output = path.resolve('test-results/org-chart-p11'); +await fs.mkdir(output, { recursive: true }); +const browser = await launchChromium({ headless: true }); +const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); +const errors = [], checks = [], measurements = {}; +page.on('pageerror', error => errors.push(error.message)); +page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); }); +const baseline = process.argv.includes('--baseline'); +const origin = process.env.ORGCHART_P11_ORIGIN ?? 'http://127.0.0.1:6026'; +async function open(runtime, mode, theme = false, extra = '') { + await page.goto(`${origin}/p11.html?runtime=${runtime}&mode=${mode}${theme ? '&theme=1' : ''}${extra}`); + await page.locator('.hm-orgchart-employee').first().waitFor(); + await page.waitForFunction(() => [...document.querySelector('.hm-orgchart-host').shadowRoot.querySelectorAll('link')].every(link => link.sheet)); + await page.evaluate(async () => { await document.fonts.ready; await new Promise(resolve => setTimeout(resolve, 600)); }); +} +async function styles() { + return page.evaluate(() => { + const shadow = document.querySelector('.hm-orgchart-host').shadowRoot; + const selectors = '.hm-orgchart-shell,.hm-orgchart-viewport,.hm-orgchart-organization-name,.hm-orgchart-employee,.hm-orgchart-status-group > .hm-orgchart-summary-title,.hm-orgchart-employee-dialog,.hm-orgchart-employee-dialog-close'; + const fields = ['backgroundColor', 'backgroundImage', 'color', 'borderTopColor', 'borderRightColor', 'borderTopWidth', 'boxShadow', 'width', 'height', 'fontSize', 'padding', 'borderRadius']; + return [...shadow.querySelectorAll(selectors)].map(el => ({ tag: el.tagName, class: el.className, company: el.dataset.companyId ?? el.closest('[data-company-id]')?.dataset.companyId, kind: el.dataset.cardKind, status: el.dataset.workStatus, css: Object.fromEntries(fields.map(field => [field, getComputedStyle(el)[field]])) })); + }); +} +async function check(name, run) { await run(); checks.push({ name, status: 'PASS' }); } +try { + for (const runtime of ['vanilla', 'react']) for (const mode of ['auto', 'fixed', 'interactive']) { + const key = `${runtime}-${mode}`; + await open(runtime, mode); + measurements[key] = await styles(); + await page.screenshot({ path: path.join(output, `${baseline ? 'before' : 'after'}-${key}.png`) }); + if (!baseline) { + const before = JSON.parse(await fs.readFile(path.join(output, 'baseline.json'), 'utf8')); + await check(`${key}: default colors and geometry identical`, () => assert.equal(JSON.stringify(measurements[key]), JSON.stringify(before[key]))); + } + // Popup is captured separately so it cannot obscure the chart screenshot. + await page.locator('.hm-orgchart-employee').first().click(); + await page.locator('.hm-orgchart-employee-dialog[open]').waitFor(); + measurements[`${key}-popup`] = (await styles()).filter(row => row.class.includes('dialog')); + if (!baseline) { + const before = JSON.parse(await fs.readFile(path.join(output, 'baseline.json'), 'utf8')); + await check(`${key}: default popup identical`, () => assert.equal(JSON.stringify(measurements[`${key}-popup`]), JSON.stringify(before[`${key}-popup`]))); + } + } + if (baseline) await fs.writeFile(path.join(output, 'baseline.json'), JSON.stringify(measurements, null, 2)); + else { + const expectedCompanies = { hanmac: 'rgb(36, 87, 167)', saman: 'rgb(56, 92, 168)', 'jangheon-industry': 'rgb(20, 120, 107)' }; + const value = (selector, property, pseudo = null) => page.locator(selector).first().evaluate((el, { property, pseudo }) => getComputedStyle(el, pseudo)[property], { property, pseudo }); + for (const runtime of ['vanilla', 'react']) { + await open(runtime, 'auto', true); + for (const [id, color] of Object.entries(expectedCompanies)) { + await check(`${runtime}: external CSS ${id} title`, async () => assert.equal(await value(`.node[data-company-id="${id}"] .hm-orgchart-organization-name`, 'backgroundColor'), color)); + await check(`${runtime}: external CSS ${id} card accent`, async () => assert.equal(await value(`.hm-orgchart-employee[data-company-id="${id}"]`, 'borderTopColor'), color)); + } + await check(`${runtime}: canvas and title text`, async () => { + assert.equal(await value('.hm-orgchart-viewport', 'backgroundColor'), 'rgb(237, 244, 255)'); + assert.equal(await value('.hm-orgchart-organization-name', 'color'), 'rgb(255, 244, 214)'); + }); + await check(`${runtime}: independent card roles and border`, async () => { + assert.equal(await value('.hm-orgchart-employee[data-card-kind="employee"]', 'backgroundColor'), 'rgb(244, 248, 255)'); + assert.equal(await value('.hm-orgchart-employee[data-card-kind="topLeader"]', 'backgroundColor'), 'rgb(210, 227, 255)'); + assert.equal(await value('.hm-orgchart-employee', 'borderRightColor'), 'rgb(82, 122, 169)'); + }); + await check(`${runtime}: employee and organization connector theme`, async () => { + assert.equal(await value('.hm-orgchart-employee-connectors path', 'stroke'), 'rgb(36, 154, 121)'); + assert.equal(await value('.hm-orgchart-engine > ul > li > ul li > .node', 'backgroundColor', '::before'), 'rgb(36, 154, 121)'); + }); + const employee = page.locator('.hm-orgchart-employee[data-card-kind="employee"]').first(); + await employee.hover(); + await check(`${runtime}: actual card hover`, async () => assert.equal(await value('.hm-orgchart-employee[data-card-kind="employee"]', 'backgroundColor'), 'rgb(193, 216, 248)')); + await page.mouse.move(0, 0); await employee.focus(); + await check(`${runtime}: actual card focus`, async () => assert.equal(await value('.hm-orgchart-employee[data-card-kind="employee"]', 'outlineColor'), 'rgb(118, 80, 165)')); + await employee.press('Enter'); + await page.locator('.hm-orgchart-employee-dialog[open]').waitFor(); + await check(`${runtime}: Core popup background, text and border`, async () => { + assert.equal(await value('.hm-orgchart-employee-dialog', 'backgroundColor'), 'rgb(238, 242, 255)'); + assert.equal(await value('.hm-orgchart-employee-dialog', 'color'), 'rgb(40, 53, 104)'); + assert.equal(await value('.hm-orgchart-employee-dialog', 'borderTopColor'), 'rgb(104, 121, 186)'); + }); + const close = page.locator('.hm-orgchart-employee-dialog-close'); + await close.hover(); await page.waitForTimeout(200); + await check(`${runtime}: popup hover`, async () => assert.equal(await value('.hm-orgchart-employee-dialog-close', 'backgroundColor'), 'rgb(220, 226, 249)')); + await page.mouse.move(0, 0); await close.focus(); await page.waitForTimeout(200); + await check(`${runtime}: popup focus`, async () => assert.equal(await value('.hm-orgchart-employee-dialog-close', 'borderTopColor'), 'rgb(137, 88, 186)')); + await page.keyboard.press('Escape'); + await check(`${runtime}: popup Escape and focus restoration`, async () => assert.equal(await employee.evaluate(el => el.getRootNode().activeElement === el), true)); + await page.screenshot({ path: path.join(output, `custom-${runtime}-auto.png`) }); + + await open(runtime, 'fixed', true); + await check(`${runtime}: Fixed headers and background`, async () => { + assert.match(await value('[data-group-variant="0"] .hm-orgchart-organization-name', 'backgroundImage'), /rgb\(36, 87, 167\)/); + assert.equal(await value('.hm-orgchart-composition', 'backgroundColor'), 'rgb(220, 232, 248)'); + assert.equal(await value('.hm-orgchart-workdesk-overlay path[data-connector]', 'stroke'), 'rgb(41, 122, 176)'); + }); + for (const [status, color] of [['dispatched', 'rgb(39, 136, 87)'], ['onLeave', 'rgb(128, 80, 161)']]) { + await check(`${runtime}: ${status} external gradient and border`, async () => { + const selector = `.hm-orgchart-status-group[data-work-status="${status}"] .hm-orgchart-employee`; + assert.equal(await value(selector, 'borderTopColor'), color); + assert.notEqual(await value(selector, 'backgroundImage'), 'none'); + }); + } + await page.screenshot({ path: path.join(output, `custom-${runtime}-fixed.png`) }); + + await open(runtime, 'auto'); + const unthemed = await styles(); + await page.evaluate(() => { + const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = window.p11HostThemeUrl; link.id = 'host-theme'; document.head.append(link); + document.querySelector('#root').classList.add('orgchart-custom-theme'); + }); + await page.waitForFunction(() => document.querySelector('#host-theme').sheet); + await check(`${runtime}: host CSS crosses Shadow boundary without inline conflict`, async () => { + assert.equal(await value('.node[data-company-id="hanmac"] .hm-orgchart-organization-name', 'backgroundColor'), expectedCompanies.hanmac); + assert.equal(await value('.node[data-company-id="saman"] .hm-orgchart-organization-name', 'backgroundColor'), expectedCompanies.saman); + assert.equal(await value('.hm-orgchart-viewport', 'backgroundColor'), 'rgb(237, 244, 255)'); + }); + await page.evaluate(() => document.querySelector('#root').classList.remove('orgchart-custom-theme')); + await check(`${runtime}: removing host theme restores defaults immediately`, async () => assert.equal(JSON.stringify(await styles()), JSON.stringify(unthemed))); + + await open(runtime, 'auto', false, '&all=1'); + await check(`${runtime}: all seven approved default company colors`, async () => { + for (const [id, color] of Object.entries({ hanmac: 'rgb(203, 46, 40)', saman: 'rgb(240, 131, 0)', 'jangheon-industry': 'rgb(69, 128, 166)', jangheon: 'rgb(29, 65, 141)', ptc: 'rgb(132, 51, 142)', halla: 'rgb(0, 104, 183)', baron: 'rgb(12, 54, 127)' })) assert.equal(await value(`.node[data-company-id="${id}"] .hm-orgchart-organization-name`, 'backgroundColor'), color); + }); + await open(runtime, 'auto', true, '&all=1&statuses=1&drag=1'); + for (const [id, color] of Object.entries({ ...expectedCompanies, jangheon: 'rgb(80, 58, 145)', ptc: 'rgb(117, 80, 36)', halla: 'rgb(32, 120, 121)', baron: 'rgb(77, 98, 149)' })) { + await check(`${runtime}: seven-company theme ${id}`, async () => assert.equal(await value(`.node[data-company-id="${id}"] .hm-orgchart-organization-name`, 'backgroundColor'), color)); + } + await check(`${runtime}: Auto status cards use external gradients and borders`, async () => { + assert.equal(await value('.hm-orgchart-employee[data-work-status="dispatched"]', 'borderRightColor'), 'rgb(39, 136, 87)'); + assert.match(await value('.hm-orgchart-employee[data-work-status="onLeave"]', 'backgroundImage'), /rgb\(214, 187, 235\)/); + }); + await check(`${runtime}: group leader has an independent background`, async () => assert.equal(await value('.hm-orgchart-employee[data-card-kind="leader"]', 'backgroundColor'), 'rgb(227, 238, 255)')); + await page.evaluate(() => { + const shadow = document.querySelector('.hm-orgchart-host').shadowRoot; + const removeHover = rules => { for (const rule of rules) { rule.style?.removeProperty('--hm-orgchart-card-hover-background'); if (rule.styleSheet) removeHover(rule.styleSheet.cssRules); if (rule.cssRules) removeHover(rule.cssRules); } }; + for (const link of shadow.querySelectorAll('link')) removeHover(link.sheet.cssRules); + }); + const statusCard = page.locator('.hm-orgchart-employee[data-work-status="dispatched"]').first(); + const statusGradient = await value('.hm-orgchart-employee[data-work-status="dispatched"]', 'backgroundImage'); + await statusCard.hover(); + await check(`${runtime}: unspecified hover preserves status gradient`, async () => assert.equal(await value('.hm-orgchart-employee[data-work-status="dispatched"]', 'backgroundImage'), statusGradient)); + await check(`${runtime}: Drag & Drop ON`, async () => assert.equal(await page.locator('.hm-orgchart-viewport').getAttribute('data-drag-drop-enabled'), 'true')); + await open(runtime, 'auto', true); + await check(`${runtime}: Drag & Drop OFF`, async () => assert.equal(await page.locator('.hm-orgchart-viewport').getAttribute('data-drag-drop-enabled'), 'false')); + await open(runtime, 'auto', true, '&groups=1'); + await check(`${runtime}: independent group background via external CSS file`, async () => assert.equal(await value('.hm-orgchart-organization-name', 'backgroundColor'), 'rgb(97, 62, 139)')); + await open(runtime, 'auto', true); + const forest = page.locator('.hm-orgchart-forest'); + const transform = await forest.evaluate(el => el.style.transform); + const viewport = await page.locator('.hm-orgchart-viewport').boundingBox(); + const mouseX = viewport.x + viewport.width - 12, mouseY = viewport.y + viewport.height - 12; + await page.mouse.move(mouseX, mouseY); await page.mouse.wheel(0, -150); await page.waitForTimeout(150); + await check(`${runtime}: direct wheel zoom`, async () => assert.notEqual(await forest.evaluate(el => el.style.transform), transform)); + const zoomed = await forest.evaluate(el => el.style.transform); + await page.mouse.move(mouseX, mouseY); await page.mouse.down(); await page.mouse.move(mouseX - 50, mouseY - 50, { steps: 5 }); await page.mouse.up(); + await check(`${runtime}: pan`, async () => assert.notEqual(await forest.evaluate(el => el.style.transform), zoomed)); + await open(runtime, 'auto', true); + const collapse = page.locator('.hm-orgchart-expanded-toggle').first(); + await collapse.click(); + await check(`${runtime}: collapse`, async () => assert.equal(await page.locator('.hm-orgchart-expanded-toggle').first().getAttribute('aria-expanded'), 'false')); + await page.locator('.hm-orgchart-expanded-toggle').first().click(); + await check(`${runtime}: expand`, async () => assert.equal(await page.locator('.hm-orgchart-expanded-toggle').first().getAttribute('aria-expanded'), 'true')); + for (const width of [390, 768, 1280, 1440]) { + await page.setViewportSize({ width, height: 900 }); + await open(runtime, 'auto', true); + await check(`${runtime}: ${width}px canvas fits and document does not overflow`, async () => { + const box = await page.locator('.hm-orgchart-viewport').boundingBox(); + assert.equal(box.width, width); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true); + }); + await open(runtime, 'fixed', true); + await check(`${runtime}: ${width}px Fixed document does not overflow`, async () => assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true)); + } + } + } + assert.deepEqual(errors, [], 'No browser errors'); +} finally { + await fs.writeFile(path.join(output, baseline ? 'baseline-result.json' : 'browser-result.json'), JSON.stringify({ checks, errors, measurements }, null, 2)); + await browser.close(); +} +console.log(`${baseline ? 'Baseline' : 'P11'}: ${checks.length} checks, ${errors.length} errors`); diff --git a/examples/org-chart-react/vite.config.ts b/examples/org-chart-react/vite.config.ts index 120994f..103783e 100644 --- a/examples/org-chart-react/vite.config.ts +++ b/examples/org-chart-react/vite.config.ts @@ -7,5 +7,5 @@ export default defineConfig({ base: './', plugins: [react()], server: { host: '127.0.0.1', port: 6026, strictPort: true }, - build: { outDir: 'dist', emptyOutDir: true, rolldownOptions: { input: { index: path.resolve(import.meta.dirname, 'index.html'), p1: path.resolve(import.meta.dirname, 'p1.html') } } }, + build: { outDir: 'dist', emptyOutDir: true, rolldownOptions: { input: { index: path.resolve(import.meta.dirname, 'index.html'), p1: path.resolve(import.meta.dirname, 'p1.html'), p11: path.resolve(import.meta.dirname, 'p11.html') } } }, }); diff --git a/examples/org-chart/verify-p11-standalone.mjs b/examples/org-chart/verify-p11-standalone.mjs new file mode 100644 index 0000000..dfb896a --- /dev/null +++ b/examples/org-chart/verify-p11-standalone.mjs @@ -0,0 +1,62 @@ +// Plain HTTP consumer: only the published Core ESM and CSS, no React runtime. +import fs from 'node:fs/promises'; +import path from 'node:path'; +import assert from 'node:assert/strict'; +import { launchChromium } from '../../scripts/orgchart-browser-runtime.mjs'; +const output = path.resolve('test-results/org-chart-p11'); +const fixture = path.resolve('examples/org-chart-react/dist/plain-p11'); +await fs.mkdir(fixture, { recursive: true }); +for (const file of ['index.js', 'style.css', 'engine.css']) await fs.copyFile(`packages/org-chart/dist/${file}`, path.join(fixture, file)); +await fs.copyFile('examples/org-chart/data-expanded-small.json', path.join(fixture, 'data.json')); +const theme = (await fs.readFile('examples/org-chart-react/external-theme.css', 'utf8')).replace("@import '@hm-design/org-chart/style.css';", "@import './style.css';"); +await fs.writeFile(path.join(fixture, 'external-theme.css'), theme); +await fs.writeFile(path.join(fixture, 'index.html'), 'Plain Core P11
'); +await fs.writeFile(path.join(fixture, 'app.js'), `import { createOrgChart, JsonOrgChartDataSource } from './index.js'; +const data = await (await fetch('./data.json')).json(); +window.chart = createOrgChart({ container: document.querySelector('#root'), dataSource: new JsonOrgChartDataSource(data), layoutMode: 'auto', presentationMode: 'chart-only', initialView: 'fit', wheelZoom: 'direct', stylesheetUrls: { engine: './engine.css', core: location.search ? './external-theme.css' : './style.css' } }); +await window.chart.mount();`); +const browser = await launchChromium({ headless: true }); +const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); +const requests = [], errors = [], checks = []; +page.on('request', request => requests.push(request.url())); +page.on('pageerror', error => errors.push(error.message)); +page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); }); +try { + for (const custom of [false, true]) { + await page.goto(`http://127.0.0.1:6032/plain-p11/index.html${custom ? '?theme=1' : ''}`); + await page.locator('.hm-orgchart-employee').first().waitFor(); + await page.waitForFunction(() => [...document.querySelector('.hm-orgchart-host').shadowRoot.querySelectorAll('link')].every(link => link.sheet)); + for (const [id, color] of Object.entries(custom ? { hanmac: 'rgb(36, 87, 167)', saman: 'rgb(56, 92, 168)' } : { hanmac: 'rgb(203, 46, 40)', saman: 'rgb(240, 131, 0)' })) { + assert.equal(await page.locator(`.node[data-company-id="${id}"] .hm-orgchart-organization-name`).first().evaluate(el => getComputedStyle(el).backgroundColor), color); + checks.push({ name: `${custom ? 'external' : 'default'} ${id}`, status: 'PASS' }); + } + if (custom) { + await page.locator('.hm-orgchart-employee').first().click(); + await page.locator('.hm-orgchart-employee-dialog[open]').waitFor(); + assert.equal(await page.locator('.hm-orgchart-employee-dialog').evaluate(el => getComputedStyle(el).backgroundColor), 'rgb(238, 242, 255)'); + checks.push({ name: 'plain HTTP @import and native popup', status: 'PASS' }); + } + } + const viewport = await page.evaluate(() => { + window.chart.fitToView(); const fit = window.chart.getViewportState(); + window.chart.zoomTo(Math.min(1, fit.scale * 1.3)); const zoom = window.chart.getViewportState(); + window.chart.panBy(30, 20); const pan = window.chart.getViewportState(); + window.chart.fitToView(); const restored = window.chart.getViewportState(); + return { fit, zoom, pan, restored }; + }); + assert.notEqual(viewport.fit.scale, viewport.zoom.scale); + assert.equal(viewport.pan.x - viewport.zoom.x, 30); + assert.equal(viewport.pan.y - viewport.zoom.y, 20); + assert.ok(['scale', 'x', 'y'].every(key => Math.abs(viewport.restored[key] - viewport.fit[key]) < 0.01)); + checks.push({ name: 'public zoomTo / panBy / fitToView', status: 'PASS' }); + await page.evaluate(() => window.chart.destroy()); + assert.equal(await page.locator('.hm-orgchart-host').count(), 0); + checks.push({ name: 'public destroy cleanup', status: 'PASS' }); + assert.ok(requests.every(url => !/react|node_modules/.test(new URL(url).pathname))); + assert.deepEqual(errors, []); + checks.push({ name: 'Core ESM runs without React or bundler requests', status: 'PASS' }); +} finally { + await fs.writeFile(path.join(output, 'standalone-vanilla-result.json'), JSON.stringify({ checks, requests, errors }, null, 2)); + await browser.close(); +} +console.log(`Standalone Vanilla: ${checks.length} checks, ${errors.length} errors`); diff --git a/packages/org-chart/build.mjs b/packages/org-chart/build.mjs index acadcbf..7ba49e4 100644 --- a/packages/org-chart/build.mjs +++ b/packages/org-chart/build.mjs @@ -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', 'org-chart-expanded.css'].map(async file => [file, await readFile(path.join(root, 'src/styles', file), 'utf8')])); +const inlinedStyles = await Promise.all(['design-tokens.css', 'color-presets.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); diff --git a/packages/org-chart/src/core/department-colors.ts b/packages/org-chart/src/core/department-colors.ts index 1ea895c..9d58d42 100644 --- a/packages/org-chart/src/core/department-colors.ts +++ b/packages/org-chart/src/core/department-colors.ts @@ -37,11 +37,11 @@ export function createDepartmentColors(document: Document, context: RenderContex ['테두리', 'border', 'current-border', 'background-color', `DESIGN_ADAPTATION: ${preset.borderColor}, 헤더 시작색 재사용; 원본 카드 테두리 NOT VERIFIED`], ['Gradient', 'gradient', 'current-gradient', 'background-image', `WORKDESK_COMPUTED: ${preset.backgroundImage}`], ]; - for (const [label, suffix, resolved, property, evidence] of entries) { + for (const [label, suffix, , property, evidence] of entries) { const term = document.createElement('dt'); term.textContent = label!; const detail = document.createElement('dd'); const swatch = document.createElement('span'); swatch.className = 'hm-orgchart-color-swatch'; - swatch.style.setProperty(property!, `var(--hm-orgchart-${resolved})`); + swatch.classList.add(`hm-orgchart-color-swatch-${suffix}`); if (property === 'color') { swatch.textContent = 'Aa'; swatch.style.background = suffix === 'department-color' ? 'var(--hm-orgchart-current-gradient)' : 'var(--hm-orgchart-current-card-background)'; diff --git a/packages/org-chart/src/core/expanded-content.ts b/packages/org-chart/src/core/expanded-content.ts index 358fa05..b250043 100644 --- a/packages/org-chart/src/core/expanded-content.ts +++ b/packages/org-chart/src/core/expanded-content.ts @@ -27,8 +27,6 @@ export function decorateExpandedNode(node: HTMLElement, value: RenderEngineNode, const vertical = value.vertical === true || (context.autoLayout?.type === 'hybrid' && Number(value.depth) + 1 >= context.autoLayout.hybridStartDepth); node.dataset.verticalLayout = String(vertical); const layoutDirection = context.autoLayout?.type === 'lr' || vertical ? 'left' : 'top'; - node.style.setProperty('--hm-orgchart-expanded-primary', preset.primary); - preset.secondary.forEach((color, index) => node.style.setProperty(`--hm-orgchart-expanded-secondary-${index + 1}`, color)); for (const edge of node.querySelectorAll('.edge')) edge.setAttribute('aria-hidden', 'true'); const leaderId = top?.employeeId === undefined ? organization.leaderId : top.employeeId; const leader = context.data.employees.find(employee => employee.id === leaderId && employee.organizationId === organization.id); @@ -62,7 +60,6 @@ export function decorateExpandedNode(node: HTMLElement, value: RenderEngineNode, card.draggable = false; const employeePreset = getCompanyColorPreset(affiliationId) ?? preset; card.dataset.companyId = employeePreset.companyId; - card.style.setProperty('--hm-orgchart-expanded-card-primary', `var(--hm-orgchart-company-${employeePreset.companyId})`); return card; }; if (leader && top?.display !== 'hidden' && top?.display !== 'titleOnly') { @@ -103,10 +100,6 @@ export function createExpandedOrgChartContent(document: Document, context: Rende content.dataset.groupHybrid = String(!!context.autoLayout?.hybridOrganizationIds.length); content.dataset.layoutStatus = 'hierarchical'; content.dataset.cardDesignStatus = 'current-presentation'; - for (const company of new Set([...context.organizationCompanies!.values(), ...context.data.employees.map(employee => employee.affiliationId)])) { - const preset = getCompanyColorPreset(company); - if (preset) content.style.setProperty(`--hm-orgchart-company-${company}`, preset.primary); - } tree.setAttribute('aria-label', `확장형 조직 계층도 · ${presentation.wheelZoom === 'direct' ? '휠로 확대·축소' : 'Ctrl 또는 Command + 휠로 확대'} · 드래그로 이동`); // Chart Only: titles inside organization nodes remain; the outer heading does not. if (presentation.chartOnly) content.append(tree); diff --git a/packages/org-chart/src/core/style-presets.ts b/packages/org-chart/src/core/style-presets.ts index f126413..fffe15d 100644 --- a/packages/org-chart/src/core/style-presets.ts +++ b/packages/org-chart/src/core/style-presets.ts @@ -59,18 +59,5 @@ export const organizationStylePresets = [ export function applyOrganizationStyle(element: HTMLElement, variant: number): void { const preset = organizationStylePresets[variant] ?? organizationStylePresets[0]; element.dataset.groupVariant = String(preset.id); - const prefix = preset.variablePrefix; - element.style.setProperty('--hm-orgchart-current-card-background', - `var(${prefix}-card-background, var(--hm-orgchart-card-background, ${preset.cardBackground}))`); - element.style.setProperty('--hm-orgchart-current-card-color', - `var(${prefix}-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)))`); - element.style.setProperty('--hm-orgchart-current-department-color', - `var(${prefix}-department-color, var(--hm-orgchart-department-color, ${preset.departmentColor}))`); - element.style.setProperty('--hm-orgchart-current-border', - `var(${prefix}-border, var(--hm-orgchart-border, ${preset.borderColor}))`); - element.style.setProperty('--hm-orgchart-current-gradient', - `var(${prefix}-gradient, var(--hm-orgchart-gradient, ${preset.backgroundImage}))`); - // Compatibility aliases remain local to this presentation element. - element.style.setProperty('--hm-orgchart-accent', 'var(--hm-orgchart-current-border)'); - element.style.setProperty('--hm-orgchart-group-background', 'var(--hm-orgchart-current-gradient)'); + // CSS owns defaults and the cascade; JS only selects the existing preset. } diff --git a/packages/org-chart/src/styles/color-presets.css b/packages/org-chart/src/styles/color-presets.css new file mode 100644 index 0000000..3f54c0a --- /dev/null +++ b/packages/org-chart/src/styles/color-presets.css @@ -0,0 +1,106 @@ +/* Public inputs remain undeclared so ancestor themes inherit through Shadow DOM. + Internal resolved values are declared at their actual presentation scope. + Values mirror the approved company/Fixed presets; no JS inline color writes. */ + +[data-group-variant="0"] { + --hm-orgchart-current-card-background: var(--hm-orgchart-business-management-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface))); + --hm-orgchart-current-card-color: var(--hm-orgchart-business-management-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary))); + --hm-orgchart-current-department-color: var(--hm-orgchart-business-management-department-color, var(--hm-orgchart-department-color, #ffffff)); + --hm-orgchart-current-border: var(--hm-orgchart-business-management-border, var(--hm-orgchart-border, #194d82)); + --hm-orgchart-current-gradient: var(--hm-orgchart-business-management-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #194d82 0%, #1d395c 100%))); + --hm-orgchart-accent: var(--hm-orgchart-current-border); + --hm-orgchart-group-background: var(--hm-orgchart-current-gradient); +} + +[data-group-variant="1"] { + --hm-orgchart-current-card-background: var(--hm-orgchart-business-planning-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface))); + --hm-orgchart-current-card-color: var(--hm-orgchart-business-planning-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary))); + --hm-orgchart-current-department-color: var(--hm-orgchart-business-planning-department-color, var(--hm-orgchart-department-color, #ffffff)); + --hm-orgchart-current-border: var(--hm-orgchart-business-planning-border, var(--hm-orgchart-border, #2164a8)); + --hm-orgchart-current-gradient: var(--hm-orgchart-business-planning-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #2164a8 0%, #254975 100%))); + --hm-orgchart-accent: var(--hm-orgchart-current-border); + --hm-orgchart-group-background: var(--hm-orgchart-current-gradient); +} + +[data-group-variant="2"] { + --hm-orgchart-current-card-background: var(--hm-orgchart-talent-growth-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface))); + --hm-orgchart-current-card-color: var(--hm-orgchart-talent-growth-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary))); + --hm-orgchart-current-department-color: var(--hm-orgchart-talent-growth-department-color, var(--hm-orgchart-department-color, #ffffff)); + --hm-orgchart-current-border: var(--hm-orgchart-talent-growth-border, var(--hm-orgchart-border, #0f6ace)); + --hm-orgchart-current-gradient: var(--hm-orgchart-talent-growth-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #0f6ace 0%, #044a94 100%))); + --hm-orgchart-accent: var(--hm-orgchart-current-border); + --hm-orgchart-group-background: var(--hm-orgchart-current-gradient); +} + +[data-group-variant="3"] { + --hm-orgchart-current-card-background: var(--hm-orgchart-integrated-system-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface))); + --hm-orgchart-current-card-color: var(--hm-orgchart-integrated-system-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary))); + --hm-orgchart-current-department-color: var(--hm-orgchart-integrated-system-department-color, var(--hm-orgchart-department-color, #ffffff)); + --hm-orgchart-current-border: var(--hm-orgchart-integrated-system-border, var(--hm-orgchart-border, #8c67b6)); + --hm-orgchart-current-gradient: var(--hm-orgchart-integrated-system-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #8c67b6 0%, #5d3494 100%))); + --hm-orgchart-accent: var(--hm-orgchart-current-border); + --hm-orgchart-group-background: var(--hm-orgchart-current-gradient); +} + +[data-group-variant="4"] { + --hm-orgchart-current-card-background: var(--hm-orgchart-design-planning-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface))); + --hm-orgchart-current-card-color: var(--hm-orgchart-design-planning-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary))); + --hm-orgchart-current-department-color: var(--hm-orgchart-design-planning-department-color, var(--hm-orgchart-department-color, #ffffff)); + --hm-orgchart-current-border: var(--hm-orgchart-design-planning-border, var(--hm-orgchart-border, #09a7a2)); + --hm-orgchart-current-gradient: var(--hm-orgchart-design-planning-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #09a7a2 0%, #00847f 100%))); + --hm-orgchart-accent: var(--hm-orgchart-current-border); + --hm-orgchart-group-background: var(--hm-orgchart-current-gradient); +} + +[data-group-variant="5"] { + --hm-orgchart-current-card-background: var(--hm-orgchart-solution-sales-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface))); + --hm-orgchart-current-card-color: var(--hm-orgchart-solution-sales-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary))); + --hm-orgchart-current-department-color: var(--hm-orgchart-solution-sales-department-color, var(--hm-orgchart-department-color, #ffffff)); + --hm-orgchart-current-border: var(--hm-orgchart-solution-sales-border, var(--hm-orgchart-border, #5a963c)); + --hm-orgchart-current-gradient: var(--hm-orgchart-solution-sales-gradient, var(--hm-orgchart-gradient, linear-gradient(180deg, #5a963c 0%, #3f7629 100%))); + --hm-orgchart-accent: var(--hm-orgchart-current-border); + --hm-orgchart-group-background: var(--hm-orgchart-current-gradient); +} + +.hm-orgchart-expanded [data-company-id="hanmac"] { + --hm-orgchart-expanded-primary: var(--hm-orgchart-company-hanmac, #CB2E28); + --hm-orgchart-expanded-card-primary: var(--hm-orgchart-company-hanmac, #CB2E28); + --hm-orgchart-expanded-secondary-1: var(--hm-orgchart-company-hanmac-secondary-1, #1D4289); +} + +.hm-orgchart-expanded [data-company-id="saman"] { + --hm-orgchart-expanded-primary: var(--hm-orgchart-company-saman, #F08300); + --hm-orgchart-expanded-card-primary: var(--hm-orgchart-company-saman, #F08300); + --hm-orgchart-expanded-secondary-1: var(--hm-orgchart-company-saman-secondary-1, #6D6C6C); +} + +.hm-orgchart-expanded [data-company-id="jangheon-industry"] { + --hm-orgchart-expanded-primary: var(--hm-orgchart-company-jangheon-industry, #4580A6); + --hm-orgchart-expanded-card-primary: var(--hm-orgchart-company-jangheon-industry, #4580A6); + --hm-orgchart-expanded-secondary-1: var(--hm-orgchart-company-jangheon-industry-secondary-1, #92BED4); +} + +.hm-orgchart-expanded [data-company-id="jangheon"] { + --hm-orgchart-expanded-primary: var(--hm-orgchart-company-jangheon, #1D418D); + --hm-orgchart-expanded-card-primary: var(--hm-orgchart-company-jangheon, #1D418D); + --hm-orgchart-expanded-secondary-1: var(--hm-orgchart-company-jangheon-secondary-1, #AE232A); +} + +.hm-orgchart-expanded [data-company-id="ptc"] { + --hm-orgchart-expanded-primary: var(--hm-orgchart-company-ptc, #84338E); + --hm-orgchart-expanded-card-primary: var(--hm-orgchart-company-ptc, #84338E); + --hm-orgchart-expanded-secondary-1: var(--hm-orgchart-company-ptc-secondary-1, #F7AF00); + --hm-orgchart-expanded-secondary-2: var(--hm-orgchart-company-ptc-secondary-2, #0066B5); +} + +.hm-orgchart-expanded [data-company-id="halla"] { + --hm-orgchart-expanded-primary: var(--hm-orgchart-company-halla, #0068B7); + --hm-orgchart-expanded-card-primary: var(--hm-orgchart-company-halla, #0068B7); + --hm-orgchart-expanded-secondary-1: var(--hm-orgchart-company-halla-secondary-1, #231815); +} + +.hm-orgchart-expanded [data-company-id="baron"] { + --hm-orgchart-expanded-primary: var(--hm-orgchart-company-baron, #0C367F); + --hm-orgchart-expanded-card-primary: var(--hm-orgchart-company-baron, #0C367F); + +} diff --git a/packages/org-chart/src/styles/employee-card.css b/packages/org-chart/src/styles/employee-card.css index b14eca7..2d5a54f 100644 --- a/packages/org-chart/src/styles/employee-card.css +++ b/packages/org-chart/src/styles/employee-card.css @@ -11,7 +11,7 @@ min-height: var(--hm-orgchart-resolved-card-height-employee); margin: 0; padding: 6px; - border: var(--hm-orgchart-resolved-border-width) solid var(--hm-orgchart-current-border, var(--hm-orgchart-resolved-stroke-default)); + border: var(--hm-orgchart-resolved-border-width) solid var(--hm-orgchart-card-border, var(--hm-orgchart-current-border, var(--hm-orgchart-resolved-stroke-default))); border-radius: var(--hm-orgchart-resolved-radius-container); background: var(--hm-orgchart-current-card-background, var(--hm-orgchart-resolved-bg-surface)); color: var(--hm-orgchart-current-card-color, var(--hm-orgchart-resolved-fg-primary)); @@ -37,7 +37,7 @@ min-height: var(--hm-orgchart-resolved-card-height-leader); gap: 8px; padding: 8px; - border-top: var(--hm-orgchart-resolved-border-top-width) solid var(--hm-orgchart-current-border, var(--hm-orgchart-resolved-action-primary)); + border-top: var(--hm-orgchart-resolved-border-top-width) solid var(--hm-orgchart-card-accent, var(--hm-orgchart-current-border, var(--hm-orgchart-resolved-action-primary))); background: var(--hm-orgchart-current-card-background, var(--hm-orgchart-resolved-bg-content)); } .hm-orgchart-employee[data-card-kind="topLeader"] { @@ -239,14 +239,14 @@ .hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]) .hm-orgchart-person-name { margin: 0; padding: 0; white-space: nowrap; } .hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"])[data-has-affiliation="true"] .hm-orgchart-person-role { padding: 0; } .hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]) .hm-orgchart-person-role { - color: var(--hm-orgchart-person-position-color); + color: var(--hm-orgchart-card-position-color, var(--hm-orgchart-person-position-color)); font-size: var(--hm-orgchart-person-position-size); font-weight: 400; line-height: var(--hm-orgchart-person-position-line-height); letter-spacing: var(--hm-orgchart-person-position-letter-spacing); } .hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]) .hm-orgchart-person-name { - color: #071D3A; + color: var(--hm-orgchart-card-color, #071D3A); font-size: var(--hm-orgchart-person-name-size); font-weight: 600; line-height: var(--hm-orgchart-person-name-line-height); @@ -255,7 +255,7 @@ text-overflow: clip; } .hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]) .hm-orgchart-person-name[data-name-overflow="true"] { text-overflow: ellipsis; } -.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]):is([data-card-asset-size="employee"], [data-card-layout="compact"]) .hm-orgchart-person-name { color: #323232; } +.hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]):is([data-card-asset-size="employee"], [data-card-layout="compact"]) .hm-orgchart-person-name { color: var(--hm-orgchart-card-color, #323232); } .hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]) .hm-orgchart-person-name:focus-visible { outline: 2px solid var(--hm-orgchart-resolved-stroke-input-focus); outline-offset: 1px; } .hm-orgchart-employee[data-card-asset-size="employee"] .hm-orgchart-affiliation-mark { top: 2px; right: 1px; width: 20px; height: 20px; min-height: 20px; } .hm-orgchart-employee:is([data-card-asset], [data-card-layout="compact"]):is([data-card-asset-size="employee"], [data-card-layout="compact"]) .hm-orgchart-affiliation-fallback { font-size: 6px; white-space: nowrap; } diff --git a/packages/org-chart/src/styles/employee-info-dialog.css b/packages/org-chart/src/styles/employee-info-dialog.css index ac42c17..4835afc 100644 --- a/packages/org-chart/src/styles/employee-info-dialog.css +++ b/packages/org-chart/src/styles/employee-info-dialog.css @@ -7,7 +7,7 @@ .hm-orgchart-organization-link:hover { text-decoration: underline; } .hm-orgchart-organization-link:focus-visible, .hm-orgchart-employee[role="button"]:focus-visible { - outline: 2px solid var(--hm-orgchart-resolved-stroke-input-focus); + outline: 2px solid var(--hm-orgchart-card-focus-color, var(--hm-orgchart-resolved-stroke-input-focus)); outline-offset: 3px; } .hm-orgchart-employee[role="button"] { cursor: pointer; } @@ -16,10 +16,10 @@ max-width: calc(100vw - 32px); max-height: calc(100dvh - 32px); padding: var(--hm-orgchart-resolved-spacing-xxl); - border: var(--hm-orgchart-resolved-border-width) solid var(--hm-orgchart-resolved-stroke-default); + border: var(--hm-orgchart-resolved-border-width) solid var(--hm-orgchart-dialog-border, var(--hm-orgchart-resolved-stroke-default)); border-radius: var(--hm-orgchart-resolved-radius-container); - background: var(--hm-orgchart-resolved-bg-surface); - color: var(--hm-orgchart-resolved-fg-primary); + background: var(--hm-orgchart-dialog-background, var(--hm-orgchart-resolved-bg-surface)); + color: var(--hm-orgchart-dialog-color, var(--hm-orgchart-resolved-fg-primary)); font: inherit; overflow: auto; } @@ -35,9 +35,9 @@ .hm-orgchart-employee-dialog-close { /* Match HM Dialog's ghost / icon-sm Button without a React or theme dependency. Fallbacks are the declared HM defaults (theme/default.css and primitives.css). */ - --hm-orgchart-dialog-close-ink: var(--foreground, var(--hm-sem-fg-primary, #0a221d)); - --hm-orgchart-dialog-close-hover: var(--muted, var(--hm-sem-bg-subtle, #f4f1ea)); - --hm-orgchart-dialog-close-ring: var(--ring, var(--hm-sem-stroke-input-focus, #266651)); + --hm-orgchart-dialog-close-ink: var(--hm-orgchart-dialog-close-color, var(--foreground, var(--hm-sem-fg-primary, #0a221d))); + --hm-orgchart-dialog-close-hover: var(--hm-orgchart-dialog-close-hover-background, var(--muted, var(--hm-sem-bg-subtle, #f4f1ea))); + --hm-orgchart-dialog-close-ring: var(--hm-orgchart-dialog-focus-color, var(--ring, var(--hm-sem-stroke-input-focus, #266651))); position: absolute; top: calc(var(--spacing, .25rem) * 2); right: calc(var(--spacing, .25rem) * 2); diff --git a/packages/org-chart/src/styles/org-chart-content.css b/packages/org-chart/src/styles/org-chart-content.css index abaea5d..53dd08a 100644 --- a/packages/org-chart/src/styles/org-chart-content.css +++ b/packages/org-chart/src/styles/org-chart-content.css @@ -70,7 +70,7 @@ width: 204px; padding: 10px; background: var(--hm-orgchart-current-card-background); - border-top: var(--hm-orgchart-resolved-border-top-width) solid var(--hm-orgchart-current-border); + border-top: var(--hm-orgchart-resolved-border-top-width) solid var(--hm-orgchart-group-border, var(--hm-orgchart-current-border)); } .hm-orgchart-engine.orgchart .node[data-depth="0"] { width: 244px; } .hm-orgchart-engine.orgchart .node[data-depth="0"] .hm-orgchart-organization { width: 244px; } @@ -92,12 +92,12 @@ margin: 8px 0 0; text-align: center; border-radius: var(--hm-orgchart-resolved-radius-container); - color: var(--hm-orgchart-current-department-color); + color: var(--hm-orgchart-group-title-color, var(--hm-orgchart-current-department-color)); } .hm-orgchart-engine.orgchart .node[data-header-role="organization"] .hm-orgchart-organization-name { min-height: var(--hm-orgchart-title-height, 40px); padding: 6px 12px; - background: var(--hm-orgchart-group-background); + background: var(--hm-orgchart-group-title-background, var(--hm-orgchart-group-background)); font-size: var(--hm-orgchart-title-font-size, 18px); font-weight: var(--hm-orgchart-resolved-font-weight-bold); line-height: var(--hm-orgchart-title-line-height, 26px); @@ -105,7 +105,7 @@ .hm-orgchart-engine.orgchart .node[data-header-role="group"] .hm-orgchart-organization-name { min-height: var(--hm-orgchart-group-header-height, 32px); padding: 4px 8px; - background: var(--hm-orgchart-current-border); + background: var(--hm-orgchart-group-title-background, var(--hm-orgchart-current-border)); font-size: var(--hm-orgchart-group-header-font-size, 14px); font-weight: var(--hm-orgchart-resolved-font-weight-bold); line-height: var(--hm-orgchart-group-header-line-height, 20px); @@ -131,6 +131,11 @@ .hm-orgchart-color-legend { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: var(--hm-orgchart-resolved-spacing-sm); } .hm-orgchart-color-legend dd { margin: 0; display: grid; gap: 4px; } .hm-orgchart-color-swatch { display: block; width: 44px; height: 22px; border: 1px solid var(--hm-orgchart-resolved-stroke-default); } +.hm-orgchart-color-swatch-card-background { background-color: var(--hm-orgchart-current-card-background); } +.hm-orgchart-color-swatch-card-color { color: var(--hm-orgchart-current-card-color); } +.hm-orgchart-color-swatch-department-color { color: var(--hm-orgchart-current-department-color); } +.hm-orgchart-color-swatch-border { background-color: var(--hm-orgchart-current-border); } +.hm-orgchart-color-swatch-gradient { background-image: var(--hm-orgchart-current-gradient); } .hm-orgchart-color-swatch[data-css-property="color"] { text-align: center; font-weight: var(--hm-orgchart-resolved-font-weight-bold); } .hm-orgchart-contrast-normal, .hm-orgchart-contrast-candidate, .hm-orgchart-contrast-chip { margin: var(--hm-orgchart-resolved-spacing-sm) 0; diff --git a/packages/org-chart/src/styles/org-chart-expanded.css b/packages/org-chart/src/styles/org-chart-expanded.css index 62f3548..6a24938 100644 --- a/packages/org-chart/src/styles/org-chart-expanded.css +++ b/packages/org-chart/src/styles/org-chart-expanded.css @@ -7,7 +7,7 @@ --hm-orgchart-expanded-employee-gap: var(--hm-orgchart-resolved-spacing-md); --hm-orgchart-expanded-line-width: var(--hm-orgchart-resolved-border-width); /* Meaningful Auto relationships: separate from neutral card/Fixed borders. */ - --hm-orgchart-expanded-line-color: #738399; + --hm-orgchart-resolved-expanded-line-color: var(--hm-orgchart-expanded-line-color, #738399); display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--hm-orgchart-resolved-spacing-lg); @@ -71,8 +71,8 @@ height: auto; min-height: 0; padding: 0; - background: var(--hm-orgchart-expanded-primary); - color: #ffffff; + background: var(--hm-orgchart-group-title-background, var(--hm-orgchart-expanded-primary)); + color: var(--hm-orgchart-group-title-color, #ffffff); font-size: var(--hm-orgchart-resolved-font-size-base); font-weight: var(--hm-orgchart-resolved-font-weight-bold); line-height: 1.5; @@ -90,11 +90,35 @@ .hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-presentation="expanded"] { margin: 0; box-sizing: border-box; - border: var(--hm-orgchart-expanded-card-border-width, var(--hm-orgchart-resolved-border-width)) solid var(--hm-orgchart-resolved-stroke-default); - background: var(--hm-orgchart-resolved-bg-surface); - color: var(--hm-orgchart-resolved-fg-primary); + border: var(--hm-orgchart-expanded-card-border-width, var(--hm-orgchart-resolved-border-width)) solid var(--hm-orgchart-card-border, var(--hm-orgchart-resolved-stroke-default)); + background: var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)); + color: var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary)); } .hm-orgchart-expanded .hm-orgchart-employee[data-card-presentation="expanded"]::before { content: none; } +.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-kind="leader"] { + background: var(--hm-orgchart-leader-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface))); +} +.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-kind="topLeader"] { + background: var(--hm-orgchart-top-leader-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface))); +} +.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="dispatched"] { + --hm-orgchart-resolved-status-card-background: var(--hm-orgchart-dispatched-card-gradient, var(--hm-orgchart-resolved-role-card-background)); + background: var(--hm-orgchart-resolved-status-card-background); + border-color: var(--hm-orgchart-dispatched-card-border, var(--hm-orgchart-card-border, var(--hm-orgchart-resolved-stroke-default))); + color: var(--hm-orgchart-dispatched-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary))); +} +.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-work-status="onLeave"] { + --hm-orgchart-resolved-status-card-background: var(--hm-orgchart-on-leave-card-gradient, var(--hm-orgchart-resolved-role-card-background)); + background: var(--hm-orgchart-resolved-status-card-background); + border-color: var(--hm-orgchart-on-leave-card-border, var(--hm-orgchart-card-border, var(--hm-orgchart-resolved-stroke-default))); + color: var(--hm-orgchart-on-leave-card-color, var(--hm-orgchart-card-color, var(--hm-orgchart-resolved-fg-primary))); +} +.hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-presentation="expanded"]:hover { + background: var(--hm-orgchart-card-hover-background, var(--hm-orgchart-resolved-status-card-background, var(--hm-orgchart-resolved-role-card-background))); +} +.hm-orgchart-expanded .hm-orgchart-employee[data-card-kind="employee"] { --hm-orgchart-resolved-role-card-background: var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface)); } +.hm-orgchart-expanded .hm-orgchart-employee[data-card-kind="leader"] { --hm-orgchart-resolved-role-card-background: var(--hm-orgchart-leader-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface))); } +.hm-orgchart-expanded .hm-orgchart-employee[data-card-kind="topLeader"] { --hm-orgchart-resolved-role-card-background: var(--hm-orgchart-top-leader-card-background, var(--hm-orgchart-card-background, var(--hm-orgchart-resolved-bg-surface))); } .hm-orgchart-expanded .hm-orgchart-employee[data-card-presentation="expanded"] .hm-orgchart-avatar { border-radius: 0; } .hm-orgchart-expanded .hm-orgchart-person-text { display: flex; flex-direction: column; gap: 0; } .hm-orgchart-expanded .hm-orgchart-employee::after { content: none; } @@ -106,7 +130,7 @@ width: 100%; height: 0; border-width: var(--hm-orgchart-expanded-line-width) 0 0; - border-color: var(--hm-orgchart-expanded-line-color); + border-color: var(--hm-orgchart-resolved-expanded-line-color); } .hm-orgchart-expanded .hm-orgchart-engine.orgchart .hierarchy:has(> .node[data-depth]) > .nodes > .hierarchy:first-child::before { left: calc(50% - 1px); width: calc(50% + 1px); } .hm-orgchart-expanded .hm-orgchart-engine.orgchart .hierarchy:has(> .node[data-depth]) > .nodes > .hierarchy:last-child::before { width: calc(50% + 1px); } @@ -115,13 +139,13 @@ top: calc(-1 * var(--hm-orgchart-expanded-level-gap) / 2); height: calc(var(--hm-orgchart-expanded-level-gap) / 2); width: var(--hm-orgchart-expanded-line-width); - background-color: var(--hm-orgchart-expanded-line-color); + background-color: var(--hm-orgchart-resolved-expanded-line-color); } .hm-orgchart-expanded .hm-orgchart-engine.orgchart .hierarchy:not(.couple) > .node:has(+ .nodes)::after { bottom: calc(-1 * var(--hm-orgchart-expanded-level-gap) / 2); height: calc(100% - var(--hm-orgchart-expanded-content-height) + var(--hm-orgchart-expanded-level-gap) / 2); width: var(--hm-orgchart-expanded-line-width); - background-color: var(--hm-orgchart-expanded-line-color); + background-color: var(--hm-orgchart-resolved-expanded-line-color); } /* Standalone content variables, independent of Fixed and HM React controls. */ @@ -171,7 +195,7 @@ padding-inline: var(--hm-orgchart-resolved-spacing-xxl); } .hm-orgchart-expanded .hm-orgchart-employee-connectors { position: absolute; inset: 0; overflow: visible; pointer-events: none; } -.hm-orgchart-expanded .hm-orgchart-employee-connectors path { fill: none; stroke: var(--hm-orgchart-expanded-line-color); stroke-width: var(--hm-orgchart-expanded-line-width); } +.hm-orgchart-expanded .hm-orgchart-employee-connectors path { fill: none; stroke: var(--hm-orgchart-resolved-expanded-line-color); stroke-width: var(--hm-orgchart-expanded-line-width); } .hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-presentation="expanded"] { padding: var(--hm-orgchart-expanded-card-padding); border-radius: var(--hm-orgchart-expanded-card-radius); @@ -185,16 +209,16 @@ } /* Inset accents preserve all outer dimensions even when accent width changes. */ .hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-presentation="expanded"][data-connection-direction="top"] { - box-shadow: inset 0 var(--hm-orgchart-expanded-card-accent-width) 0 var(--hm-orgchart-expanded-card-primary); - border-top-color: var(--hm-orgchart-expanded-card-primary); + box-shadow: inset 0 var(--hm-orgchart-expanded-card-accent-width) 0 var(--hm-orgchart-card-accent, var(--hm-orgchart-expanded-card-primary)); + border-top-color: var(--hm-orgchart-card-accent, var(--hm-orgchart-expanded-card-primary)); } .hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-presentation="expanded"][data-connection-direction="left"] { - box-shadow: inset var(--hm-orgchart-expanded-card-accent-width) 0 0 var(--hm-orgchart-expanded-card-primary); - border-left-color: var(--hm-orgchart-expanded-card-primary); + box-shadow: inset var(--hm-orgchart-expanded-card-accent-width) 0 0 var(--hm-orgchart-card-accent, var(--hm-orgchart-expanded-card-primary)); + border-left-color: var(--hm-orgchart-card-accent, var(--hm-orgchart-expanded-card-primary)); } .hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee[data-card-presentation="expanded"][data-connection-direction="right"] { - box-shadow: inset calc(-1 * var(--hm-orgchart-expanded-card-accent-width)) 0 0 var(--hm-orgchart-expanded-card-primary); - border-right-color: var(--hm-orgchart-expanded-card-primary); + box-shadow: inset calc(-1 * var(--hm-orgchart-expanded-card-accent-width)) 0 0 var(--hm-orgchart-card-accent, var(--hm-orgchart-expanded-card-primary)); + border-right-color: var(--hm-orgchart-card-accent, var(--hm-orgchart-expanded-card-primary)); } .hm-orgchart-expanded .hm-orgchart-engine.orgchart .node[data-depth] .hm-orgchart-employee .hm-orgchart-avatar { width: var(--hm-orgchart-expanded-photo-size); height: var(--hm-orgchart-expanded-photo-size); flex-basis: var(--hm-orgchart-expanded-photo-size); @@ -258,7 +282,7 @@ position: absolute; inset: 0; overflow: visible; pointer-events: none; } .hm-orgchart-expanded .hm-orgchart-organization-connectors path { - fill: none; stroke: var(--hm-orgchart-expanded-line-color); stroke-width: var(--hm-orgchart-expanded-line-width); + fill: none; stroke: var(--hm-orgchart-resolved-expanded-line-color); stroke-width: var(--hm-orgchart-expanded-line-width); } /* P9.3: outlines do not resize cards or replace company accents/connectors. */ diff --git a/packages/org-chart/src/styles/org-chart-readonly.css b/packages/org-chart/src/styles/org-chart-readonly.css index 7a9d360..319cb32 100644 --- a/packages/org-chart/src/styles/org-chart-readonly.css +++ b/packages/org-chart/src/styles/org-chart-readonly.css @@ -7,7 +7,7 @@ --hm-orgchart-group-title-font-size: 20px; --hm-orgchart-group-title-font-weight: 700; --hm-orgchart-group-title-line-height: 19.7px; - background: #F5F8FB; + background: var(--hm-orgchart-bg-content, #F5F8FB); } .hm-orgchart-composition[data-display-mode="readOnly"] .hm-orgchart-stage { position: relative; @@ -26,7 +26,7 @@ min-width: 1920px; min-height: 914.5px; padding: 12px 0; - background: #F5F8FB; + background: var(--hm-orgchart-bg-content, #F5F8FB); } .hm-orgchart-composition[data-display-mode="readOnly"] .hm-orgchart-heading { grid-area: heading; justify-self: start; align-self: start; max-width: calc(100% - 52px - 96px); margin: 0 0 0 52px; } .hm-orgchart-composition[data-display-mode="readOnly"] .hm-orgchart-title { font-size: 32px; line-height: 40px; letter-spacing: -0.5px; } @@ -121,7 +121,7 @@ min-width: 114px; min-height: 30.4px; padding: 0; - background: var(--hm-orgchart-current-gradient); + background: var(--hm-orgchart-group-title-background, var(--hm-orgchart-current-gradient)); border-radius: 7.1px; box-shadow: 0 3.6px 8px rgba(45, 73, 104, 0.2); font-size: var(--hm-orgchart-group-title-font-size); @@ -201,6 +201,8 @@ /* Keep the logo above the unchanged position row. */ .hm-orgchart-composition[data-display-mode="readOnly"] .hm-orgchart-employee[data-card-asset-size="leader"] .hm-orgchart-affiliation-mark { top: 0; } .hm-orgchart-workdesk-overlay { position: absolute; inset: 0; width: 1920px; height: 914.5px; z-index: 4; pointer-events: none; } +.hm-orgchart-workdesk-overlay path[data-connector] { stroke: var(--hm-orgchart-fixed-line-color, #45639F); } +.hm-orgchart-workdesk-overlay path[data-arrow-head] { fill: var(--hm-orgchart-fixed-line-color, #45639F); } .hm-orgchart-workdesk-decoration { position: absolute; z-index: 0; pointer-events: none; user-select: none; object-fit: contain; } .hm-orgchart-workdesk-ring { top: 0; left: 50%; width: 1757px; height: 901px; translate: -50% 0; mix-blend-mode: multiply; } .hm-orgchart-workdesk-sky { inset: 0; width: 100%; height: 100%; object-position: top center; opacity: 0.1; } @@ -270,7 +272,7 @@ min-height: 24px; margin: 0; border-radius: var(--hm-orgchart-resolved-radius-container); - color: var(--hm-orgchart-resolved-on-action-primary); + color: var(--hm-orgchart-status-title-color, var(--hm-orgchart-resolved-on-action-primary)); font-size: var(--hm-orgchart-group-title-font-size); font-weight: var(--hm-orgchart-group-title-font-weight); line-height: var(--hm-orgchart-group-title-line-height); diff --git a/packages/org-chart/src/styles/org-chart.css b/packages/org-chart/src/styles/org-chart.css index b2d41d5..ae77864 100644 --- a/packages/org-chart/src/styles/org-chart.css +++ b/packages/org-chart/src/styles/org-chart.css @@ -1,4 +1,5 @@ @import './design-tokens.css'; +@import './color-presets.css'; @import './employee-card.css'; @import './summary-sections.css'; @import './org-chart-content.css'; @@ -25,9 +26,9 @@ .hm-orgchart-engine.orgchart .node:hover, .hm-orgchart-engine.orgchart .node.focused { background: none; } .hm-orgchart-engine.orgchart .node .edge { display: none; } .hm-orgchart-engine.orgchart .hierarchy { text-align: center; } -.hm-orgchart-engine.orgchart .hierarchy::before { border-color: var(--hm-orgchart-resolved-stroke-default); } -.hm-orgchart-engine.orgchart > ul > li > ul li > .node::before { background-color: var(--hm-orgchart-resolved-stroke-default); } -.hm-orgchart-engine.orgchart .hierarchy:not(.couple) > .node:has(+ .nodes)::after { background-color: var(--hm-orgchart-resolved-stroke-default); } +.hm-orgchart-engine.orgchart .hierarchy::before { border-color: var(--hm-orgchart-fixed-line-color, var(--hm-orgchart-resolved-stroke-default)); } +.hm-orgchart-engine.orgchart > ul > li > ul li > .node::before { background-color: var(--hm-orgchart-fixed-line-color, var(--hm-orgchart-resolved-stroke-default)); } +.hm-orgchart-engine.orgchart .hierarchy:not(.couple) > .node:has(+ .nodes)::after { background-color: var(--hm-orgchart-fixed-line-color, var(--hm-orgchart-resolved-stroke-default)); } .hm-orgchart-organization { width: 244px; padding: 12px; border: 1px solid var(--hm-orgchart-resolved-stroke-default); border-radius: var(--hm-orgchart-resolved-radius-container); background: var(--hm-orgchart-resolved-bg-surface); } .hm-orgchart-kind { display: block; font-size: 11px; color: var(--hm-orgchart-resolved-fg-secondary); } .hm-orgchart-organization-name { margin: 2px 0 0; font-size: 15px; line-height: 22px; overflow-wrap: anywhere; } diff --git a/packages/org-chart/src/styles/summary-sections.css b/packages/org-chart/src/styles/summary-sections.css index 320c85a..116251e 100644 --- a/packages/org-chart/src/styles/summary-sections.css +++ b/packages/org-chart/src/styles/summary-sections.css @@ -124,18 +124,31 @@ .hm-orgchart-status-employees { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px; margin-bottom: 12px; } .hm-orgchart-status-employees .hm-orgchart-employee { margin-top: 0; } .hm-orgchart-status-group > .hm-orgchart-summary-title { - background: linear-gradient(180deg, #87909c 0%, #68727e 100%); - border: 1px solid rgba(92, 101, 112, 0.52); + background: var(--hm-orgchart-resolved-status-title-background); + color: var(--hm-orgchart-status-title-color, var(--hm-orgchart-resolved-fg-primary)); + border: 1px solid var(--hm-orgchart-status-title-border, rgba(92, 101, 112, 0.52)); box-shadow: 0 7px 16px rgba(75, 84, 96, 0.18); } .hm-orgchart-status-group .hm-orgchart-employee[data-card-kind] { margin: 0; - background-image: linear-gradient(180deg, #f4f5f6 0%, #dfe3e7 100%) !important; - border: 1px solid rgba(120, 130, 142, 0.35); + background-image: var(--hm-orgchart-resolved-status-card-gradient); + border: 1px solid var(--hm-orgchart-resolved-status-card-border); border-radius: var(--br-3_5, var(--hm-orgchart-resolved-radius-container)); cursor: pointer; } -.hm-orgchart-status-group .hm-orgchart-employee[data-card-kind] .hm-orgchart-person-role { color: #555f6a; } -.hm-orgchart-status-group .hm-orgchart-employee[data-card-kind] .hm-orgchart-person-name { color: #3e4650; } +.hm-orgchart-status-group .hm-orgchart-employee[data-card-kind] .hm-orgchart-person-role { color: var(--hm-orgchart-status-position-color, #555f6a); } +.hm-orgchart-status-group .hm-orgchart-employee[data-card-kind] .hm-orgchart-person-name { color: var(--hm-orgchart-resolved-status-card-color); } +.hm-orgchart-status-group[data-work-status="dispatched"] { + --hm-orgchart-resolved-status-title-background: var(--hm-orgchart-dispatched-title-background, var(--hm-orgchart-status-title-background, linear-gradient(180deg, #87909c 0%, #68727e 100%))); + --hm-orgchart-resolved-status-card-gradient: var(--hm-orgchart-dispatched-card-gradient, var(--hm-orgchart-status-card-gradient, linear-gradient(180deg, #f4f5f6 0%, #dfe3e7 100%))); + --hm-orgchart-resolved-status-card-border: var(--hm-orgchart-dispatched-card-border, var(--hm-orgchart-status-card-border, rgba(120, 130, 142, 0.35))); + --hm-orgchart-resolved-status-card-color: var(--hm-orgchart-dispatched-card-color, var(--hm-orgchart-status-card-color, #3e4650)); +} +.hm-orgchart-status-group[data-work-status="onLeave"] { + --hm-orgchart-resolved-status-title-background: var(--hm-orgchart-on-leave-title-background, var(--hm-orgchart-status-title-background, linear-gradient(180deg, #87909c 0%, #68727e 100%))); + --hm-orgchart-resolved-status-card-gradient: var(--hm-orgchart-on-leave-card-gradient, var(--hm-orgchart-status-card-gradient, linear-gradient(180deg, #f4f5f6 0%, #dfe3e7 100%))); + --hm-orgchart-resolved-status-card-border: var(--hm-orgchart-on-leave-card-border, var(--hm-orgchart-status-card-border, rgba(120, 130, 142, 0.35))); + --hm-orgchart-resolved-status-card-color: var(--hm-orgchart-on-leave-card-color, var(--hm-orgchart-status-card-color, #3e4650)); +} .hm-orgchart-status-group .hm-orgchart-affiliation-mark img { filter: grayscale(1); opacity: 0.72; } .hm-orgchart-content-empty { margin: 8px 0; color: var(--hm-orgchart-resolved-fg-secondary); font-size: 12px; } diff --git a/scripts/guard-orgchart-theme.test.mjs b/scripts/guard-orgchart-theme.test.mjs new file mode 100644 index 0000000..9232ec5 --- /dev/null +++ b/scripts/guard-orgchart-theme.test.mjs @@ -0,0 +1,62 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { parse } from '@babel/parser'; +import postcss from 'postcss'; +import { themeAnalysis } from './guard-theme-analysis.mjs'; + +const workspace = fileURLToPath(new URL('../', import.meta.url)); +function walk(node, visitor, parent = null, field = null) { + if (!node || typeof node !== 'object') return; + if (node.type) visitor(node, parent, field); + for (const [key, value] of Object.entries(node)) { + if (['loc', 'start', 'end', 'comments', 'tokens', 'extra'].includes(key)) continue; + if (Array.isArray(value)) value.forEach(item => walk(item, visitor, node, key)); + else if (value && typeof value === 'object') walk(value, visitor, node, key); + } +} +function analyze(edges, source) { + const issues = []; + const file = fileURLToPath(import.meta.url); + const result = themeAnalysis({ workspace, postcss, walk, selectedFile: () => true, + records: [{ file, edges, ast: parse(source, { sourceType: 'module' }) }], + add: (severity, code, file, node, message) => issues.push({ severity, code, message }), + }); + return { ...result, issues }; +} +test('OrgChart public CSS and ?url collect the same scoped defaults', () => { + const source = 'const values = "var(--hm-orgchart-current-gradient) var(--hm-orgchart-current-card-background) var(--hm-orgchart-current-border) var(--hm-orgchart-resolved-bg-surface)";'; + const direct = analyze([{ spec: '@hm-design/org-chart/style.css', runtime: true }], source); + const shadow = analyze([{ spec: '@hm-design/org-chart/style.css?url', runtime: false }], source); + assert.deepEqual(direct.definitions, shadow.definitions); + assert.deepEqual(shadow.issues, []); + assert.equal(shadow.incomplete, false); + assert.ok(shadow.references.every(ref => ref.status === 'PASS' && ref.origin === 'PUBLIC')); +}); +test('Disconnected OrgChart CSS cannot lend definitions to UI or another Story', () => { + const source = 'const value = "var(--hm-orgchart-current-gradient, red)";'; + for (const edges of [[], [{ spec: '@hm-design/ui/theme.css', runtime: true }], [{ spec: '@hm-design/org-chart/style.css?raw', runtime: false }]]) { + assert.ok(analyze(edges, source).issues.some(issue => issue.code === 'TOKEN-001' && issue.severity === 'FAIL')); + } +}); +test('Fallbacks and JS setProperty do not waive undeclared HM tokens', () => { + const result = analyze([{ spec: '@hm-design/org-chart/style.css?url', runtime: false }], 'el.style.setProperty("--hm-fake", "red"); const value = "var(--hm-fake, red)";'); + assert.ok(result.issues.some(issue => issue.code === 'TOKEN-001' && issue.severity === 'FAIL')); +}); +test('Shared UI declaration collection and unknown token policy remain intact', () => { + const result = analyze([{ spec: '@hm-design/ui/theme.css', runtime: true }], 'const value = "var(--hm-sem-fg-primary) var(--hm-missing, blue)";'); + assert.equal(result.references.find(ref => ref.name === '--hm-sem-fg-primary').status, 'PASS'); + assert.equal(result.references.find(ref => ref.name === '--hm-missing').status, 'FAIL'); +}); +test('All seven CSS company defaults and six Fixed gradients match approved presets', () => { + const css = fs.readFileSync(path.join(workspace, 'packages/org-chart/src/styles/color-presets.css'), 'utf8'); + const presets = fs.readFileSync(path.join(workspace, 'packages/org-chart/src/company-color-presets.ts'), 'utf8'); + for (const [id, color] of [['hanmac', '#CB2E28'], ['saman', '#F08300'], ['jangheon-industry', '#4580A6'], ['jangheon', '#1D418D'], ['ptc', '#84338E'], ['halla', '#0068B7'], ['baron', '#0C367F']]) { + assert.ok(presets.includes(`primary: '${color}'`)); + assert.ok(css.includes(`var(--hm-orgchart-company-${id}, ${color})`)); + } + const fixed = fs.readFileSync(path.join(workspace, 'packages/org-chart/src/core/style-presets.ts'), 'utf8'); + for (const match of fixed.matchAll(/backgroundImage: '([^']+)'/g)) assert.ok(css.includes(match[1])); +}); diff --git a/scripts/guard-theme-analysis.mjs b/scripts/guard-theme-analysis.mjs index df9ebbe..24a472b 100644 --- a/scripts/guard-theme-analysis.mjs +++ b/scripts/guard-theme-analysis.mjs @@ -81,10 +81,17 @@ export function themeAnalysis({ records, workspace, postcss, walk, selectedFile, }; for (const r of records) { if (r.css) css(r.file, true); - for (const e of r.edges.filter(e => e.runtime)) { + for (const e of r.edges) { + // OrgChart deliberately loads its public CSS inside Shadow DOM via ?url. + // Inventory that export's declarations, without treating arbitrary raw/url + // imports as applied styles or collecting tokens from unrelated packages. + const orgChartCSS = e.spec === '@hm-design/org-chart/style.css' || e.spec === '@hm-design/org-chart/style.css?url'; + if (!e.runtime && !orgChartCSS) continue; const publicCSS = { '@hm-design/ui/theme.css': 'packages/ui/src/theme.css', '@hm-design/ui/dashboard-status.css': 'packages/ui/src/patterns/dashboard-status/dashboard-status.css', + '@hm-design/org-chart/style.css': 'packages/org-chart/src/styles/org-chart.css', + '@hm-design/org-chart/style.css?url': 'packages/org-chart/src/styles/org-chart.css', }[e.spec]; if (publicCSS) css(path.join(workspace, publicCSS)); }