Files
HMDesign/packages/org-chart/README.md
shkim4 89011a2e42
Deploy Storybook / deploy (push) Successful in 1m12s
docs: standardize Git submodule and workspace installation
2026-10-23 11:06:19 +09:00

25 KiB
Raw Permalink Blame History

@hm-design/org-chart v0.1

Framework-independent TypeScript/DOM Core backed by dabeng orgchart 6.0.0. No React runtime, wrapper, Storybook, navigation or API client. This version renders organizational hierarchy and employee/leader cards; additional relations are validated but are not drawn.

P1 composes a separate top leader, presentation-only hub, hierarchy, position/affiliation summaries and dispatched/onLeave groups. The same presentation.groups fields hide each group independently. The hub does not create a fictitious organization or change parentId.

import { createOrgChart, JsonOrgChartDataSource } from '@hm-design/org-chart';

const chart = createOrgChart({
  container: document.getElementById('chart'),
  dataSource: new JsonOrgChartDataSource(configuredJsonUrl),
  // Host/bundler resolves the two exported CSS assets to absolute URLs.
  stylesheetUrls: { engine: engineCssUrl, core: coreCssUrl },
  onStateChange(state) { /* idle/loading/ready/error/destroyed */ },
});
await chart.mount();
await chart.update(nextDataset);
await chart.reload();
chart.destroy();

displayMode (optional, default 'interactive') selects the full composition presentation. 'readOnly' is the view-only Workdesk layout: the hierarchy is fully expanded with no collapse buttons or diagnostic text (kind, ID, raw counts, vacancy, description), summaries use compact presenters of the same Summary (organization-by-position table, untitled affiliation bar and status groups), and a 1920 × 914.5 design canvas scales proportionally to the available container width and height. Standalone components ignore it.

Layout boundary (P5-2 / P5-3)

layoutMode?: 'fixed' | 'auto' is independent of displayMode. The default 'fixed' keeps the existing Workdesk composition and its display-mode behavior. 'auto' renders the complete validated hierarchy through dabeng 6.0.0. Root and same-parent siblings remain in their real hierarchy; the engine allocates recursive subtree widths. The Auto-only presenter reserves the tallest content height at each depth and observes article resizing so variable staff counts and font loading keep child rows and connectors aligned. Update/reload regenerate the same layout and dispose old observers. Auto has a transformed forest, initial Fit, 5–300% configurable Zoom, Ctrl/Command wheel Zoom, pointer Pan and keyboard Pan. Ordinary wheel scrolling remains with the page. Dialogs stay outside the forest.

Set optional Organization.companyId on company roots; descendants inherit it. Child company changes are rejected instead of mixing independent trees. Legacy roots resolve from a known leader affiliation, then the companyId fallback (default hanmac). companyId is no longer a global recoloring selector. The seven IDs are hanmac, saman, jangheon-industry, jangheon, ptc, halla, and baron. Main colors style Expanded group headers and card accents; indexed secondary variables come from that same preset. Fixed ignores this option and preserves its organization gradients. Unknown Auto IDs reject construction with TypeError. The Auto presenter shares photo/position/name and employee dialog behavior without Fixed SVG cards, coordinates or scaling. Group title DOM carries the organization ID; the full title is a native toggle with aria-expanded and disabled empty groups. Separate link icons preserve page navigation. Child collapse state survives parent collapse and company reselection. Auto uses actual Employee names/positions; three-character Korean names display with spaces; position whitespace is removed only for display, preserving raw data. P5-4 retains the current Expanded cards by user decision; the two external SVG references are no longer required. Auto geometry variables live under .hm-orgchart-expanded; there is no new public layoutConfig API. The existing employee popup still uses its displayMode policy and receives raw data.

P5-4 adds group-title Hover feedback and reveals offscreen keyboard-focused cards without changing Zoom. Drag click suppression does not block Enter/Space activation. See Expanded usage and styling for complete Vanilla/React examples, Shadow DOM CSS ownership and verification boundaries.

selectedCompanyIds / setSelectedCompanies(ids?) filter the view: undefined means all, [] means empty. Summary raw counts always describe the unmodified source; the Auto selection label reports selected company/employee counts separately. getSelectedCompanies, zoomTo, panBy, fitToView, getViewportState expose view operations. Selection requires a mounted Auto handle; Fixed rejects it.

companyTopNodes maps company IDs to optional employeeId, title, name, profileImage, organizationName and display (card, titleOnly, hidden). An explicit employee must belong to that company's root; null removes top-card assignment. Overrides affect presentation only, preserving employee IDs, popup data and parentId.

employeeConnections maps organization IDs to top/left/right staff routes in TB/Hybrid (default top). Their leaders connect from the title above; LR routes all employees from the left. SVG paths use actual article/card coordinates; each endpoint shares its direction with the corresponding single-side border accent. Employee preset follows its known affiliation, otherwise its tree. All remaining borders are neutral; an inset accent width variable preserves size. Width/height/padding/radius/photo/font/spacing variables are scoped to Auto.

const chart = createOrgChart({
  ...configuredBaseOptions,
  layoutMode: 'auto',
  displayMode: 'readOnly',
  companyId: 'hanmac',
  selectedCompanyIds: ['hanmac', 'saman'],
  companyTopNodes: { hanmac: { title: '회장', display: 'card' } },
  employeeConnections: { 'hanmac-office-0': 'left' },
});

Layout mode/company/top-node/connection/zoom ranges are captured at construction. Auto autoLayout, interactions, company selection and the employee callback use their own setters on the existing handle. See the P7 matrix in the usage guide. Standalone Tree/Summary factories keep their existing rendering paths.

examples/org-chart/data-expanded-{small,medium,large}.json contain exactly 20/100/300 unique virtual employees and 3/5/7 companies. Run node examples/org-chart/create-expanded-fixtures.mjs to reproduce them. After build/prepare, open http://127.0.0.1:4178/expanded.html. Expanded OrgChart is one of the same four public Stories. Since 2026-10-22 it is a preview-only page (7 companies x 20 = 140 virtual employees, TB, Drag & Drop off, collapse / Zoom / Pan / Fit, HM Dialog) without setting panels. Fixture and company selection live in this expanded.html example; its native Core controls are the explicit framework-independent HTML adapter, not imitations of HM React UI.

Virtual example selection (P7)

createOrgChartExampleDataset(template, selection) is a pure data provider for both Vanilla and React. Supply the existing P5-3 large JSON fixture, which contains all seven company presets. The old Small/Medium/Large files and their historical 3/5/7-company policies remain unchanged; P7 selection does not require new JSON files. The helper validates and snapshots its input, reuses organization IDs and employee records, and adds deterministic virtual employees when a company needs more staff than the original fixture contains.

import { createOrgChartExampleDataset, JsonOrgChartDataSource } from '@hm-design/org-chart';

// template is the parsed examples/org-chart/data-expanded-large.json fixture.
const allCompanyExample = createOrgChartExampleDataset(template, {
  scope: 'total', employeeCount: 100,
}); // All seven companies together: exactly 100 unique employees.

const twoCompanyExample = createOrgChartExampleDataset(template, {
  scope: 'per-company', employeeCount: 100,
  companyIds: ['hanmac', 'saman'],
}); // Exactly 100 employees per company, 200 in this source.

await chart.reload(new JsonOrgChartDataSource(twoCompanyExample));

Supported scales are exactly 20 | 100 | 300. Scope 'total' allocates that count across all seven companies using the original fixture's unequal company proportions. An optional company subset then filters those allocations without redistributing them: a single selected company therefore displays only its share of the total example. Scope 'per-company' gives every selected company the requested count, up to 2,100 employees for seven companies at 300 each. Omitting companyIds selects all seven; repeated/out-of-order IDs are normalized to preset order. All returned datasets are independent, deeply frozen snapshots. Leader IDs remain references to existing employees and are never counted twice. Missing sampled leaders are cleared while the complete, valid parent hierarchy is retained, including the original deeper and unequal branches.

The helper rejects an empty companyIds list because the data contract requires organizations. To display no companies, retain a valid source and call setSelectedCompanies([]) (or pass React selectedCompanyIds={[]}). Runtime layout, collapse state, employee popup policy, and actual API sources remain separate from example selection. The 2,100-person data count is distinct from rendering/heap performance; the earlier 300-person result does not verify it.

Historical P5-1B verification and local runtime

Core/React builds and Typechecks pass. Focused browser evidence covers the complete four-depth hierarchy, unequal employee counts, updates/reload, all seven company presets, shared HTML/React cards and popup focus, viewport overflow, and minimal Fixed baseline regression. Evidence is generated under test-results/org-chart-p5-1b; the authored runner is examples/org-chart/verify-p5-1b.mjs. Its baseline must be copied from a known pre-change build before rebuilding. The runner uses configurable ORGCHART_STORY_ORIGIN, ORGCHART_PLAYWRIGHT_MODULE, and ORGCHART_CHROME. Do not assume another PC's localhost or generated dist exists.

Use npm with package-lock.json; a fresh install uses npm ci. The existing package test runner imports Node's globSync, so use a runtime providing that API, such as the verified Node 24, for focused unit tests. This PC's Node 20.20.1 built the packages successfully; the installed Codex Node 24.19.0 ran the unit tests without changing the runner. Final reference SVG design, collapse/Zoom/Pan, real screen-reader speech/touch and historical full regression suites are not verified by this phase.

P3.2 follow-up: representative badges

Representative cards never render job-title, role or status badges, empty attribute wrappers or a badge row, including standalone specimens. They display 대표이사 / 홍 길 동, retaining the approved 146×74 asset and existing typography, with no gap between position and name. The fixture's raw representative position (경영진), employee data and dialog fields remain unchanged. Other 1Depth/2Depth badge policies are preserved. This display policy takes precedence over the historical P3.2/P3.1 notes below.

P3.2 compact typography and card logos

Large department cards use 24px names / 18px positions; compact cards use 18px names / 15px positions. Representative typography and the existing weights, line heights and colors are retained. Read-only dispatched/onLeave cards share the compact content layout (108×52 card, 24×44 photo slot), with their original gray surface and text colors. Status headers share the standard 2Depth 20px/700/19.7px typography; existing department-specific exceptions remain. Position and badge whitespace is removed only for display.

Employee-card logo contain boxes scale exactly 1.5×: small 12→18px; representative/large/status 14→21px. Original assets and footer logos stay unchanged. See P3.2 report for before/after browser measurements and preservation evidence. Historical P3.1 geometry below describes that earlier stage.

P3.1 display policy and local font loading

The OrgChart-scoped default family is Noto Sans KR; existing --hm-orgchart-font-family overrides remain available. The host must register its font-face once: HM Storybook already loads the existing HM Theme face. The HTML P1 host loads the unchanged public/fonts/NotoSansKR-variable.ttf copied to assets/fonts/; the React P1 host references that same source through its bundler. Global HM Theme files are unchanged. The Korean Core shell declares lang="ko" for consistent glyph shaping across host languages.

Read-only Full Composition suppresses only the 경영진 job-title badge. Positions (including a literal 경영진 position), other raw badges, employee records, popup fields and statistics are retained. Position whitespace is removed only in the card display. Other renderer modes keep their badge policy. All card types share one rectangular neutral SVG for a missing or failed profile image; an available profile image takes precedence. Approved cards, photo-slot dimensions and P3 status-card geometry are unchanged. See P3.1 report for focused browser evidence and limits.

Optional organizationLinks: Readonly<Record<string, string | undefined>> maps existing organization IDs to host-configured URLs. Only original hierarchy depth 1 titles become native links. Missing, undefined or blank URLs keep ordinary titles; no destination is generated. HTTP(S) and relative URLs are accepted; invalid URLs, other schemes and credentials reject construction with TypeError. Group links retain native keyboard navigation and a visible focus outline, separately from employee activation.

All employee cards, including the representative and dispatched/onLeave cards, support click, Enter and Space in both modes. By default they open a read-only native information dialog in the unscaled shell, outside the Stage. It shows only existing name, position, organization, job title, affiliation and work-status values; unavailable optional values are omitted. The readOnly fictitious display name matches the activated card (홍 길 동); underlying employee data stays unchanged. Close button, ESC, modal focus trapping and card focus restoration are provided. A name-overflow tooltip closes when information UI opens.

To use a host-owned popup, provide the additive callback below. Supplying it replaces the built-in dialog; the host then owns popup accessibility and cleanup. The callback receives the unchanged Employee and resolved existing organization/affiliation, the focusable trigger card and original event.

const chart = createOrgChart({
  ...configuredBaseOptions,
  organizationLinks: configuredOrganizationLinks,
  onEmployeeClick(employee, { organization, affiliation, trigger, originalEvent }) {
    openEmployeeInformation({ employee, organization, affiliation, trigger, originalEvent });
  },
});

Omit onEmployeeClick to use the default popup. Vanilla options are captured at construction; use a new handle to change the URL map or callback. There is no public Core setOptions method.

mount(container) also accepts the container at mount time. Constructing a handle and importing the ESM facade require no DOM. Mount requires a connected HTMLElement. One handle owns one container, preserving host siblings. Duplicate mount and update/reload before mount or after destroy reject with a usage error. Data/Source failures become structured error states and resolve the operation. Destroy is synchronous, idempotent and terminal; use a new handle to mount again. subscribe(listener) returns an unsubscribe function; getState() returns the immutable current state. Consumer callback errors do not break the Core.

update captures its input immediately and preserves the current Source. reload(source?) optionally replaces it. The last started operation wins; earlier requests are aborted, and stale responses cannot commit. Call completion means the operation was applied, superseded, or produced a controlled error, rather than that every image/font finished loading. Fixed keeps its prior regeneration policy. Auto preserves valid organization collapse IDs and transform state on toggles; reload/selection fits the new visible forest. Removed toggle focus is restored to the same organization, with viewport fallback.

Data contract

Public OrgChartDataset, Organization, Employee, Affiliation, Metadata, PresentationConfig and Source types are exported with declarations. Preserve documented names: displayOrder, jobTitle, profileImage, workStatus. The supported schema version remains 0.1-draft; no silent migration is performed. The latest implementation request requires top-level schemaVersion; metadata.schemaVersion is an optional compatibility mirror and must match when supplied. metadata.updatedAt/updateNote and dataset relations/presentation are optional extensions. The original seven design documents are historical drafts and are preserved.

Organization and employee IDs have independent namespaces. All references are checked; duplicate IDs within a collection, unknown fields, missing values, type errors, invalid dates/versions/statuses, parent cycles, unsafe markup/URLs and empty organizations produce {valid:false, issues:[{code,path,message}]} from validateOrgChartDataset. Zero employees is valid. Leaders outside their assigned organization require a future approved rule and are rejected with LEADER_APPROVAL_REQUIRED. Multiple roots remain separate trees, without a fabricated parent. Order is presentation order, then displayOrder, then code-unit ID order.

normalizeOrgChartDataset and calculateSummary accept validated datasets. calculateSummary exposes raw counts for unique original employee IDs: total, direct/inclusive per organization, position, affiliation and status. These literal source counts are not an approved business headcount. No policy gives aggregation.status='unavailable'/policyRequired. A caller may provide an explicitly approved policy (policyVersion, includedStatuses, activeStatuses, includeLeaders:true, membershipMode:'primaryOnly'). Unsupported leader exclusion/multiple-membership rules or invalid policies return unsupportedPolicy. This release does not infer business meaning for dispatched/onLeave status.

JsonOrgChartDataSource accepts an inline dataset or an HTTP(S)/relative URL, optionally {fetch}. Inline input is captured at construction; every load returns a fresh immutable snapshot. External HTTP/JSON errors are sanitized and distinguished by OrgChartSourceError.code. Source output is unknown until Core validation. Future API sources implement load(options?:{signal?:AbortSignal}):Promise<unknown> and do not change Core APIs. URL paths are injected by the host, not embedded in Core. External JSON Fetch is an HTTP example; it is not supported as a file:// transport.

Theme and CSS ownership

P1 ships an independent OrgChart design. It requires no HM Theme, React/UI peer or font download. Resolve the two exported CSS assets to URLs; the Core loads them only into its dedicated open Shadow DOM. style.css includes its own scoped defaults and contains no private CSS imports. Summary-only components require only this CSS. /browser is a classic IIFE exposing window.HMOrgChart; dist/index.js is bundled ESM with no bare package imports.

All colors, typography, spacing, borders, radius and gradients belong to src/styles/design-tokens.css and src/core/style-presets.ts. Public --hm-orgchart-* inputs inherit from the caller's container; internal --hm-orgchart-resolved-* aliases resolve defaults without overriding inherited inputs. HM Theme variables do not affect this design. Workdesk organization header gradients and white heading text are measured; neutral/card styling is a documented design adaptation, not an official Workdesk value. Existing v0.1 demos and historical validation records are preserved. P1 theme-free examples are dist/p1.html (HTTP/ESM) and dist/p1-classic.html (direct file/inline JSON). See standalone component integration.

Engine disposal boundary

Upstream provides no official destroy API. The facade clears its DOM, aborts requests and generation event signals, clears callback/data/engine references and disconnects the init observer through a narrow managed subclass hook. pan/zoom/minimap/drag/export are disabled to bound resource ownership; hierarchy expansion uses native buttons and engine show/hide methods. Transition listeners are removed with their discarded DOM. Browser verification records repeated lifecycle DOM, global event listener and active observer stability. It does not establish complete heap/GC recovery. The hook depends on pinned 6.0.0 internals and must be re-audited before an engine upgrade.

Build and examples

From the repository root:

npm run typecheck:org-chart
npm run test:org-chart
npm run build:org-chart-example
npm run dev:org-chart-example

Open http://127.0.0.1:4178/index.html for bundled ESM + external JSON. Open examples/org-chart/dist/classic.html directly for classic bundle + inline fictitious JSON. Both use package build outputs, never Spike code. Example controls demonstrate reload, employee movement/addition, destroy and recreation. Build artifacts are Git-ignored; source fixtures are checked in.

npm run test:org-chart-browser expects Playwright supplied by the verification environment. If not installed locally, set ORGCHART_PLAYWRIGHT_MODULE to its absolute index.mjs/index.js and optionally ORGCHART_CHROME to a Chrome executable. Production has no browser-test dependency. Evidence is written under test-results/org-chart-browser/.

The React wrapper owns only ref/effect lifecycle, source/props updates and cleanup, including forwarding lookup and Auto view options; HTML and React render the same Core. Standalone createOrgChartTree, createPositionSummary, createAffiliationSummary and createStatusSummary factories are exported at the package root and return the existing OrgChartHandle. The four public OrgChart Stories are Logo · Personnel Summary, Department Colors, Full Composition and Expanded OrgChart. Original verification Stories and independent component Stories remain available under the excluded internal tag.

P9.3 Auto 소속 변경

P9.4부터 dragDropEnabled?: boolean의 기본값은 false다. 아래 포인터·키보드· handle.move() 소속 변경은 dragDropEnabled: true일 때만 허용하며 OFF에서는 EDITING_DISABLED 결과를 반환한다. 순수 데이터 helper의 이동 검증·적용 계약은 유지한다. handle.setDragDropEnabled(boolean) / handle.getDragDropEnabled()로 같은 Auto 인스턴스를 갱신·조회한다. OFF 전환은 진행 중 이동과 강조·Pointer capture를 취소하고 완료된 이동·데이터·Zoom/Pan·접기 상태를 유지한다. Source 재요청·레이아웃 재생성은 없다. Fixed는 생성 옵션을 무시하며 setter는 Auto 전용 사용 오류를 반환한다.

Expanded Auto는 빈 공간 Pan과 직원 카드·그룹 제목 Drag를 구분한다. 6px 이동부터 Drag를 시작하며 단순 클릭은 기존 정보 팝업·접기 동작이다. Alt+M → 방향키 → Enter로도 이동하며 Esc로 취소한다. Fixed에는 편집을 적용하지 않는다.

handle.move({ kind: 'employee' | 'organization', sourceId, targetOrganizationId })는 포인터·키보드와 같은 검증 정책을 사용한다. handle.getData()는 현재 Auto의 동결된 데이터를 반환하며 onMove(result)로 성공(moved: true)·거절 결과를 받는다.

회사 간 직원 이동은 organizationId와 affiliationId를 대상 조직·회사로 변경한다. 회사 간 그룹 이동은 parentId, 전체 하위 조직의 companyId, 해당 조직 직원들의 affiliationId를 함께 변경한다. 같은 회사 내 이동은 기존 관계사 소속을 유지한다. 회사 Root·조직장 직원·자기 자신·자손으로의 이동은 제한한다. 성공 후 기존 엔진으로 재배치하며 대상 회사 프리셋과 새로운 조직/관계사 인원 집계를 적용한다.

데이터는 브라우저 인스턴스에만 유지한다. 회사 표시 선택은 이동 내용을 보존하고, 새 source의 reload/update는 새 데이터로 대체한다. JSON 파일·서버 저장은 수행하지 않는다.

AI coding tools

AI coding tools should read ai/AI_GUIDE.md first (shared HMDesign entry point), then the OrgChart rules in ai/AI_USAGE.md (Chart Only / Full Page pre-selection, data rules), ai/COMPONENT_CATALOG.md (Core and React API), ai/DESIGN_RULES.md and ai/COMPOSITION_PATTERNS.md. ai/ is included in the packed package. To make a consumer project pick them up automatically, copy ai/AGENTS_SNIPPET.md into its root AGENTS.md.