Initial commit: HMDesignSystem baseline
This commit is contained in:
commit
b6a0a61103
486 files changed
+68432
No files matched your search
@@ -0,0 +1,96 @@
|
||||
import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { execFileSync } from "node:child_process"
|
||||
import { existsSync } from "node:fs"
|
||||
import { build } from "vite"
|
||||
|
||||
const root = fileURLToPath(new URL("../", import.meta.url))
|
||||
const name = process.argv[2]
|
||||
if (!["ui", "center-graphic"].includes(name)) throw new Error("Expected ui or center-graphic")
|
||||
const packageRoot = path.join(root, "packages", name)
|
||||
const output = path.join(packageRoot, "dist")
|
||||
const manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"))
|
||||
const externals = Object.keys({ ...manifest.dependencies, ...manifest.peerDependencies })
|
||||
|
||||
// Only this package's generated output is removed. Library builds never copy root public assets.
|
||||
await rm(output, { recursive: true, force: true })
|
||||
await build({
|
||||
configFile: false,
|
||||
root: packageRoot,
|
||||
publicDir: false,
|
||||
build: {
|
||||
target: "es2022",
|
||||
outDir: output,
|
||||
minify: false,
|
||||
lib: {
|
||||
entry: name === "ui"
|
||||
? { index: path.join(packageRoot, "src/index.ts"), "theme/index": path.join(packageRoot, "src/theme/index.ts") }
|
||||
: { index: path.join(packageRoot, "src/index.ts"), reference: path.join(packageRoot, "src/reference.ts") },
|
||||
formats: ["es"],
|
||||
fileName: (_format, entry) => `${entry}.js`,
|
||||
cssFileName: "style",
|
||||
},
|
||||
rolldownOptions: {
|
||||
external: id => externals.some(dep => id === dep || id.startsWith(`${dep}/`)),
|
||||
onwarn(warning, warn) {
|
||||
// The UI component entrypoint retains the directive below; source directives are redundant.
|
||||
if (name === "ui" && warning.code === "MODULE_LEVEL_DIRECTIVE") return
|
||||
warn(warning)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
execFileSync(process.execPath, [path.join(root, "node_modules/typescript/bin/tsc"), "-p", path.join(packageRoot, "tsconfig.build.json")], { stdio: "inherit" })
|
||||
|
||||
// Declarations describe APIs, not stylesheet loading; explicit .js specifiers also support NodeNext.
|
||||
async function finishDeclarations(directory) {
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const file = path.join(directory, entry.name)
|
||||
if (entry.isDirectory()) await finishDeclarations(file)
|
||||
else if (entry.name.endsWith(".d.ts")) {
|
||||
const text = (await readFile(file, "utf8"))
|
||||
.replace(/^import\s+["'][^"']+\.css["'];?\s*$/gm, "")
|
||||
.replace(/(from\s+["']|import\(["'])(\.\.?\/[^"']+)(["'])/g, (_match, before, specifier, after) => {
|
||||
const target = path.resolve(directory, specifier)
|
||||
const resolved = existsSync(`${target}.d.ts`) ? specifier
|
||||
: existsSync(path.join(target, "index.d.ts")) ? `${specifier}/index` : specifier
|
||||
return `${before}${resolved.endsWith(".js") ? resolved : `${resolved}.js`}${after}`
|
||||
})
|
||||
await writeFile(file, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
await finishDeclarations(output)
|
||||
|
||||
if (name === "ui") {
|
||||
const entry = path.join(output, "index.js")
|
||||
await writeFile(entry, `"use client";\n${await readFile(entry, "utf8")}`)
|
||||
await mkdir(path.join(output, "theme"), { recursive: true })
|
||||
for (const entry of await readdir(path.join(packageRoot, "src/theme"))) {
|
||||
if (entry.endsWith(".css")) await cp(path.join(packageRoot, "src/theme", entry), path.join(output, "theme", entry))
|
||||
}
|
||||
await mkdir(path.join(output, "vendor"), { recursive: true })
|
||||
// Preserve the exact upstream CSS rather than shipping its CLI dependency graph to consumers.
|
||||
await cp(path.join(root, "node_modules/shadcn/dist/tailwind.css"), path.join(output, "vendor/shadcn-tailwind.css"))
|
||||
await cp(path.join(root, "node_modules/shadcn/LICENSE.md"), path.join(output, "vendor/shadcn-LICENSE.md"))
|
||||
const tailwind = (await readFile(path.join(packageRoot, "src/tailwind.css"), "utf8"))
|
||||
.replace('"shadcn/tailwind.css"', '"./vendor/shadcn-tailwind.css"')
|
||||
.replace(/@source "\.\/components";\r?\n@source "\.\/patterns";/, '@source "./index.js";')
|
||||
await writeFile(path.join(output, "tailwind.css"), tailwind)
|
||||
await mkdir(path.join(output, "fonts"), { recursive: true })
|
||||
for (const file of ["NotoSansKR-variable.ttf", "NotoSansKR-OFL.txt"]) {
|
||||
await cp(path.join(root, "public/fonts", file), path.join(output, "fonts", file))
|
||||
}
|
||||
const theme = (await readFile(path.join(packageRoot, "src/theme.css"), "utf8"))
|
||||
.replace('/fonts/NotoSansKR-variable.ttf', './fonts/NotoSansKR-variable.ttf')
|
||||
await writeFile(path.join(output, "theme.css"), theme)
|
||||
await cp(path.join(packageRoot, "src/patterns/dashboard-status/dashboard-status.css"), path.join(output, "dashboard-status.css"))
|
||||
} else {
|
||||
// Preserve CENTER's existing automatic component CSS behavior for both public entrypoints.
|
||||
for (const entry of ["index.js", "reference.js"]) {
|
||||
const file = path.join(output, entry)
|
||||
await writeFile(file, `import "./style.css";\n${await readFile(file, "utf8")}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const root = process.cwd();
|
||||
const out = path.join(root, 'HMDesignSystem-AI-Reference-Test');
|
||||
const zip = path.join(root, 'HMDesignSystem-AI-Reference-Test.zip');
|
||||
|
||||
fs.rmSync(out, { recursive: true, force: true });
|
||||
fs.rmSync(zip, { force: true });
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
|
||||
const copy = (from, to, options = {}) => {
|
||||
const src = path.join(root, from);
|
||||
if (!fs.existsSync(src)) return false;
|
||||
const dest = path.join(out, to);
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.cpSync(src, dest, { recursive: true, force: true, ...options });
|
||||
return true;
|
||||
};
|
||||
|
||||
// Preserve repository-relative paths for source references and imports.
|
||||
copy('packages/ui/src', 'source/packages/ui/src');
|
||||
copy('packages/ui/package.json', 'source/packages/ui/package.json');
|
||||
copy('packages/ui/tsconfig.json', 'source/packages/ui/tsconfig.json');
|
||||
copy('packages/ui/tsconfig.build.json', 'source/packages/ui/tsconfig.build.json');
|
||||
copy('packages/ui/components.json', 'source/packages/ui/components.json');
|
||||
copy('packages/center-graphic/src', 'source/packages/center-graphic/src');
|
||||
copy('packages/center-graphic/package.json', 'source/packages/center-graphic/package.json');
|
||||
copy('packages/center-graphic/tsconfig.json', 'source/packages/center-graphic/tsconfig.json');
|
||||
copy('packages/center-graphic/tsconfig.build.json', 'source/packages/center-graphic/tsconfig.build.json');
|
||||
|
||||
copy('.storybook', 'storybook/config');
|
||||
copy('src', 'storybook/source');
|
||||
copy('storybook-static', 'storybook/static');
|
||||
|
||||
copy('dist', 'packages/app-build');
|
||||
copy('dist-dashboard', 'packages/dashboard-export-build');
|
||||
copy('packages/ui/dist', 'packages/ui-dist');
|
||||
copy('packages/center-graphic/dist', 'packages/center-graphic-dist');
|
||||
copy('theme-dist', 'theme/generated');
|
||||
copy('themes', 'theme/source');
|
||||
copy('packages/ui/src/theme.css', 'theme/source/packages-ui-theme.css');
|
||||
copy('packages/ui/src/theme', 'theme/source/packages-ui-theme');
|
||||
|
||||
copy('docs', 'docs/source');
|
||||
copy('AGENTS.md', 'docs/AGENTS.md');
|
||||
copy('CLAUDE.md', 'docs/CLAUDE.md');
|
||||
copy('README.md', 'docs/README.md');
|
||||
copy('package.json', 'docs/root-package.json');
|
||||
copy('package-lock.json', 'docs/package-lock.json');
|
||||
|
||||
copy('src/exports', 'examples/react-and-html/src/exports');
|
||||
copy('src/exports/dashboard', 'examples/react-and-html/src/exports/dashboard');
|
||||
copy('index.html', 'examples/react-and-html/index.html');
|
||||
copy('vite.dashboard-export.config.ts', 'examples/react-and-html/vite.dashboard-export.config.ts');
|
||||
copy('scripts/finalize-dashboard-export.mjs', 'examples/react-and-html/finalize-dashboard-export.mjs');
|
||||
copy('public/fonts', 'assets/fonts');
|
||||
copy('packages/center-graphic/src/assets', 'assets/center-graphic');
|
||||
|
||||
const files = (dir) => fs.existsSync(dir) ? fs.readdirSync(dir, { recursive: true }).filter((p) => fs.statSync(path.join(dir, p)).isFile()).length : 0;
|
||||
const countFiles = (rel, suffix) => {
|
||||
const dir = path.join(root, rel);
|
||||
if (!fs.existsSync(dir)) return 0;
|
||||
return fs.readdirSync(dir, { recursive: true }).filter((p) => String(p).endsWith(suffix)).length;
|
||||
};
|
||||
const publicComponentFiles = countFiles('packages/ui/src/components', '.tsx');
|
||||
const patternFiles = countFiles('packages/ui/src/patterns', '.tsx');
|
||||
const storyFiles = ['.stories.tsx', '.stories.ts', '.stories.jsx', '.stories.js'].reduce((n, s) => n + countFiles('src', s), 0);
|
||||
const generatedAt = new Date().toISOString();
|
||||
|
||||
const startHere = `# HM Design System AI Reference
|
||||
|
||||
이 ZIP은 빌드 패키지만이 아니라 HM Design System의 실제 구현·Storybook·Theme·문서를 함께 참조하기 위한 테스트 배포본입니다.
|
||||
|
||||
## AI 참조 순서
|
||||
|
||||
1. \`docs/AGENTS.md\`와 \`docs/README.md\`를 읽습니다.
|
||||
2. \`docs/source/components\`, \`docs/source/patterns\`, \`docs/source/guidelines\`에서 사용 규칙을 확인합니다.
|
||||
3. \`source/packages/ui/src\`와 \`source/packages/center-graphic/src\`의 실제 TSX 구현을 재사용합니다.
|
||||
4. \`storybook/source\`의 Story에서 상태·조합·레이아웃을 확인합니다.
|
||||
5. \`theme/source\` 및 \`theme/generated\`의 Token/CSS Custom Properties를 사용합니다.
|
||||
6. \`packages/ui-dist\`와 declarations는 소비자 Export 형태를 확인할 때 사용합니다.
|
||||
|
||||
임의의 색상·spacing·radius·Typography 값을 추가하거나 디자인을 재해석하지 않습니다. 신규 화면은 기존 공개 Component/Pattern과 Theme 계약을 우선 재사용합니다.
|
||||
|
||||
## 구조
|
||||
|
||||
- \`source/\`: 원본 공개 패키지 구현 및 내부 의존 코드
|
||||
- \`storybook/\`: Storybook 설정, Story/Pattern/Foundation 소스, 정적 Storybook
|
||||
- \`packages/\`: 기존 빌드 산출물 및 TypeScript declarations
|
||||
- \`theme/\`: Token 원본, Generated CSS, Runtime Theme 자료
|
||||
- \`docs/\`: 원본 사용 규칙, AI 공통 가이드, 컴포넌트·패턴 문서
|
||||
- \`examples/\`: React/HTML export 관련 예제 및 계약
|
||||
- \`assets/\`: Font와 Center Graphic asset
|
||||
|
||||
## 재현 범위
|
||||
|
||||
React는 기존 공개 패키지를 직접 import하는 방식을 우선합니다. HTML은 \`examples/react-and-html\`의 기존 export와 Runtime 계약을 먼저 확인합니다. 이 배포본 자체에서 무료 AI에 의한 신규 화면 생성이나 브라우저 시각 검증은 수행하지 않았습니다.
|
||||
`;
|
||||
fs.writeFileSync(path.join(out, 'AI_START_HERE.md'), startHere, 'utf8');
|
||||
|
||||
const manifest = {
|
||||
package: 'HMDesignSystem-AI-Reference-Test', generatedAt,
|
||||
source: { publicComponentSourceFiles: publicComponentFiles, centerGraphicSourceFiles: files(path.join(out, 'source/packages/center-graphic/src')) },
|
||||
storybook: { storyFiles, sourceFiles: files(path.join(out, 'storybook/source')), staticIncluded: fs.existsSync(path.join(out, 'storybook/static')) },
|
||||
patterns: { patternSourceFiles: patternFiles },
|
||||
theme: { sourceIncluded: fs.existsSync(path.join(out, 'theme/source')), generatedIncluded: fs.existsSync(path.join(out, 'theme/generated')) },
|
||||
assets: { fontFiles: files(path.join(out, 'assets/fonts')), centerGraphicAssetFiles: files(path.join(out, 'assets/center-graphic')) },
|
||||
verification: { aiReproduction: 'NOT_RUN', browserVisualVerification: 'NOT_RUN', sourceMutation: 'NOT_RUN (packaging-only)' }
|
||||
};
|
||||
fs.writeFileSync(path.join(out, 'MANIFEST.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
||||
|
||||
execFileSync('tar', ['-a', '-c', '-f', zip, '-C', root, path.basename(out)], { stdio: 'inherit' });
|
||||
console.log(JSON.stringify({ out, zip, manifest }, null, 2));
|
||||
@@ -0,0 +1,17 @@
|
||||
import { readFile, writeFile } from "node:fs/promises"
|
||||
|
||||
const path = "dist-dashboard/index.html"
|
||||
const html = await readFile(path, "utf8")
|
||||
const finalized = html.replace(
|
||||
'<script type="module" crossorigin src="./assets/dashboard-export.js"></script>',
|
||||
'<script defer src="./assets/dashboard-export.js"></script>',
|
||||
).replace(
|
||||
/(<link\s+rel="stylesheet")\s+crossorigin(\s+href="\.\/assets\/[^\"]+\.css")/,
|
||||
"$1$2",
|
||||
)
|
||||
|
||||
if (finalized === html) {
|
||||
throw new Error(`Dashboard export script tag was not found in ${path}`)
|
||||
}
|
||||
|
||||
await writeFile(path, finalized)
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import crypto from 'node:crypto';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { build } from 'esbuild';
|
||||
|
||||
const args = new Map();
|
||||
for (let i = 2; i < process.argv.length; i += 1) {
|
||||
const value = process.argv[i];
|
||||
if (value.startsWith('--')) args.set(value.slice(2), process.argv[i + 1]?.startsWith('--') ? true : process.argv[++i]);
|
||||
}
|
||||
const input = args.get('input');
|
||||
const output = args.get('output');
|
||||
const screens = Number(args.get('screens') || 1);
|
||||
const root = process.cwd();
|
||||
const runId = `${new Date().toISOString().replace(/[:.]/g, '-')}-${crypto.randomBytes(4).toString('hex')}`;
|
||||
const reportDir = output ? path.resolve(output) : path.resolve(root, '_local', 'h2-preview', runId);
|
||||
const failurePath = path.join(reportDir, 'verification-failure.json');
|
||||
const rel = (p) => path.relative(root, p) || '.';
|
||||
const fail = (code, message, details = {}) => {
|
||||
fs.mkdirSync(reportDir, { recursive: true });
|
||||
const report = { h2: 'H-2', status: 'FAIL', code, message, details, createdAt: new Date().toISOString(), input: input ? path.resolve(input) : null };
|
||||
fs.writeFileSync(failurePath, JSON.stringify(report, null, 2) + '\n');
|
||||
console.error(JSON.stringify(report, null, 2));
|
||||
process.exitCode = 1;
|
||||
};
|
||||
if (!input) fail('INPUT_REQUIRED', 'Use --input with a .tsx/.jsx entry file or a directory containing one.');
|
||||
else {
|
||||
const inputPath = path.resolve(input);
|
||||
const stat = fs.existsSync(inputPath) && fs.statSync(inputPath);
|
||||
if (!stat) fail('INPUT_NOT_FOUND', `Input does not exist: ${inputPath}`);
|
||||
else {
|
||||
const entry = stat.isDirectory() ? ['App.tsx', 'index.tsx', 'main.tsx', 'App.jsx', 'index.jsx', 'main.jsx'].map((x) => path.join(inputPath, x)).find(fs.existsSync) : inputPath;
|
||||
if (!entry || !/\.(tsx|jsx)$/.test(entry)) fail('INVALID_INPUT', 'Input must resolve to an independent React TSX/JSX entry.');
|
||||
else if (path.resolve(reportDir) === path.resolve(root, 'dist') || path.resolve(reportDir) === path.resolve(root, 'storybook-static')) fail('PROTECTED_OUTPUT', 'Shared dist and Storybook output are not valid H-2 output paths.');
|
||||
else if (fs.existsSync(path.join(reportDir, 'preview.html')) || fs.existsSync(path.join(reportDir, 'verification.json'))) fail('OUTPUT_EXISTS', 'Refusing to overwrite an existing H-2 output. Choose a new --output directory.');
|
||||
else {
|
||||
const before = requireGitStatus();
|
||||
const outputBefore = fs.existsSync(reportDir) ? fs.readdirSync(reportDir) : [];
|
||||
try {
|
||||
fs.mkdirSync(reportDir, { recursive: true });
|
||||
const temp = path.join(reportDir, '.bundle.js');
|
||||
await build({ entryPoints: [entry], outfile: temp, bundle: true, format: 'iife', platform: 'browser', target: 'es2020', sourcemap: false, logLevel: 'silent', absWorkingDir: path.dirname(entry) });
|
||||
const js = fs.readFileSync(temp, 'utf8');
|
||||
fs.rmSync(temp, { force: true });
|
||||
const source = fs.readFileSync(entry, 'utf8');
|
||||
const urls = [...source.matchAll(/(?:src|href)\s*=\s*["']([^"']+)["']/gi)].map((m) => m[1]);
|
||||
const external = urls.filter((u) => /^(https?:|\/\/)/i.test(u));
|
||||
const unresolved = urls.filter((u) => !/^(data:|#|https?:|\/\/|\/)/i.test(u) && !fs.existsSync(path.resolve(path.dirname(entry), u.split('#')[0])));
|
||||
if (external.length || unresolved.length) throw new Error(`External or unresolved asset references: ${JSON.stringify({ external, unresolved })}`);
|
||||
const html = `<!doctype html><html lang="ko"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>H-2 Preview</title><style>html,body,#root{margin:0;min-height:100%;width:100%;}body{background:#fff;} </style></head><body><div id="root"></div><script>${js}\n//# sourceURL=h2-preview.js</script></body></html>`;
|
||||
fs.writeFileSync(path.join(reportDir, 'preview.html'), html);
|
||||
const verification = { h2: 'H-2', status: 'PASS', input: path.resolve(entry), output: path.join(reportDir, 'preview.html'), runtime: 'REACT_TSX', screens, checks: { isolatedBuild: true, singlePreviewHtml: true, fileUrlReady: true, externalUrls: external, unresolvedRelativePaths: unresolved, console: 'NOT_RUN', runtime: 'NOT_RUN', network: 'NOT_RUN', captures: 'NOT_RUN', protectedOutputsUnchanged: true, preexistingChanges: before, postRunChanges: requireGitStatus(), outputBefore, outputAfter: fs.readdirSync(reportDir) }, note: 'Browser/runtime/capture checks remain NOT_RUN until a browser runner executes them; NOT_RUN is not PASS.' };
|
||||
fs.writeFileSync(path.join(reportDir, 'verification.json'), JSON.stringify(verification, null, 2) + '\n');
|
||||
console.log(JSON.stringify({ status: 'PASS', preview: path.join(reportDir, 'preview.html'), verification: path.join(reportDir, 'verification.json') }, null, 2));
|
||||
} catch (error) { fail('BUILD_OR_ASSET_CHECK_FAILED', error.message, { entry: rel(entry), output: reportDir }); }
|
||||
}
|
||||
}
|
||||
}
|
||||
function requireGitStatus() { try { return execFileSync('git', ['status', '--short'], { encoding: 'utf8' }).split(/\r?\n/).filter(Boolean); } catch { return ['UNAVAILABLE']; } }
|
||||
@@ -0,0 +1,66 @@
|
||||
// Portable test launcher: built-in modules only; never invokes npm or a global Node.
|
||||
const http = require('node:http');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawn } = require('node:child_process');
|
||||
const root = __dirname;
|
||||
const types = { '.html':'text/html; charset=utf-8', '.js':'text/javascript; charset=utf-8', '.mjs':'text/javascript; charset=utf-8', '.css':'text/css; charset=utf-8', '.json':'application/json; charset=utf-8', '.svg':'image/svg+xml', '.png':'image/png', '.jpg':'image/jpeg', '.jpeg':'image/jpeg', '.gif':'image/gif', '.webp':'image/webp', '.ico':'image/x-icon', '.ttf':'font/ttf', '.woff':'font/woff', '.woff2':'font/woff2', '.txt':'text/plain; charset=utf-8' };
|
||||
const server = http.createServer((req, res) => {
|
||||
if (!['GET','HEAD'].includes(req.method)) { res.writeHead(405); res.end(); return; }
|
||||
let pathname;
|
||||
try { pathname = decodeURIComponent(new URL(req.url, 'http://127.0.0.1').pathname); }
|
||||
catch { res.writeHead(400); res.end(); return; }
|
||||
// Dashboard is a production export and must be served from its own URL root.
|
||||
const dashboard = pathname === '/dashboard' || pathname.startsWith('/dashboard/');
|
||||
const dashboardPath = dashboard ? (pathname === '/dashboard' ? '/dashboard/' : pathname) : pathname;
|
||||
// Source Reference/static example paths keep their repository-relative imports.
|
||||
const reference = ['/docs/','/packages/','/src/','/public/'].some(prefix => pathname.startsWith(prefix));
|
||||
const base = dashboard ? path.join(root, 'dist-dashboard') : (reference ? root : path.join(root, 'storybook-static'));
|
||||
const relativePath = dashboard ? dashboardPath.slice('/dashboard'.length) || '/' : (pathname === '/favicon.ico' ? '/favicon.svg' : pathname);
|
||||
let file = path.resolve(base, '.' + relativePath);
|
||||
const relative = path.relative(base, file);
|
||||
if (relative.startsWith('..') || path.isAbsolute(relative) || pathname.includes('\\')) { res.writeHead(403); res.end(); return; }
|
||||
fs.stat(file, (error, stat) => {
|
||||
if (!error && stat.isDirectory()) file = path.join(file, 'index.html');
|
||||
fs.stat(file, (error, stat) => {
|
||||
if (error || !stat.isFile()) { res.writeHead(404); res.end('Not found'); return; }
|
||||
res.writeHead(200, { 'Content-Type': types[path.extname(file).toLowerCase()] || 'application/octet-stream', 'Content-Length':stat.size, 'Cache-Control':'no-store', 'X-Content-Type-Options':'nosniff' });
|
||||
if (req.method === 'HEAD') { res.end(); return; }
|
||||
const stream = fs.createReadStream(file);
|
||||
stream.on('error', () => res.destroy());
|
||||
stream.pipe(res);
|
||||
});
|
||||
});
|
||||
});
|
||||
let port = 6007;
|
||||
let browserOpened = false;
|
||||
function openDefaultBrowser(url) {
|
||||
if (browserOpened) return;
|
||||
browserOpened = true;
|
||||
const command = process.env.ComSpec || 'cmd.exe';
|
||||
const browser = spawn(command, ['/c', 'start', '', url], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true
|
||||
});
|
||||
browser.on('error', error => {
|
||||
console.error(`Browser auto-open failed: ${error.message}`);
|
||||
console.error(`Open this address manually: ${url}`);
|
||||
});
|
||||
browser.unref();
|
||||
}
|
||||
server.on('error', error => {
|
||||
if (error.code === 'EADDRINUSE' || error.code === 'EACCES') {
|
||||
if (port === 0) { console.error(error.message); process.exitCode = 1; return; }
|
||||
port = port < 6023 ? port + 1 : 0;
|
||||
server.listen(port, '127.0.0.1');
|
||||
} else { console.error(error.message); process.exitCode = 1; }
|
||||
});
|
||||
server.on('listening', () => {
|
||||
const url = `http://127.0.0.1:${server.address().port}/`;
|
||||
console.log(`HM Design System Portable Test\nNode ${process.version} (${process.arch})\nStorybook: ${url}\nDashboard: ${url}dashboard/\nStatic status example: ${url}docs/patterns/dashboard-status-static.html\nPress Ctrl+C to stop this preview.\n`);
|
||||
openDefaultBrowser(url);
|
||||
});
|
||||
process.on('SIGINT', () => server.close(() => process.exit(0)));
|
||||
process.on('SIGTERM', () => server.close(() => process.exit(0)));
|
||||
server.listen(port, '127.0.0.1');
|
||||
@@ -0,0 +1,15 @@
|
||||
@echo off
|
||||
setlocal
|
||||
chcp 65001 >nul
|
||||
title HM Design System - Portable Test Preview
|
||||
if not exist "%~dp0runtime\node\node.exe" (
|
||||
echo ERROR: Bundled runtime is missing. Extract the complete ZIP first.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
echo The actual Storybook and Dashboard addresses will be printed by the server below.
|
||||
echo.
|
||||
"%~dp0runtime\node\node.exe" "%~dp0preview-server.cjs"
|
||||
if errorlevel 1 pause
|
||||
endlocal
|
||||
@@ -0,0 +1,17 @@
|
||||
import { mkdirSync, readdirSync, writeFileSync, unlinkSync } from "node:fs"
|
||||
import { read, repositoryRoot, validateDocument } from "./theme-contract.mjs"
|
||||
|
||||
// Independent Theme artifact. Does not build UI/Graphic or generate package CSS.
|
||||
// Only top-level themes/*.json are published; historic drafts stay out.
|
||||
const directory = new URL("../themes/", import.meta.url)
|
||||
const themes = readdirSync(directory).filter(file => file.endsWith(".json")).sort()
|
||||
if (!themes.includes("default.json")) throw Error("Missing themes/default.json")
|
||||
const documents = themes.map(file => [file, validateDocument(JSON.parse(read(`themes/${file}`)), file === "default.json")])
|
||||
const output = new URL("theme-dist/themes/", repositoryRoot)
|
||||
mkdirSync(output, { recursive: true })
|
||||
// Only generated top-level JSON artifacts in this explicit workspace output.
|
||||
for (const file of readdirSync(output).filter(file => file.endsWith(".json") && !themes.includes(file))) unlinkSync(new URL(file, output))
|
||||
for (const [file, theme] of documents) {
|
||||
writeFileSync(new URL(file, output), JSON.stringify(theme, null, 2) + "\n")
|
||||
}
|
||||
console.log(`Validated/exported ${themes.length} independent Theme artifacts to theme-dist/themes/`)
|
||||
@@ -0,0 +1,25 @@
|
||||
import { readFileSync } from "node:fs"
|
||||
export const repositoryRoot = new URL("../", import.meta.url)
|
||||
export const read = path => readFileSync(new URL(path, repositoryRoot), "utf8").replace(/\r\n/g, "\n")
|
||||
const source = read("packages/ui/src/theme/contract.ts")
|
||||
export const definitions = [...source.matchAll(/"([^"]+)": \{ property: "([^"]+)", defaultValue: [^\n]+?, type: "(color|radius)" \}/g)]
|
||||
.map(([, key, property, type]) => ({ key, property, type }))
|
||||
if (!definitions.length || new Set(definitions.map(def => def.key)).size !== definitions.length || new Set(definitions.map(def => def.property)).size !== definitions.length) throw Error("Missing or duplicate contract entries")
|
||||
const groupsSource = source.slice(source.indexOf("export const runtimeGradientGroups"))
|
||||
export const gradientGroups = [...groupsSource.matchAll(/\[([^\[\]]+)\]/g)].map(([, body]) => [...body.matchAll(/"([^"]+)"/g)].map(([, key]) => key))
|
||||
if (!gradientGroups.length || gradientGroups.some(group => !group.length || group.some(key => !definitions.some(def => def.key === key)))) throw Error("Unrecognized production gradient groups")
|
||||
// Build-time check derived from the production allowlist. Runtime uses isRuntimeTheme.
|
||||
export function validateDocument(theme, fullDefault = false) {
|
||||
if (!theme || theme.schemaVersion !== 1 || !theme.tokens || typeof theme.tokens !== "object" || Array.isArray(theme.tokens)) throw Error("Expected v1 RuntimeTheme document")
|
||||
for (const [key, value] of Object.entries(theme.tokens)) {
|
||||
const definition = definitions.find(def => def.key === key)
|
||||
if (!definition || typeof value !== "string" || !(definition.type === "color" ? /^#[\da-f]{6}$/i : /^(?:[2-9]|1[0-2])px$/).test(value)) throw Error(`Invalid Theme token: ${key}`)
|
||||
}
|
||||
for (const group of gradientGroups) {
|
||||
const count = group.filter(key => Object.hasOwn(theme.tokens, key)).length
|
||||
if (count && count !== group.length) throw Error(`Incomplete gradient group: ${group[0]}`)
|
||||
}
|
||||
if (fullDefault && definitions.some(def => def.type === "color" && !Object.hasOwn(theme.tokens, def.key))) throw Error("default.json must contain every public color role/palette/gradient value")
|
||||
if (Object.keys(theme.tokens).some(key => definitions.find(def => def.key === key).type !== "color")) throw Error("Theme artifacts are color-only; geometry belongs to package CSS")
|
||||
return theme
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { writeFileSync } from "node:fs"
|
||||
import { definitions, read, repositoryRoot, validateDocument } from "./theme-contract.mjs"
|
||||
|
||||
// themes/default.json is the authored COLOR source. Templates own aliases/geometry.
|
||||
const theme = validateDocument(JSON.parse(read("themes/default.json")), true)
|
||||
const templates = ["primitives", "default"].map(name => ({ name, source: read(`scripts/theme-templates/${name}.css`) }))
|
||||
const graph = new Map(templates.flatMap(({ source }) => [...source.replace(/\/\*[\s\S]*?\*\//g, "").matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)].map(([, key, value]) => [key, value.trim()])))
|
||||
const publicProperties = new Map(definitions.filter(def => def.type === "color").map(def => [def.property, theme.tokens[def.key].toLowerCase()]))
|
||||
function resolve(value, stack = []) {
|
||||
const placeholder = /^\{\{([^|}]+)(?:\|([^}]+))?\}\}$/.exec(value)
|
||||
if (placeholder) return theme.tokens[placeholder[1]].toLowerCase()
|
||||
const alias = /^var\((--[\w-]+)\)$/.exec(value)
|
||||
if (!alias) return value
|
||||
const property = alias[1]
|
||||
if (stack.includes(property)) throw Error(`Circular fallback alias: ${property}`)
|
||||
if (publicProperties.has(property)) return publicProperties.get(property)
|
||||
if (!graph.has(property)) throw Error(`Missing fallback alias: ${property}`)
|
||||
return resolve(graph.get(property), [...stack, property])
|
||||
}
|
||||
function render(source) {
|
||||
return source.replace(/\{\{([^|}]+)(?:\|([^}]+))?\}\}/g, (_, key, alias) => {
|
||||
if (!Object.hasOwn(theme.tokens, key)) throw Error(`Missing template color: ${key}`)
|
||||
const value = theme.tokens[key].toLowerCase()
|
||||
// Preserve existing palette aliases when they match the canonical Default.
|
||||
// A separately configured role always wins if its JSON value differs.
|
||||
return alias && resolve(alias) === value ? alias : value
|
||||
})
|
||||
}
|
||||
const defaults = Object.fromEntries(definitions.map(def => [def.property, def.type === "color" ? theme.tokens[def.key].toLowerCase() : resolve(graph.get(def.property))]))
|
||||
for (const def of definitions) if (!(def.type === "color" ? /^#[\da-f]{6}$/i : /^(?:[2-9]|1[0-2])px$/).test(defaults[def.property])) throw Error(`Unresolved fallback: ${def.property}`)
|
||||
const outputs = templates.map(({name,source}) => [`packages/ui/src/theme/${name}.css`, `/* Generated from themes/default.json + scripts/theme-templates/${name}.css. Do not edit. */\n${render(source)}`])
|
||||
outputs.push(["packages/ui/src/theme/default-values.generated.ts", `// Generated by npm run generate:theme-defaults. Do not edit.\n// Colors: themes/default.json; geometry/aliases: scripts/theme-templates.\nexport const runtimeThemeDefaults = ${JSON.stringify(defaults, null, 2)} as const\n`])
|
||||
for (const [path, content] of outputs) {
|
||||
if (process.argv.includes("--check")) {
|
||||
if (read(path) !== content) throw Error(`Stale ${path}. Run npm run generate:theme-defaults.`)
|
||||
} else writeFileSync(new URL(path, repositoryRoot), content)
|
||||
}
|
||||
console.log(`Theme defaults: ${Object.keys(theme.tokens).length} JSON colors / ${definitions.length} contract defaults; ${process.argv.includes("--check") ? "generated outputs match" : "generated CSS + data"}`)
|
||||
@@ -0,0 +1,202 @@
|
||||
@import "./primitives.css";
|
||||
|
||||
/* Derived aliases and geometry. Values are generated from themes/default.json. */
|
||||
:root, [data-hm-theme] {
|
||||
--hm-color-primary: var(--hm-sem-palette-green-default);
|
||||
--hm-color-primary-hover: color-mix(in srgb, var(--hm-sem-action-primary) 85%, var(--hm-sem-palette-black));
|
||||
--hm-color-primary-active: color-mix(in srgb, var(--hm-sem-action-primary) 70%, var(--hm-sem-palette-black));
|
||||
--hm-color-surface-default: var(--hm-sem-palette-white);
|
||||
--hm-color-surface-content: var(--hm-sem-palette-beige-light);
|
||||
--hm-color-surface-subtle: var(--hm-sem-palette-warm-neutral-muted);
|
||||
--hm-color-text-primary: var(--hm-sem-palette-green-dark-1);
|
||||
--hm-color-text-secondary: var(--hm-sem-palette-warm-neutral-dark);
|
||||
--hm-color-text-inverse: var(--hm-sem-palette-white);
|
||||
--hm-color-text-placeholder: var(--hm-sem-palette-neutral-gray-500);
|
||||
--hm-color-border-default: var(--hm-sem-palette-warm-neutral-mid);
|
||||
--hm-color-border-control: var(--hm-sem-palette-warm-neutral-pale);
|
||||
/* Common Focus color. Trial value: Light 2 (#266651), pending user confirmation. Was --hm-color-primary. */
|
||||
--hm-color-border-focus: var(--hm-sem-palette-green-light-2);
|
||||
--hm-color-action-cancel: var(--hm-sem-palette-warm-neutral-dark);
|
||||
--hm-color-action-destructive: var(--hm-sem-palette-red-danger);
|
||||
--hm-color-action-destructive-hover: color-mix(in srgb, var(--hm-sem-action-destructive) 8%, var(--hm-sem-bg-surface));
|
||||
--hm-color-action-overlay-hover: color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent);
|
||||
--hm-color-action-overlay-active: color-mix(in srgb, var(--hm-sem-palette-black) 30%, transparent);
|
||||
--hm-color-action-shadow-light: color-mix(in srgb, var(--hm-sem-palette-white) 10%, transparent);
|
||||
--hm-color-action-shadow-dark: color-mix(in srgb, var(--hm-sem-palette-black) 25%, transparent);
|
||||
|
||||
--hm-radius-control-md: 4px;
|
||||
--hm-control-height-md: 34px;
|
||||
--hm-font-family-base: "Noto Sans KR", "Malgun Gothic", sans-serif;
|
||||
--hm-font-size-button-md: 13px;
|
||||
--hm-font-weight-medium: 500;
|
||||
--hm-font-weight-bold: 700;
|
||||
|
||||
/* Existing semantic names remain the shadcn and preview adapter. */
|
||||
--hm-sem-bg-content: {{color.bg.content|var(--hm-color-surface-content)}};
|
||||
--hm-sem-bg-surface: {{color.bg.surface|var(--hm-color-surface-default)}};
|
||||
--hm-sem-fg-primary: {{color.fg.primary|var(--hm-color-text-primary)}};
|
||||
--hm-sem-fg-secondary: {{color.fg.secondary|var(--hm-color-text-secondary)}};
|
||||
--hm-sem-fg-placeholder: {{color.fg.placeholder|var(--hm-color-text-placeholder)}};
|
||||
--hm-sem-action-primary: {{color.action.primary|var(--hm-color-primary)}};
|
||||
--hm-sem-action-cancel: {{color.action.cancel|var(--hm-color-action-cancel)}};
|
||||
--hm-sem-action-destructive: {{color.action.destructive|var(--hm-color-action-destructive)}};
|
||||
--hm-sem-stroke-default: {{color.stroke.default|var(--hm-color-border-default)}};
|
||||
--hm-sem-stroke-input-default: {{color.stroke.input.default|var(--hm-color-border-control)}};
|
||||
--hm-sem-stroke-input-focus: {{color.stroke.input.focus|var(--hm-color-border-focus)}};
|
||||
--hm-sem-on-action-primary: {{color.fg.inverse|var(--hm-color-text-inverse)}};
|
||||
/* Active state fill: checked / selected / on (Checkbox, Radio, Switch, Slider, Progress, Calendar).
|
||||
Sibling of the Focus chain (--hm-color-border-focus): same Light 2 today, but neither references the other. */
|
||||
--hm-sem-state-active: {{color.state.active|var(--hm-sem-palette-green-light-2)}};
|
||||
--hm-sem-on-state-active: {{color.on.state.active|var(--hm-color-text-inverse)}};
|
||||
/* Public gradient stop groups; geometry remains internal. */
|
||||
--hm-sem-brand-bg-stop-0: {{color.bg.brand.stop.0|var(--hm-sem-palette-green-dark-1)}};
|
||||
--hm-sem-brand-bg-stop-12: {{color.bg.brand.stop.12|var(--hm-sem-palette-green-dark-2)}};
|
||||
--hm-sem-brand-bg-stop-42: {{color.bg.brand.stop.42|var(--hm-sem-palette-green-default)}};
|
||||
--hm-sem-brand-bg-stop-78: {{color.bg.brand.stop.78|var(--hm-sem-palette-green-light-2)}};
|
||||
--hm-sem-brand-bg-stop-100: {{color.bg.brand.stop.100|var(--hm-sem-palette-green-light-1)}};
|
||||
--hm-sem-bg-brand: linear-gradient(to bottom,
|
||||
var(--hm-sem-brand-bg-stop-0) 0%, var(--hm-sem-brand-bg-stop-12) 12%,
|
||||
var(--hm-sem-brand-bg-stop-42) 42%, var(--hm-sem-brand-bg-stop-78) 78%,
|
||||
var(--hm-sem-brand-bg-stop-100) 100%);
|
||||
|
||||
--hm-sem-action-surface-stop-0: {{surface.action.primary.stop.0|var(--hm-sem-palette-green-tone-1)}};
|
||||
--hm-sem-action-surface-stop-30: {{surface.action.primary.stop.30|var(--hm-sem-palette-green-tone-2)}};
|
||||
--hm-sem-action-surface-stop-65: {{surface.action.primary.stop.65|var(--hm-sem-palette-green-tone-3)}};
|
||||
--hm-sem-action-surface-stop-100: {{surface.action.primary.stop.100|var(--hm-sem-palette-green-tone-4)}};
|
||||
--hm-sem-surface-action-primary: linear-gradient(180deg,
|
||||
var(--hm-sem-action-surface-stop-0) 0%, var(--hm-sem-action-surface-stop-30) 30%,
|
||||
var(--hm-sem-action-surface-stop-65) 65%, var(--hm-sem-action-surface-stop-100) 100%);
|
||||
--hm-sem-action-border-stop-0: {{surface.action.primary.border.stop.0|var(--hm-sem-palette-green-dark-1)}};
|
||||
--hm-sem-action-border-stop-50: {{surface.action.primary.border.stop.50|var(--hm-sem-palette-green-light-4)}};
|
||||
--hm-sem-action-border-stop-100: {{surface.action.primary.border.stop.100|var(--hm-sem-palette-green-tone-4)}};
|
||||
--hm-sem-border-action-primary: linear-gradient(180deg,
|
||||
var(--hm-sem-action-border-stop-0) 0%, var(--hm-sem-action-border-stop-50) 50%,
|
||||
var(--hm-sem-action-border-stop-100) 100%);
|
||||
|
||||
--hm-radius-button: var(--hm-radius-control-md);
|
||||
--hm-radius-input: var(--hm-radius-control-md);
|
||||
--hm-radius-select: var(--hm-radius-control-md);
|
||||
--hm-radius-container: var(--hm-radius-control-md);
|
||||
|
||||
/* Floating surface: anchored panels (Dropdown Menu, Context Menu, Popover, Hover Card, Menubar content, Navigation Menu panel).
|
||||
Not Tooltip (inverse face), Select, Card or the modal surfaces. Component tokens keep their own names and point here. */
|
||||
--hm-sem-floating-background: var(--hm-sem-bg-surface);
|
||||
--hm-sem-floating-border-width: 1px;
|
||||
--hm-sem-floating-border-color: var(--hm-sem-stroke-input-default);
|
||||
--hm-sem-floating-radius: var(--hm-radius-container);
|
||||
--hm-sem-floating-shadow: 0 0 12px color-mix(in srgb, var(--hm-sem-palette-black) 20%, transparent);
|
||||
|
||||
--hm-sem-bg-subtle: {{color.bg.subtle|var(--hm-color-surface-subtle)}};
|
||||
|
||||
--hm-sem-accent: {{color.accent|var(--hm-sem-palette-green-tint)}};
|
||||
|
||||
--hm-sem-on-accent: {{color.on.accent}};
|
||||
|
||||
--hm-sem-palette-green-light-4: {{color.palette.green-light-4|var(--hm-green-light-4)}};
|
||||
|
||||
--hm-sem-palette-green-light-3: {{color.palette.green-light-3|var(--hm-green-light-3)}};
|
||||
|
||||
--hm-sem-palette-green-light-2: {{color.palette.green-light-2|var(--hm-green-light-2)}};
|
||||
|
||||
--hm-sem-palette-green-light-1: {{color.palette.green-light-1|var(--hm-green-light-1)}};
|
||||
|
||||
--hm-sem-palette-green-default: {{color.palette.green-default|var(--hm-green-default)}};
|
||||
|
||||
--hm-sem-palette-green-dark-1: {{color.palette.green-dark-1|var(--hm-green-dark-1)}};
|
||||
|
||||
--hm-sem-palette-green-dark-2: {{color.palette.green-dark-2|var(--hm-green-dark-2)}};
|
||||
|
||||
--hm-sem-palette-brown-light-2: {{color.palette.brown-light-2|var(--hm-brown-light-2)}};
|
||||
|
||||
--hm-sem-palette-brown-light-1: {{color.palette.brown-light-1|var(--hm-brown-light-1)}};
|
||||
|
||||
--hm-sem-palette-brown-default: {{color.palette.brown-default|var(--hm-brown-default)}};
|
||||
|
||||
--hm-sem-palette-brown-dark-1: {{color.palette.brown-dark-1|var(--hm-brown-dark-1)}};
|
||||
|
||||
--hm-sem-palette-brown-light-4: {{color.palette.brown-light-4|var(--hm-brown-light-4)}};
|
||||
|
||||
--hm-sem-palette-orange-light-4: {{color.palette.orange-light-4|var(--hm-orange-light-4)}};
|
||||
|
||||
--hm-sem-palette-orange-default: {{color.palette.orange-default|var(--hm-orange-default)}};
|
||||
|
||||
--hm-sem-palette-warm-neutral-light: {{color.palette.warm-neutral-light|var(--hm-warm-neutral-light)}};
|
||||
|
||||
--hm-sem-palette-warm-neutral-mid: {{color.palette.warm-neutral-mid|var(--hm-warm-neutral-mid)}};
|
||||
|
||||
--hm-sem-palette-warm-neutral-dark: {{color.palette.warm-neutral-dark|var(--hm-warm-neutral-dark)}};
|
||||
|
||||
--hm-sem-palette-beige-light: {{color.palette.beige-light|var(--hm-beige-light)}};
|
||||
|
||||
--hm-sem-palette-red-danger: {{color.palette.red-danger|var(--hm-red-danger)}};
|
||||
|
||||
--hm-sem-palette-white: {{color.palette.white|var(--hm-white)}};
|
||||
|
||||
--hm-sem-palette-black: {{color.palette.black|var(--hm-black)}};
|
||||
|
||||
--hm-sem-palette-neutral-gray-500: {{color.palette.neutral-gray-500|var(--hm-neutral-gray-500)}};
|
||||
|
||||
--hm-sem-palette-green-tone-1: {{color.palette.green-tone-1|var(--hm-green-tone-1)}};
|
||||
|
||||
--hm-sem-palette-green-tone-2: {{color.palette.green-tone-2|var(--hm-green-tone-2)}};
|
||||
|
||||
--hm-sem-palette-green-tone-3: {{color.palette.green-tone-3|var(--hm-green-tone-3)}};
|
||||
|
||||
--hm-sem-palette-green-tone-4: {{color.palette.green-tone-4|var(--hm-green-tone-4)}};
|
||||
|
||||
--hm-sem-palette-warm-neutral-pale: {{color.palette.warm-neutral-pale|var(--hm-warm-neutral-pale)}};
|
||||
|
||||
--hm-sem-palette-warm-neutral-secondary: {{color.palette.warm-neutral-secondary|var(--hm-warm-neutral-secondary)}};
|
||||
|
||||
--hm-sem-palette-warm-neutral-muted: {{color.palette.warm-neutral-muted|var(--hm-warm-neutral-muted)}};
|
||||
|
||||
--hm-sem-palette-green-tint: {{color.palette.green-tint|var(--hm-green-tint)}};
|
||||
|
||||
--hm-sem-palette-yellow-accent: {{color.palette.yellow-accent|var(--hm-yellow-accent)}};
|
||||
|
||||
--hm-sem-palette-brown-deep: {{color.palette.brown-deep}};
|
||||
|
||||
--hm-sem-palette-brown-highlight: {{color.palette.brown-highlight}};
|
||||
|
||||
--hm-sem-palette-orange-light: {{color.palette.orange-light}};
|
||||
|
||||
--hm-sem-palette-orange-mid: {{color.palette.orange-mid}};
|
||||
|
||||
--hm-sem-palette-orange-dark: {{color.palette.orange-dark}};
|
||||
|
||||
--hm-sem-palette-orange-deep: {{color.palette.orange-deep}};
|
||||
|
||||
--hm-sem-palette-orange-border: {{color.palette.orange-border}};
|
||||
|
||||
--hm-sem-palette-orange-border-deep: {{color.palette.orange-border-deep}};
|
||||
|
||||
--hm-sem-palette-gray: {{color.palette.gray}};
|
||||
|
||||
--hm-sem-palette-gray-dark: {{color.palette.gray-dark}};
|
||||
|
||||
--hm-sem-palette-indicator: {{color.palette.indicator}};
|
||||
|
||||
--hm-sem-overlay-background: color-mix(in srgb, var(--hm-sem-palette-black) 10%, transparent);
|
||||
--hm-sem-surface-highlight: color-mix(in srgb, var(--hm-sem-accent) 45%, transparent);
|
||||
|
||||
/* Adapter for shadcn/Tailwind. The public contract.ts allowlist limits runtime overrides. */
|
||||
--background: var(--hm-sem-bg-content);
|
||||
--foreground: var(--hm-sem-fg-primary);
|
||||
--card: var(--hm-sem-bg-surface);
|
||||
--card-foreground: var(--hm-sem-fg-primary);
|
||||
--popover: var(--hm-sem-bg-surface);
|
||||
--popover-foreground: var(--hm-sem-fg-primary);
|
||||
--primary: var(--hm-sem-action-primary);
|
||||
--primary-foreground: var(--hm-sem-on-action-primary);
|
||||
--secondary: var(--hm-sem-palette-warm-neutral-secondary);
|
||||
--secondary-foreground: var(--hm-sem-palette-brown-default);
|
||||
--muted: var(--hm-sem-bg-subtle);
|
||||
--muted-foreground: var(--hm-sem-fg-secondary);
|
||||
--accent: var(--hm-sem-accent);
|
||||
--accent-foreground: var(--hm-sem-on-accent);
|
||||
--destructive: var(--hm-sem-action-destructive);
|
||||
--border: var(--hm-sem-stroke-default);
|
||||
--input: var(--hm-sem-stroke-input-default);
|
||||
--ring: var(--hm-sem-stroke-input-focus);
|
||||
--radius: var(--hm-radius-container);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/* HM color palette. Primitive names describe colors, not UI roles. */
|
||||
:root {
|
||||
--hm-green-light-4: {{color.palette.green-light-4}};
|
||||
--hm-green-light-3: {{color.palette.green-light-3}};
|
||||
--hm-green-light-2: {{color.palette.green-light-2}};
|
||||
--hm-green-light-1: {{color.palette.green-light-1}};
|
||||
--hm-green-default: {{color.palette.green-default}};
|
||||
--hm-green-dark-1: {{color.palette.green-dark-1}};
|
||||
--hm-green-dark-2: {{color.palette.green-dark-2}};
|
||||
|
||||
--hm-brown-light-2: {{color.palette.brown-light-2}};
|
||||
--hm-brown-light-1: {{color.palette.brown-light-1}};
|
||||
--hm-brown-default: {{color.palette.brown-default}};
|
||||
--hm-brown-dark-1: {{color.palette.brown-dark-1}};
|
||||
|
||||
--hm-brown-light-4: {{color.palette.brown-light-4}};
|
||||
|
||||
--hm-orange-light-4: {{color.palette.orange-light-4}};
|
||||
--hm-orange-default: {{color.palette.orange-default}};
|
||||
|
||||
--hm-warm-neutral-light: {{color.palette.warm-neutral-light}};
|
||||
--hm-warm-neutral-mid: {{color.palette.warm-neutral-mid}};
|
||||
--hm-warm-neutral-dark: {{color.palette.warm-neutral-dark}};
|
||||
--hm-beige-light: {{color.palette.beige-light}};
|
||||
|
||||
--hm-red-danger: {{color.palette.red-danger}};
|
||||
--hm-white: {{color.palette.white}};
|
||||
--hm-black: {{color.palette.black}};
|
||||
--hm-neutral-gray-500: {{color.palette.neutral-gray-500}};
|
||||
|
||||
/* Existing preview values without an approved palette alias. */
|
||||
--hm-green-tone-1: {{color.palette.green-tone-1}};
|
||||
--hm-green-tone-2: {{color.palette.green-tone-2}};
|
||||
--hm-green-tone-3: {{color.palette.green-tone-3}};
|
||||
--hm-green-tone-4: {{color.palette.green-tone-4}};
|
||||
--hm-warm-neutral-pale: {{color.palette.warm-neutral-pale}};
|
||||
--hm-warm-neutral-secondary: {{color.palette.warm-neutral-secondary}};
|
||||
--hm-warm-neutral-muted: {{color.palette.warm-neutral-muted}};
|
||||
--hm-green-tint: {{color.palette.green-tint}};
|
||||
}
|
||||
|
||||
/* Accent for the current parent group on the green LNB surface (ui-viz color.action.accent-on-green). */
|
||||
:root {
|
||||
--hm-yellow-accent: {{color.palette.yellow-accent}};
|
||||
}
|
||||
Reference in new issue
Block a user