feat(org-chart): support external CSS themes and align documentation

This commit is contained in:
shkim4 committed 2026-10-13 22:43:21 +09:00
1 parent dcfa4e0895
commit 346c648c09
27 files changed
+1189 -75

No files matched your search

+62
View File
@@ -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]));
});
+8 -1
View File
@@ -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));
}