@ihyeon/flow-qa 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ko.md +72 -0
- package/README.md +71 -2
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/dist/bin/flow-qa.mjs +3 -0
- package/dist/examples/demo/server.js +31 -0
- package/dist/src/cli.js +285 -0
- package/dist/src/compiler.js +62 -0
- package/dist/src/engine.js +246 -0
- package/dist/src/i18n.js +98 -0
- package/dist/src/planner.js +112 -0
- package/dist/src/report.js +315 -0
- package/dist/src/schema.js +71 -0
- package/dist/src/ui/entry.js +109 -0
- package/dist/src/ui/evidence.js +85 -0
- package/dist/src/ui/public/app.js +360 -0
- package/dist/src/ui/public/display.js +56 -0
- package/dist/src/ui/public/fonts/dm-sans.woff2 +0 -0
- package/dist/src/ui/public/fonts/noto-kr-subset.woff2 +0 -0
- package/dist/src/ui/public/i18n.js +290 -0
- package/dist/src/ui/public/index.html +117 -0
- package/dist/src/ui/public/style.css +200 -0
- package/dist/src/ui/server.js +785 -0
- package/dist/src/workspace.js +88 -0
- package/docs/architecture.md +38 -0
- package/docs/releases.md +53 -0
- package/docs/ui.md +71 -0
- package/licenses/DM-Sans-LICENSE.txt +93 -0
- package/licenses/Noto-Sans-KR-LICENSE.txt +93 -0
- package/node_modules/@hyeonse/design-system/.github/workflows/ci.yml +21 -0
- package/node_modules/@hyeonse/design-system/.npmignore +5 -0
- package/node_modules/@hyeonse/design-system/AGENTS.md +10 -0
- package/node_modules/@hyeonse/design-system/LICENSE +21 -0
- package/node_modules/@hyeonse/design-system/README.md +67 -0
- package/node_modules/@hyeonse/design-system/THIRD_PARTY_NOTICES.md +5 -0
- package/node_modules/@hyeonse/design-system/dist/components.css +105 -0
- package/node_modules/@hyeonse/design-system/dist/preferences.d.ts +21 -0
- package/node_modules/@hyeonse/design-system/dist/preferences.js +69 -0
- package/node_modules/@hyeonse/design-system/dist/react.d.ts +21 -0
- package/node_modules/@hyeonse/design-system/dist/react.js +100 -0
- package/node_modules/@hyeonse/design-system/dist/tokens.css +75 -0
- package/node_modules/@hyeonse/design-system/docs/api.md +33 -0
- package/node_modules/@hyeonse/design-system/docs/assets/fonts/dm-sans.woff2 +0 -0
- package/node_modules/@hyeonse/design-system/docs/assets/fonts/noto-kr-subset.woff2 +0 -0
- package/node_modules/@hyeonse/design-system/docs/audit.md +29 -0
- package/node_modules/@hyeonse/design-system/docs/foundations.md +30 -0
- package/node_modules/@hyeonse/design-system/docs/verification.md +13 -0
- package/node_modules/@hyeonse/design-system/index.html +51 -0
- package/node_modules/@hyeonse/design-system/licenses/DM-Sans-LICENSE.txt +93 -0
- package/node_modules/@hyeonse/design-system/licenses/Noto-Sans-KR-LICENSE.txt +93 -0
- package/node_modules/@hyeonse/design-system/licenses/arla6ka-MIT.txt +21 -0
- package/node_modules/@hyeonse/design-system/package.json +35 -0
- package/node_modules/@hyeonse/design-system/scripts/audit-tokens.mjs +48 -0
- package/node_modules/@hyeonse/design-system/scripts/build.mjs +12 -0
- package/node_modules/@hyeonse/design-system/showcase.css +75 -0
- package/node_modules/@hyeonse/design-system/showcase.mjs +63 -0
- package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/SKILL.md +18 -0
- package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/references/api.md +7 -0
- package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/references/build.md +7 -0
- package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/references/foundations.md +7 -0
- package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/references/mapping.md +7 -0
- package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/references/migration.md +5 -0
- package/node_modules/@hyeonse/design-system/skills/hyeonse-design-system/references/review-docs.md +5 -0
- package/node_modules/@hyeonse/design-system/src/components.css +105 -0
- package/node_modules/@hyeonse/design-system/src/preferences.ts +87 -0
- package/node_modules/@hyeonse/design-system/src/react.tsx +161 -0
- package/node_modules/@hyeonse/design-system/src/tokens.css +75 -0
- package/node_modules/@hyeonse/design-system/tests/audit.test.mjs +30 -0
- package/node_modules/@hyeonse/design-system/tests/preferences.test.mjs +86 -0
- package/node_modules/@hyeonse/design-system/tsconfig.json +16 -0
- package/package.json +73 -4
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
.hds-themeToggle {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
gap: 9px;
|
|
6
|
+
min-width: 44px;
|
|
7
|
+
min-height: var(--control-min-size);
|
|
8
|
+
padding: 8px 12px;
|
|
9
|
+
border: 1px solid var(--line);
|
|
10
|
+
border-radius: var(--radius-control);
|
|
11
|
+
color: var(--ink);
|
|
12
|
+
background: var(--paper);
|
|
13
|
+
font: inherit;
|
|
14
|
+
font-size: var(--text-control);
|
|
15
|
+
font-weight: 700;
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.hds-themeToggle svg { display: block; width: 19px; height: 19px; flex: none; }
|
|
20
|
+
.hds-themeToggle:hover { border-color: var(--accent-text); color: var(--accent-text); }
|
|
21
|
+
.hds-themeToggle:focus-visible, .hds-panel button:focus-visible, .hds-radioOption input:focus-visible + span {
|
|
22
|
+
outline: 3px solid var(--accent-text);
|
|
23
|
+
outline-offset: 3px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.hds-panel {
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
width: 100%;
|
|
29
|
+
padding: clamp(20px, 4vw, 48px);
|
|
30
|
+
border: 1px solid var(--line);
|
|
31
|
+
border-radius: var(--radius-card);
|
|
32
|
+
color: var(--ink);
|
|
33
|
+
background: var(--surface);
|
|
34
|
+
font-family: var(--font-sans);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.hds-heading {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: end;
|
|
40
|
+
justify-content: space-between;
|
|
41
|
+
gap: 24px;
|
|
42
|
+
padding-bottom: 26px;
|
|
43
|
+
border-bottom: 1px solid var(--line);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.hds-heading h2 { margin: 10px 0 0; font-size: clamp(30px, 4vw, 54px); line-height: 1.1; letter-spacing: -.06em; }
|
|
47
|
+
.hds-heading p { margin: 0 0 3px; color: var(--muted); font-size: 13px; }
|
|
48
|
+
.hds-kicker { color: var(--accent-text); font-size: 10px; font-weight: 800; letter-spacing: .13em; }
|
|
49
|
+
.hds-groups { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 clamp(18px, 4vw, 48px); }
|
|
50
|
+
.hds-group { min-width: 0; margin: 0; padding: 22px 0 26px; border: 0; border-bottom: 1px solid var(--line); }
|
|
51
|
+
.hds-group legend { display: block; float: left; width: 100%; margin: 0 0 13px; padding: 0; font-size: 13px; font-weight: 800; }
|
|
52
|
+
.hds-group legend + * { clear: both; }
|
|
53
|
+
.hds-options, .hds-accents { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
54
|
+
.hds-radioOption { position: relative; display: inline-flex; min-height: var(--control-min-size); cursor: pointer; }
|
|
55
|
+
.hds-radioOption input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
|
|
56
|
+
.hds-radioOption span, .hds-accentButton {
|
|
57
|
+
box-sizing: border-box;
|
|
58
|
+
display: inline-flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
min-height: var(--control-min-size);
|
|
62
|
+
padding: 8px 14px;
|
|
63
|
+
border: 1px solid var(--line);
|
|
64
|
+
border-radius: var(--radius-control);
|
|
65
|
+
color: var(--ink);
|
|
66
|
+
background: var(--surface-raised);
|
|
67
|
+
font: inherit;
|
|
68
|
+
font-size: var(--text-control);
|
|
69
|
+
font-weight: 700;
|
|
70
|
+
line-height: 1.3;
|
|
71
|
+
white-space: nowrap;
|
|
72
|
+
}
|
|
73
|
+
.hds-radioOption input:checked + span { border-color: var(--accent-text); box-shadow: inset 0 0 0 1px var(--accent-text); color: var(--accent-text); }
|
|
74
|
+
.hds-radioOption:hover span, .hds-accentButton:hover { border-color: var(--accent-text); }
|
|
75
|
+
.hds-accentButton { gap: 9px; cursor: pointer; }
|
|
76
|
+
.hds-accentButton[aria-pressed='true'] { border-color: var(--accent-text); box-shadow: inset 0 0 0 1px var(--accent-text); }
|
|
77
|
+
.hds-swatch { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; color: var(--on-accent); font-size: 14px; font-weight: 900; }
|
|
78
|
+
.hds-cobalt .hds-swatch { background: var(--palette-cobalt); }
|
|
79
|
+
.hds-forest .hds-swatch { background: var(--palette-forest); }
|
|
80
|
+
.hds-coral .hds-swatch { background: var(--palette-coral); }
|
|
81
|
+
|
|
82
|
+
.hds-footer { display: flex; justify-content: space-between; align-items: center; gap: 22px; padding-top: 28px; }
|
|
83
|
+
.hds-footer p { margin: 0; color: var(--muted); font-size: var(--text-control); line-height: 1.65; }
|
|
84
|
+
.hds-footer button {
|
|
85
|
+
flex: none;
|
|
86
|
+
min-height: var(--control-min-size);
|
|
87
|
+
padding: 8px 0;
|
|
88
|
+
border: 0;
|
|
89
|
+
border-bottom: 1px solid currentColor;
|
|
90
|
+
border-radius: 0;
|
|
91
|
+
color: var(--ink);
|
|
92
|
+
background: transparent;
|
|
93
|
+
font: inherit;
|
|
94
|
+
font-size: var(--text-control);
|
|
95
|
+
font-weight: 800;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
}
|
|
98
|
+
.hds-footer button:hover { color: var(--accent-text); }
|
|
99
|
+
|
|
100
|
+
@media (max-width: 720px) {
|
|
101
|
+
.hds-heading { display: block; }
|
|
102
|
+
.hds-heading p { margin-top: 16px; }
|
|
103
|
+
.hds-groups { grid-template-columns: 1fr; }
|
|
104
|
+
.hds-footer { align-items: start; flex-direction: column; gap: 14px; }
|
|
105
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ThemePreference = 'system' | 'light' | 'dark';
|
|
2
|
+
export type AccentPreference = 'cobalt' | 'forest' | 'coral';
|
|
3
|
+
export type DensityPreference = 'comfortable' | 'compact';
|
|
4
|
+
export type CornersPreference = 'crisp' | 'soft';
|
|
5
|
+
export type MotionPreference = 'full' | 'reduced';
|
|
6
|
+
export type Preferences = {
|
|
7
|
+
theme: ThemePreference;
|
|
8
|
+
accent: AccentPreference;
|
|
9
|
+
density: DensityPreference;
|
|
10
|
+
corners: CornersPreference;
|
|
11
|
+
motion: MotionPreference;
|
|
12
|
+
};
|
|
13
|
+
export declare const preferencesKey = "hyeonse-site-preferences";
|
|
14
|
+
export declare const defaultPreferences: Preferences;
|
|
15
|
+
export declare function validatePreferences(value: unknown): Preferences;
|
|
16
|
+
export declare function parsePreferences(raw: string | null): Preferences;
|
|
17
|
+
export declare function serializePreferences(value: Preferences): string;
|
|
18
|
+
export declare function resolvedTheme(theme: ThemePreference, systemDark: boolean): 'light' | 'dark';
|
|
19
|
+
export declare function effectiveMotion(motion: MotionPreference, systemReduced: boolean): MotionPreference;
|
|
20
|
+
export declare function createPreferenceScript(storageKey?: string): string;
|
|
21
|
+
export declare const preferenceScript: string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const preferencesKey = 'hyeonse-site-preferences';
|
|
2
|
+
export const defaultPreferences = {
|
|
3
|
+
theme: 'system',
|
|
4
|
+
accent: 'cobalt',
|
|
5
|
+
density: 'comfortable',
|
|
6
|
+
corners: 'crisp',
|
|
7
|
+
motion: 'full',
|
|
8
|
+
};
|
|
9
|
+
const options = {
|
|
10
|
+
theme: ['system', 'light', 'dark'],
|
|
11
|
+
accent: ['cobalt', 'forest', 'coral'],
|
|
12
|
+
density: ['comfortable', 'compact'],
|
|
13
|
+
corners: ['crisp', 'soft'],
|
|
14
|
+
motion: ['full', 'reduced'],
|
|
15
|
+
};
|
|
16
|
+
export function validatePreferences(value) {
|
|
17
|
+
const input = value && typeof value === 'object' && !Array.isArray(value)
|
|
18
|
+
? value
|
|
19
|
+
: {};
|
|
20
|
+
return {
|
|
21
|
+
theme: options.theme.includes(input.theme) ? input.theme : defaultPreferences.theme,
|
|
22
|
+
accent: options.accent.includes(input.accent) ? input.accent : defaultPreferences.accent,
|
|
23
|
+
density: options.density.includes(input.density) ? input.density : defaultPreferences.density,
|
|
24
|
+
corners: options.corners.includes(input.corners) ? input.corners : defaultPreferences.corners,
|
|
25
|
+
motion: options.motion.includes(input.motion) ? input.motion : defaultPreferences.motion,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function parsePreferences(raw) {
|
|
29
|
+
if (!raw)
|
|
30
|
+
return { ...defaultPreferences };
|
|
31
|
+
try {
|
|
32
|
+
return validatePreferences(JSON.parse(raw));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { ...defaultPreferences };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function serializePreferences(value) {
|
|
39
|
+
return JSON.stringify(validatePreferences(value));
|
|
40
|
+
}
|
|
41
|
+
export function resolvedTheme(theme, systemDark) {
|
|
42
|
+
return theme === 'system' ? (systemDark ? 'dark' : 'light') : theme;
|
|
43
|
+
}
|
|
44
|
+
export function effectiveMotion(motion, systemReduced) {
|
|
45
|
+
return systemReduced ? 'reduced' : motion;
|
|
46
|
+
}
|
|
47
|
+
// Root layout inserts this in <head> before CSS paints and suppresses hydration warnings on <html>.
|
|
48
|
+
export function createPreferenceScript(storageKey = preferencesKey) {
|
|
49
|
+
// A JSON string literal is used inside an inline script; escape HTML syntax too.
|
|
50
|
+
const safeKey = JSON.stringify(storageKey).replace(/[<>&\u2028\u2029]/g, character => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
51
|
+
return `(function(){
|
|
52
|
+
var defaults={theme:'system',accent:'cobalt',density:'comfortable',corners:'crisp',motion:'full'};
|
|
53
|
+
var allowed={theme:['system','light','dark'],accent:['cobalt','forest','coral'],density:['comfortable','compact'],corners:['crisp','soft'],motion:['full','reduced']};
|
|
54
|
+
var stored={};
|
|
55
|
+
try { var value=JSON.parse(localStorage.getItem(${safeKey})||'{}'); if(value&&typeof value==='object'&&!Array.isArray(value))stored=value; } catch(e) {}
|
|
56
|
+
var p={};
|
|
57
|
+
for(var key in defaults)p[key]=allowed[key].includes(stored[key])?stored[key]:defaults[key];
|
|
58
|
+
var dark=window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
59
|
+
var reduced=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
60
|
+
var root=document.documentElement;
|
|
61
|
+
root.dataset.theme=p.theme==='system'?(dark?'dark':'light'):p.theme;
|
|
62
|
+
root.dataset.themePreference=p.theme;
|
|
63
|
+
root.dataset.accent=p.accent;
|
|
64
|
+
root.dataset.density=p.density;
|
|
65
|
+
root.dataset.corners=p.corners;
|
|
66
|
+
root.dataset.motion=reduced?'reduced':p.motion;
|
|
67
|
+
})();`;
|
|
68
|
+
}
|
|
69
|
+
export const preferenceScript = createPreferenceScript();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Preferences } from './preferences.js';
|
|
3
|
+
export type Locale = 'ko' | 'en';
|
|
4
|
+
type PreferenceContext = {
|
|
5
|
+
preferences: Preferences;
|
|
6
|
+
setPreference: <K extends keyof Preferences>(key: K, value: Preferences[K]) => void;
|
|
7
|
+
resetPreferences: () => void;
|
|
8
|
+
};
|
|
9
|
+
export declare function PreferencesProvider({ children, storageKey, eventName }: {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
storageKey?: string;
|
|
12
|
+
eventName?: string;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
export declare function usePreferences(): PreferenceContext;
|
|
15
|
+
export declare function ThemeToggle({ locale }: {
|
|
16
|
+
locale?: Locale;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
18
|
+
export declare function PreferencesPanel({ locale }: {
|
|
19
|
+
locale?: Locale;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useId, useLayoutEffect, useRef, useState } from 'react';
|
|
4
|
+
import { defaultPreferences, effectiveMotion, parsePreferences, preferencesKey, resolvedTheme, serializePreferences, validatePreferences, } from './preferences.js';
|
|
5
|
+
const defaultEventName = 'hyeonse:preferences';
|
|
6
|
+
const Context = createContext(null);
|
|
7
|
+
function applyPreferences(preferences, eventName) {
|
|
8
|
+
const root = document.documentElement;
|
|
9
|
+
const dark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
|
10
|
+
const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
11
|
+
root.dataset.theme = resolvedTheme(preferences.theme, dark);
|
|
12
|
+
root.dataset.themePreference = preferences.theme;
|
|
13
|
+
root.dataset.accent = preferences.accent;
|
|
14
|
+
root.dataset.density = preferences.density;
|
|
15
|
+
root.dataset.corners = preferences.corners;
|
|
16
|
+
root.dataset.motion = effectiveMotion(preferences.motion, reduced);
|
|
17
|
+
window.dispatchEvent(new CustomEvent(eventName, { detail: preferences }));
|
|
18
|
+
}
|
|
19
|
+
function readPreferences(storageKey) {
|
|
20
|
+
try {
|
|
21
|
+
return parsePreferences(window.localStorage.getItem(storageKey));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return { ...defaultPreferences };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function PreferencesProvider({ children, storageKey = preferencesKey, eventName = defaultEventName }) {
|
|
28
|
+
const [preferences, setPreferences] = useState(defaultPreferences);
|
|
29
|
+
const preferencesRef = useRef(defaultPreferences);
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
const stored = readPreferences(storageKey);
|
|
32
|
+
preferencesRef.current = stored;
|
|
33
|
+
setPreferences(stored);
|
|
34
|
+
applyPreferences(stored, eventName);
|
|
35
|
+
const themeQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
36
|
+
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
37
|
+
const onSystemChange = () => applyPreferences(preferencesRef.current, eventName);
|
|
38
|
+
const onStorage = (event) => {
|
|
39
|
+
if (event.key !== storageKey && event.key !== null)
|
|
40
|
+
return;
|
|
41
|
+
const next = readPreferences(storageKey);
|
|
42
|
+
preferencesRef.current = next;
|
|
43
|
+
setPreferences(next);
|
|
44
|
+
applyPreferences(next, eventName);
|
|
45
|
+
};
|
|
46
|
+
themeQuery.addEventListener('change', onSystemChange);
|
|
47
|
+
motionQuery.addEventListener('change', onSystemChange);
|
|
48
|
+
window.addEventListener('storage', onStorage);
|
|
49
|
+
return () => {
|
|
50
|
+
themeQuery.removeEventListener('change', onSystemChange);
|
|
51
|
+
motionQuery.removeEventListener('change', onSystemChange);
|
|
52
|
+
window.removeEventListener('storage', onStorage);
|
|
53
|
+
};
|
|
54
|
+
}, [storageKey, eventName]);
|
|
55
|
+
const save = useCallback((next) => {
|
|
56
|
+
const safe = validatePreferences(next);
|
|
57
|
+
preferencesRef.current = safe;
|
|
58
|
+
setPreferences(safe);
|
|
59
|
+
applyPreferences(safe, eventName);
|
|
60
|
+
try {
|
|
61
|
+
window.localStorage.setItem(storageKey, serializePreferences(safe));
|
|
62
|
+
}
|
|
63
|
+
catch { /* Settings still work for this tab when storage is unavailable. */ }
|
|
64
|
+
}, [storageKey, eventName]);
|
|
65
|
+
const setPreference = useCallback((key, value) => {
|
|
66
|
+
save({ ...preferencesRef.current, [key]: value });
|
|
67
|
+
}, [save]);
|
|
68
|
+
const resetPreferences = useCallback(() => save({ ...defaultPreferences }), [save]);
|
|
69
|
+
return _jsx(Context.Provider, { value: { preferences, setPreference, resetPreferences }, children: children });
|
|
70
|
+
}
|
|
71
|
+
export function usePreferences() {
|
|
72
|
+
const context = useContext(Context);
|
|
73
|
+
if (!context)
|
|
74
|
+
throw new Error('PreferencesProvider가 필요합니다.');
|
|
75
|
+
return context;
|
|
76
|
+
}
|
|
77
|
+
const themeOptions = {
|
|
78
|
+
ko: [{ value: 'system', label: '기기 설정' }, { value: 'light', label: '밝게' }, { value: 'dark', label: '어둡게' }],
|
|
79
|
+
en: [{ value: 'system', label: 'System' }, { value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' }],
|
|
80
|
+
};
|
|
81
|
+
export function ThemeToggle({ locale = 'ko' }) {
|
|
82
|
+
const { preferences, setPreference } = usePreferences();
|
|
83
|
+
const options = themeOptions[locale];
|
|
84
|
+
const current = options.find(option => option.value === preferences.theme);
|
|
85
|
+
const next = options[(options.findIndex(option => option.value === preferences.theme) + 1) % options.length];
|
|
86
|
+
return _jsxs("button", { type: "button", className: 'hds-themeToggle', onClick: () => setPreference('theme', next.value), "aria-label": locale === 'ko' ? `현재 테마 ${current.label}. ${next.label} 모드로 변경` : `Current theme: ${current.label}. Switch to ${next.label} mode`, title: locale === 'ko' ? `현재 ${current.label} · 다음 ${next.label}` : `Current: ${current.label} · Next: ${next.label}`, children: [_jsxs("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [preferences.theme === 'system' && _jsxs(_Fragment, { children: [_jsx("rect", { x: "3", y: "4", width: "18", height: "13", rx: "1" }), _jsx("path", { d: "M8 21h8M12 17v4" })] }), preferences.theme === 'light' && _jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "4" }), _jsx("path", { d: "M12 2v2m0 16v2M2 12h2m16 0h2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4m0-14.2-1.4 1.4M6.3 17.7l-1.4 1.4" })] }), preferences.theme === 'dark' && _jsx("path", { d: "M20.3 15.7A8.5 8.5 0 0 1 8.3 3.7a8.5 8.5 0 1 0 12 12Z" })] }), _jsx("span", { "aria-hidden": "true", children: current.label })] });
|
|
87
|
+
}
|
|
88
|
+
function RadioGroup({ field, legend, choices, name, }) {
|
|
89
|
+
const { preferences, setPreference } = usePreferences();
|
|
90
|
+
return _jsxs("fieldset", { className: 'hds-group', children: [_jsx("legend", { children: legend }), _jsx("div", { className: 'hds-options', children: choices.map(choice => _jsxs("label", { className: 'hds-radioOption', children: [_jsx("input", { type: "radio", name: `${name}-${field}`, value: choice.value, checked: preferences[field] === choice.value, onChange: () => setPreference(field, choice.value) }), _jsx("span", { children: choice.label })] }, choice.value)) })] });
|
|
91
|
+
}
|
|
92
|
+
export function PreferencesPanel({ locale = 'ko' }) {
|
|
93
|
+
const { preferences, setPreference, resetPreferences } = usePreferences();
|
|
94
|
+
const panelId = useId();
|
|
95
|
+
const en = locale === 'en';
|
|
96
|
+
const accentOptions = en
|
|
97
|
+
? [{ value: 'cobalt', label: 'Cobalt' }, { value: 'forest', label: 'Forest' }, { value: 'coral', label: 'Coral' }]
|
|
98
|
+
: [{ value: 'cobalt', label: '코발트' }, { value: 'forest', label: '포레스트' }, { value: 'coral', label: '코랄' }];
|
|
99
|
+
return _jsxs("section", { className: 'hds-panel', "aria-labelledby": `${panelId}-title`, children: [_jsxs("div", { className: 'hds-heading', children: [_jsxs("div", { children: [_jsx("span", { className: 'hds-kicker', children: "DISPLAY / CONTROL" }), _jsx("h2", { id: `${panelId}-title`, children: en ? 'Display settings' : '화면 설정' })] }), _jsx("p", { children: en ? 'Adjust the display for comfortable reading.' : '읽기 편한 화면으로 바꿔 보세요.' })] }), _jsxs("div", { className: 'hds-groups', children: [_jsx(RadioGroup, { field: "theme", legend: en ? 'Theme' : '테마', choices: themeOptions[locale], name: panelId }), _jsxs("fieldset", { className: 'hds-group', children: [_jsx("legend", { children: en ? 'Accent color' : '강조 색상' }), _jsx("div", { className: 'hds-accents', children: accentOptions.map(({ value, label }) => _jsxs("button", { type: "button", className: `${'hds-accentButton'} ${`hds-${value}`}`, "aria-pressed": preferences.accent === value, "aria-label": en ? `${label} accent color${preferences.accent === value ? ', selected' : ''}` : `${label} 강조 색상${preferences.accent === value ? ', 선택됨' : ''}`, onClick: () => setPreference('accent', value), children: [_jsx("span", { className: 'hds-swatch', "aria-hidden": "true", children: preferences.accent === value ? '✓' : '' }), _jsx("span", { children: label })] }, value)) })] }), _jsx(RadioGroup, { field: "density", legend: en ? 'Spacing' : '간격', choices: en ? [{ value: 'comfortable', label: 'Default' }, { value: 'compact', label: 'Compact' }] : [{ value: 'comfortable', label: '기본' }, { value: 'compact', label: '촘촘하게' }], name: panelId }), _jsx(RadioGroup, { field: "corners", legend: en ? 'Corners' : '모서리', choices: en ? [{ value: 'crisp', label: 'Sharp' }, { value: 'soft', label: 'Rounded' }] : [{ value: 'crisp', label: '각지게' }, { value: 'soft', label: '부드럽게' }], name: panelId }), _jsx(RadioGroup, { field: "motion", legend: en ? 'Motion' : '움직임', choices: en ? [{ value: 'full', label: 'Default' }, { value: 'reduced', label: 'Reduced' }] : [{ value: 'full', label: '기본' }, { value: 'reduced', label: '줄이기' }], name: panelId })] }), _jsxs("div", { className: 'hds-footer', children: [_jsxs("p", { role: "status", "aria-live": "polite", children: [en ? 'Current settings: ' : '현재 설정: ', themeOptions[locale].find(option => option.value === preferences.theme)?.label, ", ", accentOptions.find(option => option.value === preferences.accent)?.label, ", ", en ? (preferences.density === 'compact' ? 'compact spacing' : 'default spacing') : (preferences.density === 'compact' ? '촘촘한 간격' : '기본 간격'), ", ", en ? (preferences.corners === 'soft' ? 'rounded corners' : 'sharp corners') : (preferences.corners === 'soft' ? '부드러운 모서리' : '각진 모서리'), ", ", en ? (preferences.motion === 'reduced' ? 'reduced motion' : 'default motion') : (preferences.motion === 'reduced' ? '움직임 줄이기' : '기본 움직임')] }), _jsx("button", { type: "button", onClick: resetPreferences, children: en ? 'Reset to defaults' : '기본값으로 되돌리기' })] })] });
|
|
100
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
--ink: #101114;
|
|
4
|
+
--paper: #ffffff;
|
|
5
|
+
--muted: #5a606b;
|
|
6
|
+
--line: #d8dbe3;
|
|
7
|
+
--surface: #f3f5f8;
|
|
8
|
+
--surface-raised: #ffffff;
|
|
9
|
+
--accent-solid: #204cfc;
|
|
10
|
+
--accent-text: #204cfc;
|
|
11
|
+
--palette-cobalt: #204cfc;
|
|
12
|
+
--palette-forest: #087451;
|
|
13
|
+
--palette-coral: #b93d20;
|
|
14
|
+
--on-accent: #ffffff;
|
|
15
|
+
--blue: var(--accent-text);
|
|
16
|
+
--about-surface: var(--accent-solid);
|
|
17
|
+
--about-ink: #ffffff;
|
|
18
|
+
--about-muted: #dce3ff;
|
|
19
|
+
--inverse-surface: #101114;
|
|
20
|
+
--inverse-ink: #ffffff;
|
|
21
|
+
--radius-card: 0px;
|
|
22
|
+
--radius-control: 2px;
|
|
23
|
+
--space-factor: 1;
|
|
24
|
+
--font-sans: 'DM Sans', 'Noto Sans KR', sans-serif;
|
|
25
|
+
--gutter: clamp(22px, 4.1vw, 72px);
|
|
26
|
+
--control-min-size: 44px;
|
|
27
|
+
--text-control: 12px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@media (max-width: 700px) {
|
|
31
|
+
:root { --gutter: 22px; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
:root[data-accent='forest'] { --accent-solid: #087451; --accent-text: #087451; --about-muted: #d4f5e8; }
|
|
35
|
+
:root[data-accent='coral'] { --accent-solid: #b93d20; --accent-text: #b93d20; --about-muted: #ffe1d9; }
|
|
36
|
+
:root[data-density='compact'] { --space-factor: .8; }
|
|
37
|
+
:root[data-corners='soft'] { --radius-card: 20px; --radius-control: 12px; }
|
|
38
|
+
|
|
39
|
+
:root[data-theme='dark'] {
|
|
40
|
+
color-scheme: dark;
|
|
41
|
+
--ink: #f2f4f8;
|
|
42
|
+
--paper: #111318;
|
|
43
|
+
--muted: #b2b8c4;
|
|
44
|
+
--line: #444a56;
|
|
45
|
+
--surface: #1c2028;
|
|
46
|
+
--surface-raised: #282d37;
|
|
47
|
+
--accent-text: #9aafff;
|
|
48
|
+
--about-surface: #1b2540;
|
|
49
|
+
--about-ink: #f5f7ff;
|
|
50
|
+
--about-muted: #c1c9de;
|
|
51
|
+
--inverse-surface: #f2f4f8;
|
|
52
|
+
--inverse-ink: #101114;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
:root[data-theme='dark'][data-accent='forest'] { --accent-text: #68dfb1; --about-surface: #173029; --about-muted: #b7e1d1; }
|
|
56
|
+
:root[data-theme='dark'][data-accent='coral'] { --accent-text: #ffa58e; --about-surface: #35231f; --about-muted: #e7c1b8; }
|
|
57
|
+
|
|
58
|
+
/* CSS alone follows the OS when scripting is disabled and no resolved theme is set. */
|
|
59
|
+
@media (prefers-color-scheme: dark) {
|
|
60
|
+
:root:not([data-theme]) {
|
|
61
|
+
color-scheme: dark;
|
|
62
|
+
--ink: #f2f4f8;
|
|
63
|
+
--paper: #111318;
|
|
64
|
+
--muted: #b2b8c4;
|
|
65
|
+
--line: #444a56;
|
|
66
|
+
--surface: #1c2028;
|
|
67
|
+
--surface-raised: #282d37;
|
|
68
|
+
--accent-text: #9aafff;
|
|
69
|
+
--about-surface: #1b2540;
|
|
70
|
+
--about-ink: #f5f7ff;
|
|
71
|
+
--about-muted: #c1c9de;
|
|
72
|
+
--inverse-surface: #f2f4f8;
|
|
73
|
+
--inverse-ink: #101114;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# 공개 API와 통합
|
|
2
|
+
|
|
3
|
+
패키지 이름은 `@hyeonse/design-system`입니다. 공개 진입점은 `./tokens.css`, `./components.css`, `./preferences`, `./react`입니다. 내부 `src/`나 `dist/` 경로를 소비 앱에서 직접 가져오지 않습니다.
|
|
4
|
+
|
|
5
|
+
## CSS
|
|
6
|
+
|
|
7
|
+
`tokens.css`는 역할 색과 화면 설정 선택자를 제공합니다. `components.css`는 현재 `ThemeToggle`과 `PreferencesPanel`의 `.hds-*` 스타일을 제공합니다. 일반 버튼·입력 컴포넌트 전체를 스타일링하는 전역 CSS는 아닙니다. 애플리케이션은 이 두 파일을 각각 한 번 불러와야 합니다.
|
|
8
|
+
|
|
9
|
+
## 설정 유틸
|
|
10
|
+
|
|
11
|
+
`@hyeonse/design-system/preferences`는 `Preferences` 타입과 다음 값을 공개합니다.
|
|
12
|
+
|
|
13
|
+
| 이름 | 목적 |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `preferencesKey`, `defaultPreferences` | 기본 저장 키와 설정 |
|
|
16
|
+
| `validatePreferences`, `parsePreferences`, `serializePreferences` | 저장값의 안전한 읽기·쓰기 |
|
|
17
|
+
| `resolvedTheme`, `effectiveMotion` | 기기 설정을 적용한 값 계산 |
|
|
18
|
+
| `createPreferenceScript(storageKey?)`, `preferenceScript` | 첫 페인트 전에 루트 `data-*` 속성 적용 |
|
|
19
|
+
|
|
20
|
+
스크립트는 `<head>`에서 CSS보다 먼저 실행합니다. 사용자 저장 문자열을 HTML에 직접 연결하지 말고 제공 함수를 사용합니다. 별도 저장 키를 쓰면 스크립트와 React Provider에 동일한 키를 전달합니다.
|
|
21
|
+
|
|
22
|
+
## React
|
|
23
|
+
|
|
24
|
+
`@hyeonse/design-system/react`는 `PreferencesProvider`, `usePreferences`, `ThemeToggle`, `PreferencesPanel`을 제공합니다. Provider는 자식 컴포넌트에 `preferences`, `setPreference(key, value)`, `resetPreferences()`를 제공합니다. 두 UI 컴포넌트의 `locale`은 `ko` 또는 `en`이며 기본값은 `ko`입니다.
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
<PreferencesProvider storageKey="hyeonse-site-preferences" eventName="hyeonse:preferences">
|
|
28
|
+
<ThemeToggle locale="ko" />
|
|
29
|
+
<PreferencesPanel locale="ko" />
|
|
30
|
+
</PreferencesProvider>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Provider는 현재 탭에서 값을 갱신하고 저장 가능한 환경에서는 localStorage에 보존합니다. 다른 탭의 저장 변경과 기기 색상·움직임 설정 변경도 반영합니다. `eventName`으로 설정 변경 이벤트 이름을 지정할 수 있습니다. 설정을 읽는 컴포넌트는 Provider 안에 두세요.
|
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 소비 앱 CSS 점검
|
|
2
|
+
|
|
3
|
+
`hds-audit <consumer-root> [config-relative-path]`는 소비 앱 안의 CSS를 재귀적으로 검사합니다. 설정 경로를 생략하면 `<consumer-root>/design-system.audit.json`을 읽습니다. 패키지를 빌드한 뒤 실행합니다.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
hds-audit ./my-app
|
|
7
|
+
hds-audit ./my-app config/design-system.audit.json
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
설정 예시:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"paths": ["src"],
|
|
15
|
+
"exceptions": {
|
|
16
|
+
"src/components/IdentityBadge.module.css": {
|
|
17
|
+
"reason": "physical material colors",
|
|
18
|
+
"colors": { "#30372f": 2 }
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"overrideTokens": {
|
|
22
|
+
"src/styles/global.css": ["--gutter"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`paths`는 검사할 CSS 탐색 시작 위치입니다. `exceptions`는 특정 파일의 원시 색상과 허용 횟수를 이유와 함께 적습니다. `overrideTokens`는 특정 파일에서 재정의해도 되는 토큰 이름을 명시합니다. 예외는 실제 목적에 필요한 값만 좁게 유지하세요.
|
|
28
|
+
|
|
29
|
+
검사는 주석을 제외한 CSS의 원시 hex/rgb/hsl/lab/lch 색상 사용과 공유 토큰의 허용되지 않은 재정의를 보고합니다. 새 CSS 파일도 지정 경로 아래에 있으면 검사 대상입니다. TypeScript, JavaScript, 캔버스, WebGL/3D 머티리얼, 런타임 계산 색상은 검사하지 않으므로 결과를 앱 전체의 토큰 준수 증명으로 해석하지 않습니다.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# 기초 규칙
|
|
2
|
+
|
|
3
|
+
## 시각 방향
|
|
4
|
+
|
|
5
|
+
흰 종이 같은 바탕에 선명한 검정과 코발트 한 점을 둡니다. 정보는 여백, 얇은 구분선, 명확한 활자 위계로 나눕니다. 장식보다 읽기와 조작의 정확도가 우선입니다. 포레스트와 코랄은 같은 역할의 대체 강조색입니다.
|
|
6
|
+
|
|
7
|
+
## 역할 토큰
|
|
8
|
+
|
|
9
|
+
| 역할 | 변수 | 기본값 |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| 페이지·본문 | `--paper`, `--ink` | `#ffffff`, `#101114` |
|
|
12
|
+
| 보조 글자·구분선 | `--muted`, `--line` | `#5a606b`, `#d8dbe3` |
|
|
13
|
+
| 묶음·떠 있는 면 | `--surface`, `--surface-raised` | `#f3f5f8`, `#ffffff` |
|
|
14
|
+
| 강조 면·그 위 글자 | `--accent-solid`, `--on-accent` | `#204cfc`, `#ffffff` |
|
|
15
|
+
| 링크·선택·포커스 | `--accent-text` | `#204cfc` |
|
|
16
|
+
| 반전 패널 | `--inverse-surface`, `--inverse-ink` | `#101114`, `#ffffff` |
|
|
17
|
+
|
|
18
|
+
강조색은 코발트 `#204cfc`, 포레스트 `#087451`, 코랄 `#b93d20`입니다. 다크 화면은 `#111318`을 바탕으로 사용하고 읽기용 강조색을 별도로 밝힙니다. 강조 면에는 `--on-accent`, 일반 바탕 위 링크에는 `--accent-text`를 사용합니다. 원시 색 하나를 두 역할에 공용하지 않습니다.
|
|
19
|
+
|
|
20
|
+
## 글자와 형태
|
|
21
|
+
|
|
22
|
+
영문은 DM Sans, 한국어는 Noto Sans KR을 기본 서체로 사용합니다. 이 저장소의 쇼케이스에는 포트폴리오에서 복사한 로컬 글꼴을 사용합니다. 한국어 파일은 포트폴리오 공개 문구용 서브셋이어서 새 제품의 모든 한글을 포함하지 않을 수 있습니다. 새 제품은 필요한 문자 범위에 맞는 글꼴 자산을 준비해야 합니다.
|
|
23
|
+
|
|
24
|
+
기본 카드 모서리는 `0px`, 조작 요소는 `2px`입니다. 부드러운 설정에서 각각 `20px`, `12px`가 됩니다. 촘촘한 설정은 `--space-factor`를 `1`에서 `.8`로 바꿉니다. 구성 요소가 간격을 계산할 때 이 변수를 사용해야 실제 차이가 납니다.
|
|
25
|
+
|
|
26
|
+
## 방문자 설정
|
|
27
|
+
|
|
28
|
+
테마 `system/light/dark`, 강조색 `cobalt/forest/coral`, 간격 `comfortable/compact`, 모서리 `crisp/soft`, 움직임 `full/reduced`를 허용합니다. 저장값은 열거형으로 검증합니다. 기기의 움직임 감소 설정이 켜져 있으면 화면의 유효 움직임도 줄입니다. 움직임은 정보나 기능의 유일한 전달 수단이 되지 않아야 합니다.
|
|
29
|
+
|
|
30
|
+
`data-theme`, `data-accent`, `data-density`, `data-corners`, `data-motion`이 문서 루트에 적용됩니다. `system`은 현재 기기 색상 설정에 따라 `data-theme`의 `light/dark`로 해석됩니다. JavaScript 없이도 CSS의 다크 미디어 쿼리가 기기 설정을 따릅니다.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 통합 검증 · 2026-10-08
|
|
2
|
+
|
|
3
|
+
포트폴리오에 공통 패키지를 적용하기 전후의 로컬 정적 빌드를 비교했다.
|
|
4
|
+
|
|
5
|
+
- Chrome: 9개 경로 × 390·1280px × 밝게·어둡게, 총 36개 화면. 기존 역할 토큰 값과 제목·설정 요소의 배치·글꼴·색 측정값이 같았다.
|
|
6
|
+
- 홈·작업실 8개 캡처는 pixelmatch threshold 0.1에서 차이 0픽셀이었다.
|
|
7
|
+
- 설정 변경, 새로고침 복원, 다른 탭 동기화, 기기 테마 변경, 기기 움직임 감소 우선, 초기화, 라디오 방향키 조작, 저장소 차단 시 조작, JavaScript 없는 다크 화면을 확인했다.
|
|
8
|
+
- 설정 패널 axe WCAG 2 A/AA 검사에서 위반이 없었다. 전체 사이트의 접근성 인증을 뜻하지 않는다.
|
|
9
|
+
- 견본 페이지는 390·1280px의 밝게·어둡게에서 가로 넘침과 JavaScript 오류가 없었다.
|
|
10
|
+
- 포트폴리오 TypeScript, 정적 빌드, 콘텐츠 검사(15페이지·210개 로컬 링크), CSS 토큰 검사를 통과했다.
|
|
11
|
+
- CSS 검사에 원시 색상과 공유 토큰 재정의를 심어 실패하는지 확인했다. 패키지 테스트는 잘못된 저장값, 초기 스크립트, 사용자 정의 키 이스케이프, SSR 의미 구조, 복수 패널의 ID·라디오 이름 분리를 포함한다.
|
|
12
|
+
|
|
13
|
+
기존 데스크톱 홈 배지 영역의 scrollWidth 초과는 전후에 동일하게 관찰됐다. 이번 분리 작업에서 변경하지 않았다. 실제 모바일 기기, 다른 브라우저, 프로덕션 배포는 위 로컬 검증 범위에 포함하지 않는다.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ko">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<title>Hyeonse Design System — Visual Catalog</title>
|
|
8
|
+
<meta name="description" content="Hyeonse Design System의 토큰, 화면 설정과 컨트롤 상태를 살펴보는 시각 카탈로그">
|
|
9
|
+
<link rel="stylesheet" href="./dist/tokens.css">
|
|
10
|
+
<link rel="stylesheet" href="./dist/components.css">
|
|
11
|
+
<link rel="stylesheet" href="./showcase.css">
|
|
12
|
+
<script type="module" src="./showcase.mjs"></script>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<a class="skip-link" href="#main">본문으로 건너뛰기</a>
|
|
16
|
+
<div class="catalog-shell">
|
|
17
|
+
<header class="masthead">
|
|
18
|
+
<a class="brand" href="#top" aria-label="Hyeonse Design System, 맨 위로"><span class="brand-mark" aria-hidden="true">H<span>·</span></span><span>HYEONSE<br>DESIGN SYSTEM</span></a>
|
|
19
|
+
<nav aria-label="카탈로그 목차"><a href="#foundations">기초</a><a href="#controls">컨트롤</a><a href="#preferences">설정</a><a href="./docs/api.md">API ↗</a></nav>
|
|
20
|
+
<span class="masthead-version">SYSTEM / 001</span>
|
|
21
|
+
</header>
|
|
22
|
+
<main id="main">
|
|
23
|
+
<section class="hero" id="top" aria-labelledby="hero-title">
|
|
24
|
+
<div class="hero-top"><span class="eyebrow"><span class="signal-dot"></span> PERSONAL INTERFACE SYSTEM</span><span class="hero-index">01 / VISUAL LANGUAGE</span></div>
|
|
25
|
+
<h1 id="hero-title">명확한 형태.<br><em>유연한 화면.</em></h1>
|
|
26
|
+
<div class="hero-bottom"><p>읽기 좋은 정보 구조와 선명한 조작을 위한 개인 디자인 시스템. 하나의 시각 언어를 다양한 화면에 맞게 조정합니다.</p><a class="text-action" href="#preferences">직접 조정하기 <span aria-hidden="true">↗</span></a></div>
|
|
27
|
+
<div class="hero-grid" aria-hidden="true"><span>FORM / FUNCTION</span><span>THEME / ACCENT</span><span>TYPE / SPACE</span></div>
|
|
28
|
+
</section>
|
|
29
|
+
|
|
30
|
+
<section class="catalog-section" id="foundations" aria-labelledby="foundations-title">
|
|
31
|
+
<div class="section-heading"><div><span class="eyebrow">01 — FOUNDATION</span><h2 id="foundations-title">기초 요소</h2></div><p>원시 색보다 사용 목적을 먼저 정합니다. 설정을 바꾸면 같은 역할이 다른 화면에서도 유지됩니다.</p></div>
|
|
32
|
+
<div class="foundation-grid">
|
|
33
|
+
<article class="sample-panel type-panel"><div class="panel-top"><span>01.A / TYPE</span><span>DM SANS + NOTO SANS KR</span></div><div class="type-display" aria-label="Typography sample">Aa<span class="type-dot">.</span></div><h3>정보가 먼저 보이는 글자</h3><p>큰 제목은 시선을 잡고, 본문은 적당한 길이로 읽히게 합니다. 작은 레이블도 충분한 대비를 유지합니다.</p><div class="type-meta"><span>DISPLAY / −0.06em</span><span>BODY / 1.7 LH</span></div></article>
|
|
34
|
+
<article class="sample-panel color-panel"><div class="panel-top"><span>01.B / COLOR ROLES</span><span>LIVE TOKENS</span></div><div class="swatch-stack"><div class="swatch swatch-paper"><span>Paper</span><code>--paper</code></div><div class="swatch swatch-surface"><span>Surface</span><code>--surface</code></div><div class="swatch swatch-accent"><span>Accent</span><code>--accent-solid</code></div><div class="swatch swatch-inverse"><span>Inverse</span><code>--inverse-surface</code></div></div><p class="sample-note">코발트가 기본입니다. 포레스트와 코랄에서도 강조 면과 읽기용 글자색을 나누어 사용합니다.</p></article>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="token-strip" aria-label="의미별 색상 토큰"><div><span class="token-chip token-ink"></span><code>--ink</code><small>주요 정보</small></div><div><span class="token-chip token-muted"></span><code>--muted</code><small>보조 정보</small></div><div><span class="token-chip token-line"></span><code>--line</code><small>구분</small></div><div><span class="token-chip token-accent"></span><code>--accent-text</code><small>텍스트 강조</small></div></div>
|
|
37
|
+
</section>
|
|
38
|
+
|
|
39
|
+
<section class="catalog-section" id="controls" aria-labelledby="controls-title"><div class="section-heading"><div><span class="eyebrow">02 — INTERACTION</span><h2 id="controls-title">컨트롤 상태</h2></div><p>실제 HTML 요소로 선택, 비활성, 포커스 상태를 확인합니다. 아래 견본은 화면 작성 예시입니다.</p></div>
|
|
40
|
+
<div class="control-grid"><article class="control-card"><div class="control-card-head"><span>BUTTON / LINK</span><small>PRIMARY • SECONDARY • DISABLED</small></div><h3>행동의 우선순위</h3><div class="button-row"><button class="sample-button primary" type="button" id="action-button">작업 시작 <span aria-hidden="true">↗</span></button><button class="sample-button secondary" type="button">자세히 보기</button><button class="sample-button primary" type="button" disabled>사용 불가</button></div><p class="control-feedback" id="action-feedback" role="status" aria-live="polite">버튼을 눌러 동작을 확인하세요.</p><div class="component-specimen"><span>PACKAGE CSS / .hds-themeToggle</span><button class="hds-themeToggle" type="button" id="theme-toggle-specimen" aria-label="테마 변경"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2"/></svg><span id="theme-toggle-label">기기 설정</span></button><small>React ThemeToggle의 CSS 상태 견본</small></div></article>
|
|
41
|
+
<article class="control-card"><div class="control-card-head"><span>SELECTION</span><small>RADIO • CHECKBOX</small></div><h3>선택이 보이는 형태</h3><fieldset class="sample-fieldset"><legend>보기 밀도</legend><label class="sample-choice"><input type="radio" name="sample-density" value="comfortable" checked><span>기본 간격</span></label><label class="sample-choice"><input type="radio" name="sample-density" value="compact"><span>촘촘한 간격</span></label></fieldset><label class="check-row"><input type="checkbox" checked><span>업데이트 소식 표시</span></label></article>
|
|
42
|
+
<article class="control-card form-card"><div class="control-card-head"><span>FIELD / FEEDBACK</span><small>LABEL • HINT • ERROR</small></div><h3>입력의 맥락</h3><label for="sample-name">프로젝트 이름</label><input id="sample-name" type="text" placeholder="예: 새로운 작업" aria-describedby="sample-name-hint" maxlength="36"><p id="sample-name-hint">36자 이내로 입력하세요.</p><label for="sample-disabled">비활성 입력</label><input id="sample-disabled" type="text" value="수정할 수 없음" disabled></article>
|
|
43
|
+
<article class="control-card status-card"><div class="control-card-head"><span>STATUS / SURFACE</span><small>SELECTED • NEUTRAL</small></div><h3>상태를 구분하는 정보</h3><div class="status-line"><span class="status-pill selected"><i aria-hidden="true"></i> 선택됨</span><span class="status-pill">검토 중</span><span class="status-pill muted">비활성</span></div><div class="status-preview"><span>PREVIEW / 01</span><strong>한 단계씩 명확하게.</strong><p>선택, 상태, 안내는 색만으로 구분하지 않습니다.</p></div></article></div>
|
|
44
|
+
</section>
|
|
45
|
+
|
|
46
|
+
<section class="catalog-section preferences-section" id="preferences" aria-labelledby="preferences-title"><div class="section-heading"><div><span class="eyebrow">03 — PERSONALIZATION</span><h2 id="preferences-title">화면을 조정하세요</h2></div><p>설정은 이 브라우저에 저장됩니다. 기기의 움직임 감소 설정이 우선합니다.</p></div><div class="settings-layout"><div class="settings-intro"><span class="index-large">03<span> / 03</span></span><p>같은 정보, 다른 읽기 환경. 밝기와 강조색, 간격, 모서리, 움직임을 바꾸어 시스템의 반응을 살펴보세요.</p><div class="settings-current" id="settings-current" role="status" aria-live="polite">설정 불러오는 중</div></div><form class="settings-form" id="settings-form"><fieldset><legend>테마 <span>01</span></legend><div class="segmented"><label><input type="radio" name="theme" value="system"><span>기기 설정</span></label><label><input type="radio" name="theme" value="light"><span>밝게</span></label><label><input type="radio" name="theme" value="dark"><span>어둡게</span></label></div></fieldset><fieldset><legend>강조 색상 <span>02</span></legend><div class="accent-choices"><label><input type="radio" name="accent" value="cobalt"><span class="accent-circle cobalt"></span><span>코발트</span></label><label><input type="radio" name="accent" value="forest"><span class="accent-circle forest"></span><span>포레스트</span></label><label><input type="radio" name="accent" value="coral"><span class="accent-circle coral"></span><span>코랄</span></label></div></fieldset><fieldset><legend>간격 <span>03</span></legend><div class="segmented"><label><input type="radio" name="density" value="comfortable"><span>기본</span></label><label><input type="radio" name="density" value="compact"><span>촘촘하게</span></label></div></fieldset><fieldset><legend>모서리 <span>04</span></legend><div class="segmented"><label><input type="radio" name="corners" value="crisp"><span>각지게</span></label><label><input type="radio" name="corners" value="soft"><span>부드럽게</span></label></div></fieldset><fieldset><legend>움직임 <span>05</span></legend><div class="segmented"><label><input type="radio" name="motion" value="full"><span>기본</span></label><label><input type="radio" name="motion" value="reduced"><span>줄이기</span></label></div></fieldset><button class="reset-button" type="button" id="reset-preferences">기본값으로 되돌리기 <span aria-hidden="true">↗</span></button></form></div></section>
|
|
47
|
+
</main>
|
|
48
|
+
<footer class="footer"><span>HYEONSE DESIGN SYSTEM</span><p>개인 프로젝트를 위한 시각 언어 · v0.1.0</p><a href="#top">맨 위로 ↑</a></footer>
|
|
49
|
+
</div>
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|