Initial commit: HMDesignSystem baseline
This commit is contained in:
commit
b6a0a61103
486 files changed
+68432
No files matched your search
+17
@@ -0,0 +1,17 @@
|
||||
node_modules/
|
||||
dist/
|
||||
storybook-static/
|
||||
theme-dist/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Release/test packages and generated dashboard exports are distributed outside Git.
|
||||
*.zip
|
||||
**/dist-dashboard/
|
||||
|
||||
*.log
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
_local/
|
||||
.claude/settings.local.json
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite"
|
||||
import { readFileSync } from "node:fs"
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.stories.@(ts|tsx)"],
|
||||
addons: [],
|
||||
// In dev, Vite serves both raw /themes JSON and JSON module imports (?import).
|
||||
// Storybook's static middleware would intercept the imports with application/json.
|
||||
staticDirs: (_, { configType }) => [
|
||||
{ from: "../public/fonts", to: "/fonts" },
|
||||
...(configType === "PRODUCTION" ? [{ from: "../themes", to: "/themes" }] : []),
|
||||
],
|
||||
managerHead: (head) => `${head}<link rel="icon" href="data:," /><style id="hm-manager-style">${readFileSync(new URL("./manager.css", import.meta.url), "utf8")}</style>`,
|
||||
// changeDetection off: no "new and modified stories" filter or status badges in the sidebar.
|
||||
features: { sidebarOnboardingChecklist: false, changeDetection: false },
|
||||
// Verification Stories (the same sections as a component's representative Story) stay out of the sidebar by default.
|
||||
// They keep their Story IDs and stay reachable by direct URL and the sidebar tag filter.
|
||||
tags: { verification: { defaultFilterSelection: "exclude" } },
|
||||
// Storybook replaces vite.config's `server`, so watcher excludes go here.
|
||||
// _local/ (gitignored work copies, ~20k files) would otherwise make the Vite watcher stall the dev server.
|
||||
viteFinal: (config) => {
|
||||
config.server ??= {}
|
||||
config.server.watch = { ...config.server.watch, ignored: ["**/_local/**", "**/storybook-static/**"] }
|
||||
return config
|
||||
},
|
||||
framework: "@storybook/react-vite",
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
export function decorateHanmacNavigation(api) {
|
||||
let frame;
|
||||
const write = (node, key, value) => {
|
||||
const next = String(value);
|
||||
if (node.getAttribute(key) !== next) node.setAttribute(key, next);
|
||||
};
|
||||
const ancestors = (entry, refId) => {
|
||||
const result = [];
|
||||
const seen = new Set();
|
||||
while (entry?.parent && !seen.has(entry.parent)) {
|
||||
seen.add(entry.parent);
|
||||
entry = api.resolveStory(entry.parent, refId);
|
||||
if (entry) result.push(entry);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const decorate = () => {
|
||||
frame = undefined;
|
||||
const tree = document.querySelector('#storybook-explorer-tree');
|
||||
if (!tree) return;
|
||||
const selected = api.getCurrentStoryData();
|
||||
const currentParents = new Set(ancestors(selected, selected?.refId).map((item) => item.id));
|
||||
const rows = [...tree.querySelectorAll('[data-item-id]')];
|
||||
const rendered = new Set(rows.map((row) => `${row.dataset.refId}:${row.dataset.itemId}`));
|
||||
const groups = new Map();
|
||||
let section = 0;
|
||||
for (const row of rows) {
|
||||
const refId = row.dataset.refId === 'storybook_internal' ? undefined : row.dataset.refId;
|
||||
const entry = api.resolveStory(row.dataset.itemId, refId);
|
||||
if (!entry) continue;
|
||||
// Visual depth = rendered ancestors. A component with a single story of its own name is hoisted by Storybook (the
|
||||
// story replaces it at depth 1), while the API still reports the story at depth 2.
|
||||
write(row, 'data-hm-depth', ancestors(entry, refId).filter((item) => rendered.has(`${row.dataset.refId}:${item.id}`)).length);
|
||||
write(row, 'data-hm-current-parent', currentParents.has(entry.id) && refId === selected?.refId);
|
||||
if (entry.type === 'root') {
|
||||
write(row, 'data-hm-section', section++);
|
||||
continue;
|
||||
}
|
||||
// A single overlay covers the whole expanded branch, including deeper rows.
|
||||
const branch = [...ancestors(entry, refId).reverse(), entry].find((item) => item.type !== 'root');
|
||||
const owner = rows.find((candidate) => candidate.dataset.itemId === branch?.id && candidate.dataset.refId === row.dataset.refId);
|
||||
const expanded = owner?.querySelector(':scope > button[aria-expanded="true"]');
|
||||
write(row, 'data-hm-group', !!expanded);
|
||||
write(row, 'data-hm-group-start', false);
|
||||
write(row, 'data-hm-group-end', false);
|
||||
if (expanded) {
|
||||
const key = `${row.dataset.refId}:${branch.id}`;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key).push(row);
|
||||
}
|
||||
}
|
||||
for (const members of groups.values()) {
|
||||
write(members[0], 'data-hm-group-start', true);
|
||||
write(members.at(-1), 'data-hm-group-end', true);
|
||||
}
|
||||
};
|
||||
const schedule = () => { if (frame === undefined) frame = requestAnimationFrame(decorate); };
|
||||
const observer = new MutationObserver(schedule);
|
||||
observer.observe(document.body, {
|
||||
childList: true, subtree: true, attributes: true,
|
||||
attributeFilter: ['data-selected', 'aria-expanded', 'data-item-id'],
|
||||
});
|
||||
schedule();
|
||||
window.addEventListener('pagehide', () => {
|
||||
observer.disconnect();
|
||||
if (frame !== undefined) cancelAnimationFrame(frame);
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
/* Initial Storybook manager mapping from ui_viz. Manager-only; not runtime theme tokens. */
|
||||
:root {
|
||||
--font-family-base: 'Noto Sans KR', 'Malgun Gothic', sans-serif;
|
||||
--hm-green-default: #123328;
|
||||
--hm-green-light-1: #205241;
|
||||
--hm-green-light-2: #266651;
|
||||
--hm-green-light-3: #249473;
|
||||
--hm-fg-on-green-secondary: #BDDFD4;
|
||||
--color-fg-on-green-primary: #FFFFFF;
|
||||
--color-action-primary: #123328;
|
||||
--color-action-accent-on-green: #FFC600;
|
||||
--color-bg-content: #F5F0E5;
|
||||
--color-bg-surface: #FFFFFF;
|
||||
--effect-text-border-color: rgba(0, 0, 0, .3);
|
||||
--overlay-black-20: rgba(0, 0, 0, .2);
|
||||
--overlay-black-30: rgba(0, 0, 0, .3);
|
||||
}
|
||||
/* Storybook 10.6 manager only. Source: docs/patterns/dashboard-composition.md §6.
|
||||
The 18px join / inset geometry follows the current LNB specimen, not a new token. */
|
||||
@font-face {
|
||||
font-family: 'Hanmac Manager Noto Sans KR';
|
||||
src: url('/fonts/NotoSansKR-variable.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
}
|
||||
/* Manager-only alias of the bundled Noto Sans KR asset, followed by the
|
||||
Foundation fallback chain (typography.css) instead of a bare sans-serif. */
|
||||
:root {
|
||||
--hm-manager-font: 'Hanmac Manager Noto Sans KR', var(--font-family-base);
|
||||
}
|
||||
|
||||
/* The Storybook layout panel, outside the sidebar landmark, has a separate
|
||||
border. Removing only the landmark's border leaves a one-device-pixel seam. */
|
||||
#root div:has(> div > #storybook-sidebar-region) {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
#storybook-sidebar-region {
|
||||
--hm-manager-join: 18px;
|
||||
color: var(--color-fg-on-green-primary);
|
||||
background: linear-gradient(90deg,
|
||||
var(--hm-green-default) 0%, var(--hm-green-light-1) 30%,
|
||||
var(--hm-green-light-2) 50%, var(--hm-green-light-3) 90%,
|
||||
var(--hm-green-light-2) 100%);
|
||||
border-right: 0;
|
||||
font-family: var(--hm-manager-font);
|
||||
}
|
||||
#storybook-sidebar-region [data-radix-scroll-area-content] > div {
|
||||
padding: 28px 0 28px 20px;
|
||||
}
|
||||
/* Keep the native draggable scrollbar in the unused left gutter. The selected
|
||||
item must reach the content edge without a scroll thumb crossing its curves.
|
||||
Radix supplies left/right inline, so only those two positions need priority. */
|
||||
#storybook-sidebar-region > [scrollbarsize] > [data-orientation='vertical'] {
|
||||
left: 4px !important;
|
||||
right: auto !important;
|
||||
width: 6px;
|
||||
padding: 0;
|
||||
}
|
||||
#storybook-sidebar-region > [scrollbarsize] > [data-orientation='vertical'] > div {
|
||||
background: var(--hm-fg-on-green-secondary);
|
||||
}
|
||||
#storybook-sidebar-region .sidebar-header {
|
||||
margin: 0 20px 24px 0;
|
||||
padding: 0;
|
||||
color: var(--color-fg-on-green-primary);
|
||||
}
|
||||
#storybook-sidebar-region .sidebar-header > div a {
|
||||
color: var(--color-fg-on-green-primary);
|
||||
font: 700 20px/30px var(--hm-manager-font);
|
||||
white-space: normal;
|
||||
/* §6 Text Border: brand title keeps the original 1px (no shadow). */
|
||||
-webkit-text-stroke: 1px var(--effect-text-border-color);
|
||||
paint-order: stroke fill;
|
||||
}
|
||||
#storybook-sidebar-region .sidebar-header button {
|
||||
color: inherit;
|
||||
}
|
||||
#storybook-sidebar-region [role='search'] {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
#storybook-sidebar-region .search-field {
|
||||
min-height: 44px;
|
||||
border: 1px solid var(--hm-fg-on-green-secondary);
|
||||
border-radius: 4px;
|
||||
background: var(--color-bg-surface);
|
||||
color: var(--color-action-primary);
|
||||
}
|
||||
#storybook-explorer-searchfield {
|
||||
color: var(--color-action-primary);
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
}
|
||||
#storybook-sidebar-region [role='search'] > button {
|
||||
color: var(--color-fg-on-green-primary);
|
||||
background: transparent;
|
||||
border-color: var(--hm-fg-on-green-secondary);
|
||||
min-width: 32px;
|
||||
height: 44px;
|
||||
}
|
||||
#storybook-sidebar-region [role='search'] > button:hover {
|
||||
background: rgba(0, 0, 0, .10);
|
||||
}
|
||||
|
||||
#storybook-explorer-tree .sidebar-subheading {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0 20px 0 0;
|
||||
min-height: 56px;
|
||||
color: var(--hm-fg-on-green-secondary);
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-subheading[data-hm-section]:not([data-hm-section='0']) {
|
||||
margin-top: 40px;
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-subheading[data-hm-section]:not([data-hm-section='0'])::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 0;
|
||||
right: 20px;
|
||||
height: 2px;
|
||||
background: linear-gradient(var(--overlay-black-30) 0 50%, rgba(255,255,255,.4) 50% 100%);
|
||||
mask-image: linear-gradient(90deg, transparent, #000 20%, #000 80%, transparent);
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-subheading > button {
|
||||
min-height: 56px;
|
||||
color: inherit;
|
||||
font: 700 16px/28px var(--hm-manager-font);
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 56px;
|
||||
overflow: visible;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--color-fg-on-green-primary);
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-hm-group='true'] {
|
||||
background: var(--overlay-black-20);
|
||||
box-shadow: inset 1px 0 var(--hm-green-light-2);
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-hm-group-start='true'] {
|
||||
border-radius: 8px 0 0 0;
|
||||
box-shadow: inset 1px 1px var(--hm-green-light-2);
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-hm-group-end='true'] {
|
||||
border-radius: 0 0 0 8px;
|
||||
padding-bottom: 8px;
|
||||
box-shadow: inset 1px -1px var(--hm-green-light-2);
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item > :is(a[id], button[id]:not([data-testid])) {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 56px;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 14px 32px 14px 18px;
|
||||
border: 0;
|
||||
border-radius: 4px 0 0 4px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: 700 18px/28px var(--hm-manager-font);
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-hm-depth='2'] > :is(a[id], button[id]:not([data-testid])) {
|
||||
margin-left: 32px;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-hm-depth='3'] > :is(a[id], button[id]:not([data-testid])),
|
||||
#storybook-explorer-tree .sidebar-item[data-hm-depth='4'] > :is(a[id], button[id]:not([data-testid])) {
|
||||
margin-left: 44px;
|
||||
width: calc(100% - 44px);
|
||||
}
|
||||
#storybook-explorer-tree .hm-manager-label {
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
word-break: keep-all;
|
||||
overflow-wrap: anywhere;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
}
|
||||
/* D-093 D-014 (§6 Text Border): non-selected labels, including hover and the
|
||||
yellow current parent, use the specimen's 1.5px border plus lower shadow.
|
||||
Thickness and offset are REVIEW VALUEs. Selected labels carry no effect. */
|
||||
#storybook-explorer-tree .sidebar-item[data-selected='false'] .hm-manager-label {
|
||||
-webkit-text-stroke: 1.5px var(--effect-text-border-color);
|
||||
paint-order: stroke fill;
|
||||
text-shadow: 1px 2px 1px var(--effect-text-border-color);
|
||||
}
|
||||
/* The group chevron carries the same lower shadow (specimen .ref-sidebar-chevron). */
|
||||
#storybook-explorer-tree .sidebar-item[data-selected='false'] > button[id]:not([data-testid]) > div:first-child > div > svg {
|
||||
filter: drop-shadow(1px 2px 1px var(--effect-text-border-color));
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item > :is(a[id], button[id]:not([data-testid])) > div:first-child {
|
||||
position: static;
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
/* Storybook 10.6.1: the leading wrapper has a direct svg[type] for every
|
||||
menu type (group/component/story/docs), plus a nested untyped chevron
|
||||
only on expandable rows. Remove type glyphs at every depth. */
|
||||
#storybook-explorer-tree .sidebar-item > :is(a[id], button[id]:not([data-testid])) > div:first-child > svg[type] {
|
||||
display: none;
|
||||
}
|
||||
/* Leaves have no chevron. Remove the now-empty wrapper as a flex item so
|
||||
neither its width nor the row's flex gap reserves an icon slot.
|
||||
The existing depth margins and row padding still supply hierarchy indent. */
|
||||
#storybook-explorer-tree .sidebar-item > :is(a[id], button[id]:not([data-testid])) > div:first-child:has(> svg[type]):not(:has(> div > svg)) {
|
||||
display: none;
|
||||
}
|
||||
/* First-depth leaves still sit inside their root label after losing the icon
|
||||
slot. Deeper rows retain their existing 32px / 44px depth margins. */
|
||||
#storybook-explorer-tree .sidebar-item[data-hm-depth='1'] > :is(a[id], button[id]:not([data-testid])):has(> div:first-child > svg[type]):not(:has(> div:first-child > div > svg)) {
|
||||
padding-left: 32px;
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-hm-current-parent='true'],
|
||||
#storybook-explorer-tree .sidebar-subheading[data-hm-current-parent='true'] {
|
||||
color: var(--color-action-accent-on-green);
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-selected='false'] > :is(a[id], button[id]:not([data-testid])):hover {
|
||||
background: rgba(0, 0, 0, .10);
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-selected='true'] > a[id] {
|
||||
z-index: 1;
|
||||
background: var(--color-bg-content);
|
||||
color: var(--color-action-primary);
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
/* Keep the keyboard skip link without reserving a 1px flex column.
|
||||
That column exposed the green branch background at the selected item's end. */
|
||||
#storybook-explorer-tree .sidebar-item > a[href='#storybook-preview-wrapper'] {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-selected='true'] > a[id]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
box-shadow: inset 6px 0 8px -5px var(--overlay-black-30),
|
||||
inset 0 3px 5px -3px var(--overlay-black-20),
|
||||
inset 0 -2px 4px -3px var(--overlay-black-20);
|
||||
mask-image: linear-gradient(90deg, #000 75%, transparent 100%);
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-selected='true']::before,
|
||||
#storybook-explorer-tree .sidebar-item[data-selected='true']::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: var(--hm-manager-join);
|
||||
height: var(--hm-manager-join);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-selected='true']::before {
|
||||
bottom: 100%;
|
||||
background: radial-gradient(circle at top left, transparent calc(var(--hm-manager-join) - .5px), var(--color-bg-content) var(--hm-manager-join));
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-selected='true']::after {
|
||||
top: 100%;
|
||||
background: radial-gradient(circle at bottom left, transparent calc(var(--hm-manager-join) - .5px), var(--color-bg-content) var(--hm-manager-join));
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-selected='true'][data-hm-group-end='true']::after {
|
||||
top: calc(100% - 8px);
|
||||
}
|
||||
/* Auxiliary status/create actions are separate from selected row geometry.
|
||||
Hide only these controls; the Story link and native group toggles stay intact. */
|
||||
#storybook-explorer-tree .sidebar-item[data-selected='true'] > :is([data-testid='tree-change-status-button'], [data-testid='context-menu']) {
|
||||
display: none;
|
||||
}
|
||||
#storybook-explorer-tree [data-testid='context-menu'] {
|
||||
color: inherit;
|
||||
right: 4px;
|
||||
}
|
||||
/* Storybook gives utility buttons an opaque background and a 5px halo.
|
||||
Keep their native behavior, with the same transparent / black-10% LNB states. */
|
||||
#storybook-explorer-tree :is([data-testid='context-menu'], .sidebar-subheading-action) {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
#storybook-explorer-tree :is([data-testid='context-menu'], .sidebar-subheading-action):is(:hover, [aria-expanded='true']) {
|
||||
background: rgba(0, 0, 0, .10);
|
||||
box-shadow: none;
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-selected='true'] > [data-testid='context-menu'] {
|
||||
z-index: 3;
|
||||
color: var(--color-action-primary);
|
||||
}
|
||||
#storybook-explorer-tree .sidebar-item[data-selected='true'] > [data-testid='context-menu'] svg {
|
||||
color: var(--color-action-primary);
|
||||
}
|
||||
/* Keep the manager preview frame flush without consuming iframe width. */
|
||||
main:has(#storybook-preview-wrapper) {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#storybook-sidebar-region * { scroll-behavior: auto; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createElement } from 'react'
|
||||
import { addons } from 'storybook/manager-api'
|
||||
import { create } from 'storybook/theming'
|
||||
import { decorateHanmacNavigation } from './manager-navigation.js'
|
||||
|
||||
// Match the measured Manager content background via its existing CSS variable.
|
||||
// Keep mobile navigation, addon tabs,
|
||||
// portalled menus and the preview iframe outside this toolbar scope.
|
||||
const toolbarStyle = document.createElement('style')
|
||||
toolbarStyle.id = 'hm-manager-toolbar-style'
|
||||
toolbarStyle.textContent = `
|
||||
.sb-bar[data-testid="sb-preview-toolbar"] {
|
||||
background: var(--color-bg-content);
|
||||
color: #000000;
|
||||
}
|
||||
.sb-bar[data-testid="sb-preview-toolbar"] :is(button, a, [role="tab"], label, span, p, svg) {
|
||||
color: inherit;
|
||||
}
|
||||
.sb-bar[data-testid="sb-preview-toolbar"] :is(button, a, [role="tab"]):hover {
|
||||
background: rgba(0, 0, 0, 0.10);
|
||||
}
|
||||
.sb-bar[data-testid="sb-preview-toolbar"] :is(button, a, [role="tab"]):is([aria-checked="true"], [aria-expanded="true"], [aria-selected="true"]) {
|
||||
background: rgba(0, 0, 0, 0.20);
|
||||
}
|
||||
.sb-bar[data-testid="sb-preview-toolbar"] :is(button, a, [role="tab"]):focus-visible {
|
||||
outline-color: currentColor;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(toolbarStyle)
|
||||
|
||||
addons.setConfig({
|
||||
layout: {
|
||||
showPanel: false,
|
||||
},
|
||||
theme: create({
|
||||
base: 'light',
|
||||
brandTitle: 'HM Design System',
|
||||
brandUrl: './',
|
||||
brandTarget: '_self',
|
||||
fontBase: "'Hanmac Manager Noto Sans KR', 'Noto Sans KR', 'Malgun Gothic', sans-serif",
|
||||
colorPrimary: '#123328',
|
||||
colorSecondary: '#123328',
|
||||
appBg: '#F5F0E5',
|
||||
appContentBg: '#FFFFFF',
|
||||
appPreviewBg: '#FFFFFF',
|
||||
textColor: '#123328',
|
||||
textInverseColor: '#FFFFFF',
|
||||
barSelectedColor: '#123328',
|
||||
}),
|
||||
sidebar: {
|
||||
renderLabel: (item) => createElement('span', { className: 'hm-manager-label' }, item.name),
|
||||
},
|
||||
})
|
||||
|
||||
addons.register('hm/manager-navigation-style', decorateHanmacNavigation)
|
||||
@@ -0,0 +1,2 @@
|
||||
<!-- Match the manager favicon and avoid the browser's implicit /favicon.ico request. -->
|
||||
<link rel="icon" href="data:," />
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { Preview } from "@storybook/react-vite"
|
||||
import { MINIMAL_VIEWPORTS } from "storybook/viewport"
|
||||
import { Toaster } from "@hm-design/ui"
|
||||
import { ensurePseudoStates } from "./pseudo-states"
|
||||
import { DesignerThemePreview } from "../src/theme/designer-preview"
|
||||
import "../src/index.css"
|
||||
import "../src/hm-theme.css"
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
viewport: {
|
||||
options: {
|
||||
// HM QA presets come first; keep Storybook's built-in choices available.
|
||||
hmDesktop1280: { name: "HM Desktop · 1280", styles: { width: "1280px", height: "900px" }, type: "desktop" },
|
||||
hmDesktop1024: { name: "HM Desktop Boundary · 1024", styles: { width: "1024px", height: "900px" }, type: "desktop" },
|
||||
hmMobile1023: { name: "HM Mobile Boundary · 1023", styles: { width: "1023px", height: "900px" }, type: "mobile" },
|
||||
hmMobile390: { name: "HM Mobile · 390", styles: { width: "390px", height: "844px" }, type: "mobile" },
|
||||
...MINIMAL_VIEWPORTS,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
storySort: {
|
||||
order: [
|
||||
"Foundation",
|
||||
["Color", "Typography", "Layout"],
|
||||
"Components",
|
||||
[
|
||||
"Form",
|
||||
["Button", "Input", "Textarea", "Select", "Checkbox", "Radio Group", "Switch", "Slider", "Toggle", "Toggle Group", "Label"],
|
||||
"Navigation",
|
||||
["Tabs", "Breadcrumb", "Pagination", "Navigation Menu", "Menubar"],
|
||||
"Overlay",
|
||||
["Dialog", "Alert Dialog", "Sheet", "Drawer", "Dropdown Menu", "Context Menu", "Popover", "Tooltip", "Hover Card"],
|
||||
"Data & Display",
|
||||
["Card", "Table", "Badge", "Avatar", "Progress", "Skeleton", "Separator", "Aspect Ratio", "Carousel", "Scroll Area"],
|
||||
"Disclosure",
|
||||
["Accordion", "Collapsible", "Resizable"],
|
||||
"Search & Date",
|
||||
["Command", "Combobox", "Calendar", "Date Picker"],
|
||||
"Feedback",
|
||||
["Alert", "Sonner"],
|
||||
],
|
||||
"Patterns",
|
||||
["Data", ["Data Table", "Filter Bar"], "Layout", ["GNB", "LNB", "Dashboard Layout", "Loading Screen"]],
|
||||
"Center Graphic",
|
||||
"*",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story, context) => {
|
||||
// Lets state Stories pin hover / pressed with data-sb-hover / data-sb-active.
|
||||
ensurePseudoStates()
|
||||
// One Toaster for every Story, the same way an app mounts it once at its root.
|
||||
return (
|
||||
<DesignerThemePreview enabled={!context.title.startsWith("Theme/Runtime Contract")}>
|
||||
<Story />
|
||||
<Toaster />
|
||||
</DesignerThemePreview>
|
||||
)
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default preview
|
||||
@@ -0,0 +1,34 @@
|
||||
// Pins :hover and :active visuals for state Stories (Storybook only, never loaded by the app).
|
||||
// Every rule that uses :hover or :active is cloned right after itself with the pseudo-class swapped for a
|
||||
// data attribute, so cascade order and specificity stay the same. No declaration or value is authored here.
|
||||
const HOVER = "data-sb-hover"
|
||||
const ACTIVE = "data-sb-active"
|
||||
const mirrored = new WeakSet<CSSStyleSheet>()
|
||||
|
||||
function mirror(list: CSSStyleSheet | CSSGroupingRule) {
|
||||
for (let i = list.cssRules.length - 1; i >= 0; i--) {
|
||||
const rule = list.cssRules[i]
|
||||
if (rule instanceof CSSStyleRule) {
|
||||
if (/:hover\b|:active\b/.test(rule.selectorText)) {
|
||||
list.insertRule(
|
||||
rule.cssText.replace(/:hover\b/g, `[${HOVER}]`).replace(/:active\b/g, `[${ACTIVE}]`),
|
||||
i + 1,
|
||||
)
|
||||
}
|
||||
} else if ("cssRules" in rule) {
|
||||
mirror(rule as CSSGroupingRule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function ensurePseudoStates() {
|
||||
for (const sheet of document.styleSheets) {
|
||||
if (mirrored.has(sheet)) continue
|
||||
mirrored.add(sheet)
|
||||
try {
|
||||
mirror(sheet)
|
||||
} catch {
|
||||
// Unreadable (cross-origin) sheet: nothing to mirror.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
# HMDesignSystem Storybook LAN Preview
|
||||
|
||||
Result: **PARTIAL** — same-host LAN Preview is READY. A second physical PC was unavailable, so cross-PC access and cross-PC HMR remain NOT_VERIFIED.
|
||||
|
||||
TEMP_STORYBOOK_URL: http://10.13.37.164:6008/
|
||||
|
||||
## 1. Worktree
|
||||
|
||||
- Path: `D:\디자인시스템\HMDesignSystem-storybook`
|
||||
- Branch: `chore/storybook-cleanup`
|
||||
- HEAD: `6b4a39e4e9fef7cd706630ffc72b5e97e8696823`
|
||||
- Remote: `https://gitea.hmac.kr/b25027/HMDesign.git`
|
||||
- Track A Modified: **NO**
|
||||
- Track A remains clean on `main`, HEAD `4e2402e6cb3507c4bb5e58549004323db59b66a8`.
|
||||
|
||||
## 2. Storybook Server
|
||||
|
||||
- Installed binary: Storybook **10.6.1** from Track B node_modules; dependencies already existed, so npm ci was unnecessary.
|
||||
- Port: **6008**; Bind: **0.0.0.0**; Server: **RUNNING**
|
||||
- LAN listener PID at verification: **23804**
|
||||
- Command: `node node_modules/storybook/dist/bin/dispatcher.js dev -p 6008 --host 0.0.0.0 --no-open --disable-telemetry --exact-port`
|
||||
- Local: http://localhost:6008/
|
||||
- LAN: http://10.13.37.164:6008/
|
||||
- Verified current IPv4: **10.13.37.164** (Ethernet 3); this is a temporary LAN address, not a permanent URL.
|
||||
- The old LAN process was started on 6007 and had fallen back to 6008. Only that confirmed Track B LAN process was replaced with explicit 6008/exact-port operation. The existing localhost-only Track B process (PID 33604) was preserved; both addresses use the same worktree and matched 289 Story IDs.
|
||||
- package.json's official **6007** script was unchanged. Track A and other ports/processes were not stopped.
|
||||
- Server stdout/stderr are in ignored `.storybook/qa/lan-preview.*.log` files. PIDs are session observations, not durable shutdown instructions.
|
||||
|
||||
## 3. LAN Verification
|
||||
|
||||
- localhost: **PASS**, HTTP 200 and Button render
|
||||
- LAN IP Same Host: **PASS**, HTTP 200; manager, sidebar, preview, navigation and representative stories loaded
|
||||
- SECOND_PHYSICAL_PC: **NOT_VERIFIED**
|
||||
- HMR localhost: **PASS**
|
||||
- HMR LAN Same Host: **PASS**
|
||||
- HMR Second Physical PC: **NOT_VERIFIED**
|
||||
- A temporary visible `HM LAN HMR QA` paragraph was inserted into GNB All. Both origins displayed it without reload. Removal was also applied by HMR; the Story file was restored to its exact pre-probe SHA-256. No temporary probe remains.
|
||||
- Existing firewall policy was inspected: Public network profile, enabled firewall profiles and pre-existing inbound Node allow rules. Firewall rule additions: **NO**. Network profile changes: **NO**.
|
||||
|
||||
## 4. Storybook Chrome
|
||||
|
||||
- Top Bar: **#266651 PASS** (`rgb(38, 102, 81)`)
|
||||
- Toolbar icon/text: **#FFFFFF PASS** (all sampled button/SVG computed colors were white)
|
||||
- Bottom Panel: **DEFAULT_HIDDEN PASS**
|
||||
- Alt+A: **PASS**, panel opened with Controls/Actions/Interactions; hidden state restored afterward
|
||||
- Preview Rail: **0px PASS**, preview main border-left and padding-left measured 0px
|
||||
- Final browser viewport overrides were reset, and a LAN Dashboard tab remains available in Codex.
|
||||
|
||||
## 5. Viewports
|
||||
|
||||
Measured in Chrome, browser zoom 100%, Storybook zoom 100%, devicePixelRatio 1:
|
||||
|
||||
- 1280: **PASS** — iframe / innerWidth 1280; innerHeight 900; GNB nav visible
|
||||
- 1024: **PASS** — iframe / innerWidth 1024; innerHeight 900; GNB nav visible; LNB column visible
|
||||
- 1023: **PASS** — iframe / innerWidth 1023; innerHeight 900; GNB nav hidden; LNB column hidden and Local mobile bar visible
|
||||
- 390: **PASS** — iframe / innerWidth 390; innerHeight 844; mobile UI visible
|
||||
- GNB Mobile Menu Open automatically selected 390 × 844 and its Sheet was visible.
|
||||
- Existing LNB `hmMobile` preset still selected 390 × 844. Its service-menu Sheet opened successfully, including the nested three-depth menu.
|
||||
- **Browser limitation:** the Codex in-app browser had a fractional-width anomaly at the 1023 boundary: rounded innerWidth was 1023 but both max-width 1023 and min-width 1024 media queries returned false. Chrome passed both boundaries. Production CSS and breakpoints were not changed; exact boundary QA should use Chrome.
|
||||
|
||||
## 6. Representative Stories
|
||||
|
||||
LAN rendering and navigation checks passed for:
|
||||
|
||||
- Button: **PASS** — representative variants/sizes/state board
|
||||
- GNB: **PASS** — four viewport measurements and mobile menu
|
||||
- LNB: **PASS** — representative board, both embedded standalone iframes, desktop/mobile boundary and mobile Sheet
|
||||
- Dashboard: **PASS** — current heading, status filter and data table rendered
|
||||
- Calendar: **PASS** — single/disabled/range/two-month board
|
||||
- Date Picker: **PASS** — empty/selected/format/controlled/size/disabled board
|
||||
- Theme Designer: **PASS** — editor/live preview and successful application of `/themes/default.json` and `/themes/service-example.json`; reset to HM Default afterward
|
||||
- CENTER: **PASS** — Title compositions and Tunnel reference render
|
||||
|
||||
These are targeted rendering/navigation/responsive checks, not a full interaction or accessibility audit of every Story.
|
||||
|
||||
## 7. Story Cleanup
|
||||
|
||||
- Story Files: **61**; Stories: **289**
|
||||
- Hierarchy: **Foundation → Components → Patterns → Center Graphic → Theme**
|
||||
- PRIMARY: **65**
|
||||
- VERIFICATION: **211**
|
||||
- UTILITY: **13** (executable verification, compatibility and playground tools)
|
||||
- Proven LEGACY / DUPLICATE: **0**
|
||||
- KEEP: **65**; HIDE: **224** through the existing verification filter
|
||||
- MERGE_CANDIDATE: **31**, advisory possible overlap with representative boards; all retained
|
||||
- DELETE_CANDIDATE: **0**; deleted stories: **0**
|
||||
- Full source-defined meta.title, Story export/name and meta/story/custom effective tags: `STORY-INVENTORY.md` and `story-inventory.json`.
|
||||
- Framework-injected dev/test/manifest tags are in compiled index.json rather than the source-tag inventory.
|
||||
- Before / built / inventory Story IDs: **289 / 289 / 289**, with no missing, added or renamed IDs.
|
||||
- LNB `iframe.html?id=patterns-layout-lnb--standalone-desktop` and `--standalone-mobile` references were found and both nested frames rendered.
|
||||
- Own Story change: GNB Mobile and Mobile Menu Open now start on global `hmMobile390`. Existing global presets, sorting, verification filtering and LNB 390 configuration were retained.
|
||||
|
||||
## 8. Errors
|
||||
|
||||
- Console: **no new errors observed** in the representative checks or final LAN tab; no Story not found, manager/addon or HMR errors observed after verification.
|
||||
- Network: **no asset 404 observed**. Direct LAN HTTP probes returned 200 for the font, two Theme JSON files, preview CSS, Vite client, manager JS and checked Story modules. LNB nested previews loaded successfully.
|
||||
- Known browser warning: Storybook 11 will require PopoverProvider ariaLabel. It was already present in the initial observation.
|
||||
- Known build warnings: documentation-example `bg-[var(...)]` CSS optimizer warning, chunks over 500kB, and Vite plugin timing notices. The repository's existing reports already document the CSS/chunk warnings. They did not fail the build.
|
||||
- Git emitted LF/CRLF normalization notices; `git diff --check` still passed.
|
||||
|
||||
## 9. Validation
|
||||
|
||||
- `npm run typecheck`: **PASS**, exit 0; generated Theme defaults matched, both workspace typechecks passed
|
||||
- `npm run build-storybook`: **PASS**, exit 0; local ignored `storybook-static/` output only
|
||||
- `git diff --check`: **PASS**, exit 0
|
||||
- Final integration validation source stability: **PASS**, no TS/TSX/JS/CSS/HTML files changed during the final check window
|
||||
- During an earlier validation window, another active task changed Dashboard Story/CSS. Those changes were preserved, and integration checks were rerun against the stable final source state, including the later concurrent changeDetection option in main.ts.
|
||||
|
||||
## 10. Parallel Safety
|
||||
|
||||
- packages/**: **UNCHANGED** (Production source; git clean for protected paths)
|
||||
- themes/**: **UNCHANGED**
|
||||
- package.json / package-lock.json: **UNCHANGED**
|
||||
- Protected file SHA-256 comparison: **277 files, 0 changes** from this session's baseline
|
||||
- Track A: **UNTOUCHED**, clean main and same HEAD
|
||||
- Package source edits: **NO**
|
||||
- package publish: **NO**; package version changes: **NO**
|
||||
- Runtime Theme contract/loader, exports/CSS contract and Gitea CI/CD changes: **NO**
|
||||
- Publishing Page QA / Publishing staticDirs / official hosting work: **NOT PERFORMED**
|
||||
|
||||
## 11. Git
|
||||
|
||||
Own changed/new files:
|
||||
|
||||
- `src/patterns/gnb.stories.tsx`
|
||||
- `.storybook/qa/generate-inventory.mjs`
|
||||
- `.storybook/qa/story-inventory.json`
|
||||
- `.storybook/qa/STORY-INVENTORY.md`
|
||||
- `.storybook/qa/LAN-PREVIEW.md`
|
||||
- `.storybook/qa/LAN-PREVIEW-REPORT.md`
|
||||
|
||||
Pre-existing or concurrent changes were preserved: `.storybook/main.ts`, `.storybook/manager.css`, `.storybook/preview.tsx`, `.storybook/preview-head.html`, `src/patterns/dashboard-layout.stories.tsx`, its Story-only CSS, `src/theme/designer-theme.stories.tsx`, and the six files in `src/foundation/`.
|
||||
|
||||
- Commit: **NOT CREATED**
|
||||
- Merge: **NOT PERFORMED**
|
||||
- Rebase / cherry-pick: **NOT PERFORMED**
|
||||
- No automatic main integration or push was performed.
|
||||
|
||||
## 12. Status
|
||||
|
||||
- LAN TEMP PREVIEW: **READY** for Storybook cleanup and same-host LAN QA
|
||||
- Official Static Hosting: **NOT CONFIGURED** by this batch
|
||||
- This URL is an official deployment URL: **NO**
|
||||
- Other physical PCs can be declared verified: **NO**, pending actual cross-PC access
|
||||
|
||||
## Required answers
|
||||
|
||||
1. Storybook Worktree execution: **YES**, Track B.
|
||||
2. Execution on main: **NO**.
|
||||
3. 6008 bound to 0.0.0.0: **YES**.
|
||||
4. localhost:6008 opens: **YES**.
|
||||
5. 10.13.37.164:6008 opens on the same PC: **YES**.
|
||||
6. Opens from another physical PC: **NOT_VERIFIED**.
|
||||
7. Missing cross-PC verification explicitly marked: **YES**.
|
||||
8. Top Bar #266651 maintained: **YES**.
|
||||
9. Toolbar icon/text white: **YES**.
|
||||
10. Bottom panel default hidden: **YES**.
|
||||
11. Preview rail 0px: **YES**.
|
||||
12. 1280 preset available and measured: **YES**.
|
||||
13. 1024 preset available and measured: **YES**.
|
||||
14. 1023 preset available and measured: **YES**.
|
||||
15. 390 preset available and measured: **YES**.
|
||||
16. GNB/LNB 1024→1023 transition: **PASS in Chrome**, in-app-browser caveat above.
|
||||
17. Story HMR works: **YES**.
|
||||
18. Same-host LAN receives HMR changes: **YES**; cross-PC HMR **NOT_VERIFIED**.
|
||||
19. New console errors observed: **NO**.
|
||||
20. Asset 404 observed: **NO** in checked resources/screens.
|
||||
21. Track A modified: **NO**.
|
||||
22. Package source modified: **NO**.
|
||||
23. Package publish repeated: **NO**.
|
||||
24. Official deployment URL: **NO**.
|
||||
25. Current preview usable for Storybook cleanup: **YES**; second physical PC validation remains outstanding.
|
||||
|
||||
## Time tracking (Asia/Seoul)
|
||||
|
||||
- Started At: 2026-10-07 15:31:24 KST
|
||||
- Completed At: 2026-10-07 15:56:10 KST
|
||||
- Actual Duration: 24분 46초
|
||||
- Estimated Duration: 35~50분
|
||||
- Variance: 예상보다 10분 14초 ~ 25분 14초 짧음
|
||||
@@ -0,0 +1,78 @@
|
||||
# Track B temporary Storybook LAN preview
|
||||
|
||||
Purpose: **TEMPORARY INTERNAL LAN PREVIEW / LAN TEMP PREVIEW** for Storybook cleanup. This is a development server with HMR, not official static hosting or a permanent URL.
|
||||
|
||||
- Worktree: `D:\디자인시스템\HMDesignSystem-storybook`
|
||||
- Branch: `chore/storybook-cleanup`
|
||||
- Port: `6008`; bind: `0.0.0.0`
|
||||
- TEMP_STORYBOOK_URL: http://10.13.37.164:6008/
|
||||
- Local: http://localhost:6008/
|
||||
- IPv4 `10.13.37.164` was verified on 2026-10-07. Recheck it before later sharing; do not assume a fixed address.
|
||||
|
||||
## Start and inspect
|
||||
|
||||
Run from Track B in PowerShell:
|
||||
|
||||
```powershell
|
||||
Set-Location -LiteralPath 'D:\디자인시스템\HMDesignSystem-storybook'
|
||||
git status -sb
|
||||
git branch --show-current
|
||||
git rev-parse HEAD
|
||||
# Only when node_modules is absent:
|
||||
# npm ci
|
||||
.\node_modules\.bin\storybook.cmd dev -p 6008 --host 0.0.0.0 --no-open --disable-telemetry --exact-port
|
||||
```
|
||||
|
||||
The installed Storybook binary is version 10.6.1. `--exact-port` prevents silent fallback to another port. The official package.json script stays on 6007. Do not use that script without overriding the port for Track B. If 6008 is occupied, identify its process before stopping or replacing it.
|
||||
|
||||
```powershell
|
||||
Get-NetIPAddress -AddressFamily IPv4
|
||||
Get-NetTCPConnection -State Listen -LocalPort 6008 |
|
||||
Select-Object LocalAddress, LocalPort, OwningProcess
|
||||
# For a known PID from the result:
|
||||
# Get-CimInstance Win32_Process -Filter 'ProcessId=<PID>' |
|
||||
# Select-Object ProcessId, CommandLine
|
||||
```
|
||||
|
||||
Keep the server running while the preview is in use. For shutdown, stop only the confirmed Track B listener (Ctrl+C in its owning terminal, or its identified PID). Do not stop other tracks or other Node processes.
|
||||
|
||||
The 2026-10-07 session reused an existing localhost-only Track B listener and replaced only the LAN listener with an explicit 6008 command. Thus localhost and LAN may be served by different processes using the same Track B source. Verify both addresses after subsequent changes.
|
||||
|
||||
## QA viewports
|
||||
|
||||
Use Chrome at browser zoom 100% and Storybook zoom 100%. Select a preset from the toolbar; use enough browser height to avoid clipping the configured iframe height.
|
||||
|
||||
- HM Desktop · 1280: 1280 × 900
|
||||
- HM Desktop Boundary · 1024: 1024 × 900; Desktop GNB/LNB
|
||||
- HM Mobile Boundary · 1023: 1023 × 900; Mobile GNB/LNB
|
||||
- HM Mobile · 390: 390 × 844
|
||||
|
||||
These are QA presets, not production breakpoint changes. GNB Mobile and Mobile Menu Open start at the shared `hmMobile390` preset. LNB's existing `hmMobile` 390 × 844 preset and direct iframe Story IDs remain intact.
|
||||
|
||||
The Codex in-app browser showed a fractional-width boundary anomaly at 1023: `innerWidth` rounded to 1023 while both `(max-width: 1023px)` and `(min-width: 1024px)` were false. Chrome at devicePixelRatio 1 passed the exact boundary. Use Chrome for these two boundary checks; do not compensate by changing Production CSS.
|
||||
|
||||
## Hierarchy and classification
|
||||
|
||||
The sidebar order remains Foundation → Components → Patterns → Center Graphic → Theme. Existing `verification` filtering hides QA/tool stories by default while preserving direct URLs and tag-filter access.
|
||||
|
||||
Refresh the full inventory from Track B:
|
||||
|
||||
```powershell
|
||||
node .storybook/qa/generate-inventory.mjs
|
||||
```
|
||||
|
||||
See `STORY-INVENTORY.md` and `story-inventory.json`. The helper uses the installed Storybook 10.6.1 CSF parser and reads every `src/**/*.stories.ts(x)` file. Tags in this inventory are source-defined meta/story tags; framework-generated dev/test/manifest tags are available in Storybook's index.json.
|
||||
|
||||
KEEP and HIDE describe current navigation. MERGE_CANDIDATE is an advisory review flag, not a removal decision. The current batch does not delete stories or rename titles/exports/IDs. Confirm equivalent QA coverage and search references before future merging or deletion.
|
||||
|
||||
## Preserve the boundary
|
||||
|
||||
All edits must stay in Storybook configuration, stories, or Storybook-only helpers/layout/documentation. Do not modify Track A, packages, themes, Runtime Theme contracts/loaders, package exports/CSS contracts, package versions, publishing, CI/CD, staticDirs for Publishing, or official hosting. Do not merge, rebase, or cherry-pick this batch into main.
|
||||
|
||||
For each completed cleanup batch, run typecheck, build-storybook, and git diff --check against the final source state. Build outputs are validation artifacts, not a deployment.
|
||||
|
||||
## LAN verification
|
||||
|
||||
Same-host LAN IP success does not prove access from another physical PC. On a second physical PC, check manager/sidebar/preview, fonts/themes/assets, navigation and HMR. Until then record **SECOND_PHYSICAL_PC: NOT_VERIFIED**.
|
||||
|
||||
The current network profile was Public, with firewall profiles enabled and pre-existing inbound Node allow rules. No firewall rule or network profile was changed. If a second PC cannot connect, diagnose routing, client isolation and existing firewall policy; do not automatically add rules or change profiles.
|
||||
@@ -0,0 +1,614 @@
|
||||
# Internal Story inventory (ZIP excluded)
|
||||
|
||||
Current references reconciled against the running Storybook index (288 entries). The historical Track B generator is not a recipient tool; this QA record is excluded from the test ZIP.
|
||||
|
||||
This is a classification record. No Story IDs, exports or titles are renamed. HIDE uses the existing verification filter, not deletion. MERGE_CANDIDATE is advisory and can coexist with HIDE. Utility includes executable checks and compatibility/playground tools. No legacy or delete candidate is asserted without evidence.
|
||||
|
||||
## Summary
|
||||
|
||||
- PRIMARY: 65
|
||||
- VERIFICATION: 210
|
||||
- UTILITY: 13
|
||||
- LEGACY / DUPLICATE: 0
|
||||
- KEEP: 65
|
||||
- HIDE: 223
|
||||
- MERGE_CANDIDATE: 31
|
||||
- DELETE_CANDIDATE: 0
|
||||
- Story files: 61
|
||||
- Stories: 288
|
||||
- Hierarchy: Foundation → Components → Patterns → Center Graphic → Theme
|
||||
|
||||
## Full inventory
|
||||
|
||||
### Center Graphic/Facility
|
||||
|
||||
Source: `src/center/center-facility.stories.tsx`; meta.tags: []
|
||||
|
||||
- Tunnel — 터널; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Bridge — 교량; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Intersection — 교차로; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Junction — 분기점; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Interchange — 나들목; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Roadside — 휴게소 · 졸음쉼터; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Linked — 연계 시설물; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- TunnelPlayground — 직접 테스트 (터널); tags: ["verification"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Center Graphic/Information
|
||||
|
||||
Source: `src/center/center-information.stories.tsx`; meta.tags: []
|
||||
|
||||
- Information — Information; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
|
||||
### Center Graphic/Overview
|
||||
|
||||
Source: `src/center/center-overview.stories.tsx`; meta.tags: []
|
||||
|
||||
- Overview — Overview; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
|
||||
### Center Graphic/Spatial
|
||||
|
||||
Source: `src/center/center-spatial.stories.tsx`; meta.tags: []
|
||||
|
||||
- Spatial — Spatial; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
|
||||
### Center Graphic/Title
|
||||
|
||||
Source: `src/center/center-title.stories.tsx`; meta.tags: []
|
||||
|
||||
- Compositions — Title; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Playground — Playground; tags: ["verification"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Disclosure/Accordion
|
||||
|
||||
Source: `src/components/accordion.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Accordion; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Single — Single; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Multiple — Multiple; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Disabled — Disabled; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- LongContent — Long Content; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Overlay/Alert Dialog
|
||||
|
||||
Source: `src/components/alert-dialog.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Alert Dialog; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- ConfirmationOpen — Confirmation; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- DestructiveOpen — Destructive; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Feedback/Alert
|
||||
|
||||
Source: `src/components/alert.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Alert; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Default — Default; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Destructive — Destructive; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Data & Display/Aspect Ratio
|
||||
|
||||
Source: `src/components/aspect-ratio.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Aspect Ratio; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Wide — 16:9; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Square — 1:1; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Data & Display/Avatar
|
||||
|
||||
Source: `src/components/avatar.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Avatar; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Image — Image; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- FallbackError — Fallback; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Data & Display/Badge
|
||||
|
||||
Source: `src/components/badge.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Badge; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Variants — Variants; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Playground — Playground; tags: ["verification"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Navigation/Breadcrumb
|
||||
|
||||
Source: `src/components/breadcrumb.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Breadcrumb; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Basic3 — Basic; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- WithEllipsis — Ellipsis; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LongPath — Long Path; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Form/Button
|
||||
|
||||
Source: `src/components/button.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Button; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Roles — Roles; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Sizes — Sizes; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- States — Brand States; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- StandardStates — Standard States; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LongLabel — Long Label; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- KoreanLabels — Korean Labels; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- IconTextNarrow — Icon Text Narrow; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Destructive — Destructive; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Playground — Playground; tags: ["verification"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Compatibility — Compatibility; tags: ["verification"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Search & Date/Calendar
|
||||
|
||||
Source: `src/components/calendar.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Calendar; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- SingleSelection — Single; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- DisabledSelection — Disabled Dates; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- RangeSelection — Range; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Data & Display/Card
|
||||
|
||||
Source: `src/components/card.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Card; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Surfaces — Surfaces; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Action — Action; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Compatibility — Compatibility; tags: ["verification"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Data & Display/Carousel
|
||||
|
||||
Source: `src/components/carousel.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Carousel; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- BasicCarousel — Basic; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- MultipleItems — Multiple Items; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LoopCarousel — Loop; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- VerticalCarousel — Vertical; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Form/Checkbox
|
||||
|
||||
Source: `src/components/checkbox.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Checkbox; tags: ["play-fn"]; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Unchecked — Unchecked; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Checked — Checked; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Indeterminate — Indeterminate; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Disabled — Disabled; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Error — Error; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Playground — Playground; tags: ["verification"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Disclosure/Collapsible
|
||||
|
||||
Source: `src/components/collapsible.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Collapsible; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Closed — Closed; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Open — Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Disabled — Disabled; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- LongContent — Long Content; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Search & Date/Combobox
|
||||
|
||||
Source: `src/components/combobox.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Combobox; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- EmptyCombobox — Empty; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- ControlledCombobox — Controlled; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LongOption — Long Option; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- OpenBottom — Open Bottom; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- OpenTop — Open Top; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Search & Date/Command
|
||||
|
||||
Source: `src/components/command.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Command; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Search — Search; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- GroupedList — Grouped; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Overlay/Context Menu
|
||||
|
||||
Source: `src/components/context-menu.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Context Menu; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Open — Open; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Search & Date/Date Picker
|
||||
|
||||
Source: `src/components/date-picker.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Date Picker; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- EmptyPicker — Empty; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- InvalidFocus — Invalid Focus; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- SelectedPicker — Selected; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- ControlledPicker — Controlled; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Overlay/Dialog
|
||||
|
||||
Source: `src/components/dialog.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Dialog; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Open — Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LongContentOpen — Long Content; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- WideContentOpen — Wide Content; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- BodyScrollOpen — Body Scroll; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- DeleteConfirmationOpen — Delete Confirmation; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Overlay/Drawer
|
||||
|
||||
Source: `src/components/drawer.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Drawer; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Open — Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Form — Form; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LongContent — Long Content; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Overlay/Dropdown Menu
|
||||
|
||||
Source: `src/components/dropdown-menu.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Dropdown Menu; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Open — Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Overlay/Hover Card
|
||||
|
||||
Source: `src/components/hover-card.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Hover Card; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Open — Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Form/Input
|
||||
|
||||
Source: `src/components/input.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Input; tags: ["play-fn"]; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Default — Default; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Focus — Focus; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Error — Error; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- InvalidFocus — Invalid Focus; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- ErrorMessage — Error Message; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Required — Required; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Disabled — Disabled; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Readonly — Readonly; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Sizes — Compare Sizes; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Small — Small; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Medium — Medium; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Large — Large; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Playground — Playground; tags: ["verification"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Form/Label
|
||||
|
||||
Source: `src/components/label.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Label; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
|
||||
### Components/Navigation/Menubar
|
||||
|
||||
Source: `src/components/menubar.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Menubar; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- TriggerFocus — Trigger Focus; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- OpenFocus — Open Focus; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Navigation/Navigation Menu
|
||||
|
||||
Source: `src/components/navigation-menu.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Navigation Menu; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Default — Default; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- NoViewport — No Viewport; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- IndicatorAlignment — Indicator Alignment; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Navigation/Pagination
|
||||
|
||||
Source: `src/components/pagination.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Pagination; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- FirstPage — First Page; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LastPage — Last Page; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LongRange — Long Range; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Overlay/Popover
|
||||
|
||||
Source: `src/components/popover.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Popover; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Open — Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Data & Display/Progress
|
||||
|
||||
Source: `src/components/progress.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Progress; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Zero — Zero; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Middle — Middle; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Full — Full; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- NonDefaultMax — Non Default Max; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Form/Radio Group
|
||||
|
||||
Source: `src/components/radio-group.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Radio Group; tags: ["play-fn"]; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Vertical — Vertical; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Horizontal — Horizontal; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Disabled — Disabled; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Error — Error; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Disclosure/Resizable
|
||||
|
||||
Source: `src/components/resizable.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Resizable; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- HorizontalLayout — Horizontal; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- VerticalLayout — Vertical; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- MinMaxLayout — Min Max; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Data & Display/Scroll Area
|
||||
|
||||
Source: `src/components/scroll-area.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Scroll Area; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- VerticalScroll — Vertical; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- HorizontalScroll — Horizontal; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Form/Select
|
||||
|
||||
Source: `src/components/select.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Select; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Default — Default; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- InvalidFocus — Invalid Focus; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Open — Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Selected — Selected; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Disabled — Disabled; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Sizes — Compare Sizes; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Medium — Medium; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Large — Large; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LongContent — Long Content; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- OpenLargeShort — Open Large Short; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- OpenMediumLong — Open Medium Long; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- OpenLargeLong — Open Large Long; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- OpenTop — Open Top; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- OpenTopLong — Open Top Long; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- ListSizeComparison — Medium Trigger / List Size Comparison; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Scroll — Scroll; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Playground — Playground; tags: ["verification"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Data & Display/Separator
|
||||
|
||||
Source: `src/components/separator.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Separator; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Horizontal — Horizontal; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Vertical — Vertical; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
|
||||
### Components/Overlay/Sheet
|
||||
|
||||
Source: `src/components/sheet.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Sheet; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Right — Right; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Left — Left; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Top — Top; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Bottom — Bottom; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Form — Form; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LongContent — Long Content; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Data & Display/Skeleton
|
||||
|
||||
Source: `src/components/skeleton.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Skeleton; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Text — Text; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- CardList — Card List; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Form/Slider
|
||||
|
||||
Source: `src/components/slider.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Slider; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- SingleValue — Single; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- RangeValue — Range; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- DisabledSlider — Disabled; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Focus — Focus; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Feedback/Sonner
|
||||
|
||||
Source: `src/components/sonner.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Sonner; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
|
||||
### Components/Form/Switch
|
||||
|
||||
Source: `src/components/switch.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Switch; tags: ["play-fn"]; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Off — Off; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- On — On; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Disabled — Disabled; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
|
||||
### Components/Data & Display/Table
|
||||
|
||||
Source: `src/components/table.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Table; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Basic — Basic; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Selected — Selected; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- LongText — Long Text; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Overflow — Horizontal Overflow; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Navigation/Tabs
|
||||
|
||||
Source: `src/components/tabs.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Tabs; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Secondary — Secondary; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- SelectedFirst — Selected First; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- SelectedLast — Selected Last; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Focus — Focus; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Disabled — Disabled; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Overflow — Overflow; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- SecondaryOverflow — Secondary Overflow; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LongLabel — Long Label; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Form/Textarea
|
||||
|
||||
Source: `src/components/textarea.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Textarea; tags: ["play-fn"]; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Default — Default; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Focus — Focus; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Error — Error; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- InvalidFocus — Invalid Focus; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- ErrorMessage — Error Message; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Required — Required; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Readonly — Readonly; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Disabled — Disabled; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- LongContent — Long Content; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Playground — Playground; tags: ["verification"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Form/Toggle Group
|
||||
|
||||
Source: `src/components/toggle-group.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Toggle Group; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- SingleSelection — Single; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- MultipleSelection — Multiple; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- VerticalGroup — Vertical; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- ContentNodes — Content Nodes; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Form/Toggle
|
||||
|
||||
Source: `src/components/toggle.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Toggle; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- States — States; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- IconOnly — Icon Only; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- DisabledToggle — Disabled; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- ContentNodes — Content Nodes; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Overlay/Tooltip
|
||||
|
||||
Source: `src/components/tooltip.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Tooltip; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Open — Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Foundation/Color
|
||||
|
||||
Source: `src/foundation/color.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Color; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
|
||||
### Foundation/Layout
|
||||
|
||||
Source: `src/foundation/layout.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Layout; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
|
||||
### Foundation/Typography
|
||||
|
||||
Source: `src/foundation/typography.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Typography; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
|
||||
### Patterns/Layout/Dashboard
|
||||
|
||||
Source: `src/patterns/dashboard-layout.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Dashboard; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- WithSidebar — Dashboard Header + Sidebar; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- ThirdLevel — Third Level; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- SidebarOnly — Dashboard Sidebar; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LongContent — Long Content; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Loading — Loading; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Empty — Empty; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Framed — Framed; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Patterns/Data/Data Table
|
||||
|
||||
Source: `src/patterns/data-table.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Data Table; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- WithFilterBarStory — With Filter Bar; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Loading — Loading; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Empty — Empty; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Patterns/Data/Filter Bar
|
||||
|
||||
Source: `src/patterns/filter-bar.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Filter Bar; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
|
||||
### Patterns/Layout/GNB
|
||||
|
||||
Source: `src/patterns/gnb.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — GNB; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Default — Default; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Sticky — Sticky; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- NavigationMenuOpen — Navigation Menu Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- ShortDepthOpen — Short Depth Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- RightEdgeLongDepth — Right Edge Long Depth; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- FirstDepthShort — First Depth Short; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- FirstDepthLong — First Depth Long; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- MiddleDepthShort — Middle Depth Short; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- MiddleDepthLong — Middle Depth Long; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LastDepthShort — Last Depth Short; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LastDepthLong — Last Depth Long; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- EdgeFirstLong — Edge First Long; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- EdgeLastLong — Edge Last Long; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- UserMenuOpen — User Menu Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LongLabels — Long Labels; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- WithLogo — With Logo; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Utilities — Utilities; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Mobile — Mobile; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- MobileMenuOpen — Mobile Menu Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Patterns/Layout/LNB
|
||||
|
||||
Source: `src/patterns/lnb.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — LNB; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Default — Default; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Selected — Selected; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Expanded — Expanded; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- SelectedChild — Selected Child; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- CollapsedWithSelection — Collapsed With Selection; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- WithLogo — With Logo; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LongLabels — Long Labels; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Scroll — Scroll; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- ThirdLevel — Third Level; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- ThirdLevelLongLabels — Third Level Long Labels; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- ThirdLevelScroll — Third Level Scroll; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- WithGNB — With GNB; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- StandaloneDesktop — LNB Standalone Desktop; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LNBProfileMenuOpen — LNB Profile Menu Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- StandaloneMobile — LNB Standalone Mobile; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- StandaloneMobileMenuOpen — LNB Standalone Mobile Menu Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Patterns/Layout/Loading Screen
|
||||
|
||||
Source: `src/patterns/loading-screen.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Loading Screen; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Inline — Inline; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Theme/Designer Playground
|
||||
|
||||
Source: `src/theme/designer-theme.stories.tsx`; meta.tags: []
|
||||
|
||||
- Editor — Designer Playground; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- LiveVerification — Live Verification; tags: ["verification","play-fn"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Theme/Runtime Contract/Loader
|
||||
|
||||
Source: `src/theme/runtime-loader.stories.tsx`; meta.tags: ["verification"]
|
||||
|
||||
- CdnVerification — Cdn Verification; tags: ["verification","play-fn"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Theme/Runtime Contract
|
||||
|
||||
Source: `src/theme/runtime-theme.stories.tsx`; meta.tags: ["verification"]
|
||||
|
||||
- Comparison — Comparison; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- GlobalExternal — Global External; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Verification — Verification; tags: ["verification","play-fn"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Dashboard — Dashboard; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
@@ -0,0 +1,38 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { loadCsf } from 'storybook/internal/csf-tools'
|
||||
|
||||
// Run from the Track B root: node .storybook/qa/generate-inventory.mjs
|
||||
// Only writes Storybook documentation; never renames or removes stories.
|
||||
const root = process.cwd()
|
||||
if (path.basename(root) !== 'HMDesignSystem-storybook') throw new Error('Run only from Track B')
|
||||
const walk = dir => fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)])
|
||||
const files = walk(path.join(root, 'src')).filter(f => /\.stories\.tsx?$/.test(f)).sort()
|
||||
const inventory = files.map(file => {
|
||||
const csf = loadCsf(fs.readFileSync(file, 'utf8'), { makeTitle: title => title }).parse()
|
||||
const title = csf.meta.title
|
||||
if (!title) throw new Error(`Missing meta.title: ${file}`)
|
||||
const metaTags = csf.meta.tags ?? []
|
||||
const stories = Object.entries(csf._stories).map(([exportName, story]) => {
|
||||
const storyTags = story.tags ?? []
|
||||
const effectiveTags = [...new Set([...metaTags, ...storyTags])]
|
||||
const utility = /Playground|Compatibility/.test(exportName) || /Verification$/.test(exportName) || exportName === 'Verification'
|
||||
const category = utility ? 'UTILITY' : effectiveTags.includes('verification') ? 'VERIFICATION' : 'PRIMARY'
|
||||
const duplicateCandidate = effectiveTags.includes('verification') && /^(Default|Sizes|Roles|Variants|States|Basic|Selected|Disabled|Horizontal|Vertical)$/.test(exportName)
|
||||
return { id: story.id, exportName, name: story.name, storyTags, effectiveTags, category, decision: effectiveTags.includes('verification') ? 'HIDE' : 'KEEP', candidate: duplicateCandidate ? 'MERGE_CANDIDATE' : null, reason: duplicateCandidate ? 'Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.' : effectiveTags.includes('verification') ? 'Keep direct URL and tag-filter access; excluded from default sidebar.' : 'Representative story or distinct reference page.' }
|
||||
})
|
||||
return { file: path.relative(root, file).replaceAll('\\', '/'), title, metaTags, stories }
|
||||
})
|
||||
const all = inventory.flatMap(f => f.stories)
|
||||
const summary = { storyFiles: files.length, stories: all.length, categories: Object.fromEntries(['PRIMARY', 'VERIFICATION', 'UTILITY', 'LEGACY / DUPLICATE'].map(c => [c, all.filter(s => s.category === c).length])), decisions: Object.fromEntries(['KEEP', 'HIDE', 'MERGE_CANDIDATE', 'DELETE_CANDIDATE'].map(c => [c, all.filter(s => s.decision === c || s.candidate === c).length])), hierarchy: ['Foundation', 'Components', 'Patterns', 'Center Graphic', 'Theme'] }
|
||||
const output = path.join(root, '.storybook', 'qa')
|
||||
fs.mkdirSync(output, { recursive: true })
|
||||
fs.writeFileSync(path.join(output, 'story-inventory.json'), JSON.stringify({ summary, inventory }, null, 2) + '\n')
|
||||
const lines = ['# Track B Story inventory', '', 'Generated from every `src/**/*.stories.ts(x)` using the installed Storybook CSF parser. Run `node .storybook/qa/generate-inventory.mjs` from Track B to refresh.', '', 'This is a classification record. No Story IDs, exports or titles are renamed. HIDE uses the existing verification filter, not deletion. MERGE_CANDIDATE is advisory and can coexist with HIDE. Utility includes executable checks and compatibility/playground tools. No legacy or delete candidate is asserted without evidence.', '', '## Summary', '', ...Object.entries(summary.categories).map(([k,v]) => `- ${k}: ${v}`), ...Object.entries(summary.decisions).map(([k,v]) => `- ${k}: ${v}`), `- Story files: ${summary.storyFiles}`, `- Stories: ${summary.stories}`, `- Hierarchy: ${summary.hierarchy.join(' → ')}`, '', '## Full inventory', '']
|
||||
for (const f of inventory) {
|
||||
lines.push(`### ${f.title}`, '', `Source: \`${f.file}\`; meta.tags: ${JSON.stringify(f.metaTags)}`, '')
|
||||
for (const s of f.stories) lines.push(`- ${s.exportName} — ${s.name}; tags: ${JSON.stringify(s.effectiveTags)}; ${s.category}; ${s.decision}${s.candidate ? `; ${s.candidate}` : ''}. ${s.reason}`)
|
||||
lines.push('')
|
||||
}
|
||||
fs.writeFileSync(path.join(output, 'STORY-INVENTORY.md'), lines.join('\n'))
|
||||
console.log(JSON.stringify(summary))
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,76 @@
|
||||
# HMDesignSystem 작업 지침
|
||||
|
||||
## AI 화면 구현 Quick Start
|
||||
|
||||
화면 구현 요청을 받으면 아래 순서를 그대로 따른다. 파일이나 API의 존재를 확인할 수 없으면 추측하지 말고 미확인으로 기록한다.
|
||||
|
||||
1. **프로젝트 구조와 Runtime 확인**: 제공된 파일 범위를 살펴보고 `REACT_VITE | STATIC_HTML | VANILLA_WEB | OTHER_RUNTIME` 중 대상을 정한다. Component·Pattern 파일이 실제 범위에 있는지 확인한다.
|
||||
2. **Component 검색**: `packages/ui/src/components/ui/`와 `packages/ui/src/index.ts`에서 공개 Component·Props를 찾고, `docs/components/<component>.md`를 확인한다. 기존 공개 Component를 우선 사용한다.
|
||||
3. **Pattern 검색**: `packages/ui/src/patterns/`, `src/patterns/`, `packages/ui/src/index.ts`와 `docs/patterns/`에서 맞는 Pattern·공개 API를 찾아 구조와 상태 규칙을 따른다.
|
||||
4. **Storybook 확인**: `src/components/`, `src/patterns/`, `src/theme/`의 대표 `All` Story와 관련 문서를 확인한다. `verification` 전용 Story는 일반 화면의 기준으로 쓰지 않는다. Storybook을 실행할 수 없으면 Story 소스와 문서만 확인하고 시각 동작은 추측하지 않는다.
|
||||
5. **Theme 연결**: `themes/default.json`, `packages/ui/src/theme.css`, Runtime 계약과 Token 가이드를 확인해 기존 CSS Custom Properties 구조를 사용한다.
|
||||
6. **화면 구성**: Component와 Pattern을 조합하고 아래 단일 Navigation 정책을 지킨다. 기존 구성으로 해결되지 않을 때만 새 Component를 검토한다.
|
||||
7. **결과 확인 및 보고**: 실제 적용한 Component·Pattern, Theme 연결, Navigation Mode, 실행/화면 확인 결과와 미검증 항목을 보고한다.
|
||||
|
||||
**Storybook / 화면 생성 분리**: Storybook은 기존 Component·Pattern·Theme·Variant·State·Interaction을 확인하는 Reference 환경이다. 일반 업무 화면 생성은 Storybook Story·사이드바에 등록하지 않고, 기존 대표 Story를 생성 화면으로 교체하지 않으며, Storybook 실행을 필수 확인 경로로 요구하지 않는다. 별도의 React 화면과 독립 미리보기를 만들고 화면별 빌드·필요한 검증을 수행한다. H-2(AI 생성 화면의 자동 빌드·캡처·검증, Storybook 배포와 별도이며 구현 완료 여부는 현재 파일로 확인)나 유사 자동화가 사용 가능한 경우에도 `preview.html`·desktop/mobile 이미지 같은 독립 미리보기 산출에만 사용하며 Storybook 등록과 혼동하지 않는다. Storybook 수정은 Component/Pattern 추가·수정, 대표/Verification Story 작성 또는 Storybook Reference 개선을 사용자가 명시한 경우에만 허용한다. Storybook 전용 helper/CSS는 운영 화면에 사용하지 않는다.
|
||||
|
||||
상세 규칙은 이 문서의 [구현 기본값](#ai-implementation-defaults), [Runtime 계약](#runtime-portability-policy), [단일 Navigation 정책](#navigation-single-mode-policy)과 [Component / Pattern Reference Index](docs/guidelines/component-pattern-reference.md), [Component 구현 원칙](docs/guidelines/shadcn-기반-구현-원칙.md), [Runtime Theme](docs/guidelines/runtime-theme.md), [Navigation 정책](docs/guidelines/navigation-single-mode.md)을 따른다.
|
||||
|
||||
**검색 원칙(도구 중립)**: 변경과 관련된 소스 경로부터 좁혀 검색하고, 생성물(`node_modules`, `dist`, `storybook-static`)과 로컬 작업본은 기본 제외한다. 필요한 최소 범위만 검증하며 실제로 확인하지 않은 기능을 PASS로 보고하지 않는다. 존재하지 않는 Component·Pattern·공개 API를 임의로 만들지 않고 없으면 없다고 보고한다.
|
||||
|
||||
**Story 구분**: 운영 기준은 대표 `All` Story이고 `verification`·Compatibility·Fixture Story는 운영 예제가 아니다. Storybook 문서 화면은 `StoryPage` 공통 Inner를 사용하며, 이는 Storybook 표현용으로 `@hm-design/ui` 공개 API가 아니다.
|
||||
|
||||
**화면 생성 시 필수 확인**: Navigation을 지정받으면 우선 적용하고, 미지정이면 화면 구조·대표 Pattern에서 선택 근거를 기록한다([선택 기준](docs/guidelines/navigation-single-mode.md#선택-근거)). GNB는 Desktop 메뉴와 `GNBMobileMenu`를 함께 구성하며 내부 Sidebar도 모바일 Sheet 하나만 사용한다([GNB 모바일](docs/patterns/gnb.md#8-desktop--mobile)). Dashboard 운영 화면에는 Sidebar를 생성하지 않는다. `DashboardSidebar` 공개 Export와 선택적 `sidebar` slot은 기존 호환성·검증용이며 운영 허용을 뜻하지 않는다. Dashboard 상태는 Theme 다음에 공개 Status CSS를 로드하고 기존 Badge mapping을 소비한다([상태 예제](docs/patterns/dashboard-layout.md#dashboard-업무-상태-공통-계약)). FilterBar의 입력 조건과 조회 적용 조건·초기화 책임을 구분한다([조회 예제](docs/patterns/filter-bar.md#61-조회-버튼으로-적용하는-소비-예제)). `REACT_VITE` 독립 화면은 bootstrap에서 기존 앱 CSS entry(`@/index.css` → `@/hm-theme.css` → 필요 시 공개 상태 CSS)를 로드하며, `@hm-design/ui/tailwind.css`만 단독 import하지 않는다. 빌드 후 `.flex`, `.grid` 등 대표 utility의 생성·실제 적용과 캡처 레이아웃을 확인하고, 빌드 성공만으로 화면 검증을 PASS 처리하지 않는다. React 소비자는 Tailwind 검색 범위와 HM package source를 확인하고([Tailwind](docs/guidelines/runtime-portability.md#21-tailwind-v4-소비자-검색-범위)), 폰트·Asset 경로는 실행 위치별로 확인한다([Asset](docs/guidelines/runtime-portability.md#4-asset과-실행-위치)).
|
||||
|
||||
## AI Implementation Defaults
|
||||
|
||||
**사용자가 별도로 "기존 컴포넌트/디자인 시스템을 사용해줘", "테마를 적용해줘", "Semantic Token을 사용해줘", "Navigation 정책을 따라줘"라고 요청하지 않아도 AI는 이를 모든 신규·수정 UI의 기본 구현 조건으로 자동 적용한다.**
|
||||
|
||||
- 구현 전에 `STORYBOOK_ONLY | PRODUCTION_UI | BOTH` 중 변경 범위를 정한다.
|
||||
- 기존 Component와 유사 대표 Story를 먼저 검색한다. `packages/ui`, `src/components`, `src/patterns`, `src/center`, 기존 Storybook stories와 shared helpers를 확인하고 도메인에 맞는 구현을 재사용한다.
|
||||
- 재사용 순서: 기존 HM Component → 기존 Variant / Prop → 기존 Composition → shadcn → Radix Primitive → 최소 Custom. 새 Component는 기존 Component·Composition으로 표현 불가능하고 Variant 확장이 부적절하며 shadcn/Radix 조합도 어려운 경우에만 만든다. 재사용 불가 사유와 최소 추가 범위를 설명한다.
|
||||
- Storybook은 구현 Reference Catalog다. 대표/PRIMARY Story의 시각 언어와 공통 상태 mapping을 재사용하고 검증 전용 Story를 운영 예제로 삼지 않는다. 신규 업무 화면의 등록 대상이 아니며, 생성 결과는 별도 React 화면·독립 미리보기로 제공한다.
|
||||
- Theme은 구현 시작부터 적용한다. 기존 Semantic Token → 해당 Component Token → 기존 CSS Variable을 먼저 확인하고, 없을 때만 필요한 최소 Semantic 확장을 검토한다. 기존 Token이 있는 색상·spacing·값을 하드코딩하거나 같은 Token을 여러 파일에 중복 정의하지 않는다.
|
||||
- Theme 사용은 별도 승인 없이 기본 적용한다. Theme Source/값 수정은 명시적 필요성과 영향 범위를 확인한다. `themes/*.json` → Runtime Loader → CSS Custom Properties → Semantic → Component Mapping과 Primary / On Accent의 독립 fallback을 유지한다.
|
||||
- 한 페이지에 `GNB | LNB | DASHBOARD` 중 하나만 사용한다. 반응형도 같은 Navigation Mode를 유지한다.
|
||||
- 화면 구현 순서는 Quick Start를 따른다. 변경 작업 범위(STORYBOOK_ONLY | PRODUCTION_UI | BOTH)는 구현 전에 정한다.
|
||||
- package exports, public props/types, package entry, Story ID, `meta.title`을 불필요하게 변경하지 않는다. 필요한 변경은 소비처·호환성 영향을 먼저 확인한다. Storybook 표현 문제와 Production 동작 문제는 해당 범위에서 해결한다.
|
||||
- Badge/Button/FilterBar를 재사용하더라도 동일한 상태 ID·라벨·필터 매칭이 반복되면 화면별 helper로 남기지 말고 Runtime-neutral 상태 계약을 먼저 검토한다. 계약은 React 구현과 분리하고 `data-status`, 독립 flag attribute, `aria-pressed` 등 다른 Runtime이 재현할 수 있는 상태 vocabulary를 정의한다.
|
||||
- 명시적 실험·Theme 미적용 prototype·외부 디자인 완전 재현·독립 POC만 별도 격리 범위에서 예외를 검토한다. Navigation 혼합은 아래 단일 모드 정책의 별도 기준을 따른다.
|
||||
- Button은 `brand | solid | outline`의 사용 위치와 세트 규칙을 `docs/components/button.md` 기준으로 따른다: 실제 화면 Brand 최대 1개·팝업 Brand 금지, 일반 확인/작성은 Solid 세트, 삭제 확인·행/반복 작업은 Outline 세트이며 세트의 appearance와 size를 통일한다. Runtime 변환은 `docs/guidelines/runtime-portability.md`의 DOM/Theme/keyboard 계약을 함께 보존한다.
|
||||
- Dashboard 운영 화면은 전용 Header(48px)·업무 상태 필터와 Content로 구성하며 Sidebar 사용을 금지한다. Dashboard 대표 `Patterns/Layout/Dashboard → Dashboard` Story와 `docs/patterns/dashboard-layout.md`를 기준으로 하며, Header를 GNB로 대체하거나 일반 LNB/GNB를 함께 넣지 않는다. Sidebar verification Story는 신규 운영 화면의 Reference가 아니다.
|
||||
- Dashboard 및 DataTable은 기존 Pattern 구조를 우선한다. `Data Table` 대표 Story를 기준으로 하고 숨김 `With Filter Bar`는 verification 용도다.
|
||||
- Story의 fixture나 장식은 Component API나 Production 규칙과 구분한다. 대표 Story와 공개 API·사용 문서를 함께 확인한다.
|
||||
|
||||
상세 Source of Truth: [AI/Component 구현 원칙](docs/guidelines/shadcn-기반-구현-원칙.md), [Theme/Token 정책](docs/guidelines/테마-토큰-가이드.md), [Runtime Theme 계약](docs/guidelines/runtime-theme.md). 이 기본 정책은 과거 문서·예제의 상충 지침보다 우선한다.
|
||||
|
||||
## Runtime Portability Policy
|
||||
|
||||
- 설치 없는 테스트 배포의 현재 검증 기준은 `HMDesignTest-Portable-Test-r3.zip`이다(r2·원본 `HMDesignTest-Portable-Test.zip`은 과거 기록). 포터블 ZIP은 기본 포트 6007이며 충돌 시 자동으로 다른 포트를 쓰고, 개발 저장소 Storybook은 6024다. 이 ZIP은 `start-preview.bat`과 동봉된 Windows x64 Node 런타임으로 사전 빌드된 Storybook을 실행한다. 테스트 사용자는 ZIP 해제 → 배치 더블클릭 → 표시된 로컬 URL 열기만 수행하며 Node/npm 설치, npm 명령, Git 작업, CDN/운영 업로드가 필요 없다. 아래 의존성 복원·빌드 절차는 소스 개발자 전용이다. [테스트 배포 안내](docs/guidelines/portable-test-distribution.md).
|
||||
- Runtime 구분: React 화면은 실제 `packages/ui` 공개 API와 workspace를 쓰는 `REACT_VITE`; 압축 ZIP에서 React 개발환경을 쓸 때는 먼저 `npm ci`로 의존성을 복원한다. `STATIC_HTML`은 파일을 `file://`로 직접 여는 HTML이며 Vite나 bare package import에 의존하지 않는다. `VANILLA_WEB`은 서버/정적 호스팅의 HTML·CSS·JS다. Storybook은 구현 기준을 확인하는 Reference 환경이며 Production Runtime을 뜻하지 않는다.
|
||||
- `HMDesignTest.zip`에는 `.storybook/` 설정·Story 소스·패키지 소스와 dist가 있지만 `node_modules/`, `storybook-static/`은 없다. 실제 포함 파일과 `package.json` scripts를 먼저 확인한다. `HMDesignTest.zip`의 Storybook 개발 서버는 기본 포트 6007이며(현재 개발 저장소는 6024) 정적 빌드가 필요하면 의존성을 복원한 뒤 `npm run build-storybook`을 실행한다. 현재 개발 저장소와 ZIP의 설정 차이가 있으면 대상 `package.json`을 다시 확인한다. 생성되기 전의 `storybook-static/`을 필수 Reference로 요구하지 않는다.
|
||||
- React는 구현 방식 중 하나일 뿐이다. 신규·변환 작업은 먼저 `REACT_VITE | STATIC_HTML | VANILLA_WEB | OTHER_RUNTIME` 중 Target Runtime을 정하고, HM Design/Theme/Component Contract를 유지한다.
|
||||
- `themes/default.json`, `packages/ui/src/theme/contract.ts`, `packages/ui/src/theme/runtime.ts`, `packages/ui/src/theme.css`와 [Runtime 계약표](docs/guidelines/runtime-theme-contract.md)를 실제 Source of Truth로 사용한다. 색상·spacing·radius·Typography를 추측하거나 하드코딩하지 않는다.
|
||||
- HTML/다른 Runtime 결과물은 기존 React를 대체하거나 별도 디자인으로 재작성하지 않고, 동일한 시각·상태·접근성·responsive 계약을 소비하는 adapter로 만든다. `STATIC_HTML`은 `file://` 직접 실행 검증을 포함한다.
|
||||
- Dashboard 상태 색상은 `src/patterns/dashboard-status-contract.css`의 Runtime-neutral 변수(`--hm-dashboard-status-active|hold|done|issue`)를 우선 사용한다. 이 파일의 active/hold/issue 값은 기존 승인된 Story 색상을 보존하는 compatibility mapping이며 Public Theme token으로 승격하지 않는다. 실제 Production 공통 소비가 확인될 때만 Semantic Token 승격을 제안한다.
|
||||
- 상세한 Target Runtime, asset 경로, 변환 규칙과 검증 기준은 [Runtime Portability Policy](docs/guidelines/runtime-portability.md)를 따른다.
|
||||
|
||||
## Navigation Single Mode Policy
|
||||
|
||||
**ONE PAGE = ONE NAVIGATION MODE = ONE HEADER/NAVIGATION EXPERIENCE**
|
||||
|
||||
- 페이지 구현 전에 `navigationMode = GNB | LNB | DASHBOARD` 중 정확히 하나를 선택한다.
|
||||
- 허용: GNB ONLY, LNB ONLY, DASHBOARD ONLY.
|
||||
- GNB ONLY 내부 Sidebar는 기존 LNB 부품을 재사용하는 허용 Composition이며 독립 LNB Mode가 아니다. 브랜드·사용자와 모바일 Header/Sheet는 GNB 하나가 담당한다([상세 범위](docs/guidelines/navigation-single-mode.md#gnb-only-내부-sidebar)).
|
||||
- 금지: GNB + LNB, GNB + Dashboard, LNB + Dashboard, GNB + LNB + Dashboard.
|
||||
- 이 정책은 AI 기반 코드 생성·제안, 신규 페이지 구현, 기존 화면 수정, 프로토타입, Storybook, 실제 Production 구현 전체에 동일하게 적용한다.
|
||||
- AI는 별도 요청이 없는 한 Navigation 조합을 생성하거나 제안하지 않는다. 별도 요청이 있더라도 기존 단일 모드 정책과 충돌함을 알리고 정책 변경인지 기술 검증인지 구분한다.
|
||||
- 반응형에서도 선택한 체계를 유지한다. LNB Desktop(≥1024px) → LNB Mobile Header(≤1023px)는 같은 LNB Mode이며 GNB로 전환하지 않는다.
|
||||
- Dashboard 운영 화면은 전용 Header/Menu와 Content만 사용하며 Sidebar는 금지한다. 외부 GNB나 일반 LNB를 slot에 넣지 않는다. 기존 `DashboardSidebar`와 `sidebar` slot은 호환성·검증용으로 유지하며 신규 운영 화면에 사용하지 않는다.
|
||||
- Dashboard Header는 GNB가 아니다. Dashboard 화면은 Dashboard Pattern과 대표 Story를 따르고, Header의 역할을 GNB로 대체하지 않는다.
|
||||
- GNB/LNB/Dashboard의 함께 쓰기는 위 단일 모드 허용 범위만 따른다. 과거 혼합 Navigation verification Story는 운영 화면 기준으로 사용하지 않는다.
|
||||
- Dashboard 운영 최소 폭은 Sidebar 없는 960px이며 그 이하에서는 Dashboard 내부 가로 스크롤을 유지한다. Sidebar 포함 1232px은 기존 호환성·검증 구성의 값으로 현재 운영 기준이 아니다.
|
||||
- Navigation 간 CSS 변수, height/width 계산, breakpoint, spacing override의 교차 의존성을 추가하지 않는다.
|
||||
- 대표/PRIMARY Story는 단일 모드만 사용한다. 기존 혼합 Story를 기술 검증 목적으로 유지할 경우 `verification` 태그와 **VERIFICATION ONLY** 표시를 모두 적용하고 운영 예제로 추천하지 않는다.
|
||||
|
||||
상세 기준: [Navigation Single Mode Policy](docs/guidelines/navigation-single-mode.md). 이 사용자 확정 정책은 이전 Navigation 조합 문서·예제보다 우선한다.
|
||||
@@ -0,0 +1,69 @@
|
||||
# HMDesignSystem
|
||||
|
||||
shadcn/ui(Radix, Tailwind v4, CVA) 기반 HM 디자인 시스템 시제품. React + TypeScript + Vite, 개발 Storybook 포트 6024(포터블 테스트 ZIP은 기본 6007, 충돌 시 자동 변경). 실행법과 파일 위치는 `README.md`를 따른다.
|
||||
|
||||
## 지침 문서
|
||||
|
||||
규칙의 기준은 아래 문서이며, 이 파일에는 규칙 본문을 복사하지 않는다. 작업 전에 해당 문서를 읽는다.
|
||||
|
||||
- `AGENTS.md` — AI Implementation Defaults와 Navigation 필수 요약. 기존 Component / Storybook Reference 우선, Theme / Semantic 기본 적용, 단일 Navigation, 최소 Custom은 사용자의 별도 지시 없이 모든 신규·수정 UI에 적용한다.
|
||||
- 일반 업무 화면은 Storybook에 등록하지 않고 별도 React 화면·독립 미리보기로 제공한다. Storybook 수정은 Component/Pattern 또는 명시적 Storybook Reference 작업에 한정한다. 상세한 Storybook / 화면 생성 분리는 `AGENTS.md`와 `docs/guidelines/shadcn-기반-구현-원칙.md`를 따른다.
|
||||
- `docs/guidelines/프로젝트-지침.md` — §6 컴포넌트·Storybook·CSS 작업 규칙 (모든 컴포넌트 작업 전 필독)
|
||||
- `docs/guidelines/shadcn-기반-구현-원칙.md` — Component / Pattern / Story의 재사용 우선순위와 Custom 구현 허용 조건 (관련 작업 전 필독)
|
||||
- `docs/guidelines/테마-토큰-가이드.md` — Token 구조, 이름 규칙, HM Default/External Runtime 계약, Light/Dark 없음
|
||||
- `docs/guidelines/개발-적용-가이드.md`, `운영-배포-가이드.md`, `theme-delivery.md` — JSON 원본/Runtime URL Loader/독립 Theme 배포와 향후 실제 CDN·Publish의 경계
|
||||
|
||||
> `docs/guidelines/`의 개정안/검토용 표기와 관계없이 2026-10-07 사용자 확정 AI 기본 구현 정책과 2026-10-08 Navigation Sidebar 운영 정책은 필수 적용한다. 과거 로컬 지침은 현재 정책을 대체하지 않는다.
|
||||
|
||||
## 반드시 지킬 것 (요약)
|
||||
|
||||
- 구현 우선순위는 기존 `@hm-design/ui` → 기존 Variant / Prop → 기존 Composition → shadcn → Radix Primitive → 최소 Custom이다. Visual은 처음부터 기존 Theme / Semantic / Component Mapping에 연결한다. 상세 탐색·범위 분류·예외 조건은 `docs/guidelines/shadcn-기반-구현-원칙.md`를 따른다.
|
||||
- Behavior / Structure는 shadcn/Radix를 유지하고 Visual은 HM Theme로 조정한다. 디자인 차이만으로 별도 Component, interaction logic 또는 Production API를 만들지 않는다.
|
||||
- HM이 값을 지정한 속성은 Theme Token으로, 지정하지 않은 속성은 shadcn 기본 구현 그대로 둔다.
|
||||
- shadcn 구조(CVA, `data-slot`, `cn(..., className)`)와 공개 API를 불필요하게 바꾸지 않는다. wrapper를 추가하지 않는다.
|
||||
- `<Button />`처럼 기본 API만으로 HM 디자인이 적용되어야 한다. 소비자가 `hm-*` class를 붙이는 구조를 새로 만들지 않는다. `className`은 사용자 확장용이다.
|
||||
- Story는 소비 프로젝트와 같은 API로 작성한다. Story 전용 class로 외형을 보정하지 않는다.
|
||||
- HM Default와 외부 JSON은 같은 Runtime 계약을 쓴다(`docs/guidelines/runtime-theme.md`). 컬러 원본은 `themes/*.json`, package fallback은 생성한다. Storybook의 Designer JSON Preview/Editor는 이 계약의 소비자다. Light/Dark preset이나 `dark:` 분기/Dark Token은 추가하지 않는다.
|
||||
- 컴포넌트 CSS에 `html`, `body`, `*` 전역 규칙이나 페이지 레이아웃을 넣지 않는다.
|
||||
|
||||
## 검색·검증 효율
|
||||
|
||||
- 변경과 관련된 소스 경로(`packages/ui/src`, `src/components`, `src/patterns`, `docs/<영역>`)부터 검색한다. 드라이브나 저장소 전체 탐색은 하지 않는다.
|
||||
- `rg`를 우선 사용하고 `node_modules`, `_local`, `dist`, `storybook-static` 등 생성물·로컬 작업본은 기본 검색에서 제외한다(`-g '!…'`). 전체 검색 후 `grep -v`로 걸러내는 방식은 쓰지 않는다.
|
||||
- 검증은 변경 범위에 필요한 최소 단위로 한다. 공유 `dist`를 쓰는 build는 동시에 실행하지 않는다.
|
||||
- 속도를 이유로 파일 삭제나 캐시 정리를 하지 않는다.
|
||||
- 실제로 실행·확인하지 않은 항목은 PASS로 보고하지 않고 미검증으로 기록한다.
|
||||
|
||||
## 디자인 규격 (ui_viz)
|
||||
|
||||
> 2026-10-02: ui_viz는 최종 기준이 아니라 이전 테스트/검증 프로젝트이며 참고 자료다. 새 컴포넌트의 기준은 `프로젝트-지침.md` §6.9와 `docs/components/<name>.md`를 따른다. 아래 항목은 이 기준으로 읽는다.
|
||||
|
||||
- 정합성 감사의 1차 Source of Truth는 현재 Production 코드와 실제 Storybook이다. 이 저장소 문서는 코드와 대조하며 과거 보고서는 참고 자료다. 외부 ui_viz는 ZIP 미포함 선택적 Reference이며, 제공받은 경우에만 당시 `Confirmed` 근거를 참고한다. `TBD`/`HOLD`/`Candidate`/`Temporary`/`REVIEW VALUE`는 확정값이 아니다.
|
||||
- 해당 도메인의 HM 문서와 대표 Story부터 읽는다: UI는 `docs/components/<component>.md`, Layout은 `docs/patterns/`, Theme은 `docs/guidelines/runtime-theme.md`를 따른다. 외부 ui_viz의 owner 문서·asset은 제공된 경우에만 관련 범위에서 참고한다. CENTER 자료는 `center-graphic` 작업에만 사용하며 UI 규격과 섞지 않는다.
|
||||
- UI 컴포넌트를 수정할 때는 이 저장소의 `docs/components/<component>.md`를 먼저 읽고 현재 코드와 대조한다. HM 사용자 확정 결정은 외부 ui_viz보다 우선한다. 외부 문서 경로는 이 저장소의 동명 문서와 구분한다. 상세는 `프로젝트-지침.md` §6.8.
|
||||
- ui_viz의 Web class와 DOM은 복사하지 않고 shadcn API로 구현한다. 상세는 `프로젝트-지침.md` §6.7.
|
||||
- Layout/Pattern(GNB, LNB, Dashboard, Page) 작업도 현재 HM 대표 Story / Pattern과 단일 Navigation 정책을 먼저 따른다. 외부 ui_viz의 기존 테스트 레이아웃(ZIP 미포함)은 현재 HM 기준과 충돌하지 않는 구조·시각 참고 자료로 사용한다(`프로젝트-지침.md` §6.11).
|
||||
|
||||
## 작업 방식
|
||||
|
||||
- 요청하지 않은 리팩토링을 하지 않는다. 변경은 요청된 컴포넌트와 직접 필요한 범위로 제한한다.
|
||||
- 새 패턴은 기준 컴포넌트(Button)에서 먼저 검증한 뒤 다른 컴포넌트로 확장한다.
|
||||
- 다른 작업자나 AI가 같은 파일을 수정 중일 수 있다. 수정 전 파일의 현재 상태를 다시 읽는다.
|
||||
|
||||
## 현재 상태
|
||||
|
||||
- Button(`variant`=역할, `appearance`=treatment, `size`), Input(`size`, native 상태 selector), Select(Trigger `size`, Radix 상태 속성)는 ui_viz Confirmed 규격으로 구현했다. Card도 `<Card />`만으로 HM 스타일이 적용된다(ui_viz Content Card, `packages/ui/src/theme/card.css`). Dialog도 `<DialogContent />`만으로 HM 스타일이 적용된다(`packages/ui/src/theme/dialog.css`).
|
||||
- 구조(2026-10-02 패키지 분리, npm workspaces): UI 컴포넌트·Theme 구현은 `packages/ui`(`@hm-design/ui`, 컴포넌트 `src/components/ui/`, Theme `src/theme/*.css`, 진입점 `src/theme.css`·`src/tailwind.css`), 컴포넌트 문서는 `docs/components/`, Storybook·Demo는 루트(`.storybook/`, `src/`; Demo 스타일은 `src/hm-theme.css`), `packages/center-graphic`(`@hm-design/center-graphic`)은 CENTER 구현(제목·정보·노선/공간·시설물), 기준 문서는 `docs/center-graphic/`다.
|
||||
- 작업 순서: 컴포넌트 문서 정리, Card, Dialog, 패키지 분리, CENTER 구현, 1단계 UI 컴포넌트(Textarea, Tabs, Label, Checkbox, Radio Group, Switch, Badge, Alert, Separator)와 2단계 A묶음 Floating(Dropdown Menu, Popover, Tooltip, Hover Card)과 B묶음 Modal(Sheet, Drawer(vaul), Alert Dialog)과 C묶음 Expand(Accordion, Collapsible)과 D묶음 Navigation(Breadcrumb, Pagination, Navigation Menu, Menubar)까지 완료로 2단계 UI가 끝났다. 3단계 A묶음 Data/Feedback(Table, Progress, Skeleton, Avatar, Scroll Area)과 B묶음 Selection Controls(Slider, Toggle, Toggle Group)과 C묶음 Search/Date(Calendar, Command, Date Picker, Combobox)까지 완료(새 dependency: `react-day-picker`, `cmdk`)과 D묶음 Layout/Media(Carousel, Aspect Ratio, Resizable)까지 완료로 3단계 UI가 끝났다(새 dependency: `embla-carousel-react`, `react-resizable-panels`). floating surface Semantic 정합성 작업도 완료했다. 4단계 A묶음 독립 UI(Context Menu, Toast/Sonner)까지 완료(새 dependency: `sonner`). 4단계 B묶음 업무 Pattern(Data Table, Filter Bar)까지 완료(새 dependency: `@tanstack/react-table` v9)로 4단계 핵심이 끝났다. Layout Pattern은 GNB·LNB·Page Header·Dashboard Layout까지 완료(`docs/patterns/gnb.md`, `lnb.md`, `page-header.md`, `dashboard-layout.md`, ui-viz 시각 기준)했다. 각 Navigation Mode는 독립 레이아웃을 소유한다. GNB ONLY는 기존 LNB 메뉴 부품을 재사용하는 내부 Sidebar를 허용하며 외곽 배경만 Green Light 2로 적용한다. DASHBOARD ONLY 운영 화면은 전용 Header(48px)·업무 상태 필터 + Content로 구성하고 Sidebar를 금지한다. 운영 최소 폭 960px·내부 가로 스크롤을 유지한다. 기존 DashboardSidebar Export·sidebar slot·1232px 구성은 호환성·검증용이며 신규 운영 Reference가 아니다. LNB 3뎁스(`LNBGroup` 중첩, 2026-10-06)와 모바일 Sheet 중첩 그룹까지 완료(`docs/patterns/lnb.md` §3a). 현재 LNB 모바일은 독립 LNB Mobile Header와 Sheet를 사용한다(`lnb.md` §3b). 과거 GNB+LNB 조합·Brand ownership·gradient 접점 기준은 Navigation Single Mode Policy로 대체되었다. 한 페이지에는 GNB / LNB / DASHBOARD 중 하나만 사용하며 기존 혼합 Story는 verification / VERIFICATION ONLY 조건의 기술 검증에만 한정한다. 경로: UI primitive `packages/ui/src/components/ui`, Pattern `packages/ui/src/patterns`, Pattern 문서 `docs/patterns`. Field는 코드 없이 `docs/components/field.md` 소비 가이드만 둔다.
|
||||
- 두 패키지는 서로 의존하지 않으며 `private: false`, compiled dist export(JS·타입·CSS)와 library build가 구현되어 있다. Registry publish 이력은 ZIP 검증 범위 밖이다. Runtime Theme URL Loader와 Theme JSON 독립 산출은 구현되었고 실제 CDN infrastructure/publish pipeline은 미구현이다. CENTER 기준은 `docs/center-graphic/README.md`, `packages/center-graphic/src/`, `src/center/`의 현재 HM 구현과 대표 Story다. 외부 ui_viz owner 자료는 제공된 경우에만 참고하며 필수 실행 경로가 아니다. UI와 CENTER 규격을 섞지 않는다.
|
||||
|
||||
> 위 작업 순서는 구현 이력이다. 현재 API·제약은 코드와 Component/Pattern 문서를 대조한다.
|
||||
|
||||
## Runtime Theme 구현 (2026-10-06)
|
||||
|
||||
- 공개 계약/API: packages/ui/src/theme/contract.ts 및 runtime.ts, @hm-design/ui/theme 진입점. src/theme/preview.ts는 호환 어댑터다.
|
||||
- HM Default CSS preset과 defaultRuntimeTheme는 같은 계약이다. Partial override, atomic invalid fallback, cleanup, Global/독립 Scoped override를 지원한다.
|
||||
- Scoped Theme의 Portal은 DOM 경계를 따른다. SelectContent/PopoverContent/DatePicker에는 현재 container 전달 API가 없으므로 wrapper Scoped Theme가 body Portal에 자동 전달되지 않는다. Global Theme는 Portal에도 적용된다. Scoped popup 지원은 별도 API 검토 항목이다.
|
||||
- Runtime의 fetch/resolve/load API는 구현되어 있고 실제 CDN/Publish/pipeline은 미구현이다. `build:themes`는 UI/Graphic build 없이 JSON만 산출한다. Theme Runtime은 HM_THEME / Theme infrastructure이며 새 Component HM_CUSTOM 항목이 아니다.
|
||||
- 상세 계약·제한·사용법: `docs/guidelines/runtime-theme.md`, `docs/guidelines/runtime-theme-contract.md`.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# HMDesignTest Portable r4 Verification
|
||||
|
||||
- Candidate: `HMDesignTest-Portable-Test-r4-final-2026-10-08.zip`
|
||||
- 기준: `HMDesignTest-Portable-Test-r3.zip` 보존, r3 구조를 staging 기준으로 재사용
|
||||
- Storybook 정적 빌드: PASS (`npm run build-storybook`)
|
||||
- Dashboard Production export build: PASS (`npm run build:dashboard-export`)
|
||||
- Dashboard 엔트리: PASS — `src/exports/dashboard/main.tsx`가 `DashboardScreen`을 사용하며 Storybook 모듈을 import하지 않음
|
||||
- Portable HTTP server: PASS — 내장 Node HTTP 서버와 `/storybook-static` 정적 경로 확인
|
||||
- Dashboard 별도 경로: NOT RUN — 현재 포터블 서버는 Dashboard export를 별도 URL로 서비스하지 않으며, 실행기 변경은 범위 밖
|
||||
- Storybook 대표 Dashboard: PASS 대상 — `/` 및 Storybook story route로 제공
|
||||
- 브라우저 실제 확인: NOT RUN (현재 환경에서 포터블 브라우저 검증을 수행하지 않음)
|
||||
- H-2: Storybook과 분리된 `scripts/h2-preview.mjs` 및 관련 소스 유지
|
||||
- 기존 r3 ZIP: 보존 확인
|
||||
|
||||
경고: Storybook 빌드에서 기존 CSS arbitrary-value 경고와 chunk-size 경고가 출력되었으나 빌드는 성공했다.
|
||||
@@ -0,0 +1,53 @@
|
||||
# HMDesignTest Portable r5 candidate verification
|
||||
|
||||
## Result
|
||||
|
||||
PORTABLE EXECUTION RESULT: PARTIAL
|
||||
|
||||
The portable server starts with the bundled Node runtime and prints separate Storybook and Dashboard URLs. The r5 candidate routes `/` to `storybook-static/` and `/dashboard/` to `dist-dashboard/` without changing UI source, Theme, Component, Pattern, or Storybook files.
|
||||
|
||||
## Candidate artifact
|
||||
|
||||
- Path: `HMDesignTest-Portable-Test-r5-candidate-2026-10-08.zip`
|
||||
- Size: 108,362,960 bytes
|
||||
- SHA-256: `2CD5BE30C6B6D7CCAC21D1081E9A0152CDA402D3D2F9218CCBCA6E253A0B9A38`
|
||||
- ZIP entries: 2,065
|
||||
- Duplicate archive paths: none
|
||||
|
||||
## Portable execution
|
||||
|
||||
- Bundled runtime: present at `HMDesignTest/runtime/node/node.exe`
|
||||
- Server startup: PASS; bundled Node v22.23.3 started the candidate server.
|
||||
- Example selected port during verification: 6009.
|
||||
- Printed addresses: `http://127.0.0.1:6009/` and `http://127.0.0.1:6009/dashboard/`.
|
||||
- Port collision fallback: retained; server increments from 6007 through 6023 and then fails cleanly if all are unavailable.
|
||||
- Binding: retained at `127.0.0.1`.
|
||||
- Path traversal and backslash rejection: retained.
|
||||
|
||||
## Static checks
|
||||
|
||||
- Storybook entry present: `HMDesignTest/storybook-static/index.html`
|
||||
- Dashboard entry present: `HMDesignTest/dist-dashboard/index.html`
|
||||
- Dashboard JS present: `HMDesignTest/dist-dashboard/assets/dashboard-export.js`
|
||||
- Dashboard CSS present: `HMDesignTest/dist-dashboard/assets/style-BJ7gb3jH.css`
|
||||
- Dashboard Noto Sans KR font present: `HMDesignTest/dist-dashboard/assets/NotoSansKR-variable-k7NQXPol.ttf`
|
||||
- Dashboard source is the production export; no Storybook story import was added.
|
||||
- r3/r4 preserved: PASS; no existing archive was overwritten.
|
||||
|
||||
## Browser verification
|
||||
|
||||
NOT RUN for the r5 candidate. The available browser could not connect to the locally spawned candidate server (`ERR_CONNECTION_TIMED_OUT`), and the shell HTTP client was blocked from accessing the local test socket by the execution sandbox. Therefore HTTP 200, rendered production Dashboard, filter/search/reset interaction, and console-error checks are not claimed as PASS.
|
||||
|
||||
## Inventory comparison
|
||||
|
||||
| Archive | Bytes | Entries | SHA-256 |
|
||||
|---|---:|---:|---|
|
||||
| r3 | 59,256,057 | 1,214 | `DA1F6DAD261D3F780BEE1E70F734F2464C5E6648A8BE94A75720B6F3D8F8FF08` |
|
||||
| r4 | 108,362,799 | 2,065 | `3ED64BAD1E9BF062CC168B6F253C6905244F3645F75EE70CFA7FAAB63C9A1F31` |
|
||||
| r5 candidate | 108,362,960 | 2,065 | `2CD5BE30C6B6D7CCAC21D1081E9A0152CDA402D3D2F9218CCBCA6E253A0B9A38` |
|
||||
|
||||
The r5 delta from r4 is limited to the launcher/server patch and ZIP metadata; no duplicate archive paths were found.
|
||||
|
||||
## User test status
|
||||
|
||||
Not yet fully user-testable as a verified release because browser-level HTTP/rendering and interaction checks remain NOT RUN. The candidate is suitable for a manual local test once the ZIP is extracted on a machine where localhost browser access is permitted.
|
||||
@@ -0,0 +1,42 @@
|
||||
# HMDesignTest Portable r5 Verification
|
||||
|
||||
## Result
|
||||
|
||||
PORTABLE R5 AUTO OPEN RESULT: PARTIAL
|
||||
|
||||
## Artifact
|
||||
|
||||
- Path: `HMDesignTest-Portable-Test-r5-2026-10-08.zip`
|
||||
- Size: `108,382,038` bytes
|
||||
- SHA-256: `547FB98292406D5BF3FC8B8EF4FA229196115FB1C3111358B5F56EE85D6FC234`
|
||||
- ZIP entries: `2,062`
|
||||
- Inventory file count: `1,946`
|
||||
- ZIP root: `HMDesignTest/`
|
||||
|
||||
## Applied change
|
||||
|
||||
- Reused the existing candidate/r4-derived portable contents.
|
||||
- Replaced only `HMDesignTest/preview-server.cjs` with the current source launcher.
|
||||
- Browser launch occurs from the server `listening` event, uses the selected `127.0.0.1` port, invokes the Windows default browser through `cmd /c start`, and is guarded to one launch per process.
|
||||
- Browser launch errors are reported while the server remains running and the URL is printed.
|
||||
|
||||
## Verification
|
||||
|
||||
| Check | Result | Evidence |
|
||||
|---|---|---|
|
||||
| Bundled Node runtime | PASS | Node v22.23.3 (x64) started the staged server |
|
||||
| Default port 6007 | PASS | Server printed `http://127.0.0.1:6007/` |
|
||||
| 6007 collision fallback | PASS | Test occupant remained running; server selected port 6009 and printed its URL |
|
||||
| Browser launch code path | PASS by execution path | `listening` invoked the launch function in both runs |
|
||||
| Visual browser opened once | NOT RUN | Browser UI was not independently observable in this environment |
|
||||
| BAT double-click | NOT RUN | Same limitation; equivalent bundled-node server invocation was run |
|
||||
| Korean/space extraction path | NOT RUN | No separate user-style extraction/double-click run performed |
|
||||
| Storybook render, Theme, Font, Asset | NOT RUN | No browser-level rendering verification claimed |
|
||||
| Dashboard and H-2 | NOT RUN | Not changed or rebuilt |
|
||||
|
||||
## Source/archive note
|
||||
|
||||
The requested `HMDesignTest-Portable-Test-r4-final-2026-10-08.zip` was not present at the specified path. The existing `HMDesignTest-Portable-Test-r5-candidate-2026-10-08.zip`, together with `PORTABLE-R4-VERIFICATION.md`, was used as the preserved r4-derived staging source. The candidate launcher did not contain the browser patch; only the launcher was replaced.
|
||||
|
||||
Original r3/r4 archives were not overwritten. No Git commit/push, npm install/ci, production upload, component/pattern/theme change, or additional ZIP was performed.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# HMDesignTest Portable r6 Verification
|
||||
|
||||
## Result
|
||||
|
||||
PORTABLE R6 DUAL LAUNCH RESULT: PARTIAL
|
||||
|
||||
## Artifact
|
||||
|
||||
- ZIP: `HMDesignTest-Portable-Test-r6-2026-10-08.zip`
|
||||
- Size: `108,409,779` bytes
|
||||
- SHA-256: `BCBFDFEE73CA3AE0CBDF8E810DAFAB6535578A655E3334CEB65B0E73F61C0ED0`
|
||||
- Source candidate: `HMDesignTest-Portable-Test-r5-candidate-2026-10-08.zip`
|
||||
- Source candidate SHA-256: `2CD5BE30C6B6D7CCAC21D1081E9A0152CDA402D3D2F9218CCBCA6E253A0B9A38`
|
||||
|
||||
## Packaging verification
|
||||
|
||||
- ZIP entries: `1,948` regular files; all paths begin with `HMDesignTest/`.
|
||||
- Independent extraction: PASS.
|
||||
- Extracted files: `1,948`.
|
||||
- Inventory-managed files: `1,946`.
|
||||
- Explicitly excluded: `FILE-INVENTORY-R6.json`, `PORTABLE-R6-VERIFICATION.md`.
|
||||
- Inventory hash comparison: PASS; `0` mismatches.
|
||||
- `node_modules/` and `.git/`: absent.
|
||||
- Root layout includes `HMDesignTest/start-preview.bat`, `preview-server.cjs`, `runtime/node/`, `storybook-static/`, `dist-dashboard/`, `packages/`, `themes/`, `docs/`, `AGENTS.md`, `CLAUDE.md`, and `README.md`.
|
||||
|
||||
## Execution verification
|
||||
|
||||
- Bundled Node: PASS — `v22.23.3 (x64)`.
|
||||
- Extracted BAT: PASS — `cmd.exe /c start-preview.bat` launched the bundled server.
|
||||
- Default port: PASS — both URLs printed on `6007`.
|
||||
- Port fallback: PASS — a second server selected `6009` when lower ports were occupied; both printed URLs used that port.
|
||||
- Browser launch path: PASS by execution path — Dashboard and Storybook each have one guarded Windows default-browser launch call after `listening`.
|
||||
- Korean/space path: PASS — extracted under `D:\디자인시스템\HMDesignSystem\r6 verify 한글 space\` and BAT started successfully.
|
||||
|
||||
## Not run
|
||||
|
||||
The shell and available browser could not access the locally spawned localhost socket (`access denied` / `ERR_CONNECTION_TIMED_OUT` or `ERR_CONNECTION_REFUSED`). Therefore HTTP 200, actual browser-rendered Dashboard/Storybook, Theme/font/asset loading in-browser, Dashboard filter/search/reset interaction, and console/runtime error checks are `NOT RUN`, not PASS.
|
||||
|
||||
## Scope preservation
|
||||
|
||||
Dashboard asset hashes match the current repository `dist-dashboard` outputs. No Component, Pattern, Theme, Storybook, H-2, source, existing ZIP, Git, npm install, full build, commit/push, or upload was performed.
|
||||
@@ -0,0 +1,114 @@
|
||||
# HM Design System
|
||||
|
||||
React + TypeScript + Vite와 shadcn/Radix 기반 HM 디자인 시스템입니다. UI Component·업무/Layout Pattern·CENTER Graphic을 독립 workspace 패키지와 통합 Storybook으로 확인합니다.
|
||||
|
||||
## Navigation 정책
|
||||
|
||||
**ONE PAGE = ONE NAVIGATION MODE.** GNB ONLY / LNB ONLY / DASHBOARD ONLY 중 하나를 선택한다. 모바일도 선택한 Navigation 자체의 Responsive Variant를 사용한다. 이 정책은 AI 코드 생성·제안, 신규 페이지, 기존 화면 수정, 프로토타입, Storybook, Production에 동일하게 적용한다. [상세 정책](docs/guidelines/navigation-single-mode.md), [AI 작업 지침](AGENTS.md).
|
||||
|
||||
새 화면을 구현하는 AI는 [AI 화면 구현 Quick Start](AGENTS.md#ai-화면-구현-quick-start)의 7단계를 순서대로 따른다. 세부 Component·Pattern·Theme·Runtime 계약과 대표 Story 경로도 그 지침에서 연결한다.
|
||||
|
||||
## Storybook Reference와 화면 생성
|
||||
|
||||
Storybook은 기존 HM Component·Pattern·Theme·Variant·State·Interaction을 확인하는 Reference 환경이다. 일반적인 업무 화면 생성에서는 신규 화면을 Storybook Story나 사이드바에 등록하지 않고, 대표 Story를 교체하지 않으며, Storybook 실행을 필수 미리보기 경로로 요구하지 않는다. AI는 별도의 React 화면을 만들고 화면별 빌드·필요한 검증과 독립 미리보기를 제공한다. H-2 또는 유사 자동화가 사용 가능한 경우에는 `preview.html`·desktop/mobile 이미지 같은 독립 미리보기 산출에만 사용하며 Storybook 등록 기능으로 취급하지 않는다. Storybook 전용 helper/CSS는 운영 화면에 사용하지 않는다. Component·Pattern 추가/수정, 대표/Verification Story 작성 또는 Storybook Reference 개선을 명시적으로 요청한 경우에만 Storybook을 수정한다.
|
||||
|
||||
## 설치 없는 테스트 배포
|
||||
|
||||
현재 검증된 테스트 배포 기준은 `HMDesignTest-Portable-Test-r3.zip`입니다. 사전 빌드된 Storybook과 Windows x64 포터블 Node를 포함하며 정확한 포함 범위는 r3의 BUILD-INFO·Inventory를 기준으로 확인합니다. `HMDesignTest-Portable-Test-r2.zip`과 `HMDesignTest-Portable-Test.zip`은 이전 스냅샷 기록으로 보존하며 현재 기준이 아닙니다. 포터블 ZIP의 기본 포트는 6007이고 충돌 시 자동으로 다른 포트를 사용합니다. 개발 저장소 Storybook의 6024와는 별개입니다. 테스트 사용자는 ZIP 해제 → `HMDesignTest/start-preview.bat` 더블클릭 → 표시된 로컬 URL 열기만 수행합니다. 별도 Node/npm 설치, npm 명령, Git 작업 또는 운영 업로드는 필요 없습니다. [실행 및 포함 범위](docs/guidelines/portable-test-distribution.md).
|
||||
|
||||
## 오전 소스 ZIP / 개발자 전용 절차 (AI 참조용)
|
||||
|
||||
오전 소스 배포의 역사적 기준은 `HMDesignTest.zip`입니다. 당시 전달 구성에서는 `.git` 디렉터리와 Git 이력·원격 연결 정보를 제외하고 `.gitignore`와 소스·Storybook·문서·패키지 설정을 유지했습니다. 이 절의 수량·실행 조건은 오전 ZIP에 대한 기록이며 현재 포터블 r3의 포함 범위는 위 절과 BUILD-INFO를 기준으로 확인합니다. `HMDesignSystem-test-2026-10-08-r4.zip`, `HMDesignSystem-Distribution-Test-v2.zip`, `HMDesignSystem-AI-Reference-Test.zip`은 오전 기준을 대체하지 않습니다.
|
||||
|
||||
기준 ZIP에는 UI·Pattern·CENTER Graphic, 공개 API, Theme, Storybook 소스, 문서, 예제, 패키지 dist, lockfile 및 폰트가 포함됩니다. `node_modules`와 `storybook-static`은 포함되지 않으므로 ZIP 단독으로 정적 Storybook을 바로 열 수는 없습니다.
|
||||
|
||||
오전 소스 ZIP을 개발 환경으로 실행할 경우 Node.js 20.20.1 / npm 10.8.2 환경의 프로젝트 루트에서 다음을 실행합니다.
|
||||
|
||||
```powershell
|
||||
# ZIP을 풀고 프로젝트 루트에서 실행
|
||||
npm ci
|
||||
npm run build:packages
|
||||
npm run dev
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
HM 패키지는 ZIP 안의 workspace를 사용하므로 HM Registry 설치가 필요하지 않습니다. `npm ci`로 의존성을 복원하고 `npm run build:packages`로 패키지를 빌드한 뒤 앱과 Storybook을 시작합니다. 정적 Storybook이 필요하면 `npm run build-storybook`을 실행합니다. 이 문서는 배포본의 새 환경 실행 결과를 보증하지 않습니다.
|
||||
|
||||
- Storybook: 기준 ZIP의 `npm run storybook`은 기본 포트 6007입니다. 다른 포트는 `npm run storybook -- --port <available-port>`로 지정할 수 있습니다.
|
||||
- ZIP 확인 빌드: `npm run build`, `npm run build-storybook`
|
||||
|
||||
현재 개발 저장소는 ZIP과 별개로 `npm run storybook` 기본 포트가 6024이며(포터블 ZIP 6007과 통일하지 않음), `npm run build:dashboard-export` → `dist-dashboard/index.html`도 제공합니다. `dist-dashboard`는 H-1(독립 단일 HTML 미리보기 POC, PASS)의 성공 산출물과 동일하지 않으며 ZIP에도 있다고 가정하지 않습니다. H-2(AI 생성 화면의 자동 빌드·캡처·검증)는 Storybook 배포와 별도 기능이며 구현 완료 여부는 현재 파일과 별도 작업 결과로 확인합니다.
|
||||
|
||||
## 사용 (workspace)
|
||||
|
||||
```ts
|
||||
import { Button } from "@hm-design/ui"
|
||||
import { CenterTitle, TunnelFacility } from "@hm-design/center-graphic"
|
||||
import { DataTable, FilterBar, useDataTable } from "@hm-design/ui" // Patterns: docs/patterns/
|
||||
```
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "@hm-design/ui/tailwind.css";
|
||||
@import "@hm-design/ui/theme.css";
|
||||
```
|
||||
|
||||
이 CSS entry를 소비 앱 bootstrap에서 import합니다. Dashboard 업무 상태를 표시할 때는 Theme 다음에 `@hm-design/ui/dashboard-status.css`를 추가하고 [기존 Badge 색상 mapping과 React 예제](docs/patterns/dashboard-layout.md#dashboard-업무-상태-공통-계약)를 적용합니다. API import나 Status CSS만으로 Badge 색상이 자동 선택되지는 않습니다.
|
||||
|
||||
별도 React 앱은 [Tailwind v4 검색 범위 가이드](docs/guidelines/runtime-portability.md#21-tailwind-v4-소비자-검색-범위)를 확인합니다. `source("./")`는 CSS 위치에 따라 범위가 달라지며 HM `tailwind.css`의 명시 source를 유지해야 합니다. 공개 package Theme는 동봉된 `dist/fonts/`를 상대 URL로 참조합니다. 개발 저장소·포터블 ZIP·React 소비자·단일 HTML의 차이는 [폰트·Asset 경로](docs/guidelines/runtime-portability.md#41-noto-sans-kr-경로-실행-환경별-확인)를 따릅니다.
|
||||
|
||||
화면 생성 Reference: [Navigation 선택 근거](docs/guidelines/navigation-single-mode.md#선택-근거), [GNB Desktop·Mobile 구성](docs/patterns/gnb.md#8-desktop--mobile)과 [내부 Sidebar](docs/patterns/gnb.md#15-gnb-only-sidebar-composition-2026-10-08), [FilterBar 조회·초기화 소비 예제](docs/patterns/filter-bar.md#61-조회-버튼으로-적용하는-소비-예제). 일반 업무 목록의 모드를 임의 확정하거나 FilterBar에 조회 API가 있다고 가정하지 않습니다.
|
||||
|
||||
루트 `package.json`의 `workspaces`(`packages/*`)로 연결됩니다. 확인: `npm run typecheck`, `npm run build`. 두 패키지는 `private: false`이며 ESM JS·타입 선언·CSS를 `dist`에서 export합니다. `npm run build:packages`로 재빌드할 수 있고, Registry publish 이력은 이번 ZIP 검증 범위에 포함하지 않습니다.
|
||||
|
||||
## 파일 위치
|
||||
|
||||
아래 경로는 저장소 루트(압축 해제 시 `HMDesignTest/`) 기준입니다. `packages/ui/` 하위 경로도 전체 상대경로로 표기합니다.
|
||||
|
||||
- `packages/ui/` (`@hm-design/ui`): UI Component·업무/Layout Pattern과 HM Theme
|
||||
- `packages/ui/src/components/ui/`: shadcn 기반 컴포넌트, `packages/ui/src/patterns/`: 조합 Pattern(Data Table, Filter Bar 등), `packages/ui/src/index.ts`: 공개 export
|
||||
- `packages/ui/src/theme/primitives.css`, `packages/ui/src/theme/default.css`: Default JSON에서 생성한 package fallback과 Semantic/shadcn 연결
|
||||
- `packages/ui/src/theme/<component>.css`: 컴포넌트별 Theme 매핑과 상태 스타일
|
||||
- `packages/ui/src/theme.css`: Theme CSS 진입점, `packages/ui/src/tailwind.css`: Tailwind 쪽 설정
|
||||
- `packages/center-graphic/` (`@hm-design/center-graphic`): CENTER 그래픽(제목·정보·노선/공간·시설물), `@hm-design/ui`에 의존하지 않음
|
||||
- `src/center/`: CENTER Storybook (`Center Graphic` 그룹)
|
||||
- `docs/center-graphic/`: CENTER 구현 기준 문서
|
||||
- `src/App.tsx`: 기본 UI 확인 화면 (`@hm-design/ui` 사용)
|
||||
- `src/hm-theme.css`: App/Storybook 예시 화면 스타일. 장식용 시제품 색상은 고정값으로 남아 있음
|
||||
- `themes/default.json`, `themes/service-example.json`: 공식 Default 컬러와 partial 서비스 예제; UI package와 별도 배포
|
||||
- `scripts/theme-templates/`, `scripts/theme-defaults.mjs`: JSON에서 package fallback CSS/데이터 생성; 값은 JSON만 편집
|
||||
- `packages/ui/src/theme/contract.ts`, `runtime.ts`, `loader.ts`: 공개 Runtime 계약·검증·Global/Scoped 적용 및 URL Loader. `@hm-design/ui/theme`으로 export
|
||||
- `src/theme/preview.ts`: 패키지 Runtime API의 호환 어댑터. `themes/drafts/`는 Theme 초안 자료
|
||||
- `src/components/*.stories.tsx`, `src/patterns/*.stories.tsx`: Component/Pattern 대표 Story와 숨김 verification Story
|
||||
- `src/theme/*.stories.tsx`: Default/External 및 Runtime 계약 검증 Story
|
||||
- `docs/components/`: 컴포넌트별 현재 구현 기준 문서
|
||||
- `public/fonts/`: 기존 자료의 Noto Sans KR 글꼴과 라이선스
|
||||
- `docs/guidelines/`: 프로젝트 지침·구현 원칙·Theme Token·적용/배포 가이드
|
||||
- 로컬 검증·감사·진단 자료는 테스트 ZIP에서 제외합니다.
|
||||
|
||||
## 참고 범위
|
||||
|
||||
초기 디자인의 색상, Typography, Radius와 Button·Input·Select·Dialog에는 외부 ui_viz Reference를 참고했습니다. 이 Reference는 ZIP에 포함되지 않으며 실행에 필요하지 않습니다. 현재 Source of Truth는 HM Component/Pattern 구현, 대표 Storybook Story와 아래 공식 정책 문서입니다.
|
||||
|
||||
HM Default 컬러의 운영 원본은 `themes/default.json`이며 생성한 CSS preset은 최초 렌더와 네트워크 실패 시 package fallback입니다. 외부 Theme도 같은 공개 계약을 사용합니다. Active와 Focus는 독립 Semantic이고 Global override와 독립 Scoped override를 지원합니다. `Theme/Designer Playground/Editor`에서 JSON URL/파일 Preview와 컬러 편집·Reset·Export/Import를 지원합니다. Portal은 실제 DOM scope를 따릅니다.
|
||||
|
||||
Component Mapping은 Semantic/Theme palette를 참조하며 기본 API로 HM 스타일이 적용됩니다. Input/Select 크기 규격, Calendar/Date Picker, GNB/LNB 3Depth·Standalone Mobile, Dashboard 내부 Navigation까지 구현되어 있습니다. 현재 상태와 제한은 `docs/components/`, `docs/patterns/`, `docs/guidelines/runtime-theme.md`에서 확인합니다.
|
||||
|
||||
하나의 Git Repository에서 UI/Graphic/Theme를 관리합니다. UI는 `.`·`/theme`·`/theme.css`·`/dashboard-status.css`·`/tailwind.css`, CENTER는 `.`·`/reference`·`/style.css`를 compiled dist에서 export합니다. Runtime URL Loader와 Theme JSON 독립 산출은 구현했습니다. `npm run build:themes`는 `theme-dist/themes/`만 생성하며 UI/Graphic build나 version up을 수행하지 않습니다. Default fallback을 새 package에 반영할 때는 `npm run generate:theme-defaults`를 사용합니다. 상세 적용·배포: [Theme delivery](docs/guidelines/theme-delivery.md). 디자인팀 사용법: [Theme guide](docs/guidelines/designer-theme-guide.md).
|
||||
|
||||
## 공식 가이드 / Source of Truth
|
||||
|
||||
- AI 구현·Component 재사용: [AGENTS](AGENTS.md), [Component 구현 원칙](docs/guidelines/shadcn-기반-구현-원칙.md)
|
||||
- Component / Pattern 탐색: [통합 Reference Index](docs/guidelines/component-pattern-reference.md), `packages/ui/src/index.ts`, 구현 소스·사용 문서·대표 Story
|
||||
- Theme: [Token 정책](docs/guidelines/테마-토큰-가이드.md), [Runtime 계약·사용법](docs/guidelines/runtime-theme.md)
|
||||
- Runtime portability: [Runtime / AI Independent Portability Policy](docs/guidelines/runtime-portability.md)
|
||||
- Navigation: [단일 모드 정책](docs/guidelines/navigation-single-mode.md)
|
||||
- Dashboard: [Dashboard Pattern](docs/patterns/dashboard-layout.md)
|
||||
- CENTER: [CENTER 구현·사용 범위](docs/center-graphic/README.md)
|
||||
- 프로젝트·서비스 적용: [프로젝트 지침](docs/guidelines/프로젝트-지침.md), [개발 적용](docs/guidelines/개발-적용-가이드.md)
|
||||
|
||||
## Package Registry / Publish (별도 내부 운영 정보)
|
||||
|
||||
패키지 build와 Registry publish는 별도 단계입니다. Registry 설치·publish·실제 CDN 운영은 이번 테스트 ZIP의 실행 조건이나 검증 범위가 아닙니다. 서비스 배포·버전·캐시·롤백 계획은 [운영/배포 가이드](docs/guidelines/운영-배포-가이드.md)를 따릅니다.
|
||||
|
||||
CENTER Graphic은 현재 내부 테스트/검토 목적 범위에서 제공합니다. 외부 공개 또는 사외 배포 전 별도 License 확정이 필요합니다. 임의의 오픈소스 License를 부여하지 않습니다.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Center Graphic (`@hm-design/center-graphic`)
|
||||
|
||||
CENTER 도메인(제목 · 정보 표기 · 노선/공간 · 시설물)의 현재 HM 구현 기준 문서. UI 컴포넌트 문서(`docs/components/`)와 별개이며 UI 규격을 근거로 쓰지 않는다. 공통 원칙은 `docs/guidelines/프로젝트-지침.md` §5.1을 따른다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | ui-viz **CENTER Web Reference 구현을 TypeScript 패키지로 이식** (Title · Information · Spatial · Facility). 규격을 새로 만들거나 재해석하지 않았다 |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 패키지 | `packages/center-graphic` (`@hm-design/center-graphic`, `private: false`, compiled dist JS·타입·CSS export) |
|
||||
| Storybook | 최상위 그룹 `Center Graphic` (`src/center/*.stories.tsx`). UI는 기존 `Components` 유지 |
|
||||
| 의존 | `@hm-design/ui`에 의존하지 않음. peer는 `react`, `react-dom`뿐 |
|
||||
|
||||
## 2. 원본 근거
|
||||
|
||||
현재 Source of Truth는 이 문서, `packages/center-graphic/src/`와 `src/center/`의 대표 Story다. 아래 표는 초기 이식 당시 외부 ui_viz Reference의 출처 목록이며 ZIP 내부 경로가 아니다. 외부 자료는 ZIP에 포함되지 않고 실행에 필요하지 않으며 제공된 경우에만 참고한다.
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `docs/governance/center-domain.md` (§0.1, §1, §2.4, §5) | CENTER 색인, 상태 어휘, 미결(TBD/HOLD) registry |
|
||||
| `docs/usage/center-usage.md` | CENTER 구성과 사용 순서, 승인 asset 위치 |
|
||||
| `docs/visualization/README.md` | visualization owner 문서 목록 |
|
||||
| `docs/foundation/center-typography.md` (family, 폰트 fallback 부분) | 글꼴 Noto Sans KR |
|
||||
| `implementations/web/center/components/*.jsx, *.css` | Web Reference 구현 (이식 원본) |
|
||||
| `implementations/web/assets/center/facility/` | Reference가 불러오는 runtime SVG 128개 |
|
||||
| `assets/center/source/facility/` | 승인 Canonical Asset 128개 (runtime 사본과 바이트 비교용) |
|
||||
| `implementations/web/storybook/center/*.stories.jsx`, `center-shared.jsx` | Story 구성 참고 |
|
||||
|
||||
외부 Reference가 제공된 경우의 참고 우선순위: owner 문서의 명시적 상태(`CONFIRMED_CANONICAL` / `CONFIRMED_BY_DESIGN` → `PLATFORM_MAPPING` → `Candidate`/`TBD`/`HOLD`) → 승인 asset → Web Reference 구현. `docs/visualization/*.md`는 이름에 `center-`가 없어도 CENTER owner 문서다.
|
||||
|
||||
**읽지 않은 것(검증 한계)**: owner 문서(`route.md`, `facility.md`, `spatial-map.md`, `spatial-section.md`, `center-title.md`, `center-information-*.md`, `center-scale.md`, `center-color.md` 등)의 본문 전체와 `_local/`, `generated/`, `verification/`. 이식은 Web Reference 구현의 동작·값을 그대로 가져온 것이며, 구현 값이 owner 문서의 각 절과 일치하는지를 절 단위로 다시 대조하지는 않았다.
|
||||
|
||||
문서 간 충돌: 읽은 범위에서 값 충돌은 발견하지 못했다. Reference 코드 주석이 말하는 `KNOWN_DEVIATION`(예: 구도·면도·이도 윤곽, 고속도로 노선명 높이)은 원본 그대로 보존된다.
|
||||
|
||||
## 3. 구현 목록
|
||||
|
||||
| 영역 | 컴포넌트 | 비고 |
|
||||
|---|---|---|
|
||||
| 배율 | `CenterRepresentation` | 제작 대상 7680×2160 = 1.00, 3840×2160 = 0.75 (`CONFIRMED_BY_DESIGN` 배율 쌍). 다른 크기는 `PREVIEW_ONLY` 1.00 |
|
||||
| 제목 | `CenterTitle` | 유형 1–4 (`projectName`, `projectNameRoute`, `routeProjectName`, `routeProjectNameRoute`). HTML 텍스트 + CSS, Source SVG 미사용 |
|
||||
| 정보 | `CenterInformationBox`(list / definition), `CenterInformationTable`, `CenterInformationEmphasis` | 팔레트 5종 `CENTER_INFORMATION_PALETTES`. chevron / arrow는 인라인 벡터 |
|
||||
| 노선/공간 | `RouteLine`, `ProjectRoute`, `RouteNameBadge`, `RouteNumberBadge`, `DirectionBadge`, `AdministrativeLabel`, `NorthArrow`, `BoundaryLine`, `ProjectExtent` | 노선 분류 7종 `ROUTE_CLASSES`, 상태 `existing` / `underConstruction` / `planned`. SVG asset을 불러오지 않고 규칙으로 그림 |
|
||||
| 시설물 | `TunnelFacility`, `LocalAssetFacility` + `FACILITY_CONFIGS`(bridge · intersection · junction · interchange), `RoadsideFacilityTag`, `CompactFacilityConnection` | 승인 SVG 128개를 **바이트 그대로** 사용. 좌·중·우 조각 중 중앙만 가로로 확장 |
|
||||
|
||||
구현하지 않은 것: 표시 배율 HOLD(Q2b·Q3) 화면, 통합 배치(`IntegratedPlacementScene`, `PREVIEW_ONLY`), `CenterCompositions` · Facility `Assets` Story, POI/Terrain Symbol(`symbol.md`, 구현 없음), 후보(Candidate) SVG 64종.
|
||||
|
||||
## 4. 규격
|
||||
|
||||
초기 수치와 규칙의 출처는 외부 ui-viz owner 문서다. 현재 HM 구현·검토는 포함된 코드·대표 Story와 이 문서를 기준으로 하며 외부 자료 접근을 전제로 하지 않는다. 구현에서 확인한 값:
|
||||
|
||||
- 시설물 총 너비(기준 이름): 터널 위치도 210 · 실사화 220 (Source reference total과 일치).
|
||||
- 노선: 중심선 18u, 윤곽 중심선의 20%, 그림자 층 순서 Shadow → Outline → Primary Stroke(`CONFIRMED`).
|
||||
- 글꼴: Noto Sans KR(Canonical). 폰트 파일 배포와 fallback 순서는 `PLATFORM_MAPPING`/`TBD`이며 이 패키지는 폰트를 포함하지 않는다.
|
||||
- 상태: 노선만 상태(기본/공사중/계획)를 가진다. Facility는 정적이며 Selected/Hover가 없다.
|
||||
|
||||
## 5. Public API
|
||||
|
||||
```ts
|
||||
import { CenterRepresentation, CenterTitle, CenterInformationBox, RouteLine, TunnelFacility, LocalAssetFacility, FACILITY_CONFIGS } from "@hm-design/center-graphic"
|
||||
// Storybook 검수용 demo 페이지 (컴포넌트 API 아님)
|
||||
import { CenterTitleReferenceDemo } from "@hm-design/center-graphic/reference"
|
||||
```
|
||||
|
||||
전체 export 목록은 `packages/center-graphic/src/index.ts`. Reference 코드의 `className` 등 기존 prop은 그대로이며 타입만 추가했다(`CenterTitleComposition`, `RouteClassKey`, `FacilityConfig` 등).
|
||||
|
||||
## 6. Token / Style
|
||||
|
||||
- CENTER 전용 token은 별도 파일 없이 각 컴포넌트 CSS 안에 있다(`--cid-*`, `--csi-*`, `--tunnel-*` 등). UI의 `--hm-*` token을 쓰지 않는다.
|
||||
- CSS는 컴포넌트가 import하므로 별도 theme.css entry를 만들지 않았다.
|
||||
- `--center-font-family`: Reference CSS가 UI 쪽 `--font-family-base`를 참조하던 두 곳을 CENTER 전용 변수로 바꿨다(기본값 `'Noto Sans KR', 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif` — Title이 이미 쓰던 체인, FNT-03 승인 fallback). 독립성을 위한 유일한 CSS 변경이다.
|
||||
- Reference **demo 프레임**(`.cid-demo__*`, `.center-title-reference__*`, `.center-representation__note`)은 `--color-fg-secondary` 등 외부 token을 읽는다. 그래픽 자체는 쓰지 않으며, Storybook은 `src/center/center-stories.css`에서 값을 공급한다.
|
||||
|
||||
## 7. Storybook
|
||||
|
||||
`Center Graphic` 그룹(기본 sidebar): `Overview`, `Title`, `Information`, `Spatial`, `Facility`(터널 · 교량 · 교차로 · 분기점 · 나들목 · 휴게소·졸음쉼터 · 연계 시설물), verification 숨김: Title `Playground`, Facility `직접 테스트 (터널)`. 제작 대상 해상도는 주소 `?centerResolution=3840x2160`으로 지정한다.
|
||||
|
||||
## 8. 미확정 / 보류 (원본 기준)
|
||||
|
||||
center-domain.md §5 registry를 따른다: 표시 배율 Q2b·Q3 `HOLD`, Facility §12.24 `OWNER_REVIEW`, Information 일부 `TBD`/`Candidate`, Title Resolution B `TBD`, 전체 layer order·collision `TBD`, 철도 윤곽 두께·line join `TBD`, `symbol.md` 정확한 기하·팔레트 `TBD`, 폰트 binary 승인 `TBD`. Storybook `Overview`에 상태 표가 있다. 이 구현은 위 항목을 확정하지 않는다.
|
||||
|
||||
## 9. 관련 파일
|
||||
|
||||
```
|
||||
packages/center-graphic/src/index.ts, reference.ts, env.d.ts
|
||||
packages/center-graphic/src/components/*.tsx, *.css (7개 컴포넌트)
|
||||
packages/center-graphic/src/assets/facility/ (SVG 128)
|
||||
src/center/*.stories.tsx, center-shared.tsx, center-stories.css
|
||||
```
|
||||
|
||||
## 10. 검증 상태
|
||||
|
||||
- TypeScript: 루트 `npm run typecheck`(두 패키지 포함) 통과. `npm run build` 통과(루트 Demo는 center-graphic을 import하지 않는다).
|
||||
- Storybook: 11개 Center story가 렌더되고 오류 없음. 11개 story의 `img` 전부 로드(깨진 이미지 0). 터널 기준 이름의 총 너비가 위치도 210 · 실사화 220으로 reference total과 일치. Title 유형 1–2, Spatial 노선 영역, Facility 터널 화면을 눈으로 확인.
|
||||
- 승인 asset 바이트 일치: 패키지의 SVG 128개(batch 122 + 터널 6) 전부가 `assets/center/source/facility/` 승인 원본과 바이트 동일(상이 0, 누락 0).
|
||||
- 미확인: 원본 Storybook과의 픽셀 단위 비교, owner 문서 절 단위 대조, 3840×2160(0.75) 화면, 교량·교차로·분기점·나들목·휴게소·연계 시설물·Information 화면의 눈 확인(렌더 오류와 이미지 로드만 확인), 접근성/긴 label 추가 검증.
|
||||
|
||||
## 내부 테스트 사용 범위 / License
|
||||
|
||||
CENTER Graphic은 현재 내부 테스트/검토 목적 범위에서 제공합니다. CENTER 자체 License는 미확정이며 외부 공개 또는 사외 배포 전 별도 License 확정이 필요합니다. 특정 오픈소스 License를 부여하지 않습니다.
|
||||
@@ -0,0 +1,112 @@
|
||||
# Accordion
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Accordion 고유의 API·Token·상태·결정만 기록한다. Collapsible(`collapsible.md`)과 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Accordion (Radix `Accordion`) |
|
||||
| 대표 Story | `Components/Disclosure/Accordion` → `All` (이름 "Accordion") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Accordion이 컴포넌트 이름으로만 있고 규격은 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계(fg, stroke, divider 계열 Primitive)로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Accordion 구조(Root·Item·Header·Trigger·Content), shadcn chevron과 open/close animation을 유지하고 theme CSS로 HM 스타일을 연결.
|
||||
- **Collapsible과의 구분**: Accordion은 여러 item이 연속된 정보 구조라 item 사이 divider가 구조를 만든다. 단일 독립 disclosure는 Collapsible을 쓴다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 | 비고 |
|
||||
|---|---|---|
|
||||
| Container | 바깥 박스·배경 없음 | |
|
||||
| Item divider | 1px `--hm-sem-stroke-input-default`, 마지막 item 제외 | Dialog·Card와 같은 윤곽색(값 참조만) |
|
||||
| Trigger | min-height 48px, padding `12px 0`, gap 16px, 위 정렬이 아니라 세로 가운데 | |
|
||||
| Trigger typography | 16 / 24 / 500 | |
|
||||
| Trigger 전경 | `--hm-sem-fg-primary` | |
|
||||
| Indicator | shadcn `ChevronDownIcon` 20px, `--hm-sem-fg-secondary`, 열리면 180° 회전(200ms) | shadcn 유지 |
|
||||
| Content | 14 / 20 / 400, `--hm-sem-fg-primary`, 아래 padding 16px | 안쪽 div에 padding |
|
||||
| 긴 텍스트 | `overflow-wrap: anywhere`, 고정 높이 없음 | |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Closed / Open | Radix `data-state` |
|
||||
| Hover | Trigger background `transparent`, cursor pointer. 기존 underline/focus 정책 유지, disabled 제외 |
|
||||
| Focus (`:focus-visible`) | shadcn ring (`ring-3 ring-ring/50`) 유지 |
|
||||
| Disabled | `opacity: 0.6`(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), `cursor: not-allowed`, 키보드 이동에서 제외 |
|
||||
| Open / Close animation | shadcn `accordion-down` / `accordion-up` 0.2s ease-out (height). 첫 렌더에서는 Radix가 animation을 막는다 |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Accordion props 그대로. export: `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`.
|
||||
|
||||
- `Accordion`: `type="single" | "multiple"`, `collapsible`(single), `value`/`defaultValue`/`onValueChange`, `disabled`, `orientation`, `dir`.
|
||||
- `AccordionItem`: `value`(필수), `disabled`.
|
||||
- `AccordionTrigger`: chevron을 내장한다. 출력 `data-slot="accordion-trigger"`, 아이콘 `data-slot="accordion-trigger-icon"`.
|
||||
- `AccordionContent`: `className`은 안쪽 div(`data-slot="accordion-content-inner"`)에 적용된다. Radix Content(`data-slot="accordion-content"`)는 `overflow-hidden`과 animation만 가진다.
|
||||
- `variant`, `size`, `appearance` 등 HM 전용 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/accordion.css` (`--hm-accordion-*`).
|
||||
|
||||
`text`, `content-text`, `divider-width`, `divider-color`, `trigger-min-height`, `trigger-padding-y`, `trigger-gap`, `trigger-font-size|line-height|font-weight`, `icon-size`, `icon-color`, `content-font-size|line-height`, `content-padding-bottom`, `disabled-opacity`. Semantic과 Theme palette만 참조하고 다른 컴포넌트의 Component Token을 쓰지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`aria-expanded`, `aria-controls`, `role=region`)·키보드(Arrow Up/Down은 trigger 사이 이동하며 끝에서 순환, Home/End, Enter/Space)·single/multiple/collapsible 동작, Header 요소(`h3`), `data-slot`, `cn(..., className)`, chevron 회전, `accordion-down/up` animation, focus ring.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `py-4 text-sm font-medium hover:underline`, `size-4` 아이콘, Item `border-b`, Content `pb-4 text-sm` class를 제거하고 theme CSS로 이전.
|
||||
- shadcn은 Content 안쪽 div가 없는 구조도 있었으나 height animation이 padding에 의해 흔들리지 않도록 padding은 안쪽 div에 둔다.
|
||||
- Trigger의 `disabled:pointer-events-none`을 `disabled:cursor-not-allowed`로 교체(커서가 보이도록).
|
||||
- animation을 `animate-[accordion-down_0.2s_ease-out]` 임의 값으로 연결(`animate-accordion-*` utility theme 변수가 이 패키지에 없다. keyframe은 `shadcn/tailwind.css`가 제공).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Accordion"): `StateBoard`로 Single(첫 항목 열림), Collapsible Single, Multiple(두 항목 열림), Disabled Item, Long Content(여러 줄 Trigger + 긴 콘텐츠 + 공백 없는 문자열)를 한 페이지에 둔다. 모두 실제 클릭·키보드로 동작.
|
||||
- verification: `Single`, `Multiple`, `Disabled`, `Long Content`.
|
||||
- meta에 `component`를 지정하지 않는다(Accordion props가 `single | multiple` union이라 args 없는 Story가 타입을 통과하지 못함).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(48px, 16/24/500, padding, divider 색, Disabled opacity 0.6)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 현재 Hover는 `transparent`다. Focus Light 2 50% 3px ring과 Disabled opacity 0.6·cursor not-allowed는 유지.
|
||||
- Indicator는 shadcn chevron 유지(Select의 HM glyph와 별개). 별도 Source glyph 없음.
|
||||
- Motion 규칙이 없어 shadcn animation 유지. `prefers-reduced-motion` 대응은 추가하지 않았다(기존 구현에도 없음).
|
||||
- Container 외곽 박스/Card형 variant는 만들지 않았다.
|
||||
- 소비 조합: Trigger 안 아이콘·부가 정보 슬롯은 정의하지 않았다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/accordion.tsx
|
||||
packages/ui/src/theme/accordion.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/accordion.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
> HISTORICAL_RECORD: 아래 날짜별 검증은 당시 구현의 기록이다. 과거 soft surface 측정은 보존하며, 현재 Hover 규격은 §4의 `transparent`를 따른다.
|
||||
|
||||
- 2026-10-06 Visual QA: 1280/1024/768/390px에서 Closed/Open Hover의 전체 soft surface·pointer, Focus 3px ring, Disabled opacity 0.6·not-allowed 및 가로 overflow 없음 확인. `typecheck`, `build`, `build-storybook` 통과.
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): ArrowDown으로 trigger 이동(비활성 항목 건너뜀, 끝에서 처음으로 순환), Home/End, Enter로 열기, Space로 닫기, single(다른 항목을 열면 이전 항목 닫힘, 열린 항목 재클릭은 닫히지 않음), collapsible single(재클릭으로 모두 닫힘), multiple(여러 항목 동시 열림), disabled 항목 클릭 무반응(`disabled`, `data-disabled`).
|
||||
- animation: 열 때 `accordion-down`이 적용되고 높이가 5 → 36px로 변하는 것을 샘플링으로 확인.
|
||||
- computed style: trigger 48px·16/24/500, 전경 `rgb(10,34,29)`, divider `1px rgb(222,219,210)`, icon 20px `rgb(110,103,89)`, content 14/20·아래 padding 16px. 긴 콘텐츠에서 가로 overflow 없음, 긴 Trigger는 여러 줄로 줄바꿈.
|
||||
- 미확인: ArrowUp(ArrowDown과 대칭), 닫힘 animation 시각, 실제 pointer hover/focus ring 시각, 390px 폭.
|
||||
@@ -0,0 +1,108 @@
|
||||
# Alert Dialog
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Alert Dialog 고유의 API·Token·상태·결정만 기록한다. Dialog(`dialog.md`)와 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Alert Dialog (Radix `AlertDialog`) |
|
||||
| 대표 Story | `Components/Overlay/Alert Dialog` → `All` (이름 "Alert Dialog") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 Dialog 문서가 Alert Dialog를 FOLLOW_UP(DIALOG-03)으로만 두고 규격은 없다. 의도(확인·취소를 명시적으로 요구하는 중요한 결정)를 참고했다. 현재 Button 조합은 사용자 확정 규칙에 따라 일반 확인은 취소 Gray Solid / 확인 Green Solid, 삭제 확인은 취소 Gray Outline / 삭제 Red Outline이며 같은 세트의 appearance·size를 통일한다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. Dialog의 접근성 원칙과 `asChild` 처리 규칙은 재사용하고 수치는 현재 HM Semantic으로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix AlertDialog 구조 유지. Dialog + destructive 플래그로 합치지 않고 별도 컴포넌트로 둔다.
|
||||
- **Dialog와의 차이**: Close(X) 없음, 바깥 클릭으로 닫히지 않음, 초기 focus가 Cancel, Body·Header divider 없음, 폭 400px 고정 상한.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Surface / 전경 | `--hm-sem-bg-surface` / `--hm-sem-fg-primary` |
|
||||
| Border / Radius / Shadow | 1px `--hm-sem-stroke-input-default` / 8px / `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| 위치 | viewport 가운데(shadcn 기본) |
|
||||
| 폭 | `min(400px, 100vw − 32px)`, 최대 높이 `100dvh − 32px`(넘치면 content scroll) |
|
||||
| Padding | 24px 사방 |
|
||||
| Title | 18 / 28 / 700 |
|
||||
| Description | 14 / 20 / 400, `--hm-sem-fg-secondary`, Title과 gap 8px |
|
||||
| Footer(Actions) | Header와 24px 간격, gap 8px, 오른쪽 정렬(좁은 화면은 세로, Cancel이 아래) |
|
||||
| Action / Cancel | HM Button(아래 §5) |
|
||||
| Overlay / animation | shadcn 기본(`bg-black/10` + blur, fade + zoom) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Closed / Open | Radix `data-state`, 열림 시 modal, 배경 aria-hidden, scroll lock, `role=alertdialog` |
|
||||
| 초기 focus | **Cancel**(Radix AlertDialog 기본: 파괴적 action 대신 안전한 쪽) |
|
||||
| 닫힘 조건 | Cancel, Action, Escape. **바깥 클릭으로는 닫히지 않음** |
|
||||
| focus 복귀 | 닫히면 trigger로 복귀 |
|
||||
| Confirmation / Destructive | 컴포넌트 상태가 아니라 Button 조합(§5) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix AlertDialog props 그대로. export: `AlertDialog`, `AlertDialogTrigger`, `AlertDialogPortal`, `AlertDialogOverlay`, `AlertDialogContent`, `AlertDialogHeader`, `AlertDialogFooter`, `AlertDialogTitle`, `AlertDialogDescription`, `AlertDialogAction`, `AlertDialogCancel`.
|
||||
|
||||
- `AlertDialogAction`: HM Button을 Radix Action 위에 `asChild`로 합성(`<Button asChild><AlertDialogPrimitive.Action/></Button>`). 기본 `variant="primary"` `appearance="solid"`, `size` 기본 `default`. 위험한 작업은 `variant="destructive" appearance="outline"`.
|
||||
- `AlertDialogCancel`: 같은 방식, 기본 `variant="cancel"` `appearance="solid"`.
|
||||
- `AlertDialogAction variant="destructive"`만 지정하면 기본 `solid`가 적용되어 **미지원 Destructive Solid**가 된다. 삭제 확인은 Action과 Cancel 모두 `appearance="outline"`을 명시하고 같은 size를 사용한다. 자동 appearance 변경은 없다. [Button 허용 조합·기본값 주의](button.md#62-기본값별칭합성-주의)를 따른다.
|
||||
- 합성한 Radix 요소에는 `data-slot`을 붙이지 않는다. 붙이면 `asChild` Slot 병합에서 자식 값이 이겨 Button의 `data-slot="button"`이 사라지고 HM Button 스타일이 적용되지 않는다(Dialog에서 해결한 문제와 같은 원인). 출력 속성은 Button의 `data-slot="button"`, `data-variant`, `data-appearance`, `data-size`.
|
||||
- Alert Dialog 자체에는 Button 스타일을 정의하지 않는다. Button API(`variant`/`appearance`/`size`)만 전달한다.
|
||||
- `AlertDialogContent`에 Close(X)는 없다. 새 prop(`size`, `variant`, `destructive`)은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/alert-dialog.css` (`--hm-alert-dialog-*`).
|
||||
|
||||
`background`, `text`, `border-width`, `border-color`, `radius`, `shadow`, `width`, `edge`, `padding`, `header-gap`, `actions-gap`, `actions-margin-top`, `title-*`, `description-*`. Semantic과 Theme palette만 참조하며 `--hm-dialog-*`를 참조하지 않는다. 신규 Primitive 없음. Button 색은 Button 문서의 token을 따른다.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix AlertDialog 구조 전부: Portal·Overlay·modal·focus trap·초기 focus(Cancel)·Escape·바깥 클릭 무시·focus 복귀·`role=alertdialog`·`aria-labelledby/describedby`, 중앙 위치 class, 열림/닫힘 animation class, `data-slot`(Content·Header·Footer·Title·Description).
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-background`, `ring-1`, `rounded-xl`, `p-4`, `gap-4`, `max-w-xs`/`sm:max-w-sm`, size 변형 제거 → theme CSS.
|
||||
- `AlertDialogAction`/`Cancel`을 `buttonVariants` class 적용 방식에서 **HM Button 합성**으로 변경. 선택지 `variant`/`appearance`/`size`를 Button API로 받는다.
|
||||
- 방향별 Header/Footer 배치와 `AlertDialogMedia` 같은 shadcn 부가 요소는 추가하지 않았다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Alert Dialog"): 트리거 2개(로그아웃 = 일반 확인, 삭제 = destructive)가 각각 실제 Alert Dialog를 연다.
|
||||
- verification: `Confirmation`, `Destructive`(처음부터 열림).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(400px, 18px Title, 24px padding, 그림자)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Overlay 색·blur, Motion은 shadcn 기본 유지.
|
||||
- 파괴적 작업의 기본 강조(Red outline)는 Dialog 삭제 확인과 같은 조합이며 Alert Dialog 고유 규칙이 아니다.
|
||||
- 아이콘/Media slot, non-dismissible 정책 변경(예: Escape 막기)은 만들지 않았다.
|
||||
- 소비 화면의 공통 header/footer 높이 대응 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/alert-dialog.tsx
|
||||
packages/ui/src/theme/alert-dialog.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/alert-dialog.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 키보드(Enter)로 열기, `role=alertdialog`, 초기 focus가 "취소", Tab으로 "삭제" 이동 후 다시 Tab하면 "취소"로 순환(focus trap), 바깥 클릭에 닫히지 않음, Escape 닫힘과 trigger focus 복귀, Cancel·Action 클릭 닫힘과 trigger 복귀, Close(X) 없음.
|
||||
- Button 합성: Action·Cancel 모두 `data-slot="button"`, `data-variant`(destructive/cancel), `data-appearance`가 유지되고 HM 색(Cancel `rgb(118,118,118)`, 삭제 글자 `rgb(211,47,47)`)이 적용됨.
|
||||
- computed style: width 400px, radius 8px, padding 24px, shadow `0 0 12px .2`.
|
||||
- 390×844: 폭 358px(좌우 16px), Footer 세로 쌓임(Cancel 아래), 가로 overflow 없음.
|
||||
- 미확인: Enter/Space로 Action 키보드 실행, Radix `AlertDialogAction` 비동기 처리, 닫기 animation 시각.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Alert
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Alert 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Alert (CVA, grid + 선행 svg) |
|
||||
| 대표 Story | `Components/Feedback/Alert` → `All` (이름 "Alert") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz 기존 테스트 규격에는 Alert 규격이 없다(후보 목록, 상태 색 success/warning/information 미정). Toast와 별개로 참고만 했다.
|
||||
- **현재 HM 규격**: §3~§4.
|
||||
- **구현 방식**: shadcn Alert 구조(`role="alert"`, 선행 svg 아이콘 grid, `AlertTitle`, `AlertDescription`)를 유지하고 `data-variant`로 theme CSS를 선택.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Container | border 1px, radius 4px, padding `12px 16px`, 면 `--hm-sem-bg-surface` |
|
||||
| Icon | 16px (소비자가 lucide 등 svg를 첫 자식으로), 컬럼 gap 12px |
|
||||
| Title | 14 / 20 / 500 |
|
||||
| Description | 14 / 20 / 400, 색 `--hm-sem-fg-secondary` |
|
||||
| Close action | 없음 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| variant | border | Title·icon 색 |
|
||||
|---|---|---|
|
||||
| `default` | `--hm-sem-stroke-default` | `--hm-sem-fg-primary` |
|
||||
| `destructive` | `--hm-sem-action-destructive` | `--hm-sem-action-destructive` |
|
||||
|
||||
아이콘이 없으면 텍스트가 왼쪽에서 시작한다.
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
Alert: variant?: "default" | "destructive" 기본 "default"
|
||||
AlertTitle, AlertDescription
|
||||
```
|
||||
`Alert`는 `role="alert"`로 렌더. 출력 속성: `data-slot="alert|alert-title|alert-description"`, `data-variant`.
|
||||
|
||||
## 6. Token
|
||||
|
||||
`packages/ui/src/theme/alert.css` (`--hm-alert-*`): `radius`, `padding-x/y`, `border-width`, `icon-size`, Title/Description typography, variant별 `{default,destructive}-{background,border-color,title-color,description-color}`. Semantic 참조. Primitive 신규 추가 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
`role="alert"`, grid 레이아웃(`has-[>svg]`), `data-slot`, `cn(..., className)`, `AlertTitle` line-clamp.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
shadcn `rounded-lg border px-4 py-3 text-sm`, destructive의 색 class 제거 → theme CSS. 아이콘을 16px로 고정.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
대표 Story `All`: `default`, `destructive`, `no-icon`, `title-only`, `long`. verification: `Default`, `Destructive`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 이 컴포넌트에 대한 개별 수치 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- success / warning / information variant: HM 상태 색 미정이라 만들지 않았다(neutral·destructive만).
|
||||
- 닫기 버튼, action 슬롯: 없음. Toast와 혼합하지 않는다.
|
||||
- 수치(12/16 padding, 16px 아이콘)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/alert.tsx
|
||||
packages/ui/src/theme/alert.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Alert, AlertTitle, AlertDescription)
|
||||
src/components/alert.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- computed style: `role=alert`, border 1px, radius 4px, padding `12px 16px`, Title 14px/500, destructive border·Title `rgb(211,47,47)`, 아이콘 16px, 아이콘 유무에 따른 Title 시작 위치(45px / 17px) 확인.
|
||||
- 미확인: 사이드바 화면, 390px 폭.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Aspect Ratio
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Aspect Ratio 고유의 API·결정만 기록한다.
|
||||
|
||||
## 1. 상태와 성격
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 성격 | **Layout primitive** (비율 유지). 시각 디자인이 없는 utility |
|
||||
| 구현 상태 | 구현 완료 |
|
||||
| 기준일 | 2026-10-03 |
|
||||
| 기반 | shadcn/ui Aspect Ratio = Radix `AspectRatio` |
|
||||
| 대표 Story | `Components/Data & Display/Aspect Ratio` → `All` (이름 "Aspect Ratio") |
|
||||
| 보류 | 아래 §10 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Aspect Ratio가 없다. 참고할 수치가 없다.
|
||||
- **현재 HM 규격**: 시각 규격이 없다. 비율 박스만 제공하며 surface, background, radius, border, 이미지 처리는 소비자가 자식에 구성한다(HM 디자인을 이유로 이미지 스타일을 내부에 넣지 않는다).
|
||||
- **구현 방식**: Radix AspectRatio를 그대로 감싼다(`data-slot="aspect-ratio"`).
|
||||
- **보류**: §10.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Ratio | `ratio` prop(너비 / 높이, 예: `16 / 9`). 기본 1 |
|
||||
| 폭 | 부모 폭 100%(Radix wrapper가 `padding-bottom`으로 높이를 계산) |
|
||||
| 시각 | 없음(배경, 윤곽, radius 모두 투명/0) |
|
||||
| 자식 | Radix가 `position:absolute; inset:0`으로 채운다. 이미지는 소비자가 `size-full object-cover rounded-md` 등으로 구성 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
상태 없음.
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix AspectRatio props 그대로. export: `AspectRatio`.
|
||||
|
||||
- `ratio`, `asChild`, 그리고 `div` props. `size`, `variant`, `mediaType`은 만들지 않았다.
|
||||
|
||||
## 6. Dependency
|
||||
|
||||
새 dependency 없음(`radix-ui`에 이미 포함).
|
||||
|
||||
## 7. Token / Theme CSS
|
||||
|
||||
**없다.** 시각 규격이 없어 `--hm-aspect-ratio-*` Token과 `aspect-ratio.css`를 만들지 않았고 `theme.css`에도 연결하지 않았다.
|
||||
|
||||
## 8. 소비자 확장성
|
||||
|
||||
- 폭은 부모가 정한다(Story는 wrapper의 `max-width`). `className`/`style`이 그대로 적용된다.
|
||||
- 이미지 fit, radius, border는 자식 요소에서 소비자가 정한다. Card와 조합해도 AspectRatio는 Card에 의존하지 않는다(Story에서 Card 안에 4:3으로 조합).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Aspect Ratio"): 16:9, 4:3, 1:1 이미지(inline data URI, 네트워크 비의존), video-like placeholder, Card 안 조합을 한 페이지에 둔다.
|
||||
- verification: `16:9`, `1:1`.
|
||||
|
||||
## 10. 사용자 확정 결정 / 보류
|
||||
|
||||
- 2026-10-03: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 시각 규격은 정하지 않는다.
|
||||
- 보류: 비율 preset(예: `ratio="video"`), 이미지 로딩 placeholder, 모서리 radius 기본값은 만들지 않았다.
|
||||
- CSS `aspect-ratio` 속성 기반 구현(Radix는 `padding-bottom` hack)은 Radix 구현을 따르며 바꾸지 않았다.
|
||||
|
||||
## 11. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/aspect-ratio.tsx
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/aspect-ratio.stories.tsx
|
||||
```
|
||||
|
||||
## 12. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-03).
|
||||
- 레이아웃(Browser pane, 390×844): 16:9는 320×180(비율 1.778), 4:3은 320×240(1.333), 1:1은 320×320(1.000), video placeholder 320×180, Card 안 4:3은 270×203(1.333). wrapper style은 `position: relative; width: 100%; padding-bottom: 56.25%` 등 Radix 계산이며 이미지가 같은 크기로 채워지고(`object-fit: cover`, 로드됨) 배경·border·radius가 없다. 페이지 가로 overflow 없음.
|
||||
- 부모 폭이 바뀌면 높이가 비율대로 변한다(Story의 max-width 320 → 390px 뷰포트에서 320, Card 안에서는 270).
|
||||
- 미확인: 부모 폭을 연속으로 resize할 때의 reflow, `ratio` 동적 변경.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Avatar
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Avatar 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Avatar (Radix `Avatar`) |
|
||||
| 대표 Story | `Components/Data & Display/Avatar` → `All` (이름 "Avatar") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Avatar가 컴포넌트 이름으로만 있고 규격은 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3. 현재 HM Semantic(accent 면)으로 새로 정했다. size 체계는 Button/Input size를 복사하지 않고 shadcn Avatar API(`sm`/`default`/`lg`)에 맞춰 24/32/40px로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Avatar 구조(Root, Image, Fallback)를 유지하고 theme CSS로 연결.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Size | `sm` 24px, `default` 32px, `lg` 40px (정사각형) |
|
||||
| Shape | 원형(radius 9999px), `overflow: hidden` |
|
||||
| Image | `size-full`, `object-cover`, `aspect-square` (중앙 crop) |
|
||||
| Fallback 면 | `--accent` (green tint), 글자 `--accent-foreground` |
|
||||
| Fallback typography | weight 500, `sm` 10px / `default` 12px / `lg` 14px, line-height 1, 한 줄 `nowrap`, 넘치면 잘림 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| 이미지 로드 성공 | Radix가 Image를 렌더하고 Fallback은 렌더하지 않는다 |
|
||||
| 이미지 로드 실패 / 로딩 중 | Fallback 표시(`delayMs` prop으로 지연 가능) |
|
||||
| Focus / Disabled | 없음(비 interactive) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Avatar props 그대로. export: `Avatar`, `AvatarImage`, `AvatarFallback`.
|
||||
|
||||
- `Avatar`: `size` `"sm" | "default" | "lg"`(기본 `default`, shadcn API 유지). 출력 `data-size`.
|
||||
- `AvatarImage`: `src`, `alt`, `onLoadingStatusChange`. `AvatarFallback`: `delayMs`.
|
||||
- 접근성: 이름이 보이는 곳에서는 `alt=""`, 단독으로 쓰면 `alt`로 이름을 준다.
|
||||
- 새 prop은 만들지 않았다. 크기는 `size` prop(또는 `--hm-avatar-size-*` Token)으로만 바꾼다. theme CSS가 레이어 밖이라 `className="size-12"` 같은 utility는 이기지 못한다.
|
||||
- shadcn 최신 버전에 있는 `AvatarBadge`/`AvatarGroup`은 만들지 않았다(필요가 생기면 추가).
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/avatar.css` (`--hm-avatar-*`).
|
||||
|
||||
`size-sm|md|lg`, `radius`, `fallback-background`, `fallback-text`, `fallback-font-weight`, `fallback-font-size-sm|md|lg`. Semantic(`--accent`, `--accent-foreground`)만 참조한다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 이미지 로드 상태 처리와 Fallback 표시 규칙, 구조, `size` prop, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `size-8`/`data-[size=lg]:size-10`, `bg-muted` fallback, 글자 크기 class를 제거하고 Token으로 연결. 크기는 24/32/40.
|
||||
- Fallback 면을 `muted`가 아니라 accent(green tint)로 했다(HM 팔레트에서 대비가 있는 면).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Avatar"): Image(sm/default/lg, data URI 이미지로 네트워크 비의존), Fallback(깨진 이미지 주소로 initials: 한글 두 글자, 영문 두 글자, 한 글자, 긴 fallback), Composition(이름 + 소속)을 한 페이지에 둔다.
|
||||
- verification: `Image`, `Fallback`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- Size(24/32/40)와 fallback 색은 현재 HM 규격이며 Dashboard/GNB의 실제 사용이 정해지면 재검토.
|
||||
- 긴 fallback(예: 10자)은 가운데에서 잘린다. 1~2자 initials가 전제이며 줄임 규칙은 만들지 않았다.
|
||||
- Icon fallback(사람 아이콘) 규칙, 상태 badge, 겹침 그룹은 정의하지 않았다.
|
||||
- 이미지 비율이 다른 원본은 중앙 crop(`object-cover`)만 한다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/avatar.tsx
|
||||
packages/ui/src/theme/avatar.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/avatar.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 렌더(Browser pane): 로드되는 이미지 Avatar는 `img`만 있고 fallback 요소가 없음(24/32/40px, radius 9999px). 깨진 이미지 주소의 Avatar는 `img` 없이 fallback이 렌더되고 `김한`/`HM`/`한`/`ABCDEFGHIJ`가 32×32 안에서 레이아웃 변동 없이 표시, 배경 `rgb(189,223,212)`·글자 `rgb(18,51,40)`·12px.
|
||||
- Responsive(390×844): Avatar 구성이 한 줄로 들어가고 가로 overflow 없음.
|
||||
- 미확인: 이미지가 느리게 로드될 때 fallback에서 image로 전환되는 순간의 깜빡임, `delayMs`, 대비 비율의 수치 측정.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Badge
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Badge 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Badge (CVA, `asChild` Slot) |
|
||||
| 대표 Story | `Components/Data & Display/Badge` → `All` (이름 "Badge") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz 기존 테스트 규격에는 Badge 문서가 없다(Border 1px 등 일부 언급만). 참고만 했다.
|
||||
- **현재 HM 규격**: §3~§4.
|
||||
- **구현 방식**: shadcn Badge 구조(CVA `variant`, `data-slot`, `asChild`, `cn(..., className)`)를 유지하고 `data-variant`로 theme CSS를 선택. Button variant를 재사용하지 않는다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Typography | 12 / 18 / 500 |
|
||||
| Padding | `2px 8px` (높이 24px) |
|
||||
| Border / Radius | 1px / `--hm-radius-control-md` (4px) |
|
||||
| Gap | shadcn `gap-1`, 아이콘 12px (`size-3`) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| variant | 면 | 글자 | 테두리 |
|
||||
|---|---|---|---|
|
||||
| `default` | `--hm-sem-action-primary` (Primary Green `#123328`) | `--hm-sem-on-action-primary` (White) | 면과 동일 |
|
||||
| `secondary` | `--hm-sem-bg-subtle` | `--hm-sem-fg-secondary` | 면과 동일 |
|
||||
| `destructive` | `--hm-sem-action-destructive` | White | 면과 동일 |
|
||||
| `outline` | `--hm-sem-bg-surface` | `--hm-sem-fg-primary` | `--hm-sem-stroke-default` |
|
||||
|
||||
상호작용 상태 없음 (`asChild`로 링크/버튼에 감쌀 때 그 요소가 담당).
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
Badge:
|
||||
variant?: "default" | "secondary" | "destructive" | "outline" 기본 "default"
|
||||
asChild?: boolean
|
||||
```
|
||||
나머지는 `<span>` props. 출력 속성: `data-slot="badge"`, `data-variant`. `badgeVariants`도 export.
|
||||
|
||||
## 6. Token
|
||||
|
||||
`packages/ui/src/theme/badge.css` (`--hm-badge-*`): `radius`, `padding-x/y`, `border-width`, `font-size/line-height/font-weight`, variant별 `{default,secondary,destructive,outline}-{background,text,border-color}`. Semantic 참조. Primitive 신규 추가 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
CVA 구조, `asChild`(Radix Slot), `data-slot`, `cn(..., className)`, 아이콘 크기 규칙(`[&>svg]:size-3`).
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
shadcn `rounded-full`·`px-2 py-0.5 text-xs`·focus/invalid ring·hover class 제거 → theme CSS(radius 4). variant 이름은 shadcn 4종을 유지하고 색만 HM으로 매핑.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
대표 Story `All`: `variants`, `icon`, `long`, `link`(asChild). verification: `Variants`, `Playground`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 이 컴포넌트에 대한 개별 수치 결정은 없다.
|
||||
|
||||
2026-10-06 (Badge 원복): 직전 Active Semantic 후속에서 `default`를 Light 2(`--hm-sem-state-active`)로 바꾼 변경을 취소했다. `default`는 Strong Brand / Primary인 Primary Green(`--hm-sem-action-primary` / `--hm-sem-on-action-primary`)을 쓰며 Active Semantic 적용 대상이 아니다. Component Token 이름은 유지하고 참조만 되돌렸다. `secondary`·`outline`·`destructive`는 그대로다. Badge는 비인터랙티브 label이라 hover 면을 두지 않는다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- success / warning / information 같은 상태 variant: HM 상태 색이 정해지지 않아 만들지 않았다.
|
||||
- size 변형, 점(dot) badge: 미결정.
|
||||
- 수치(12/18/500, 2/8, radius 4)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/badge.tsx
|
||||
packages/ui/src/theme/badge.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Badge, badgeVariants)
|
||||
src/components/badge.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- computed style: 4 variant 모두 12px/18px/500, padding `2px 8px`, radius 4px, 높이 24px, 색 위 표와 일치, 아이콘 12px.
|
||||
- 미확인: `asChild` 링크 focus 표시, 사이드바 화면, 390px 폭.
|
||||
- 2026-10-06 Badge 원복: `default` 면·테두리 `rgb(18,51,40)`(`#123328`), 글자 `rgb(255,255,255)`. secondary·destructive·outline 값 변화 없음.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Breadcrumb
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Breadcrumb 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Breadcrumb (Radix 없음, `nav > ol > li` + `Slot`) |
|
||||
| 대표 Story | `Components/Navigation/Breadcrumb` → `All` (이름 "Breadcrumb") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Breadcrumb이 컴포넌트 이름으로만 있고 규격은 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계(fg-primary/secondary)로 새로 정했다.
|
||||
- **구현 방식**: shadcn Breadcrumb 구조 유지, theme CSS로 HM 스타일 연결. 페이지 안에 직접 놓이는 navigation primitive라 Portal·overlay가 없다.
|
||||
- **역할 범위**: 현재 위치와 상위 이동만 담당한다. 메뉴 시스템·자동 접기(`maxItems`)는 없다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Typography | 14 / 20 / 400 |
|
||||
| Item 간격 | 8px(Item 안 gap, 목록 gap) |
|
||||
| Link | `--hm-sem-fg-secondary`, hover 시 `--hm-sem-fg-primary` + 밑줄(offset 4px) |
|
||||
| Current Page | `--hm-sem-fg-primary`, weight 700, 기본 커서 |
|
||||
| Separator | shadcn `ChevronRightIcon` 16px, `--hm-sem-fg-secondary` |
|
||||
| Ellipsis | 20px 영역, 아이콘 16px, `--hm-sem-fg-secondary` |
|
||||
| 긴 경로 | `flex-wrap`으로 줄바꿈 (`break-words`) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Hover (Link) | 전경 primary + 밑줄 |
|
||||
| Focus (`:focus-visible`) | ring 모양 `0 0 0 3px ring 50%`, radius 2px |
|
||||
| Current | `aria-current="page"`, `aria-disabled="true"`, `role="link"` (shadcn), 링크가 아닌 `span` |
|
||||
| Disabled | 없음(없는 상태를 만들지 않음) |
|
||||
|
||||
## 5. API
|
||||
|
||||
export: `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage`, `BreadcrumbSeparator`, `BreadcrumbEllipsis`.
|
||||
|
||||
- `BreadcrumbLink`: 기본 `a`, `asChild`로 router Link 등과 조합(특정 router에 의존하지 않음).
|
||||
- `BreadcrumbPage`: 현재 위치 표시용 `span`. 클릭할 수 없다.
|
||||
- `BreadcrumbSeparator`: 기본 chevron, `children`으로 교체(예: `/`). `aria-hidden`, `role="presentation"`.
|
||||
- `BreadcrumbEllipsis`: 표시용(`aria-hidden`, sr-only "More"). Dropdown과 자동 결합하지 않는다. 메뉴 연결은 소비 조합(Story 예시).
|
||||
- `variant`, `size`, `maxItems`는 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/breadcrumb.css` (`--hm-breadcrumb-*`).
|
||||
|
||||
`font-size`, `line-height`, `font-weight`, `gap`, `link-color`, `link-hover-color`, `page-color`, `page-font-weight`, `separator-color`, `separator-size`, `ellipsis-size`, `focus-radius`. Semantic만 참조하고 다른 컴포넌트 Token을 쓰지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn에서 유지한 항목
|
||||
|
||||
`nav aria-label="breadcrumb"`, `ol`/`li` 구조, `BreadcrumbPage`의 ARIA, separator의 `aria-hidden`, `asChild`(Slot), chevron·`MoreHorizontal` 아이콘, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `text-sm text-muted-foreground gap-1.5 sm:gap-2.5`, Link `hover:text-foreground`, Page `font-normal text-foreground`, 아이콘 `size-3.5`/`size-9` class 제거 → theme CSS.
|
||||
- Current Page를 weight 700으로 강조(shadcn은 normal).
|
||||
- Link에 밑줄 hover 및 focus ring 추가(shadcn은 색 변화만).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Breadcrumb"): Basic(3 depth), Ellipsis(Dropdown Menu 조합), Long path, Custom link(asChild)와 custom separator를 한 페이지에 둔다.
|
||||
- verification: `Basic`, `Ellipsis`, `Long Path`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(14/20, gap 8px, Current weight 700, chevron 16px)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 긴 경로의 자동 접기(`maxItems`)와 모바일 축약 표현은 만들지 않았다. 지금은 줄바꿈만 한다.
|
||||
- Separator 색은 Link와 같은 secondary(별도 색 결정 없음).
|
||||
- Ellipsis는 표시용이다. 접힌 항목을 보여주는 메뉴 규칙은 소비 조합으로 남긴다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/breadcrumb.tsx
|
||||
packages/ui/src/theme/breadcrumb.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/breadcrumb.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): Link는 `a`, Current Page는 `span`(`aria-current="page"`, `aria-disabled="true"`, 커서 default), Tab으로 Link 사이 이동하고 Current Page는 포커스 대상이 아님, Separator·Ellipsis `aria-hidden`.
|
||||
- computed style: Link `rgb(110,103,89)` 14/20, Current `rgb(10,34,29)` 700.
|
||||
- Responsive: 390×844에서 목록 폭 358px, 긴 경로(7 depth)는 4줄(76px)로 줄바꿈되고 가로 overflow 없음.
|
||||
- 미확인: 실제 pointer hover/focus ring 시각, Ellipsis 안 Dropdown Menu 키보드 흐름.
|
||||
@@ -0,0 +1,191 @@
|
||||
# Button
|
||||
|
||||
현재 HM 구현과 Runtime-neutral 사용 기준 문서. 공통 원칙은 `AGENTS.md`와 `docs/guidelines/runtime-portability.md`를 따른다. 이 문서는 Button API·Token·상태·사용 위치·조합 계약을 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (`hm-*` class 없음) |
|
||||
| 기준일 | 2026-10-08 (Role × Appearance 계약·현재 Story 대조) |
|
||||
| 기반 | shadcn/ui Button (CVA, Radix `Slot`) |
|
||||
| 대표 Story | `Components/Form/Button` → `All` (이름 "Button") |
|
||||
| 보류 | Brand label Text Border 두께(EXPERIMENTAL, 미적용), Focus(ui_viz TBD) |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **ui_viz Confirmed**: size, padding-x, label size/line-height(button.md §26, D-087), Brand gradient·bevel(D-087), Standard Solid/Outline 색(§28, D-086, D-087), Hover/Pressed 동일 Black 20%(§12), Red Outline Hover/Pressed Red 8%.
|
||||
- **HM 사용자 확정 규칙**: 실제 화면 Brand 최대 1개·팝업 Brand 금지, 일반 확인/작성 Solid 세트, 삭제 확인·행/반복 작업 Outline 세트, 같은 세트의 appearance와 size 통일(§5). Disabled `0.6`은 ui_viz 규격 값이며, Input·Select의 2026-10-02 결정과는 별개다.
|
||||
- **shadcn 유지**: 구조(CVA, `data-slot`, `cn(..., className)`), focus/invalid ring class, `ghost`·`link` variant와 기존 size 이름.
|
||||
- **미정의/TBD**: Focus 표현(ui_viz TBD → shadcn 기본 focus ring 유지), `aria-invalid` 표현(ui_viz에 없음 → shadcn class 유지), `ghost`·`link`의 규격(ui_viz 없음).
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| Size | Height(min) | Padding-x | Font / Line-height |
|
||||
|---|---|---|---|
|
||||
| `sm` | 28 | 10 | 12 / 18 |
|
||||
| `default` | 34 | 14 | 13 / 20 |
|
||||
| `lg` | 42 | 18 | 15 / 22 |
|
||||
|
||||
- 높이는 `min-height`다. 기본 라벨은 `nowrap`으로 한 줄을 유지한다. 여러 줄이 필요한 경우 기존 `className="whitespace-normal"`을 명시하며, 이때만 줄바꿈에 따라 높이가 늘어난다(가운데 정렬, `overflow-wrap: anywhere`).
|
||||
- 세로 padding은 `(height − line-height) / 2 − border`로 계산한다.
|
||||
- Radius `--hm-radius-button`, font-family `--hm-font-family-base`.
|
||||
- Weight: Brand 700, Standard 500.
|
||||
- 아이콘 전용 size(`icon*`)와 `xs`는 shadcn 기본이며 HM 규격이 없다(`data-appearance`가 있는 버튼에 한해 `default|sm|lg`만 HM 크기 적용).
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Default | treatment별 face/border (아래 Token) | ui_viz Confirmed |
|
||||
| Hover / Pressed (Brand, Solid) | face 위에 Black 20% overlay (`inset` shadow) | ui_viz §12 Confirmed |
|
||||
| Hover (Outline) | role ink로 채움 + 흰 글자 | ui_viz Confirmed |
|
||||
| Pressed (Outline) | 흰 face + Black 20% overlay | ui_viz Confirmed |
|
||||
| Red Outline Hover/Pressed | Red 8% face, Black overlay 없음 | ui_viz D-087 Confirmed |
|
||||
| Disabled | `opacity: 0.6`, `cursor: not-allowed`, Default paint 유지 | ui_viz Confirmed (HM roles) |
|
||||
| Focus | shadcn 기본 `focus-visible` ring | ui_viz TBD → shadcn 유지 |
|
||||
| Invalid | shadcn `aria-invalid` ring | ui_viz 미정의 → shadcn 유지 |
|
||||
|
||||
- Brand/Solid의 Hover·Pressed는 `:not(:disabled)`에만 적용된다.
|
||||
- transition과 눌림 이동(`translate-y-px`)은 HM role에서 제거한다.
|
||||
- **`ghost`·`link`의 Disabled는 `opacity-50`(shadcn 기본)** 이다. HM role(`0.6`)과 값이 다르다. 규격이 없는 호환 variant라 그대로 둔다(아래 §11).
|
||||
|
||||
## 5. 사용 위치와 버튼 세트
|
||||
|
||||
- `brand`: 페이지 대표 실행에 사용한다. 실제 서비스 화면당 최대 1개이며 팝업에서는 사용하지 않는다. Storybook 비교 갤러리는 예외다.
|
||||
- `solid`: 일반 실행과 등록·저장·확인에 사용한다.
|
||||
- `outline`: 반복·보조 작업과 표/목록 행 작업에 사용한다.
|
||||
- 일반 작성·수정창: 취소 Solid + 등록/저장 Solid.
|
||||
- 일반 확인창: 취소 Solid + 확인 Solid.
|
||||
- 삭제 확인창: 취소 Outline + 삭제 Outline.
|
||||
- 표/목록 행과 반복·보조 작업: 동일 `appearance`의 Outline 조합.
|
||||
- FilterBar 대표 Story의 초기화·조회: `cancel` + `primary`, 둘 다 Outline·`size="default"`. 목록의 반복 조회·보조 작업 세트다.
|
||||
- 하나의 세트는 `appearance`와 `size`를 통일한다. 역할 차이는 variant의 기존 역할색과 문구로 표현한다.
|
||||
|
||||
사용 위치를 기준으로 appearance를 명시하며 버튼 문구만으로 판단하지 않는다. 컴포넌트는 appearance 생략 시 기본 `brand`를 출력하므로, 팝업·버튼 세트에서는 기본값에 의존하지 않는다.
|
||||
|
||||
## 6. API
|
||||
|
||||
```
|
||||
variant: shadcn "default | outline | secondary | ghost | destructive | link"
|
||||
→
|
||||
HM "primary | secondary | modify | destructive | cancel"
|
||||
+ 별칭 "default"(= primary), "outline"(= primary + outline appearance)
|
||||
+ 호환 "ghost", "link"
|
||||
appearance: (신규) "brand | solid | outline" 기본 "brand"
|
||||
size: "default | xs | sm | lg | icon | icon-xs | icon-sm | icon-lg" (shadcn 그대로)
|
||||
asChild: shadcn 그대로
|
||||
```
|
||||
|
||||
- 기본값: `variant="primary"`, `appearance="brand"`, `size="default"`.
|
||||
- 출력 속성: `data-slot="button"`, `data-variant`(별칭은 role로 정규화), `data-appearance`(`ghost`/`link`은 없음), `data-size`.
|
||||
- `secondary`의 의미가 바뀌었다: shadcn 회색 보조 → ui_viz Brown 역할. 기존 사용처 영향은 별도 확인하지 않았다.
|
||||
|
||||
### 6.1 허용 Role × Appearance (AI Reference)
|
||||
|
||||
현재 `button.css`의 역할별 Mapping과 대표 `All` Story의 `rolesFor`가 지원하는 조합은 다음과 같다.
|
||||
|
||||
- `primary` (Green): `brand`, `solid`, `outline`.
|
||||
- `secondary` (Brown): `brand`, `solid`, `outline`.
|
||||
- `modify` (Orange): `brand`, `solid`. **`outline` 미지원**.
|
||||
- `cancel` (Gray): `solid`, `outline`. **`brand` 미지원**.
|
||||
- `destructive` (Red): `outline`만 지원. **`brand`, `solid` 미지원**.
|
||||
|
||||
공개 Props는 `variant`와 `appearance`를 독립적으로 받는다. TypeScript 통과나 Storybook Controls에서 선택 가능하다는 사실은 해당 조합의 디자인 지원을 뜻하지 않는다. 미지원 조합에는 필요한 역할별 CSS Custom Property가 없으며 자동 보정·fallback도 없다. 기존 CSS 변수에 임의 fallback이나 새로운 역할색을 추가하지 않는다.
|
||||
|
||||
### 6.2 기본값·별칭·합성 주의
|
||||
|
||||
- `<Button>`은 Primary Brand다. `variant="destructive"`만 지정하면 미지원 Destructive Brand, `variant="cancel"`만 지정하면 미지원 Cancel Brand가 된다. 각각 허용 appearance를 명시한다.
|
||||
- `variant="default"`는 Primary 별칭이며 appearance 생략 시 Brand다. `variant="outline"`는 Primary Outline 별칭이며 전달한 appearance보다 우선한다. Red Outline은 `variant="destructive" appearance="outline"`로 작성한다.
|
||||
- `ghost`/`link`는 HM Role × Appearance 계약 밖의 shadcn 호환 variant다. `data-appearance`를 출력하지 않으며 appearance를 전달해도 HM treatment를 적용하지 않는다.
|
||||
- `AlertDialogAction`과 `AlertDialogCancel`의 기본 appearance는 Button과 달리 `solid`다. `<AlertDialogAction variant="destructive">`만 지정하면 미지원 Destructive Solid다. 삭제 확인은 Action과 Cancel **모두** `appearance="outline"`을 명시한다. 자세한 합성 API는 [Alert Dialog](alert-dialog.md#5-api)를 따른다.
|
||||
|
||||
```tsx
|
||||
// 삭제·취소: 같은 appearance와 size (DialogClose asChild로도 합성 가능)
|
||||
<Button variant="cancel" appearance="outline" size="default">취소</Button>
|
||||
<Button variant="destructive" appearance="outline" size="default">삭제</Button>
|
||||
|
||||
// Alert Dialog 삭제 확인: 두 요소 모두 Outline을 명시
|
||||
<AlertDialogCancel appearance="outline" size="default">취소</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" appearance="outline" size="default">삭제</AlertDialogAction>
|
||||
```
|
||||
|
||||
미지원 조합 추가, role별 자동 appearance 선택, 기본 appearance 변경은 사용자 결정이 필요한 별도 변경이다. 현재 계약과 공개 API를 유지한다.
|
||||
|
||||
## 7. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/button.css` (`--hm-button-*`). 공통 값은 `packages/ui/src/theme/default.css`, Primitive는 `packages/ui/src/theme/primitives.css`.
|
||||
|
||||
- 공통 Semantic 재사용: `--hm-radius-button`, `--hm-font-family-base`, `--hm-font-weight-*`, `--hm-color-text-inverse`, `--hm-color-surface-default`, `--hm-color-action-*`, `--hm-sem-surface/border-action-primary`.
|
||||
- `--hm-button-height-md`는 `--hm-control-height-md`(34)를 참조한다. sm/lg 높이는 Button 전용 값이다.
|
||||
- Brand/Solid의 색 Token은 role별(`green|brown|orange|gray`)로 Component 안에 둔다. Brown/Orange gradient의 일부 stop과 Gray 값은 `default.css`의 공개 Theme palette를 참조한다. `button.css`에 직접 HEX는 남아 있지 않다.
|
||||
- Input·Select와 높이 값(28/34/42)이 겹쳐도 alias하지 않는다. 각 ui_viz 규격이 따로 확정한 값이기 때문이다.
|
||||
|
||||
## 8. Runtime-neutral DOM 계약
|
||||
|
||||
React가 아닌 Runtime도 배포된 `@hm-design/ui/theme.css` 또는 승인된 standalone Theme CSS를 먼저 로드하고, semantic `<button>`에 다음 속성을 사용한다.
|
||||
|
||||
- `data-slot="button"`, `data-variant="primary|secondary|modify|destructive|cancel"`, `data-appearance="brand|solid|outline"`, `data-size="default|sm|lg"`.
|
||||
- Role × Appearance는 §6.1의 허용 조합만 사용한다. 위 속성 vocabulary가 모든 곱집합을 허용하는 것은 아니다.
|
||||
- CSS는 `--hm-button-*` Component variable과 `--hm-sem-*`/기존 Theme variable을 소비한다. HEX와 임의 variable을 추가하지 않는다.
|
||||
- native `disabled`, `:hover`, `:active`, `:focus-visible`로 Disabled/Hover/Pressed/Focus 계약을 유지한다. `aria-disabled`만으로 native disabled를 대체하지 않는다.
|
||||
- 팝업의 취소/확인 조합은 같은 `size`를 사용하고, 삭제 확인은 취소·삭제 모두 Outline이어야 한다. Escape, Tab 순서, 초기 포커스와 닫힌 뒤 trigger 포커스 복귀는 modal primitive 또는 동등한 keyboard contract로 보존한다.
|
||||
|
||||
React의 `variant` prop을 그대로 복제할 필요는 없지만, 동일한 role/treatment attribute와 시각·상태·접근성 계약을 유지해야 한다.
|
||||
|
||||
## 9. shadcn / Radix에서 유지한 항목
|
||||
|
||||
CVA 구조, `data-slot`, `asChild`(Radix `Slot`), `className` 확장, focus-visible/invalid ring class, `[&_svg]` 아이콘 규칙, 기존 size 이름, `ghost`·`link` variant.
|
||||
|
||||
## 10. HM에서 변경한 항목
|
||||
|
||||
- `variant`를 ui_viz 역할로 재정의하고 `appearance` 추가.
|
||||
- HM role의 transition·눌림 이동 제거, `disabled:cursor-not-allowed`.
|
||||
- 높이를 `min-height` + 계산 padding으로 구성. 일반 라벨은 한 줄 유지하며, Long Label specimen은 명시적 `whitespace-normal`로 줄바꿈 지원.
|
||||
- shadow에 shadcn ring shadow(`--tw-ring-*`)를 끝에 포함해 bevel과 ring이 함께 보이게 함(지침 §6.7).
|
||||
- `hm-*` class 제거.
|
||||
|
||||
## 11. Storybook
|
||||
|
||||
- 대표 Story `All`("Button")이 기본 sidebar의 유일한 Button 항목이다 → 1뎁스.
|
||||
- `All`의 구역: Overview → Sizes → Variants → States. 기본 버튼·크기·허용 조합·Brand/Standard 상태를 보여준다. Red Outline은 Variants와 Standard States에도 포함되며, 삭제·취소 세트와 Long Label은 verification Story에서 따로 확인한다.
|
||||
- verification(`tags: ["verification"]`, 기본 sidebar 제외): `Roles`, `Sizes`, `States`("Brand States"), `StandardStates`, `LongLabel`, `KoreanLabels`, `IconTextNarrow`, `Destructive`, `Playground`(HM 전용), `Compatibility`(ghost/link, default/outline 별칭). Controls는 조합별 선택을 제한하지 않으므로 §6.1의 허용 조합을 직접 지킨다. Verification은 기술 점검용이며 운영 화면의 사용 위치·세트 규칙은 §5가 우선한다.
|
||||
- 대표 경로/ID: `Components/Form/Button → All` (`components-form-button--all`), `Components/Overlay/Dialog → All` (`components-overlay-dialog--all`), `Components/Overlay/Alert Dialog → All` (`components-overlay-alert-dialog--all`), `Patterns/Data/Filter Bar → Filter Bar` (`patterns-data-filter-bar--all`). 삭제 세트 점검 ID는 `components-form-button--destructive`(verification)다.
|
||||
- 운영 규칙은 `프로젝트-지침.md` §6.3, §7 참조.
|
||||
|
||||
## 12. 사용자 확정 결정
|
||||
|
||||
사용자가 확정한 사용 위치·세트 규칙은 §5에 기록한다. 2026-10-08 정합성 점검에서는 기존 허용 조합과 기본값을 유지하고 미지원 조합을 새로 지원하지 않는다.
|
||||
|
||||
## 13. 미확정 / 보류
|
||||
|
||||
- **EXPERIMENTAL**: Brand label Text Border 두께 — 적용하지 않음.
|
||||
- **TBD(ui_viz)**: Focus 표현.
|
||||
- **ui_viz 미정의**: `aria-invalid`, `ghost`/`link`, `xs`·`icon*` size, `ghost`/`link` Disabled(현재 0.5, shadcn 기본).
|
||||
- Disabled `0.6`은 Input·Select의 사용자 결정과 별개이며, 전역 Disabled Token이 아니다.
|
||||
|
||||
## 14. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/button.tsx
|
||||
packages/ui/src/theme/button.css
|
||||
src/components/button.stories.tsx
|
||||
src/components/story-board.tsx
|
||||
```
|
||||
|
||||
## 15. 검증 상태
|
||||
|
||||
### 2026-10-08 Button 조합 정합성 점검
|
||||
|
||||
- 기존 6024 Storybook 재사용. 대표 Button의 허용 조합·고정 Hover/Pressed·Disabled를 computed style로 확인: 높이 28/34/42px, Disabled opacity `0.6`, Solid Hover/Pressed Black 20%, Red Outline Hover/Pressed Red 8%와 Red 전경 유지. 삭제 버튼의 실제 포인터 Hover도 같은 면을 확인했다.
|
||||
- Playground에서 Destructive Brand/Solid와 Modify Outline을 직접 렌더했다. Destructive Brand/Solid는 역할 배경 변수가 비어 투명 배경·흰 글자, Modify Outline은 역할 ink 변수가 비어 기본 전경색으로 표시된다. 디자인 계약상 미지원이며 이 점검에서 지원을 추가하지 않았다.
|
||||
- 대표 Dialog / Alert Dialog: 일반 확인 Solid 세트·삭제 확인 Outline 세트, 동일 `size="default"`·높이 34px 확인. AlertDialog Action/Cancel의 `data-slot="button"`과 역할·appearance 합성이 유지된다. Dialog의 Escape, Alert Dialog의 취소·Escape 닫기 후 trigger 포커스 복귀, Alert Dialog 초기 Cancel focus·Tab 순환 확인.
|
||||
- 대표 Filter Bar: 초기화 Cancel Outline / 조회 Primary Outline·동일 크기 확인. 검색 조건 입력 후 초기화로 빈 검색어·조건 0개 복원. 조회는 기존 callback 없는 조합 예시다.
|
||||
- 관련 브라우저 Console warning/error 없음. `npm run check:theme-defaults` 통과. Component·CSS·Theme 값·공개 API 변경 없음. 전체 회귀·build·브라우저별 비교는 요청 범위에 따라 생략했다.
|
||||
|
||||
### 과거 검증 기록
|
||||
|
||||
- TypeScript, `npm run build`: 통과(2026-10-02, 복구 후).
|
||||
- 2026-10-06 ui_viz Storybook 화면 비교: 대표·verification Story의 버튼 51개(+대표 49개) computed style 일치(Red Outline 8% 면은 흰 바탕 합성값으로 같은 RGB, `cursor`는 ui_viz도 Web Mapping으로 분류해 shadcn 기본 유지). 대표 Story를 ui_viz 4구역으로 복원(Story만 변경).
|
||||
- 과거 Storybook 기록: Long Label·Destructive 및 verification Story ID 렌더 확인. 현재 `All` 구역 구성은 §11을 따른다.
|
||||
- 당시 미확인: 실제 사이드바 화면, Disabled computed opacity(Button), 브라우저별 렌더, `secondary` 의미 변경의 기존 사용처 영향. Disabled computed opacity는 위 2026-10-08 점검에서 확인했다.
|
||||
@@ -0,0 +1,120 @@
|
||||
# Calendar
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Calendar 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태와 성격
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 성격 | **Primitive component** (날짜 선택 UI). Date Picker 자체가 아니다 |
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Calendar = **react-day-picker v10** |
|
||||
| 대표 Story | `Components/Search & Date/Calendar` → `All` (이름 "Calendar") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Calendar가 없다(인벤토리에 Date Picker 이름만). 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다. Button/Input/Popover Token은 참조하지 않는다(Navigation 버튼이 Button처럼 보여도 Calendar Token으로 관리).
|
||||
- **구현 방식**: react-day-picker가 날짜 계산, 월 이동, 키보드, 선택 모드, ARIA grid를 소유한다. HM은 시각만 입힌다. 라이브러리의 기본 class(`rdp-*`)와 날짜 셀의 `data-*` 상태 속성을 그대로 두고 `[data-slot="calendar"]` 아래에서 CSS로 스타일한다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Surface | 없음(투명). 감싸는 컨테이너(Popover, Card 등)가 면과 윤곽을 가진다 |
|
||||
| Padding / 폭 | 12px / 내용 폭(`fit-content`, 한 달 276px) |
|
||||
| Caption | 가운데, 높이 32px, 14 / 20 / 700 |
|
||||
| Navigation 버튼 | 28 × 28px, radius 4px, 아이콘 16px(lucide chevron), Caption 줄 양끝에 절대 배치 |
|
||||
| Weekday | 12 / 18 / 500, `--hm-sem-fg-secondary`, 폭 36px |
|
||||
| Day | 36 × 36px 셀과 버튼, radius 4px, 14 / 20 |
|
||||
| 여러 달 | `numberOfMonths`에 따라 flex로 나란히, 좁으면 줄바꿈(gap 16px) |
|
||||
| Locale | 기본 **한국어**(`react-day-picker/locale`의 `ko`, 추가 dependency 없음). `locale` prop으로 교체 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Default | 투명 |
|
||||
| Hover | 배경 `--accent` 45% |
|
||||
| Focus (`:focus-visible`) | ring 모양 `0 0 0 3px ring 50%` |
|
||||
| Today (`data-today`) | 배경 `--hm-sem-bg-subtle`, weight 700 |
|
||||
| Selected (`data-selected`) | 배경 `--hm-sem-state-active`(Light 2 `#266651`), 글자 `--hm-sem-on-state-active`(흰색) (hover해도 유지, focus 시 fill + Focus ring) |
|
||||
| Outside (`data-outside`) | 글자 `--hm-sem-fg-secondary`, opacity 0.6 |
|
||||
| Disabled (`data-disabled`) | opacity 0.4(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), cursor not-allowed, 선택 불가 |
|
||||
| Range start / middle / end (`rdp-range_*`) | start·end는 Active 면(`--hm-calendar-range-edge-*` → `--hm-sem-state-active`, hover해도 유지, 안쪽 모서리 직각), middle은 `--accent` 45% 면 + 직각 |
|
||||
| Nav 버튼 disabled | opacity 0.4 (`startMonth`/`endMonth` 경계) |
|
||||
|
||||
기반 라이브러리가 실제로 제공하는 상태만 쓴다. HM 전용 상태 속성은 없다.
|
||||
|
||||
## 5. API
|
||||
|
||||
react-day-picker `DayPicker` props 그대로. export: `Calendar`.
|
||||
|
||||
- `mode` `"single" | "multiple" | "range"`, `selected`, `onSelect`, `required`, `defaultMonth`, `month`/`onMonthChange`, `disabled`(Matcher), `startMonth`/`endMonth`, `numberOfMonths`, `captionLayout`, `weekStartsOn`, `today`, `locale` 등.
|
||||
- Calendar가 바꾸는 기본값: `showOutsideDays=true`, `locale=ko`, Chevron 아이콘을 lucide로 교체(`components.Chevron`).
|
||||
- 출력 `data-slot="calendar"`. 날짜 셀은 `td.rdp-day`(`data-selected`, `data-today`, `data-outside`, `data-disabled`, `data-focused`, `aria-selected`).
|
||||
- `variant`, `size`는 만들지 않았다.
|
||||
|
||||
## 6. Dependency
|
||||
|
||||
- `react-day-picker` ^10.0.2 (`packages/ui`에 추가, shadcn Calendar의 기반). 날짜 계산용으로 `date-fns`와 `@date-fns/tz`가 이 패키지의 dependency로 따라 들어온다. HM이 직접 date 라이브러리를 추가하지 않았다.
|
||||
- locale은 같은 패키지의 `react-day-picker/locale`(`ko`)을 사용(추가 설치 없음).
|
||||
- CENTER 패키지에는 추가하지 않았다.
|
||||
|
||||
## 7. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/calendar.css` (`--hm-calendar-*`).
|
||||
|
||||
`padding`, `text`, `month-gap`, `caption-*`, `nav-size|radius|icon-size|hover-background`, `weekday-*`, `day-size|radius|font-size|line-height|hover-background`, `today-background|font-weight`, `selected-background|text`, `range-edge-background|text`, `range-middle-background|text`, `outside-color|opacity`, `disabled-opacity`. Semantic/Theme palette를 참조한다. Selected·Range edge는 `--hm-sem-state-active` / `--hm-sem-on-state-active`(2026-10-06 Active Semantic)를 참조한다. 신규 Primitive 없음.
|
||||
|
||||
## 8. react-day-picker에서 유지한 항목
|
||||
|
||||
날짜 계산, 월 이동, 키보드 모델(방향키, Home/End, PageUp/PageDown), 선택 모드와 `required`, disabled/outside/hidden 날짜, 숫자·날짜 접근성 label(locale 기반), `role="grid"`/`gridcell`, `aria-selected`, roving tabindex.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Calendar"): Single(오늘 10/2, 선택 10/15), Disabled dates(5일 이전·일요일), Range(10/8–10/14), Two months를 한 페이지에 둔다. `today`는 라이브러리 prop으로 2026-10-02에 고정(어느 날 열어도 같게 보이기 위함).
|
||||
- 2026-10-06 Visual QA: Two months 구역만 `StateBoard`의 `wide`를 사용한다. Desktop에서 252px month 두 개 + 16px gap + Calendar padding 24px + frame border 2px가 들어가며 좁은 폭에서는 기본 wrap을 유지한다. Production Calendar API/CSS는 변경하지 않았다.
|
||||
- verification: `Single`, `Disabled Dates`, `Range`.
|
||||
- meta에 `component`를 지정하지 않는다(props가 선택 모드 union이라 args 없는 Story가 타입을 통과하지 못함).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(36px 셀, 12px padding, Today 면, Selected 면)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 기본 locale을 한국어로 둔 것은 현재 HM 서비스 기준이며, 다국어는 소비자가 `locale`을 넘긴다. i18n 시스템은 만들지 않았다.
|
||||
- 주 시작 요일은 locale 기본(한국어는 일요일).
|
||||
- `captionLayout="dropdown"`(월/연도 select)의 시각은 스타일링하지 않았다(`.rdp-dropdown*`). 사용이 정해지면 추가.
|
||||
- 주 번호(`showWeekNumber`), footer, 달력 애니메이션(`animate`)은 스타일링하지 않았다.
|
||||
- **모바일**: 한 달은 276px로 390px 폭에 들어간다. `numberOfMonths > 1`은 좁은 화면에서 줄바꿈으로 세로로 쌓이며 별도 responsive calendar는 만들지 않았다.
|
||||
- Range의 hover 미리보기, 최소/최대 길이 표현은 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/calendar.tsx
|
||||
packages/ui/src/theme/calendar.css
|
||||
packages/ui/package.json (dependency: react-day-picker)
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/calendar.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-06 Visual QA: Two months는 1280/1024/768px 한 행, 390px wrap, 모든 month 폭 252px 및 페이지 가로 overflow 없음. Production CSS 변경 없음. `typecheck`, `build`, `build-storybook` 통과.
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 렌더/DOM(Browser pane): 한국어 caption "2026년 10월", 요일 "일월화수목금토", `role="grid"`, 날짜 폭 36px, Selected `rgb(18,51,40)`·흰 글자, Today `rgb(244,241,234)`·700, 이전/다음 달 버튼 label이 한국어("이전 달로 이동"/"다음 달로 이동").
|
||||
- pointer: 날짜 클릭으로 선택(`aria-selected=true`), 다음/이전 달 버튼으로 월 이동(10월 → 11월, 10월 → 9월).
|
||||
- keyboard: 포커스 후 ArrowRight 10 → 11, ArrowDown 11 → 18(다음 주), Enter와 Space로 선택, PageDown으로 다음 달(11월), Home으로 주의 첫 날. roving tabindex가 선택된 날짜에 있음.
|
||||
- disabled: 5일 이전과 일요일은 `data-disabled`, 버튼 `disabled`, opacity 0.4, 클릭해도 선택 안 됨. 가능한 날짜는 선택됨.
|
||||
- range: 시작/끝은 Active 면(안쪽 직각), 중간은 accent 45% 면(모서리 0)으로 구분됨(처음에는 `data-selected` 규칙이 중간 면을 덮는 버그가 있어 specificity를 올려 수정).
|
||||
- Responsive(390×844): Calendar 폭 276px(두 달은 줄바꿈되어 356px), 페이지 가로 overflow 없음.
|
||||
- 미확인: 실제 pointer hover 면 시각, focus ring 시각, `multiple` 모드, `captionLayout="dropdown"`, RTL, 타임존(`@date-fns/tz`) 동작.
|
||||
@@ -0,0 +1,181 @@
|
||||
# Card
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Card 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
> 아래 외부 ui_viz 문서는 ZIP 미포함 초기 출처이며 실행·구현의 필수 의존성이 아니다. 현재 기준은 이 문서와 HM 구현·대표 Story다. ui_viz에는 `docs/components/card.md`가 없다. Card 규격은 `docs/patterns/content-container.md`(Type B **Content Card**)와 `docs/patterns/content-header.md`(`content` profile)가 소유한다. 두 문서는 "범용 Card System이 아니다"라고 명시하며, 상태·Footer·Media·elevation을 만들지 않았다(Out of Scope). HM Card는 이 Type B를 shadcn Card에 적용한 것이다. Type A(Section Panel)는 구현하지 않았다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (`<Card />`만으로 적용, `hm-card` class 없음) |
|
||||
| 기준일 | 2026-10-02 (문서·주석 정합성 정정 2026-10-08, 디자인 값 변경 없음) |
|
||||
| 기반 | shadcn/ui Card (div 기반, Radix 없음) |
|
||||
| 대표 Story | `Components/Data & Display/Card` → `All` (이름 "Card") |
|
||||
| 보류 | Eyebrow, Divider, Footer·Media, 반응형, 폭 (§11) |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **ui_viz Confirmed**: 컨테이너 radius 4px(`radius.container.default`), 표면 White(`color.bg.surface`)/Beige(`color.bg.content`), 제목 전경 `color.fg.primary`.
|
||||
- **ui_viz 채택(SOURCE_ADOPTED, Canonical Pattern Q6 `YES`, 2026-09-28)**: border 1px, body-padding 24px 사방, header↔body 간격 24px(D-093 D-042/D-059), title↔action 간격 20px, 교차축 중앙 정렬, 상태 없음. 이 값들은 문서상 `Confirmed` 표기가 아니라 owner가 채택한 Canonical 값이다. HM Card는 이 채택 값을 적용한다.
|
||||
- **ui_viz LOCAL_EXCEPTION(컴포넌트 범위 값)**: border 색 `#DEDBD2`(전역 token 아님, Tabs·Search Field와 값만 같고 alias 금지), Beige border `#ECD4A2`(Primitive `hm-brown-light-4`와 같은 값). **현재 HM 구현은 이 두 값을 컴포넌트 전용 값으로 두지 않고 공유 Semantic Token에 연결한다.** 차이는 §6.1에 기록한다.
|
||||
- **ui_viz CANONICAL_OVERRIDE**: Title Source 19px/700 → `heading-md` 20/28/600.
|
||||
- **HM 사용자 결정**: 없음.
|
||||
- **shadcn 유지**: 아래 §7.
|
||||
- **ui_viz 미정의**: Description, Footer, `size="sm"`, Media, Hover/Selected/Clickable, shadow.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Background | White (`--hm-sem-bg-surface`) / Beige (`--hm-sem-bg-content`) | Confirmed |
|
||||
| Border | 1px solid `--hm-sem-stroke-input-default`(Default `#dedbd2`) / Beige는 `--hm-sem-palette-brown-light-4`(Default `#ecd4a2`) | 두께는 채택. 색 값은 LOCAL_EXCEPTION 값과 같고 Token 연결은 §6.1 |
|
||||
| Radius | 4px | Confirmed |
|
||||
| Shadow | 없음 | ui_viz: "Source의 컨테이너는 그림자를 쓰지 않는다" |
|
||||
| Padding | 24px 사방 (Card 위/아래, Header·Content 좌우) | 채택 |
|
||||
| Header ↔ Content 간격 | 24px | 채택 (D-093) |
|
||||
| 자식 clipping | 하지 않음 (`overflow: visible`) | 채택 (Type B) |
|
||||
| Title | 20px / 28px / 600, `--hm-sem-fg-primary` | CANONICAL_OVERRIDE |
|
||||
| Header 안 Title ↔ Action 간격 | 20px (`column-gap`) | 채택 |
|
||||
| Action 정렬 | Title 반대쪽 끝, 교차축 중앙 | 채택 |
|
||||
| Description | shadcn 기본 (`text-sm text-muted-foreground`), HM Card CSS 미적용. 사용 범위는 §5.1 | ui_viz 미정의 |
|
||||
| 폭/높이 | 내용과 레이아웃이 결정. 고정 token 없음 | ui_viz: "Card width token을 만들지 않는다" |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
- 존재하는 상태는 **Default 하나**다. Hover / Focus / Pressed / Selected / Disabled는 ui_viz가 `NOT_APPLICABLE`로 명시했고 추가하지 않았다. Clickable·Interactive 변형도 없다.
|
||||
- Card 안의 Button·Input 등은 각자의 컴포넌트 문서에 따른 상태를 가진다.
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
Card:
|
||||
size?: "default" | "sm" (shadcn 그대로)
|
||||
→
|
||||
size?: "default" | "sm" (유지)
|
||||
surface?: "surface" | "content" (신규) 기본 "surface" — ui_viz surface 축 (White / Beige)
|
||||
|
||||
CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter: shadcn API 그대로
|
||||
```
|
||||
|
||||
- 출력 속성: `data-slot="card"`, `data-size`, `data-surface`.
|
||||
- 새 prop은 `surface` 하나다. ui_viz가 Content Card의 표면 축(`surface`/`content`)을 정의했기 때문이다. `variant`·`interactive`·`selected` 등은 근거가 없어 만들지 않았다.
|
||||
- Eyebrow는 shadcn Card에 대응 요소가 없다. 필요하면 소비자가 조합한다(미구현, §11).
|
||||
|
||||
### 5.1 CardDescription 사용 범위
|
||||
|
||||
- 공개 API로 export되며(`packages/ui/src/index.ts`) shadcn 구현 그대로다. `packages/ui/src/theme/card.css`는 `card-description`을 선택하지 않으므로 HM 전용 Typography·색이 없다(`text-sm text-muted-foreground`).
|
||||
- ui_viz Content Card Anatomy에 Description이 없다. 대표 Story `All`에는 넣지 않으며, HM Card 규격 요소가 아니라 **shadcn 호환 요소**로만 둔다.
|
||||
- 현재 저장소의 실제 사용처:
|
||||
|
||||
| 위치 | 용도 | 비고 |
|
||||
|---|---|---|
|
||||
| `src/components/card.stories.tsx` `Compatibility` | shadcn 호환 확인 | verification 전용 |
|
||||
| `src/patterns/dashboard-layout.stories.tsx` | 업무 목록 건수(`role="status"`), KPI 라벨(Title 없이 Description만), 가로 스크롤 안내 | Pattern Story 안의 소비 예시. 별도 스타일 없음 |
|
||||
| `src/App.tsx` (Demo) | Panel 부제 | `.hm-panel-header`가 11px·`--hm-muted`로 덮어씀. Demo 전용(§8.1) |
|
||||
|
||||
- Header에 Description이 있으면 shadcn의 `has-data-[slot=card-description]:grid-rows-[auto_auto]`로 2행 grid가 되고, `CardAction`은 `row-span-2`로 두 행에 걸친다. 이 동작은 shadcn 그대로다.
|
||||
- Description의 HM Token·크기·색은 미정의다. 필요해지면 이 문서에서 결정한 뒤 Token으로 추가한다(§11).
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/card.css` (`--hm-card-*`), `packages/ui/src/theme.css`에서 import.
|
||||
|
||||
- 재사용한 공통 값: `--hm-radius-container`(radius), `--hm-sem-bg-surface`, `--hm-sem-bg-content`, `--hm-sem-fg-primary`, `--hm-sem-stroke-input-default`(White border), `--hm-sem-palette-brown-light-4`(Beige border). Border 연결 상세는 §6.1.
|
||||
- Card 전용 값: `--hm-card-radius`, `--hm-card-border-width`(1px), `--hm-card-padding`(24px), `--hm-card-border-color`, `--hm-card-border-color-on-content`, `--hm-card-surface`, `--hm-card-surface-content`, `--hm-card-text`, `--hm-card-header-gap`(20px), `--hm-card-title-font-size|line-height|font-weight|color`.
|
||||
- **Primitive 신규 추가 1건**: `--hm-brown-light-4: #ecd4a2` (`packages/ui/src/theme/primitives.css`). ui_viz가 Confirmed Primitive `hm-brown-light-4`로 명시한 값이다. Card CSS는 이 Primitive를 직접 참조하지 않고 Semantic alias `--hm-sem-palette-brown-light-4`를 참조한다.
|
||||
- shadcn의 `--card-spacing` 변수를 24px(`--hm-card-padding`)로 채운다. `size="sm"`은 TSX class의 12px을 그대로 쓴다.
|
||||
- Input·Select·Button과 값(4px, `#dedbd2`)이 겹쳐도 그쪽 Component Token을 alias하지 않는다. `--hm-card-radius`는 `--hm-radius-container`를 참조하고 `--hm-radius-input`과는 연결하지 않는다(ui_viz가 alias 금지를 명시).
|
||||
- `--card-spacing`과 Title 규칙은 `:where()`로 감싸 specificity를 낮췄으므로 소비자 `className`과 Demo 스타일이 덮어쓸 수 있다. border·radius·surface 규칙(`[data-slot="card"]`, `[data-surface="content"]`)은 `:where()` 없는 attribute selector다.
|
||||
|
||||
### 6.1 Border Token 실제 연결 (LOCAL_EXCEPTION과의 차이)
|
||||
|
||||
| Card 변수 | 실제 참조 (`card.css`) | Runtime 계약 key (`contract.ts`) | HM Default 해석 경로 (`default.css` → `primitives.css`) | Default 값 |
|
||||
|---|---|---|---|---|
|
||||
| `--hm-card-border-color` | `var(--hm-sem-stroke-input-default)` | `color.stroke.input.default` | `--hm-color-border-control` → `--hm-sem-palette-warm-neutral-pale` → `--hm-warm-neutral-pale` | `#dedbd2` |
|
||||
| `--hm-card-border-color-on-content` | `var(--hm-sem-palette-brown-light-4)` | `color.palette.brown-light-4` | `--hm-brown-light-4` | `#ecd4a2` |
|
||||
|
||||
- **ui_viz 기술**: 두 border 색은 LOCAL_EXCEPTION 컴포넌트 값이며, 전역 token이 아니고 다른 컴포넌트 값과 alias하지 않는다.
|
||||
- **현재 구현**: Default 렌더 값은 ui_viz 값과 같다. 다만 White border는 Input·Select·Dialog·Separator 등이 함께 쓰는 공유 Semantic `--hm-sem-stroke-input-default`를, Beige border는 공유 Palette Semantic `--hm-sem-palette-brown-light-4`를 참조한다. 다른 Component Token(`--hm-input-*` 등)은 alias하지 않는다.
|
||||
- **결과**: 외부 Theme JSON이 `color.stroke.input.default` 또는 `color.palette.brown-light-4`를 override하면 Card border도 함께 바뀐다. Card만 따로 조정하려면 Component 변수 `--hm-card-border-color` / `--hm-card-border-color-on-content`를 덮어써야 하며, 이 변수는 Runtime 계약 key가 아니다.
|
||||
- 이 절은 문서 정정이다. Token 연결과 렌더 값은 바꾸지 않았다. Card 전용 border token으로 분리할지는 미결정이다(§11).
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
semantic HTML(`div`), React composition(Header/Title/Description/Action/Content/Footer), `data-slot`, `cn(..., className)`, header의 grid 구조와 `has-data-[slot=card-action]` 2열 전환, Footer(`border-t`, `bg-muted/50`, 전체 폭), 첫/마지막 `img` 처리, `has-data-[slot=card-footer]:pb-0`, `size="sm"`, Description 스타일. Radix 사용 없음.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- `rounded-xl`, `bg-card`, `ring-1 ring-foreground/10`, `overflow-hidden`, 기본 `--card-spacing: 16px`을 TSX에서 제거하고 theme CSS(border, radius 4px, surface, 24px)가 담당.
|
||||
- ring 대신 실제 1px border 사용, shadow 없음.
|
||||
- Title을 20/28/600으로 변경(`size="sm"` 제외).
|
||||
- Header 열 간격 20px, Action 교차축 중앙 정렬.
|
||||
- Footer 아래 모서리를 컨테이너 radius(border 안쪽 3px)에 맞춤. Footer 표면은 shadcn 그대로다.
|
||||
- `surface` prop 추가.
|
||||
|
||||
### 8.1 Demo `.hm-panel`과 운영 Card 구분
|
||||
|
||||
- **운영 Card**: `@hm-design/ui`의 `<Card />`와 `packages/ui/src/theme/card.css`. 이 문서 §3의 값(padding 24px, Title 20/28/600 등)과 대표 Story `All`이 기준이다. 소비 프로젝트는 `hm-panel*` class 없이 이 API만 쓴다.
|
||||
- **Demo Panel**: `src/App.tsx`의 4개 Card에 붙은 `hm-panel`, `hm-panel-header`, `hm-panel-icon`, `hm-panel-content` class와 `src/hm-theme.css` 규칙. 패키지(`packages/ui`)에 포함되지 않으며 Card 규격 Reference가 아니다.
|
||||
- `.hm-panel`은 Card 외형(border, radius, background, shadow)을 지정하지 않는다. 다만 Demo class 묶음은 운영 Card 값을 다음처럼 덮어쓴다.
|
||||
|
||||
| Demo class | 덮어쓰는 값 | 운영 Card 값 |
|
||||
|---|---|---|
|
||||
| `.hm-panel` | `gap: 0`, `--card-spacing: 16px` | gap·padding 24px |
|
||||
| `.hm-panel-header` | 아이콘 34px 열 grid, `column-gap: 11px`, `border-bottom: 1px solid #ede9df`, `padding: 22px 23px 17px` | Header 좌우 24px, column-gap 20px, divider 없음 |
|
||||
| `.hm-panel-header [data-slot="card-title"]` | 15px / 1.35 / 700, letter-spacing -.02em | 20 / 28 / 600 |
|
||||
| `.hm-panel-header [data-slot="card-description"]` | 11px, `--hm-muted`, margin-top 2px | shadcn 기본 `text-sm text-muted-foreground` |
|
||||
| `.hm-panel-icon` | 34px 아이콘 박스 | 해당 요소 없음 |
|
||||
| `.hm-panel-content` | `padding: 23px` | 좌우 24px |
|
||||
|
||||
- Demo 값은 HM Token이 아닌 hex 값을 포함한다. Card Token·Story의 근거로 쓰지 않으며, Demo 화면을 Card 디자인 Reference로 캡처·인용하지 않는다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Card")이 기본 sidebar의 유일한 Card 항목이다 → 1뎁스. 구역: `surface`(White), `content`(Beige), `action`(Title + Action), `form`(본문에 Input).
|
||||
- verification(`tags: ["verification"]`): `Surfaces`, `Action`, `Compatibility`(HM 전용: Description, Footer, `size="sm"`).
|
||||
- 운영 규칙은 `프로젝트-지침.md` §6.3, §7 참조.
|
||||
- 대표 Story에 Hover/Selected/Footer/Media는 넣지 않았다(ui_viz 미정의).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
추가 사용자 결정 없음.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- **Eyebrow**: ui_viz `content` header는 Eyebrow(11→12px/700, letter-spacing .12em, `#3E3523`)를 필수 요소로 둔다. shadcn Card에 대응 요소가 없고 이번 범위에 추가하지 않았다. 현재 Title만 있는 Header는 ui_viz Anatomy의 부분 구현이다.
|
||||
- **Divider(선택)**: ui_viz는 Header 뒤 24px + 1px + 18px의 선택 divider를 정의한다(`CONTENT_CARD_DIVIDER_OPTIONAL`). 기본은 없음이라 구현하지 않았다.
|
||||
- **Tabs Meta**: Meta가 Tabs일 때 Header↔Body 간격이 0이 되는 예외는 구현하지 않았다.
|
||||
- **ui_viz 미정의**: Description, Footer(`footer variant`는 Out of Scope), Media, `size="sm"`, 반응형, Card 폭.
|
||||
- **자식 clipping**: ui_viz Type B는 하지 않음이다. `overflow-hidden`을 제거했다. 대신 Footer 모서리를 CSS로 맞췄고 이미지 모서리는 shadcn `rounded-*`가 남아 있다.
|
||||
- **LOCAL_EXCEPTION**: ui_viz는 border 색 두 값을 전역 token이 아닌 컴포넌트 값으로 두고 tokenization을 follow-up으로 둔다. 현재 HM 구현은 공유 Semantic(`color.stroke.input.default`, `color.palette.brown-light-4`)에 연결되어 있다(§6.1). Card 전용 Semantic/Runtime key로 분리할지는 미결정이며, 이번 정정에서 Token 연결을 바꾸지 않았다.
|
||||
- **CardDescription HM 규격**: Typography·색·간격 미정의. shadcn 호환 요소로만 둔다(§5.1).
|
||||
- **Type A Section Panel**: 구현하지 않았고 미구현 상태를 유지한다. Demo `.hm-panel`은 Type A 구현이 아니며 대체 Reference로 쓰지 않는다(§8.1).
|
||||
- **Confirmed 표기 해석**: border 1px·padding 24px 등은 ui_viz에서 `Confirmed`가 아니라 owner 채택 Canonical 값이다. Confirmed만 적용한다는 기준으로 보면 재확인이 필요한 항목이다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/card.tsx
|
||||
packages/ui/src/theme/card.css
|
||||
packages/ui/src/theme/primitives.css (--hm-brown-light-4, --hm-warm-neutral-pale)
|
||||
packages/ui/src/theme/default.css (--hm-sem-stroke-input-default, --hm-sem-palette-brown-light-4 연결)
|
||||
packages/ui/src/theme/contract.ts (Runtime key color.stroke.input.default, color.palette.brown-light-4)
|
||||
themes/default.json (HM Default 컬러 원본)
|
||||
packages/ui/src/theme.css (card.css import)
|
||||
src/components/card.stories.tsx
|
||||
src/components/story-board.tsx
|
||||
src/patterns/dashboard-layout.stories.tsx (CardDescription 소비 예시)
|
||||
src/App.tsx (Demo, hm-panel* class 사용)
|
||||
src/hm-theme.css (.hm-panel* Demo 전용 스타일, §8.1)
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- TypeScript, `npm run build`: 통과(2026-10-02).
|
||||
- Storybook computed style(대표 Story 4개 구역): background White/Beige(`rgb(255,255,255)` / `rgb(245,240,229)`), border 1px `rgb(222,219,210)` / `rgb(236,212,162)`, radius 4px, shadow none, 위 padding 24px, Header·Content 좌우 24px, Header→Content 24px, overflow visible, Title 20px/28px/600 `rgb(10,34,29)`, column-gap 20px, Action 교차축 center, Action–Title 간격 20px — 문서 값과 일치.
|
||||
- Compatibility: Footer 아래 모서리 3px, `size="sm"` 위 padding 12px·Title 14px 확인.
|
||||
- Demo(App): 4개 Card가 border 1px/radius 4px/shadow none, 위 padding 16px, row-gap 0으로 렌더 확인. 이 값은 Demo `.hm-panel` 결과이며 운영 Card 값이 아니다(§8.1).
|
||||
- 2026-10-08 문서 정정: Border Token 연결(`card.css` ↔ `default.css` ↔ `contract.ts` ↔ `themes/default.json`), CardDescription 사용처, Demo class 규칙을 코드와 대조했다. `card.tsx`·`card.css`는 주석만 고쳤고 CSS 값·Component 동작·공개 API·Story는 바꾸지 않았다.
|
||||
- Story: `All` + verification 3개 ID 확인, 기본 sidebar는 `All` 하나만 보이는 1뎁스 구조(실제 사이드바 화면은 확인하지 못함).
|
||||
- 미확인: 사이드바 실제 화면, 브라우저별 렌더, Demo 화면 시각 비교(그림자 제거로 달라짐), 이미지 포함 Card.
|
||||
@@ -0,0 +1,116 @@
|
||||
# Carousel
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Carousel 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태와 성격
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 성격 | **Interactive layout component** (slide 탐색). 단순 가로 scroll이 아니다 |
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-03 |
|
||||
| 기반 | shadcn/ui Carousel = **Embla** (`embla-carousel-react` 8.6.0) |
|
||||
| 대표 Story | `Components/Data & Display/Carousel` → `All` (이름 "Carousel") |
|
||||
| 보류 | 아래 §12 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Carousel이 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다. Button/Scroll Area Token은 참조하지 않는다.
|
||||
- **구현 방식**: Embla가 drag/swipe, snap, loop, align, plugin, scroll 계산을 소유한다. Carousel은 이전/다음 control, 방향키, carousel 접근성 속성을 더하고 Embla API를 `setApi`로 노출한다. 데이터 fetching이나 paging은 소유하지 않는다.
|
||||
- **보류**: §12.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Slide 폭 | 기본 `basis-full`(한 개). 여러 개는 소비자가 `CarouselItem`에 `basis-1/2` 등을 준다. 카드 폭을 컴포넌트가 강제하지 않는다 |
|
||||
| Gap | 16px (Track이 `-16px`만큼 당겨지고 각 Slide가 16px padding, 가로는 left / 세로는 top) |
|
||||
| Control | 32 × 32px 원형 버튼, 흰 면 + 1px `--hm-sem-stroke-input-default` + `--hm-sem-fg-primary` 화살표 16px |
|
||||
| Control 위치 | **carousel 가장자리 안쪽 8px**(가로: 좌우 가운데, 세로: 위/아래 가운데이며 화살표를 90° 회전). shadcn은 바깥(-48px)이라 좁은 부모에서 잘리는 문제가 있어 안쪽으로 정했다 |
|
||||
| Content 커서 | `grab` / 누르는 동안 `grabbing` |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Initial | Previous disabled(loop가 아니면), Next enabled |
|
||||
| Disabled (경계) | opacity 0.6(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), cursor not-allowed. loop일 때는 비활성되지 않는다 |
|
||||
| Hover | Control 배경 `--accent` |
|
||||
| Focus (`:focus-visible`) | Control에 ring 모양 `0 0 0 3px ring 50%` |
|
||||
| Dragging | Embla pointer drag, 커서 `grabbing` |
|
||||
| Active slide | 컴포넌트 속성은 없다. `setApi`로 `selectedScrollSnap()`을 읽어 소비자가 표시한다(Story의 `slide n / total`) |
|
||||
| Vertical | `orientation="vertical"`, Control이 위/아래로 이동하고 화살표가 회전 |
|
||||
|
||||
## 5. API
|
||||
|
||||
shadcn 구조 그대로. export: `Carousel`, `CarouselContent`, `CarouselItem`, `CarouselPrevious`, `CarouselNext`, `useCarousel`, type `CarouselApi`.
|
||||
|
||||
- `Carousel`: `orientation` `"horizontal" | "vertical"`, `opts`(Embla options: `loop`, `align`, `dragFree`, `startIndex`…), `plugins`, `setApi`. 출력 `data-orientation`.
|
||||
- `CarouselContent`: Embla viewport(`overflow-hidden`)와 track을 렌더한다(`data-slot="carousel-content"`, 안쪽 `carousel-track`). `className`은 track에 적용된다.
|
||||
- `CarouselItem`: slide. `className`으로 `basis-*` 지정.
|
||||
- `CarouselPrevious`/`CarouselNext`: Carousel 전용 `button`(Button 컴포넌트가 아님). sr-only 텍스트 "Previous slide" / "Next slide".
|
||||
- `variant`, `size`, `cardType` 등은 만들지 않았다.
|
||||
- **접근성**: Root `role="region"` + `aria-roledescription="carousel"`(이름은 소비자의 `aria-label`), Slide `role="group"` + `aria-roledescription="slide"`, Control은 disabled일 때 native `disabled`, ArrowLeft/ArrowRight가 이전/다음(캡처 단계에서 처리).
|
||||
- **Root `min-w-0`**: grid/flex 부모의 `min-width:auto`가 Embla track의 intrinsic width로 커져 페이지가 밀리는 것을 막기 위해 Root에 `min-w-0`을 둔다. 한 단계 더 감싼 wrapper(grid item)에는 소비자가 `min-w-0`을 줘야 한다(Story가 그렇게 한다).
|
||||
|
||||
## 6. Dependency
|
||||
|
||||
- `embla-carousel-react` ^8.6.0 (`packages/ui`에 추가, shadcn Carousel의 기반). 사용: Carousel. CENTER 패키지에는 추가하지 않았다.
|
||||
|
||||
## 7. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/carousel.css` (`--hm-carousel-*`).
|
||||
|
||||
`gap`, `control-size`, `control-inset`, `control-icon-size`, `control-background`, `control-text`, `control-border-width|color`, `control-hover-background`, `disabled-opacity`. Semantic과 Primitive(`--hm-warm-neutral-pale`)만 참조한다. 신규 Primitive와 Semantic 없음. 모든 규칙이 `@layer components`에 있어 소비자의 utility(`className`: gap, control 위치와 크기)가 이긴다.
|
||||
|
||||
## 8. Embla에서 유지한 항목
|
||||
|
||||
pointer drag/swipe와 snap, loop·align·dragFree·skipSnaps 등 options, plugins, `canScrollPrev/Next`, `reInit`/`select` 이벤트, `scrollPrev/Next` 구현.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Carousel"): Basic(한 개 + `setApi`로 `slide n / 5` 표시), Multiple items(`basis-1/2`), Loop(3개), Vertical(세로), Media(AspectRatio 조합)를 한 페이지에 둔다. Card와 AspectRatio는 Story 조합이며 Carousel은 둘을 import하지 않는다.
|
||||
- verification: `Basic`, `Multiple Items`, `Loop`, `Vertical`.
|
||||
|
||||
## 10. 소비자 확장성
|
||||
|
||||
- Slide 폭: `className="basis-1/2"`이 그대로 적용된다(확인).
|
||||
- Gap/Control 위치·크기: CSS가 `@layer components`라 `className`의 utility가 이긴다(예: `CarouselPrevious className="-left-12"`로 바깥 배치 가능).
|
||||
- Track 높이(세로): `CarouselContent style/className`으로 높이를 준다.
|
||||
|
||||
## 11. 사용자 확정 결정
|
||||
|
||||
- 2026-10-03: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). Control은 Button 조합이 아니라 Carousel 전용 control로 둔다(Button Token과 분리).
|
||||
|
||||
## 12. 미확정 / 보류
|
||||
|
||||
- 수치(32px control, 8px inset, 16px gap)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Control이 slide 위에 겹치므로 카드 안쪽 콘텐츠를 가릴 수 있다(특히 세로 carousel의 첫/끝 항목). 바깥 배치는 `className`으로 선택한다.
|
||||
- 인디케이터(dot), autoplay, 썸네일 동기화는 만들지 않았다(Embla plugin과 `setApi`로 소비자가 조합).
|
||||
- 반응형(breakpoint별 `basis`)은 소비자 `className`(`md:basis-1/3`)이다. 자동 responsive carousel은 만들지 않았다.
|
||||
- 포인터가 컨트롤 위일 때 drag 시작 동작, 스크린리더가 slide 변경을 알리는 live region은 정의하지 않았다.
|
||||
|
||||
## 13. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/carousel.tsx
|
||||
packages/ui/src/theme/carousel.css
|
||||
packages/ui/package.json (dependency: embla-carousel-react)
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/carousel.stories.tsx
|
||||
```
|
||||
|
||||
## 14. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-03).
|
||||
- 실제 DOM(Browser pane): Root `role=region`, `aria-roledescription=carousel`, `aria-label`, Slide `role=group`/`slide`, 5개 slide, 초기 Previous `disabled`, Control 32px·radius 9999px·흰 면, `setApi` readout이 `slide 1 / 5`.
|
||||
- navigation: Next 클릭으로 `slide 2 / 5`와 Previous 활성화, Previous 클릭으로 `slide 1 / 5`와 Previous 비활성화. 버튼에 focus를 둔 채 ArrowRight/ArrowLeft로 3 → 2. 끝(5/5)에서 Next `disabled`.
|
||||
- pointer: 왼쪽 drag로 `slide 2 → 3`, 오른쪽 drag로 `3 → 2`.
|
||||
- loop: 첫 slide에서 Previous가 활성이고 클릭하면 `3 / 3`, Next로 `1 / 3`, 계속 눌러도 Next가 비활성되지 않음.
|
||||
- vertical: `data-orientation=vertical`, Previous `top: 8px`·`rotate: 90deg`, Next `bottom: 8px`, Next 클릭 후 Previous 활성화, slide가 위아래로 쌓임.
|
||||
- 레이아웃: Slide 폭이 Root 폭(`basis-full`), `basis-1/2`이면 절반(390px에서 187px), gap 16px(Track `-16px`, Item padding 16px).
|
||||
- Responsive(390×844): 모든 carousel이 358px 안(좌 16px)에 들어가고 Control이 안쪽에 있어 잘리지 않으며 페이지 가로 overflow 없음. (처음에는 grid wrapper의 `min-width:auto` 때문에 페이지가 436px로 밀려 Root에 `min-w-0`과 Story wrapper의 `minWidth:0`을 추가해 해결했다.)
|
||||
- 참고: 이 검증 환경은 페이지가 화면에 그려질 때만 animation frame이 진행되어, scroll 위치(translate)는 screenshot으로 frame을 만든 뒤에야 최종값에 도달했다. 위 검증은 Embla의 `selectedScrollSnap`(readout)과 버튼 상태 기준이다.
|
||||
- 미확인: 터치 기기에서의 swipe(마우스 drag로 대체), `dragFree`·plugin(autoplay), 스크린리더 실제 읽기, 소비자의 `md:basis-*` breakpoint 동작.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Checkbox
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Checkbox 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Checkbox (Radix `Checkbox`) |
|
||||
| 대표 Story | `Components/Form/Checkbox` → `All` (이름 "Checkbox") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz 기존 테스트 규격에는 Checkbox 문서가 없다(border·radius·indicator는 TBD, mark는 native UA 소유, HOLD). 구현 값으로 쓸 수 있는 확정 규격이 없어 참고만 했다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계(surface, stroke, action-primary, state-active, on-state-active)로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Checkbox 구조(Root/Indicator, `data-state`, indeterminate, 키보드, Label 연결, 폼 동작)를 유지하고 theme CSS로 HM 스타일을 연결.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 | 비고 |
|
||||
|---|---|---|
|
||||
| Box | 18 × 18px | 현재 HM |
|
||||
| Border | 1px `--hm-sem-stroke-default` | Semantic |
|
||||
| Radius | `--hm-radius-control-md` (4px) | 공통 control radius |
|
||||
| Face | `--hm-sem-bg-surface` | Semantic |
|
||||
| Indicator | shadcn `CheckIcon` / `MinusIcon`, 14px, `--hm-sem-on-state-active` | shadcn 아이콘 유지 |
|
||||
| Hit area | 18px box + 연결된 `Label` 클릭 | 별도 확장 없음 |
|
||||
|
||||
shadcn(lucide) 아이콘을 유지했다. 별도 HM glyph는 만들지 않았다.
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Unchecked | 위 기본 규격 |
|
||||
| Checked (`data-state="checked"`) | border·face `--hm-sem-state-active`, 체크 아이콘 |
|
||||
| Indeterminate (`data-state="indeterminate"`) | Checked와 같은 면, 마이너스 아이콘 |
|
||||
| Hover (미체크) | border `--hm-sem-action-primary` |
|
||||
| Hover (체크·indeterminate) | Black 20% overlay (`--hm-color-action-overlay-hover`) |
|
||||
| Focus (`:focus-visible`) | shadcn ring (`ring-3 ring-ring/50`) 유지 |
|
||||
| Error (`aria-invalid="true"`) | border `--hm-sem-action-destructive` |
|
||||
| Disabled | `opacity: 0.6` (이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), `cursor: not-allowed` |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Checkbox props 그대로 (`checked` / `defaultChecked` / `onCheckedChange`, `"indeterminate"`, `disabled`, `required`, `name`, `value`, `form`). 추가 prop 없음. 출력 속성: `data-slot="checkbox"`, Radix `data-state`·`data-disabled`.
|
||||
|
||||
- Label과의 연결은 `<Label htmlFor>` 또는 감싸기. Label·Helper·Error 조합은 소비 조합(`field.md`).
|
||||
|
||||
### Form 연결 및 Reset
|
||||
|
||||
- Form 내부에서는 기존 native Form 연결을 사용한다. 외부에서는 `form="target"`과 `<form id="target">`으로 연결한다.
|
||||
- 기본 버튼 렌더링에 `form`을 지정하면 HM은 Radix의 공개 `asChild` 경로로 같은 Checkbox 버튼에도 `form`을 전달한다. 추가 DOM wrapper, 별도 상태, Reset listener 또는 숨김 input 강제 조작은 없다. 호출자가 명시한 `asChild` 경로는 기존대로 유지한다.
|
||||
- Uncontrolled는 `defaultChecked`로 초기 상태를 지정한다. Native Reset 버튼과 `form.reset()`은 Radix가 보관한 최초 상태를 복원한다. 이후 `defaultChecked` 변경을 현재 선택값 변경으로 사용하지 않는다.
|
||||
- Controlled는 부모의 `checked`가 상태의 기준이다. 부모 `onReset`에서 초기값을 복원하고 `onCheckedChange`로 변경 요청을 반영한다. 외부 Form에도 내부 Form과 같은 Radix Reset callback 계약이 적용된다.
|
||||
- Checked는 `name` / `value`로 제출된다(기본 `value="on"`). Unchecked와 indeterminate는 제출되지 않는다. Indeterminate는 `aria-checked="mixed"`이며 native input의 `checked`는 false다. Disabled는 버튼과 숨김 input 모두 비활성화되어 제출되지 않는다.
|
||||
|
||||
```tsx
|
||||
const initialChecked = true
|
||||
const [checked, setChecked] = React.useState(initialChecked)
|
||||
|
||||
return (
|
||||
<>
|
||||
<form id="target" onReset={() => setChecked(initialChecked)}>
|
||||
<button type="reset">초기화</button>
|
||||
</form>
|
||||
<Checkbox
|
||||
form="target"
|
||||
name="agreement"
|
||||
checked={checked}
|
||||
onCheckedChange={setChecked}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
```
|
||||
|
||||
Uncontrolled 예제는 `checked` / `onCheckedChange` 대신 `defaultChecked={true}`를 사용하며 부모의 Reset 상태 관리는 필요하지 않다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/checkbox.css` (`--hm-checkbox-*`).
|
||||
|
||||
`size`, `radius`, `border-width`, `icon-size`, `background`, `border-color`, `hover-border-color`, `checked-background`, `checked-border-color`, `checked-hover-overlay`, `indicator-color`, `error-border-color`. Semantic(`--hm-sem-*`)을 참조하고 Input 등 다른 컴포넌트 Token을 쓰지 않는다. Primitive 신규 추가 없음(`--hm-sem-action-destructive`는 기존 Primitive를 직접 참조).
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`role=checkbox`, `aria-checked`)·키보드, `data-slot`, `cn(..., className)`, `transition-colors`, `outline-none` + focus ring, disabled 커서, lucide `CheckIcon`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `size-4 rounded-[4px] border shadow-xs`, `dark:` 및 `data-[state=checked]:bg-primary` 계열 class 제거 → theme CSS.
|
||||
- `MinusIcon` 추가(shadcn 기본은 indeterminate 아이콘이 없다). 두 아이콘은 `data-state`에 따라 CSS로 전환.
|
||||
- Disabled opacity 50 → 60.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Checkbox"): `unchecked`, `checked`, `indeterminate`, `interactive`(controlled), `hover`, `focus`, `error`, `disabled`, `long`. Focus는 `play`로 실제 focus.
|
||||
- verification: `Unchecked`, `Checked`, `Indeterminate`, `Disabled`, `Error`, `Playground`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- Checkbox Group 간격, group Label·Error 슬롯, option label 전경 role: 미결정.
|
||||
- 수치(18px, 14px 아이콘 등)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Checkbox Group, Error 문구 슬롯: 컴포넌트 없음(소비 조합).
|
||||
- Hit area 확장(24px 타깃 등) 미적용.
|
||||
- Disabled opacity `0.6`: 이 컴포넌트 값이며 전역 Disabled 규칙으로 승격하지 않는다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/checkbox.tsx
|
||||
packages/ui/src/theme/checkbox.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Checkbox)
|
||||
src/components/checkbox.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- Storybook computed style: 18×18, border 1px `rgb(154,146,132)`, radius 4px; Checked/Indeterminate bg·border `rgb(18,51,40)`; Error border `rgb(211,47,47)`; Disabled opacity 0.6; 체크/마이너스 아이콘이 상태에 따라 하나만 표시.
|
||||
- 실제 interaction: controlled indeterminate → click → checked → click → unchecked, Label 클릭으로 토글 확인.
|
||||
- 2026-10-08, React 19.3 / Radix Checkbox 1.3.11: 프로젝트 외부 격리 환경의 수정된 HM 패키지로 실제 브라우저 검증. 수정 전 외부 Form의 true·false·indeterminate Reset 불일치를 재현했고, 수정 후 모두 PASS.
|
||||
- Form 내부·외부에서 Space로 상태 변경 후 Native Reset 버튼 및 `form.reset()`을 실행: `defaultChecked` true·false·indeterminate와 Controlled 부모 `onReset`의 상태·ARIA·숨김 input·FormData 복원 확인. Indeterminate 복원 시 숨김 input은 `checked=false`, `indeterminate=true`, 제출값 없음.
|
||||
- 기존 Radix 동작도 구분해 확인: 최초 indeterminate 렌더에서는 숨김 input의 `indeterminate` 속성이 false다(내부·외부 및 수정 전·후 동일). UI/ARIA는 mixed, input의 `checked=false`, FormData 제외는 정상이며 이번 외부 Form Reset 수정에서 이 초기화 동작은 변경하지 않는다.
|
||||
- Native Form HTTP POST 8건: 내부·외부·Controlled의 Checked 값은 `choice=accepted`, Unchecked·indeterminate·Disabled 값은 제외. `required` native 유효성 및 ARIA, Disabled Tab 제외, BUTTON ref 유지 확인.
|
||||
- 수정 전·후 같은 Theme에서 Checked·Unchecked·Indeterminate·Disabled computed style과 아이콘 일치: 18×18px, border 1px, radius 4px, icon 14px, Disabled opacity 0.6. 키보드 focus-visible 3px ring도 일치. CSS·Theme·대표 Story 변경 없음.
|
||||
- UI 전체 타입 검사 및 공식 UI 패키지 빌드를 격리 경로에서 통과. 테스트에 사용한 Checkbox 소스와 실제 수정본의 SHA-256 일치. 공용 dist는 이 검증에서 빌드하지 않음.
|
||||
- 미확인: 실제 pointer hover, 사이드바 화면, 390px 폭. Hover 관련 CSS와 class는 변경하지 않음.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Collapsible
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Collapsible 고유의 API·Token·상태·결정만 기록한다. Accordion(`accordion.md`)과 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Collapsible (Radix `Collapsible`) |
|
||||
| 대표 Story | `Components/Disclosure/Collapsible` → `All` (이름 "Collapsible") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Collapsible이 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Collapsible 구조(Root·Trigger·Content)를 유지. shadcn Collapsible은 스타일이 없는 primitive라, 일반 trigger(asChild 없음)에만 텍스트 trigger 모양을 주고 content 간격·타이포를 정했다.
|
||||
- **Accordion과의 구분**: Collapsible은 단일 영역의 간단한 disclosure다. divider 구조와 chevron 내장이 없다. Accordion을 항목 하나로 쓰는 용도가 아니다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Trigger(일반) | inline-flex, gap 8px, 배경·border·padding 없음, 14 / 20 / 500, `--hm-sem-fg-primary`, 포인터 커서 |
|
||||
| Trigger(`asChild`) | 자식이 자기 스타일을 유지(예: HM Button). HM trigger 스타일은 적용되지 않는다 |
|
||||
| Indicator | trigger 바로 아래 자식 `svg`가 있으면 16px, `--hm-sem-fg-secondary`, 열리면 180° 회전 |
|
||||
| Content | 위 padding 8px, 14 / 20 / 400, `--hm-sem-fg-primary` |
|
||||
| 긴 텍스트 | `overflow-wrap: anywhere`, 고정 높이 없음 |
|
||||
| Divider / border | 없음 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Closed / Open | Radix `data-state`. 닫힌 content는 `hidden`(display: none) |
|
||||
| Hover | 일반 trigger 밑줄(disabled 제외) |
|
||||
| Focus (`:focus-visible`) | ring 모양 `0 0 0 3px ring 50%` (일반 trigger) |
|
||||
| Disabled | `opacity: 0.6`(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), `cursor: not-allowed` |
|
||||
| Animation | 없음(shadcn Collapsible에 없고 HM Motion 규칙도 없다) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Collapsible props 그대로. export: `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent`.
|
||||
|
||||
- `Collapsible`: `open`/`defaultOpen`/`onOpenChange`, `disabled`.
|
||||
- `CollapsibleTrigger`: `asChild`. **`asChild`일 때는 `data-slot`을 붙이지 않는다**(DialogClose와 같은 규칙). 붙이면 자식 Button의 `data-slot="button"`이 덮여 HM Button 스타일이 사라진다. 일반 trigger 출력은 `data-slot="collapsible-trigger"`.
|
||||
- `CollapsibleContent`: 출력 `data-slot="collapsible-content"`.
|
||||
- 새 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/collapsible.css` (`--hm-collapsible-*`).
|
||||
|
||||
`trigger-text`, `trigger-gap`, `trigger-font-size|line-height|font-weight`, `icon-size`, `icon-color`, `content-gap`, `content-text`, `content-font-size|line-height`, `disabled-opacity`. Semantic만 참조하고 `--hm-accordion-*` 등 다른 컴포넌트의 Token을 쓰지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`aria-expanded`, `aria-controls`)·Enter/Space·controlled/uncontrolled·disabled·`asChild`, `data-slot`, `cn(..., className)`, Content의 `hidden` 처리.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- 일반 trigger의 텍스트 trigger 스타일, Content 간격·타이포, indicator 회전은 shadcn에 없던 시각 규격을 추가한 것이다.
|
||||
- `CollapsibleTrigger`의 `asChild` data-slot 처리(위 §5).
|
||||
- Animation은 추가하지 않았다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Collapsible"): `StateBoard`로 Closed, Open, Disabled, Long Content, Controlled + Button trigger를 한 페이지에 둔다. trigger 안 chevron은 일반 소비 조합(`lucide-react` 아이콘)이다.
|
||||
- verification: `Closed`, `Open`, `Disabled`, `Long Content`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(14/20/500, gap 8px, Disabled opacity 0.6)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 열림/닫힘 animation 없음. Motion 규칙이 정해지면 Accordion과 함께 재검토.
|
||||
- indicator는 컴포넌트에 내장하지 않고 소비 조합으로 둔다(Accordion과 다름). 내장 여부는 사용 사례가 쌓이면 결정.
|
||||
- 박스형(border/배경) Content variant는 만들지 않았다.
|
||||
- Hover 밑줄은 일반 trigger에만 적용한다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/collapsible.tsx
|
||||
packages/ui/src/theme/collapsible.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/collapsible.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): Enter로 열기(`aria-expanded=true`, indicator 180° 회전), Space로 닫기, 닫힌 content `display: none`, disabled 클릭 무반응(opacity 0.6, cursor not-allowed), controlled(Button trigger asChild)에서 클릭으로 열리고 상태 문구와 버튼 라벨(`접기`)이 갱신됨.
|
||||
- Button trigger: `data-slot="button"`, `data-variant="primary"`와 HM Button 색이 유지됨.
|
||||
- computed style: 일반 trigger 14/20/500, `rgb(10,34,29)`, 배경 투명, icon 16px, content 위 padding 8px. 긴 콘텐츠에서 가로 overflow 없음.
|
||||
- 미확인: 포인터 클릭(JS click과 키보드만 사용), focus ring 시각, 390px 폭, uncontrolled `defaultOpen=false`에서 trigger 클릭(키보드 Enter로 대체 확인).
|
||||
@@ -0,0 +1,132 @@
|
||||
# Combobox
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Combobox 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태와 성격
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 성격 | **Reusable component, Popover + Command 조합** (typed `options` wrapper) |
|
||||
| 구현 상태 | 구현 완료 (single-select, 현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | `Popover`(public API) + `Command`(public API) 조합. shadcn 공식 Combobox는 예시 조합이다 |
|
||||
| 대표 Story | `Components/Search & Date/Combobox` → `All` (이름 "Combobox") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Combobox가 없다(Select와 Search Field는 다른 컴포넌트). 참고할 수치가 없었다.
|
||||
- **Component로 만든 이유**: 검색 가능한 단일 선택은 서비스에서 반복되고(trigger 텍스트, 검색, 선택 표시, 닫힘, focus 복귀가 동일) 최소 API(`options`)로 충분해서 typed wrapper를 제공한다. 더 복잡한 항목(그룹, 아이콘 등)은 소비자가 `Command`를 직접 조합한다.
|
||||
- **구현 방식**: Popover(Portal, collision, Escape, focus 복귀)와 Command(검색, 필터링, 키보드, listbox ARIA)를 그대로 조합한다. Select의 구현이나 연결형 윤곽을 복제하지 않았다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. Select와의 구분
|
||||
|
||||
| | Select | Combobox |
|
||||
|---|---|---|
|
||||
| 용도 | 정해진 짧은 목록에서 선택, 검색 불필요 | 검색/필터가 필요한 선택 |
|
||||
| 구현 | Radix Select | Popover + Command(cmdk) |
|
||||
| 목록 | trigger에 붙은 연결형 윤곽, 선택 항목은 면으로 표시 | 독립 floating 패널(검색 Input + 목록), 선택 항목은 Accent 45% 면 + check 아이콘 |
|
||||
| 접근성 모델 | Radix Select(listbox) | `role=combobox` trigger + 패널 안 cmdk combobox/listbox |
|
||||
|
||||
## 4. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Size | `large`(기본, 44px, 16 / 24), `medium`(34px, 14 / 20) — Select와 같은 두 크기 |
|
||||
| Trigger | 폭 100%(부모 폭), padding-x 12px, 1px `--hm-sem-stroke-input-default`, radius 4px, 흰 면, 오른쪽 chevron 18px |
|
||||
| 값 / placeholder | 선택된 option의 label, 없으면 placeholder("선택", `--hm-sem-fg-placeholder`), 길면 말줄임 |
|
||||
| Popup | padding 0, 폭 = trigger 폭(`--radix-popover-trigger-width`), 독립 4px gap. Focus Light 2 border, input radius 4px(네 모서리), shadow `0 2px 24px black 12%` |
|
||||
| 검색 | Command Input(placeholder 기본 "검색...") |
|
||||
| 목록 | Command List(최대 280px), Item 높이 36px·padding-x 12px·radius 0·weight 400. Large 16/24, Medium 14/20. 선택 check 16px, 긴 label은 말줄임 |
|
||||
| Empty | "결과가 없습니다."(`emptyText`) |
|
||||
|
||||
## 5. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Focus / Open | trigger 테두리 전체가 `--hm-sem-stroke-input-focus` (Input·Select와 같은 방식) |
|
||||
| Highlighted option | `--hm-sem-surface-highlight` (Accent 45%) |
|
||||
| Selected option | check 아이콘 + Accent 45% 면(`data-checked`). Command highlight가 다른 항목으로 옮겨져도 유지 |
|
||||
| Disabled (combobox) | opacity 0.6(이 컴포넌트 값), cursor not-allowed, 열리지 않음 |
|
||||
| Disabled option | Command의 disabled(opacity 0.6), 방향키 이동·선택 제외 |
|
||||
| 선택 후 | 값 갱신, Popover 닫힘, focus가 trigger로 복귀. 다시 열면 검색어가 비어 있다(Command가 닫을 때 unmount됨) |
|
||||
|
||||
## 6. API
|
||||
|
||||
export: `Combobox`, types `ComboboxOption`(`{ value, label, disabled? }`), `ComboboxSize`.
|
||||
|
||||
- `options`(필수), `value`/`defaultValue`/`onValueChange(value)`, `placeholder`, `searchPlaceholder`, `emptyText`, `size`, `disabled`, 그리고 `button` props(`id`, `aria-label`, `className`…).
|
||||
- 필터링: option의 `value`와 `label`(keywords)에 cmdk 기본 fuzzy 필터를 쓴다.
|
||||
- 이미 선택된 option을 다시 선택해도 값은 유지된다(해제 UI는 없다).
|
||||
- **접근성**: Trigger는 `role="combobox"`, `aria-haspopup="listbox"`, `aria-expanded`. 패널 안 Command Input이 `role=combobox`(`aria-controls` → `role=listbox`), option은 `role=option`. 이름은 `<label htmlFor=id>` 또는 `aria-label`을 소비자가 준다. 실제 ARIA combobox(편집 가능한 trigger input)가 아니라 **버튼 trigger + 패널 내 검색 combobox** 구조다(shadcn 공식 구조).
|
||||
- generic data framework, 다중 선택, 비동기 로딩은 만들지 않았다.
|
||||
|
||||
### 값 소유권과 외부 초기화
|
||||
|
||||
- `controlled?: boolean`은 선택적 값 소유권 설정이다. `true`이면 `value`가 `undefined`여도 Controlled이며, 부모 값만 표시한다. `undefined`는 선택 없음이다. 사용자 선택은 기존 `(string) => void` 콜백으로 전달하며, 부모가 값을 반영하지 않으면 표시값은 유지된다.
|
||||
- `controlled={false}`이면 내부 state가 소유권을 가진다. `value`는 사용하지 않고 `defaultValue`를 최초 마운트 시에만 적용한다. 이후 `defaultValue` 변경은 초기화가 아니다. 사용자 선택은 내부 state를 갱신하고 콜백을 호출한다.
|
||||
- **Legacy 호환**: `controlled`를 생략하면 기존 `value !== undefined` 판정을 유지한다. 문자열이 있으면 Controlled, `undefined`이면 Uncontrolled이다. 이 방식으로 외부 값을 `undefined`로 바꾸면 과거 내부 값/defaultValue가 다시 표시될 수 있다. 기존 소비 코드의 동작은 변경하지 않았다.
|
||||
- 신규 Controlled 사용과 FilterBar 연동은 `controlled={true}`를 사용한다. `controlled` 옵션과 값 소유권 모드는 마운트 중 변경하지 않는 사용 계약을 권장한다. Controlled에서 `defaultValue`를 함께 전달해도 부모 `value`가 우선한다.
|
||||
- `""`은 유효한 문자열 옵션 값이며 `undefined`와 동일한 빈 값으로 정규화하지 않는다. `value=""`는 빈 문자열 옵션이 있으면 그 label을 표시하고, 없으면 기존처럼 placeholder를 표시한다. 빈 문자열을 가진 항목을 선택하면 콜백은 `""`을 전달한다.
|
||||
|
||||
```tsx
|
||||
const [category, setCategory] = useState<string | undefined>()
|
||||
|
||||
<Combobox controlled={true} options={categories} value={category} onValueChange={setCategory} />
|
||||
// 외부 항목 지정/갱신: setCategory("design")
|
||||
// 외부 초기화: setCategory(undefined) — key remount 불필요
|
||||
```
|
||||
|
||||
외부 값 갱신/초기화는 `onValueChange`를 호출하지 않는다. FilterBar의 부모는 항목 state와 조회 조건을 함께 초기화한다. 선택값 clear는 팝업 닫기나 검색어 초기화를 의미하지 않는다. 열린 팝업의 검색어를 강제로 지우는 새 계약이나 clear UI는 추가하지 않는다. `controlled`를 생략한 Legacy 호출의 remount 우회는 이 신규 계약과 구분한다.
|
||||
|
||||
## 7. Dependency
|
||||
|
||||
새 dependency 없음(`Command`의 `cmdk`, `Popover`의 Radix를 그대로 사용).
|
||||
|
||||
## 8. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/combobox.css` (`--hm-combobox-*`). Trigger와 선택 check만 스타일하며 Popover와 Command의 스타일은 중복 정의하지 않는다.
|
||||
|
||||
`height-md|lg`, `border-width`, `padding-x`, `gap`, `radius`, `font-size|line-height-md|lg`, `background`, `text`, `placeholder`, `border-color`, `focus-border-color`, `icon-size|color`, `check-size`, `disabled-opacity`. Semantic/Theme palette를 참조하며 `--hm-popover-*`, `--hm-command-*`, `--hm-select-*`는 참조하지 않는다. 기본 모양은 `@layer components`(소비자의 `className`이 이김). Trigger에는 `min-width: 0; max-width: 100%`를 두어 긴 값이 grid/flex 부모를 넓히지 않고 말줄임된다.
|
||||
|
||||
**Popover 결합 지점**: Popup의 padding과 폭을 `PopoverContent`의 inline style로 `--hm-popover-padding: 0px`, `--hm-popover-width: var(--radix-popover-trigger-width)`로 지정해 Popover token 값을 인스턴스 단위로 덮는다(Popover 코드는 변경하지 않음).
|
||||
|
||||
## 9. 조합한 컴포넌트에서 유지한 항목
|
||||
|
||||
Popover: Portal, collision, Escape, 바깥 클릭 닫힘, trigger focus 복귀. Command: 필터링, 방향키/Home/End/Enter, disabled 건너뜀, Empty, listbox/option ARIA.
|
||||
|
||||
## 10. Storybook
|
||||
|
||||
- 대표 Story `All`("Combobox"): Empty placeholder + search(Remix는 disabled option), Selected(controlled, 값 표시), Medium, Long option, Disabled를 한 페이지에 둔다. Label은 Story 조합이다.
|
||||
- verification: `Empty`, `Controlled`, `Long Option`.
|
||||
|
||||
## 11. 사용자 확정 결정 / 보류
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다. Combobox는 최소 `options` wrapper로 제공한다(작업 지시 B안).
|
||||
- 보류: 수치(44/34px, check 아이콘 위치)는 현재 HM 규격이며 디자인 확정 시 재검토. clear 버튼, 다중 선택(chips), 그룹 option, 새 항목 생성, 비동기 검색, 편집 가능한 trigger(input형 combobox)는 만들지 않았다. 선택 option의 label 줄바꿈 표시(긴 옵션 전체 보기)는 말줄임으로 대체.
|
||||
- Popup 방향/collision은 Popover 기본(`align="start"`).
|
||||
- 2026-10-06 Visual QA: `combobox.css`의 `[data-combobox-popup]` scope로 Select의 시각 언어와 정합. Command/Popover 공용 CSS·동작은 유지. 대표 Story의 Select 비교 구역에서 두 size를 확인한다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/combobox.tsx
|
||||
packages/ui/src/theme/combobox.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/combobox.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-08 Controlled 초기화: 실제 소스를 직접 묶은 격리 Headless Edge 환경에서 수정 전 과거 값 재표시를 재현했다. `controlled={true}`의 undefined → A → B → undefined, 최초 Controlled 값 clear, 부모 선택 거부, 외부 값 갱신, 콜백 횟수, Legacy/defaultValue/명시적 Uncontrolled, 빈 문자열 옵션, 열린 팝업 clear와 검색어/focus 유지, 키보드 선택·disabled 제외·focus 복귀를 확인했다. 소스 기준 영향 범위 타입 검사 통과. 공용 dist / Storybook 빌드 및 전체 시각 회귀는 수행하지 않았다.
|
||||
|
||||
- 2026-10-06 Visual QA: 1280/1024/768/390px에서 popup 폭=trigger, gap 4px, 선택 면 지속, 검색/Empty/키보드 선택·focus 복귀·disabled 건너뜀·긴 label 말줄임 통과. Select 비교에서 Item 높이는 36/42px를 유지하고 Medium 14/20·Large 16/24 typography를 일치시켰다. Command/Popover 공용 변경 없음. `typecheck`, `build`, `build-storybook` 통과.
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): trigger에서 Enter로 열림(`role=combobox`, `aria-expanded` true, Popup 폭 = trigger 폭 320px), 검색 Input으로 focus 이동, 첫 항목 highlight, 검색 "nu" → Nuxt만 남아 highlight, Enter로 선택하면 trigger가 "Nuxt"로 바뀌고 Popover가 닫히며 focus가 trigger로 복귀. ArrowDown 이동에서 disabled `Remix` 건너뜀(Nuxt → Astro), Escape로 닫힘과 trigger focus 복귀(값 유지), 결과 없음 "zzz"는 Empty 문구, 다시 열면 검색어가 비어 있고 전체 5개 표시.
|
||||
- 선택 표시: 선택된 option에만 check 아이콘(opacity 1)과 `data-checked`. 클릭으로 선택하면 trigger와 controlled 값이 갱신됨.
|
||||
- Popover 스타일 확인: border `rgb(222,219,210)`, radius 4px, 그림자, padding 0, 흰 면(처음에 `data-slot` 덮어쓰기로 이 스타일이 빠진 문제를 수정함).
|
||||
- Responsive(390×844): trigger 320px, popup(304px)이 viewport 안에 들어감, 긴 option의 값과 항목이 말줄임, 페이지 가로 overflow 없음(처음에는 trigger의 `min-width:auto` 때문에 페이지가 833px로 밀리는 문제가 있어 `min-width:0`으로 수정).
|
||||
- 미확인: 클릭 선택 직후 닫힘 animation 시각, 포인터 hover 시각, 화면 하단에서 popup이 위로 flip 되는 경우, 스크린리더 실제 읽기.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Command
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Command 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태와 성격
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 성격 | **Primitive component** (검색 가능한 command/list). Combobox 자체가 아니다 |
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Command = **cmdk** |
|
||||
| 대표 Story | `Components/Search & Date/Command` → `All` (이름 "Command") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Command가 없다(Search Field의 후보 목록은 다른 컴포넌트). 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다. Input·Popover·Dropdown Menu Token은 참조하지 않는다(Command Input은 Command 전용 Token).
|
||||
- **구현 방식**: cmdk가 검색 필터링, 방향키/Home/End 이동, Enter 선택, disabled 건너뜀, 그룹, Empty, listbox/option ARIA를 소유한다. HM은 cmdk의 속성(`data-selected`, `data-disabled`, `[cmdk-group-heading]`)에 시각을 연결한다.
|
||||
- **범위**: 전역 Command Palette(`CommandDialog`)는 만들지 않았다(별도 고급 패턴).
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Command | 흰 면(`--hm-sem-bg-surface`), 글자 `--hm-sem-fg-primary`, radius 4px. **border·shadow 없음**(Popover 등 컨테이너 안에서 쓰는 것을 전제) |
|
||||
| Input | 높이 40px, padding-x 12px, 14 / 20, 아래 1px `--hm-sem-stroke-input-default` 구분선, 검색 아이콘 16px `--hm-sem-fg-secondary`, placeholder `--hm-sem-fg-placeholder` |
|
||||
| List | 최대 높이 280px(넘치면 세로 scroll), padding 4px |
|
||||
| Item | 최소 높이 36px, padding-x 12px, gap 8px, radius 2px, 14 / 20, 아이콘 16px |
|
||||
| Group heading | 12 / 18 / 500, `--hm-sem-fg-secondary`, padding `6px 12px` |
|
||||
| Separator | 1px `--hm-sem-stroke-input-default`, List 폭 전체 |
|
||||
| Shortcut | 12px, `--hm-sem-fg-secondary`, letter-spacing .05em |
|
||||
| Empty | padding 24px 12px, 가운데, `--hm-sem-fg-secondary` |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Highlighted (`data-selected="true"`) | 배경 Accent 45%(`--hm-sem-surface-highlight`), 글자 `--accent-foreground`. 포인터 hover와 키보드 이동이 공유 |
|
||||
| Disabled (`data-disabled="true"`) | opacity 0.6(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), cursor not-allowed, 방향키 이동에서 제외 |
|
||||
| Empty | 필터 결과가 없을 때 `CommandEmpty` 표시 |
|
||||
| Focus | Input에 있음(`outline-hidden`). 이동은 Highlighted 면으로 표시 |
|
||||
|
||||
## 5. API
|
||||
|
||||
cmdk props 그대로. export: `Command`, `CommandInput`, `CommandList`, `CommandEmpty`, `CommandGroup`, `CommandItem`, `CommandSeparator`, `CommandShortcut`.
|
||||
|
||||
- `Command`: `value`/`onValueChange`(highlight), `filter`(사용자 정의 필터), `shouldFilter`, `loop`, `label`.
|
||||
- `CommandItem`: `value`, `keywords`, `disabled`, `onSelect`.
|
||||
- `CommandInput`: Input + 검색 아이콘 wrapper(`data-slot="command-input-wrapper"`)를 포함한다.
|
||||
- **필터링은 cmdk 기본(fuzzy) 그대로**이며 HM이 검색 알고리즘을 만들지 않았다.
|
||||
- 접근성: `CommandInput`은 `role="combobox"`, `aria-expanded`, `aria-controls`(listbox), `aria-autocomplete="list"`; `CommandList`는 `role="listbox"`; `CommandItem`은 `role="option"`.
|
||||
|
||||
## 6. Dependency
|
||||
|
||||
- `cmdk` ^1.1.1 (`packages/ui`에 추가, shadcn Command의 기반). CENTER 패키지에는 추가하지 않았다.
|
||||
|
||||
## 7. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/command.css` (`--hm-command-*`).
|
||||
|
||||
`background`, `text`, `radius`, `input-*`(height, padding-x, gap, font, placeholder, icon), `divider-color`, `list-max-height|padding`, `item-*`(height, padding-x, gap, radius, font), `highlight-background|text`, `group-label-*`, `shortcut-*`, `empty-*`, `disabled-opacity`. Semantic과 Primitive(`--hm-warm-neutral-pale`)만 참조한다. 신규 Primitive와 Semantic 없음. 이 CSS는 cmdk 내부 속성에 맞춘 일반 CSS이며 `@layer`를 쓰지 않는다(`className`으로 Command의 높이 등을 바꾸는 것은 TSX class 영역).
|
||||
|
||||
## 8. cmdk에서 유지한 항목
|
||||
|
||||
필터링과 정렬, 키보드(ArrowUp/Down, Home/End, Enter, disabled 건너뜀), 그룹 숨김(결과 없는 그룹), Empty, `data-selected`/`data-disabled`, listbox/option ARIA, 마우스 hover가 highlight를 옮기는 동작.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Command"): Search + disabled item(선택 로그), Groups·icons·shortcut(`CalendarIcon`, `UserIcon`, `SettingsIcon`), Long list(30개, List가 scroll)를 한 페이지에 둔다. 테두리 박스는 Story 조합이다.
|
||||
- verification: `Search`, `Grouped`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(40px Input, 36px Item, 280px List 최대 높이)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Command Palette(`CommandDialog`, 전역 단축키 호출)는 별도 패턴으로 남겼다.
|
||||
- 로딩 상태(`Command.Loading`), 중첩 페이지는 스타일링하지 않았다.
|
||||
- 선택됨(value) 표시는 Command에 없다(highlight와 다르다). Combobox가 check 아이콘으로 따로 표시한다.
|
||||
- Command의 Escape는 Command 자체에서는 하는 일이 없다(Popover/Dialog 안에서는 컨테이너가 닫는다).
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/command.tsx
|
||||
packages/ui/src/theme/command.css
|
||||
packages/ui/package.json (dependency: cmdk)
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/command.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- ARIA(Browser pane): Input `role=combobox`·`aria-expanded=true`·`aria-controls`·`aria-autocomplete=list`, List `role=listbox`, Item `role=option`, disabled Item에 `data-disabled="true"`.
|
||||
- keyboard: 첫 항목이 처음부터 highlight, ArrowDown으로 이동(비활성 항목 건너뜀: 바나나 → 포도), Enter로 선택(`onSelect` 로그 갱신), 그룹 간 이동(ArrowUp 환경설정 → 프로필), End로 마지막 활성 항목(비활성 `결제`는 제외).
|
||||
- 검색: 한글 "바나" 입력 시 바나나만 표시, 결과 없는 "zzz"는 Empty 문구, 입력을 지우면 전체 4개로 복귀하고 첫 항목이 highlight. Escape는 Input focus를 유지(Command 단독에서는 동작 없음).
|
||||
- computed style: Input 래퍼 40px, List 최대 280px·padding 4px, Item 36px·padding `0 12px`·14/20, Highlighted `rgb(189,223,212)`, Group heading 12px/500 `rgb(110,103,89)`, Shortcut 12px.
|
||||
- 미확인: 긴 List의 키보드 scroll 시각, 모바일 폭(Combobox 안에서만 확인), pointer hover 시각, `CommandItem`의 `keywords` 외 사용자 정의 `filter`.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Context Menu
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Context Menu 고유의 API·Token·상태·결정만 기록한다. Dropdown Menu와 같은 메뉴 계열이므로 상세 값은 `dropdown-menu.md`와 대응한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-03 |
|
||||
| 기반 | shadcn/ui Context Menu (Radix `ContextMenu`, `radix-ui` 1.6.7 / `@radix-ui/react-context-menu` 2.3.7) |
|
||||
| 대표 Story | `Components/Overlay/Context Menu` → `All` (이름 "Context Menu") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz에는 Context Menu 규격이 없다. 최종 기준은 현재 `@hm-design/ui` 구현과 Dropdown Menu 문서다.
|
||||
- **현재 HM 규격**: 면·윤곽·radius·그림자는 Floating Surface Semantic(`--hm-sem-floating-*`)을 source로 쓰고, item·text 값은 Dropdown Menu와 같은 현재 HM 값이다.
|
||||
- **구현 방식**: shadcn / Radix Context Menu 구조 유지. 상태는 Radix 속성(`data-highlighted`, `data-disabled`, `data-state`)만 쓴다.
|
||||
|
||||
## 3. Dropdown Menu와 역할 차이
|
||||
|
||||
| | Dropdown Menu | Context Menu |
|
||||
|---|---|---|
|
||||
| 여는 방법 | 명시적 버튼(Trigger) 클릭 / Enter / Space / 방향키 | 대상 영역 우클릭, context-menu key / Shift+F10, 터치 long press |
|
||||
| 위치 | Trigger 기준 anchored (`sideOffset` 4) | 포인터 위치 기준 (`sideOffset` 없음) |
|
||||
| 용도 | 버튼이 가진 Action 목록 | 대상 객체에 대한 보조 Action |
|
||||
| 닫힐 때 focus | Trigger로 복귀 | Trigger가 포커스 가능한 요소일 때만 복귀(§7) |
|
||||
|
||||
브라우저 native context menu 대신 HM Context Menu가 열린다(Radix가 `contextmenu` 이벤트를 막음).
|
||||
|
||||
## 4. 기본 규격 / 상태 규격
|
||||
|
||||
`dropdown-menu.md` §3·§4와 값이 같다(Surface White, 1px pale border, radius 4px, 12px glow 그림자, item 36px·14/20, Highlighted Accent 45%(`--hm-sem-surface-highlight`), Disabled opacity 50, Destructive `--hm-sem-action-destructive`, Check 16px / Radio 8px). 값이 같아도 `--hm-dropdown-menu-*`는 참조하지 않는다.
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Context Menu props 그대로. export: `ContextMenu`, `ContextMenuTrigger`, `ContextMenuContent`, `ContextMenuGroup`, `ContextMenuLabel`, `ContextMenuItem`, `ContextMenuCheckboxItem`, `ContextMenuRadioGroup`, `ContextMenuRadioItem`, `ContextMenuSeparator`, `ContextMenuShortcut`, `ContextMenuSub`, `ContextMenuSubTrigger`, `ContextMenuSubContent`, `ContextMenuPortal`.
|
||||
|
||||
- `ContextMenuItem`: shadcn `variant="default" | "destructive"`, `inset`. Label·CheckboxItem·RadioItem·SubTrigger: `inset`.
|
||||
- 출력 속성: `data-slot="context-menu-*"`, Radix `data-state`·`data-highlighted`·`data-disabled`·`data-side`.
|
||||
- HM 전용 prop 없음.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/context-menu.css` (`--hm-context-menu-*`).
|
||||
|
||||
- Surface 5개(`background`, `border-width`, `border-color`, `radius`, `shadow`)는 **Floating Surface Semantic을 source로 참조**한다: `--hm-sem-floating-background|border-width|border-color|radius|shadow`.
|
||||
- 나머지(`text`, `padding`, `item-*`, `font-*`, `highlight-*`, `destructive-*`, `label-*`, `shortcut-*`, `separator-color`, `indicator-size`, `radio-dot-size`)는 Dropdown Menu와 같은 현재 HM 값을 자체 정의한다.
|
||||
- 다른 컴포넌트의 Component Token을 쓰지 않는다. 신규 Primitive·Semantic 없음. 기존 Floating Semantic을 유지한다.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`role=menu`, `menuitem*`)·키보드(방향키, Home/End, Enter/Space, 오른쪽 방향키 sub menu, Escape, typeahead)·Portal·collision·`data-slot`·`cn(..., className)`·open/close animation class·`z-50`·lucide 아이콘.
|
||||
|
||||
**접근성 주의**: Radix `ContextMenuTrigger`는 기본적으로 포커스할 수 없는 `span`이다. 키보드 사용자가 여는 경로(Shift+F10 / context-menu key)를 제공하려면 소비자가 Trigger 영역에 `tabIndex={0}`을 주거나 `asChild`로 포커스 가능한 요소(버튼, 행 등)를 감싸야 한다. 우클릭만으로 접근 가능한 기능을 만들지 않는다. 대표 Story의 영역은 `tabIndex={0}`을 준다.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
Dropdown Menu와 동일: shadcn 시각 class(`rounded`, `shadow`, `p-1`, `focus:bg-accent` 등)를 제거하고 theme CSS + `data-highlighted`로 대체. Radio는 CSS 크기 token을 쓰는 원(`context-menu-radio-dot`), Check / Radio indicator span에 `data-slot="context-menu-item-indicator"` 추가. Context Menu에는 `sideOffset`이 없다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Context Menu"): 점선 영역을 우클릭하면 전체 메뉴가 열린다(Label, Item + Shortcut, disabled, Separator, Checkbox, Radio Group, Sub menu, destructive).
|
||||
- verification: `Open`(로드 시 합성 `contextmenu` 이벤트로 열린 상태, fullscreen).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-03: Context Menu는 Dropdown Menu와 같은 floating menu 계열이므로 Floating Surface Semantic을 Component Token의 source로 쓴다. Component Token은 유지하고 `--hm-dropdown-menu-*`는 참조하지 않는다(작업 지시).
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치는 Dropdown Menu와 같은 현재 HM 규격이며 디자인 확정 시 함께 재검토.
|
||||
- Home/End·typeahead·long press(터치)는 확인하지 못했다.
|
||||
- Shift+F10 / context-menu key는 자동화 입력이 브라우저의 native `contextmenu` 이벤트를 만들지 못해 확인하지 못했다(Radix는 이 이벤트로 여는 구조이며 우클릭과 같은 경로).
|
||||
- 메뉴가 닫힌 뒤 Trigger가 포커스 불가면 focus는 body에 남는다(Radix 동작).
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/context-menu.tsx
|
||||
packages/ui/src/theme/context-menu.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/context-menu.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-03).
|
||||
- 실제 interaction(Browser pane): 우클릭 시 native 메뉴 대신 HM 메뉴가 열림, 방향키 이동(disabled 항목 건너뜀), Enter로 Checkbox 토글(메뉴 유지), 방향키 아래 + 오른쪽으로 Sub menu 열림과 첫 항목 focus, Escape 두 번으로 sub → menu 순서 닫힘.
|
||||
- computed style: Content·Sub Content 모두 white, `1px solid rgb(222,219,210)`, radius 4px, `0 0 12px .2` 그림자, item 36px, Dropdown Menu(`Open` Story) 값과 동일. role `menu`.
|
||||
- Dropdown Menu의 같은 값이 변하지 않음을 확인(회귀 없음).
|
||||
@@ -0,0 +1,142 @@
|
||||
# Date Picker
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Date Picker 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태와 성격
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 성격 | **Reusable component, Popover + Calendar 조합** (새 날짜 engine 없음) |
|
||||
| 구현 상태 | 구현 완료 (single-date, 현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | `Popover`(public API) + `Calendar`(public API) 조합. shadcn 공식 Date Picker는 예시 조합이다 |
|
||||
| 대표 Story | `Components/Search & Date/Date Picker` → `All` (이름 "Date Picker") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Date Picker가 이름뿐이고 규격이 없다. 참고할 수치가 없었다.
|
||||
- **Component로 만든 이유**: 서비스의 모든 날짜 선택이 같은 trigger 텍스트 형식, 선택 후 닫힘, 값 처리, focus 복귀를 필요로 해 반복 사용이 확실하다고 판단했다. 값 처리(`value`/`defaultValue`/`onValueChange`)는 패키지가 소유한다.
|
||||
- **구현 방식**: Popover(Portal, collision, Escape, focus 복귀)와 Calendar(날짜 모델, 키보드)를 그대로 조합한다. Popover 내부 구현을 복제하지 않았다.
|
||||
- **Trigger**: Button이 아니라 Input/Select의 field language를 따르는 필드형 버튼이다. Button/Input/Select Token은 참조하지 않고 `--hm-date-picker-*`로 관리한다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Size | `large`(기본, 안쪽 42px + 1px border = 44px, 16 / 24), `medium`(안쪽 32px = 34px, 14 / 20) — Select와 같은 두 크기 |
|
||||
| Trigger | 폭 100%(부모 폭), padding-x 12px, 1px `--hm-sem-stroke-input-default`, radius 4px, 흰 면, 오른쪽 달력 아이콘 18px `--hm-sem-fg-secondary` |
|
||||
| 값 / placeholder | 선택값은 `--hm-sem-fg-primary`, placeholder("날짜 선택")는 `--hm-sem-fg-placeholder`, 길면 말줄임 |
|
||||
| 날짜 표시 | 로컬 `Date`의 연·월·일 → `YYYY.MM.DD` (예: `2026.01.09`, `2026.10.06`). `format` prop으로 교체 |
|
||||
| Popup | Popover 면(흰 면, 1px pale border, 4px radius, 그림자)에 Popover padding 0, 폭 `auto`로 Calendar에 맞춤 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Empty / Selected | `data-empty`가 있으면 placeholder 색 |
|
||||
| Focus / Open | 테두리 전체가 `--hm-sem-stroke-input-focus`(ring 없음, Input·Select와 같은 방식). 열림은 Radix `data-state="open"` |
|
||||
| Invalid (`aria-invalid="true"`) | Trigger border 1px `--hm-sem-action-destructive`. 새 `invalid` / `error` / `status` prop 없음 |
|
||||
| Invalid + Keyboard Focus (`:focus-visible`) | Destructive border 유지 + Focus Semantic 3px / 50% outer ring |
|
||||
| Invalid + Open | Trigger Error border 유지. Calendar Selected는 기존 Active, 날짜 Keyboard Focus는 기존 Focus Semantic 유지 |
|
||||
| Disabled | opacity 0.6(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), cursor not-allowed, 열리지 않음 |
|
||||
| 선택 후 | 값 갱신, Popover 닫힘, focus가 trigger로 복귀 |
|
||||
|
||||
## 5. API
|
||||
|
||||
export: `DatePicker`, type `DatePickerSize`.
|
||||
|
||||
- `value`/`defaultValue`(Date), `onValueChange(date)`, `placeholder`, `size` `"medium" | "large"`(기본 `large`), `disabled`, `format(date) => string`, `calendarProps`(`disabled`, `startMonth`, `endMonth`, `locale`, `captionLayout`, `weekStartsOn`, `today`), 그리고 `button` props(`id`, `aria-label`, `className`…).
|
||||
- 선택은 `required`라 선택된 날짜를 다시 눌러도 해제되지 않는다(해제/clear UI는 없음).
|
||||
- **접근성**: Trigger는 `button`에 Radix Popover의 `aria-haspopup="dialog"`, `aria-expanded`, 팝업은 `role="dialog"`. 접근 가능한 이름은 `<label htmlFor=id>`(Story) 또는 `aria-label`을 소비자가 준다. 선택값은 버튼 텍스트로 읽힌다.
|
||||
- Popover가 열릴 때 Radix 기본은 첫 tabbable(이전 달 버튼)에 focus하므로, DatePicker가 `onOpenAutoFocus`로 **Calendar의 tab stop(선택일, 없으면 오늘)에 focus**를 보낸다.
|
||||
- 범위(range) Date Picker는 만들지 않았다(필요가 명확해질 때 추가).
|
||||
|
||||
### 값 소유권과 외부 초기화
|
||||
|
||||
- `controlled?: boolean`은 선택적 값 소유권 설정이다. `true`이면 `value`가 `undefined`여도 Controlled이며, 부모 값만 표시한다. `defaultValue`는 표시값을 대신하지 않는다. 사용자 선택은 기존 `(Date | undefined) => void` 콜백으로 전달하며, 부모가 값을 반영하지 않으면 표시값은 유지된다.
|
||||
- `controlled={false}`이면 내부 state가 소유권을 가진다. `value`는 사용하지 않고 `defaultValue`를 최초 마운트 시에만 적용한다. 이후 `defaultValue` 변경은 초기화가 아니다. 사용자 선택은 내부 state를 갱신하고 콜백을 호출한다.
|
||||
- **Legacy 호환**: `controlled`를 생략하면 기존 `value !== undefined` 판정을 유지한다. 날짜가 있으면 Controlled, `undefined`이면 Uncontrolled이다. 이 방식으로 외부 값을 `undefined`로 바꾸면 과거 내부 값/defaultValue가 다시 표시될 수 있다. 기존 소비 코드의 동작은 변경하지 않았다.
|
||||
- 신규 Controlled 사용과 FilterBar 연동은 `controlled={true}`를 사용한다. `controlled` 옵션과 값 소유권 모드는 마운트 중 변경하지 않는 사용 계약을 권장한다. Controlled에서 `defaultValue`를 함께 전달해도 부모 `value`가 우선한다.
|
||||
|
||||
```tsx
|
||||
const [date, setDate] = useState<Date | undefined>()
|
||||
|
||||
<DatePicker controlled={true} value={date} onValueChange={setDate} />
|
||||
// 외부 날짜 지정/갱신: setDate(new Date(2026, 9, 6))
|
||||
// 외부 초기화: setDate(undefined) — key remount 불필요
|
||||
```
|
||||
|
||||
외부 값 갱신/초기화는 `onValueChange`를 호출하지 않는다. FilterBar의 부모는 날짜 state와 조회 조건을 함께 초기화한다. 선택값 clear는 팝업 닫기나 달력의 현재 월/focus 초기화를 의미하지 않으며, 새 clear UI를 추가하지 않는다. `controlled`를 생략한 Legacy 호출의 remount 우회는 이 신규 계약과 구분한다.
|
||||
|
||||
## 6. Dependency
|
||||
|
||||
새 dependency 없음(`Calendar`의 `react-day-picker`, `Popover`의 Radix를 그대로 사용). 날짜 표시는 `Intl.DateTimeFormat`, 값은 native `Date`이며 별도 date 라이브러리를 추가하지 않았다.
|
||||
|
||||
## 7. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/date-picker.css` (`--hm-date-picker-*`). Trigger만 스타일하며 Popover와 Calendar의 스타일은 중복 정의하지 않는다. 선택일 색은 Calendar Selected(`--hm-sem-state-active`)를 그대로 상속하며 Date Picker 전용 selected 색이나 scope override는 없다.
|
||||
|
||||
`height-md|lg`, `border-width`, `padding-x`, `gap`, `radius`, `font-size|line-height-md|lg`, `background`, `text`, `placeholder`, `border-color`, `focus-border-color`, `icon-size|color`, `disabled-opacity`. Semantic/Theme palette를 참조하며 `--hm-popover-*`와 `--hm-calendar-*`는 참조하지 않는다. 기본 모양은 `@layer components`에 두어 소비자의 `className`(폭, 높이)이 이긴다.
|
||||
|
||||
**Popover 결합 지점**: Popup의 padding과 폭을 `PopoverContent`의 inline style로 `--hm-popover-padding: 0px`, `--hm-popover-width: auto`를 지정해 Popover의 token 값을 인스턴스 단위로 덮는다. Popover 코드를 바꾸지 않는 선택이며, Popover token 이름에 대한 약한 결합이다.
|
||||
|
||||
## 8. 조합한 컴포넌트에서 유지한 항목
|
||||
|
||||
Popover: Portal, collision 처리, Escape, 바깥 클릭 닫힘, trigger focus 복귀, `role=dialog`. Calendar: 키보드(방향키, Enter/Space, PageUp/Down), disabled 날짜, 월 이동, locale(기본 한국어).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Date Picker"): Empty, Selected(`2026.10.06`), 기본 형식(`2026.01.09`)/custom override, Controlled(값 표시), Medium, Disabled dates(5일 이전·일요일), Disabled를 한 페이지에 둔다. Label은 Story 조합이며 `today`는 Calendar prop으로 2026-10-02에 고정.
|
||||
- verification: `Empty`, `Selected`, `Controlled`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다. Date Picker는 실제 소비 API가 필요한 Component로 만든다(작업 지시).
|
||||
- 2026-10-06 Visual QA: Trigger 기본 formatter만 `YYYY.MM.DD`로 변경. Calendar 날짜 셀·caption·locale, custom `format`은 유지.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 기본 표시 형식은 `YYYY.MM.DD`로 확정했다. 다른 형식은 `format`으로 교체한다.
|
||||
- 선택 해제(clear) 버튼, 직접 입력(typing) 입력, Range Date Picker, 시간 선택은 만들지 않았다.
|
||||
- 타임존: native `Date`의 로컬 시간 기준이다.
|
||||
- Popup의 방향/collision은 Popover 기본(`align="start"`, 아래쪽 우선).
|
||||
- 좁은 화면 전용 picker(바텀시트 등)는 만들지 않았다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/date-picker.tsx
|
||||
packages/ui/src/theme/date-picker.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/date-picker.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-08 Controlled 초기화: 실제 소스를 직접 묶은 격리 Headless Edge 환경에서 수정 전 과거 값 재표시를 재현했다. `controlled={true}`의 undefined → A → B → undefined, 최초 Controlled 값 clear, 부모 선택 거부, 외부 값 갱신, 콜백 횟수, Legacy/defaultValue/명시적 Uncontrolled, 열린 팝업 clear, 키보드 날짜 선택·focus 복귀를 확인했다. 소스 기준 영향 범위 타입 검사 통과. 공용 dist / Storybook 빌드 및 전체 시각 회귀는 수행하지 않았다.
|
||||
|
||||
- 2026-10-06 Visual QA: 1280/1024/768/390px에서 `2026.10.06`, `2026.01.09`, custom override, Korean caption, 키보드 날짜 선택과 focus 복귀 통과. `typecheck`, `build`, `build-storybook` 통과. 아래 2026-10-02 interaction의 긴 한국어 날짜 표시는 당시 기록이다.
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): trigger에서 Enter로 열림(`aria-haspopup=dialog`, `aria-expanded` true), 선택된 날짜/오늘에 focus, ArrowRight 2 → 3, Enter로 선택하면 trigger 텍스트가 `2026년 10월 3일`로 바뀌고 Popover가 닫히며 focus가 trigger로 복귀(controlled 값 `2026. 10. 3.`). 다시 열면 선택일(3)에 focus, Escape로 닫혀도 값은 유지되고 trigger로 복귀, 날짜 클릭(20)으로도 선택과 닫힘. Disabled trigger는 클릭해도 열리지 않음.
|
||||
- computed style: Large 44px / 16·24, Medium 34px / 14, border `rgb(222,219,210)`, radius 4px, placeholder `rgb(153,153,153)`, Disabled opacity 0.6·cursor not-allowed.
|
||||
- Responsive(390×844): trigger 폭 320px, popup 264px(왼쪽 23px)이 viewport 안에 들어가고 페이지 가로 overflow 없음.
|
||||
- 수정한 문제: 처음에 `PopoverContent`에 `data-slot="date-picker-content"`를 넘겨 Popover의 `data-slot`을 덮어쓰는 바람에 Popover 스타일(윤곽, 그림자)이 빠졌다. 제거했다.
|
||||
- 미확인: Disabled dates가 popup 안에서 선택 불가한 것(Calendar 문서의 검증으로 갈음), 포인터 hover 시각, 좁은 viewport의 collision flip, `calendarProps.locale` 교체.
|
||||
|
||||
- 숨김 `InvalidFocus` Story (`tags: ["verification"]`)를 추가하여 기본 Sidebar는 유지한다.
|
||||
|
||||
|
||||
### Invalid + Focus 검증 (2026-10-07)
|
||||
|
||||
- Focus ring: `--ring` → `--hm-sem-stroke-input-focus` → `--hm-color-border-focus`. Invalid border: 기존 `--hm-sem-action-destructive`. 새 Runtime Role / Production Error prop / 메시지 컴포넌트 없음.
|
||||
- Chromium computed style: Invalid border `rgb(211, 47, 47)`, Invalid + Keyboard Focus box-shadow `color(srgb 0.14902 0.4 0.317647 / 0.5) 0px 0px 0px 3px`, `outline-style: none`. Invalid 단독은 `box-shadow: none`.
|
||||
- Theme Designer UI에서 Focus `#c75b00`, Destructive `#d41466`, Active `#007f73`, Accent `#ffd36d`로 분리. Invalid border `rgb(212, 20, 102)` + Focus shadow `color(srgb 0.780392 0.356863 0 / 0.5) 0px 0px 0px 3px` 확인.
|
||||
- `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. 기존 문서 예시 CSS warning 및 Storybook chunk 크기 / plugin timing warning 유지.
|
||||
- 검증 범위: Chromium 환경. 다른 브라우저의 렌더링은 별도 확인이 필요하다.
|
||||
- 숨김 `InvalidFocus`의 실제 Trigger DOM `aria-invalid="true"` 확인. Trigger Invalid border computed 1px, Focus 전후 shadow none → 3px Focus ring. 대표 `All` Default / Selected / Medium / Disabled 유지.
|
||||
- Enter / Space open, ArrowRight 날짜 이동, Enter 선택 후 닫힘 및 Trigger focus return, Escape close / focus return, Tab / Shift+Tab 확인. 2026.10.06 → 2026.10.07 선택값 표시 확인.
|
||||
- Calendar Selected `rgb(0, 127, 115)`(Active), 선택 날짜 Focus `color(srgb 0.780392 0.356863 0 / 0.5) 0px 0px 0px 3px` 유지. 일반 Calendar 대표 Story의 Single 및 Range 끝점은 Active, Range 중간 면은 Accent 45% 유지. Calendar CSS/API 수정 없음.
|
||||
@@ -0,0 +1,156 @@
|
||||
# Dialog
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Dialog 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
> 근거 문서: ui_viz `docs/components/dialog.md`(Dialog / Modal). 문서가 스스로 `NO_SOURCE_EVIDENCE`(Source Dashboard에 Dialog specimen 없음)라고 밝히며, 값의 상당수는 D-093(2026-09-29) Dialog-local Confirmed다. Foundation `elevation.*`, `z-index.*`, `motion.*`는 TBD이고 Dialog 범위 값을 Foundation token으로 올리지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (`<DialogContent />`만으로 적용, `hm-dialog` class 없음) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Dialog + Radix `Dialog` |
|
||||
| 대표 Story | `Components/Overlay/Dialog` → `All` (이름 "Dialog") |
|
||||
| 보류 | Overlay 수치, Close 버튼 규격, 초기 focus 대상, 좁은 화면 Actions 배치 등 (§11) |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **ui_viz Confirmed (Dialog-local)**: radius 8px(USER_DECISION 2026-09-17), shadow `0 0 12px rgba(0,0,0,.2)`(D-093 D-088), 내용 기준 크기와 최대 너비·높이 공식(D-088), Header `24/24/16`·Title↔Description 8·Body 24·Actions `0/24/24`·Action gap 8(D-093), Header 아래 inset divider 위치·두께와 Footer divider 없음, Body만 scroll, 긴 텍스트 줄바꿈, 표는 Body 안 가로 scroll, 표면 `color.bg.surface`·전경 `color.fg.primary`, Title/Description/Body 타이포.
|
||||
- **CANONICAL_OVERRIDE**: Title `heading-md` 20/28 중 weight **700**은 Dialog Title에만 적용(D-006, 600 대체). Foundation `heading-md`는 600 그대로.
|
||||
- **현재 HM Actions**: Footer padding은 `16px 24px 24px`이며 divider는 없다. 위 ui_viz 도입 근거의 `0/24/24`와 구분한다.
|
||||
- **Temporary / TBD(적용했으나 확정값 아님)**: 외곽선·Header divider 색 `#DEDBD2`는 `Temporary`, Token relation TBD(DIALOG-10 FOLLOW_UP). 이전 확정 색(`#9A9284`/`#E0DACE`)은 대체됐고 D-093 실행 화면과 같은 값이라 현재 구현을 따랐다. 색 역할이 정해지기 전까지 Dialog 범위 값이다.
|
||||
- **PARTIAL / REFERENCE_ONLY(적용)**: Description 전경 `color.fg.secondary`(DIALOG-06 PARTIAL, 현재 style 보존). edge 16px(REVIEW).
|
||||
- **REFERENCE_ONLY(미적용)**: Backdrop Black 42%. 확정값이 아니어서 shadcn 기본 Overlay를 유지했다.
|
||||
- **HM 사용자 결정**: 없음.
|
||||
- **shadcn 유지**: Overlay, 열림/닫힘 animation, Close 버튼, Footer `showCloseButton`, `showCloseButton` prop.
|
||||
- **Radix 유지**: 아래 §7.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Overlay | shadcn 기본(`bg-black/10`, `backdrop-blur-xs`, `fixed inset-0`, z-50) | ui_viz backdrop 수치 TBD/REFERENCE_ONLY |
|
||||
| Container 크기 | 내용 기준(`width: max-content` + max-width = `min(내용, 최대 너비)`). `fit-content`는 shadcn `left: 50%` 배치에서 가용 폭이 화면 절반이 되어 쓰지 않는다(2026-10-06) | Confirmed D-088 |
|
||||
| max-width | `100vw − 2 × edge(16px)` | Confirmed 공식 / edge는 REVIEW |
|
||||
| max-height | `100dvh − app header − app footer − 2 × edge` | Confirmed 공식, header/footer 기본 0 |
|
||||
| 위치 | 가용 영역(앱 header·footer 제외) 가운데 | Confirmed |
|
||||
| Background / 글자 | `--hm-sem-bg-surface` / `--hm-sem-fg-primary` | Confirmed reuse |
|
||||
| Border | 1px solid `#DEDBD2` | 두께 Confirmed / 색 Temporary |
|
||||
| Radius | 8px (Container만) | Confirmed |
|
||||
| Shadow | `0 0 12px rgb(0 0 0 / 0.2)` | Confirmed (Dialog-local) |
|
||||
| Container padding | 0 (영역별 padding) | 구조 |
|
||||
| Header | padding `24px 0 15px` + 아래 1px divider `#DEDBD2`(= ui_viz `24/24/16`, 선은 16px 안쪽 마지막 1px), 좌우 margin 24px(inset 24px) | Confirmed 위치·두께 / 색 Temporary |
|
||||
| Title ↔ Description | gap 8px (Description이 없으면 없음) | Confirmed |
|
||||
| Title | 20px / 28px / 700, `fg.primary` | Confirmed (Dialog-local) |
|
||||
| Description | 14px / 20px / 400, `fg.secondary` | 크기 Confirmed / 색 PARTIAL |
|
||||
| Body | padding 24px 사방, 16px / 24px / 400, 첫·마지막 child margin 0 | Confirmed |
|
||||
| Footer(Actions) | padding `16px 24px 24px`, gap 8px, 오른쪽 정렬, 위 divider 없음 | 현재 HM 구현 |
|
||||
| 긴 텍스트 | `overflow-wrap: anywhere` | Confirmed |
|
||||
| Close 버튼 | shadcn 기본 (ghost `icon-sm` 28px, 오른쪽 위 8px 안쪽, `XIcon`, sr-only "Close") | ui_viz 미정의 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Closed / Open | Radix `data-state` (`open`/`closed`). Open 시 modal, 배경 inert, scroll lock | Radix |
|
||||
| Open animation | shadcn 기본(fade + zoom, 100ms) | ui_viz motion TBD → shadcn 유지 |
|
||||
| Focus | 전역 focus ring은 ui_viz TBD. 조합한 Button이 각자의 focus를 가짐 | TBD |
|
||||
| Long Content | Header·Footer 고정, Body만 세로 scroll, 짧으면 scrollbar 없음 | Confirmed |
|
||||
| Wide Content | Body 안 가로 scroll, 페이지 가로 overflow 없음 | Confirmed |
|
||||
| Confirmation / Destructive | 컴포넌트 상태가 아니라 Button 조합: 일반 확인은 취소 `cancel`+`solid` / 확인 `primary`+`solid`, 삭제 확인은 취소 `cancel`+`outline` / 삭제 `destructive`+`outline`. 같은 세트의 appearance·size 통일, Brand 금지 | HM 사용자 확정 Button 세트 규칙 |
|
||||
|
||||
Loading, Selected, Disabled 등 Dialog 자체의 다른 상태는 ui_viz에 없어 만들지 않았다.
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
Dialog, DialogTrigger, DialogPortal, DialogOverlay, DialogContent,
|
||||
DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose:
|
||||
shadcn / Radix API 그대로 (open, defaultOpen, onOpenChange, modal, asChild, className ...)
|
||||
|
||||
DialogBody:
|
||||
(신규) <div data-slot="dialog-body"> — ui_viz Body region
|
||||
|
||||
DialogClose:
|
||||
shadcn: 항상 data-slot="dialog-close"
|
||||
→
|
||||
HM: asChild일 때는 data-slot을 붙이지 않음 (자식 Button의 data-slot="button" 유지)
|
||||
```
|
||||
|
||||
- `DialogContent`의 `showCloseButton`(기본 true), `DialogFooter`의 `showCloseButton`(기본 false)은 shadcn 그대로.
|
||||
- 새 prop(`size`, `variant`, `destructive`, `loading`, `placement`, `dismissible`)은 근거가 없어 만들지 않았다.
|
||||
- `DialogBody`를 추가한 이유: ui_viz가 Body를 필수 region이자 유일한 scroll owner로 정의하기 때문이다. Body 없이도 동작하지만(자식이 그대로 놓임) 이 경우 Body padding과 내부 scroll이 적용되지 않는다.
|
||||
- `DialogClose` 변경 이유: shadcn은 `DialogClose asChild`의 `data-slot="dialog-close"`가 자식 `<Button>`의 `data-slot="button"`을 덮어써 HM Button 스타일(`[data-slot="button"]`)이 사라진다. Dialog Actions는 Button 조합이 규격(§4)이라 Dialog 쪽에서 고쳤다.
|
||||
- Actions의 Button에는 appearance를 명시한다. Button 기본값은 Brand이며, Destructive는 Outline만 지원한다. [Button 허용 조합·기본값 주의](button.md#61-허용-role--appearance-ai-reference)를 따른다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/dialog.css` (`--hm-dialog-*`), `packages/ui/src/theme.css`에서 import.
|
||||
|
||||
- 재사용한 공통 값: `--hm-sem-bg-surface`, `--hm-sem-fg-primary`, `--hm-sem-fg-secondary`, `--hm-sem-stroke-input-default`(`#dedbd2`).
|
||||
- Dialog 전용 값: `--hm-dialog-radius`(8px), `--hm-dialog-shadow`, `--hm-dialog-background`, `--hm-dialog-text`, `--hm-dialog-border-width|color`, `--hm-dialog-divider-color`, `--hm-dialog-padding`(24px), `--hm-dialog-header-padding-bottom`(16px), `--hm-dialog-header-gap`(8px), `--hm-dialog-actions-gap`(8px), Title/Description/Body 글자 크기·줄 높이·굵기, `--hm-dialog-description-color`, `--hm-dialog-edge`(16px, REVIEW).
|
||||
- 소비 화면이 설정하는 Web Mapping 이름: `--hm-dialog-app-header-height`, `--hm-dialog-app-footer-height` (기본 0). ui_viz도 Canonical token이 아니라고 명시한다.
|
||||
- Primitive 신규 추가 없음.
|
||||
- Card(radius 4px, border `#dedbd2`)·Input·Button과 값이 겹쳐도 alias하지 않는다. 8px radius는 ui_viz가 Dialog container에만 허용하고 다른 곳으로 확장하지 말라고 명시한다.
|
||||
- Overlay·Close 버튼·animation은 token을 만들지 않았다(shadcn 기본).
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조 전부를 그대로 둔다: Portal(`DialogPortal`), Overlay, modal 동작과 배경 `aria-hidden`/inert, focus trap, 열릴 때 initial focus(Radix 기본), 닫힐 때 trigger로 focus 복귀, Escape 닫기, outside interaction 처리, `role="dialog"`와 `aria-labelledby`/`aria-describedby` 연결(`DialogTitle`/`DialogDescription`), Tab 순환, body scroll lock, controlled/uncontrolled(`open`/`defaultOpen`/`onOpenChange`), `asChild`, Close 동작(`DialogPrimitive.Close` 유지, `setOpen` 직접 호출 없음), Close 버튼의 sr-only 레이블과 focus-visible, 열림/닫힘 animation class, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- `DialogContent`에서 `grid`, `w-full max-w-[calc(100%-2rem)] sm:max-w-sm`, `gap-4`, `rounded-xl`, `bg-popover`, `p-4`, `ring-1`, `text-popover-foreground`를 제거하고 theme CSS로 이전: 내용 기준 크기(`max-content` + max-width), 최대 너비·높이 공식, 8px radius, 1px border, shadow, 흰 표면.
|
||||
- Header 아래 inset divider, Header·Body·Footer 간격 24/16/8, 타이포.
|
||||
- `DialogFooter`의 `-mx-4 -mb-4`, `border-t`, `bg-muted/50`, `rounded-b-xl`, `p-4`, `gap-2` 제거(경계·배경 없는 Actions 영역).
|
||||
- `DialogBody` 추가, `DialogClose asChild`의 data-slot 처리.
|
||||
- `hm-dialog` class 제거. Demo의 `.hm-dialog-summary`(Dialog 안 내용 레이아웃)는 Demo 전용으로 남겼다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Dialog")이 기본 sidebar의 유일한 Dialog 항목이다 → 1뎁스. overlay라 StateBoard를 쓰지 않고, 트리거 버튼 6개(기본, 긴 내용, 넓은 표, 본문 스크롤, 삭제 확인, 폼)가 각각 실제 Dialog를 연다.
|
||||
- 본문 스크롤은 ui_viz "세로 스크롤 · 본문만"과 같은 조건(앱 공통 Header 70px / Footer 48px 제외)이다. Portal 때문에 앱 높이는 trigger wrapper가 아니라 `DialogContent`의 `style`로 `--hm-dialog-app-header-height`/`--hm-dialog-app-footer-height`를 넘긴다.
|
||||
- verification(`tags: ["verification"]`, 닫힌 채가 아니라 처음부터 열림): `Open`, `Long Content`, `Wide Content`, `Body Scroll`, `Delete Confirmation`.
|
||||
- pseudo-state helper로 열린 상태를 흉내 내지 않는다.
|
||||
- 운영 규칙은 `프로젝트-지침.md` §6.3, §7 참조.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
추가 사용자 결정 없음. (Radius 8px의 2026-09-17 USER_DECISION은 ui_viz 쪽 기록이다.)
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- **Overlay**: Black 42%는 REFERENCE_ONLY, 색·opacity는 TBD. 현재 shadcn 기본 `black/10` + blur를 쓰며 ui_viz는 blur를 정의하지 않는다. DECISION_REQUIRED.
|
||||
- **외곽선·Header divider 색**: `#DEDBD2`는 Temporary, Token relation TBD(DIALOG-10 FOLLOW_UP).
|
||||
- **초기 focus**: ui_viz(DIALOG-07, 2026-09-17)는 미지정 시 Dialog 자체가 initial focus를 받는다고 정했다. 현재는 Radix 기본(첫 tabbable 요소; 대표 Story에서는 "취소")이다. Radix 동작을 바꾸지 않기 위해 구현하지 않았다. DECISION_REQUIRED.
|
||||
- **Close(X) 버튼**: ui_viz에 규격이 없다. shadcn 기본이며 긴 Title이 버튼과 겹칠 수 있는지 확인하지 않았다.
|
||||
- **좁은 화면 Actions 배치**: ui_viz TBD. shadcn(`flex-col-reverse sm:flex-row`)을 유지.
|
||||
- **Description 전경**: PARTIAL. **edge 16px**: REVIEW. **motion·z-index·focus ring**: TBD.
|
||||
- **ui_viz FOLLOW_UP**: Alert Dialog·non-dismissible(DIALOG-03), Nested Dialog(DIALOG-04), Select portal 조합과 업무 Dialog 검수(DIALOG-09), `heading-md` 600→700(DIALOG-11). 현재 Alert Dialog는 별도 Component로 구현되어 있다(`alert-dialog.md`). non-dismissible/Nested 조합의 별도 검수는 여전히 보류다.
|
||||
- 공통 header/footer 높이 변수는 소비 화면이 연결해야 하며 이번에 실제 화면에 연결하지 않았다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/dialog.tsx
|
||||
packages/ui/src/theme/dialog.css
|
||||
packages/ui/src/theme.css (dialog.css import)
|
||||
src/components/dialog.stories.tsx
|
||||
src/App.tsx (Demo Dialog: hm-dialog 제거, DialogBody 사용)
|
||||
src/hm-theme.css (.hm-dialog 규칙 제거, .hm-dialog-summary 유지)
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
> HISTORICAL_RECORD: 아래 측정값은 각 검증 당시 기록이다. 현재 Footer top padding은 §3의 16px이며 과거 크기·padding 측정값을 현재값으로 덮어쓰지 않는다.
|
||||
|
||||
- TypeScript, `npm run build`: 통과(2026-10-02).
|
||||
- Storybook 실제 상호작용(1280×800): 트리거 클릭으로 열림, Escape로 닫힘, 닫힌 뒤 trigger로 focus 복귀, Close(X) 클릭으로 닫힘 후 trigger focus 복귀, `role="dialog"`·`aria-labelledby`·`aria-describedby`, body scroll lock(`data-scroll-locked`) 확인.
|
||||
- computed style(기본 Dialog): background `rgb(255,255,255)`, border 1px `rgb(222,219,210)`, radius 8px, shadow `0 0 12px rgba(0,0,0,.2)`, 크기 344×227(fit-content), 화면 정중앙, max-width 1248px(=1280−32)·max-height 768px(=800−32), Title 20px/28px/700 `rgb(10,34,29)`, Description 14px/20px/400 `rgb(110,103,89)`, Title↔Description 8px, Header margin-inline 24px·padding 24/16·divider 1px, Body padding 24px·16px/24px, Footer padding `0 24px 24px`·gap 8px·border 0·배경 투명, Close 28×28 오른쪽·위 9px(8px + 외곽선 1px).
|
||||
- Long Content: Dialog 730px(viewport 안), Header·Footer 보임, Body만 scroll(client 613 / scroll 1712), 페이지 가로 overflow 없음. Wide Content: Body 가로 scroll, 페이지 가로 overflow 없음.
|
||||
- Actions: 취소 Gray Solid(`rgb(118,118,118)`/`rgb(90,90,90)`), 삭제 Red Outline(`rgb(211,47,47)`) 적용 확인(`DialogClose` 수정 후).
|
||||
- Story: `All` + verification 4개 ID 확인. 기본 sidebar는 `All` 하나만 보이는 1뎁스 구조(실제 사이드바 화면은 확인하지 못함).
|
||||
- 2026-10-06 ui_viz Storybook 화면 비교(1280/768/390, Chromium): REGRESSION 2건 수정 — ① `fit-content`가 shadcn `left: 50%` 배치에서 화면 절반만 가용 폭으로 받아 390px에서 Dialog가 195px로 좁아짐 → `width: max-content` + max-width, ② Header가 divider 1px만큼 큼(97px) → bottom padding에서 divider 두께를 뺌(96px). 수정 후 Header 96px·기본 높이 228px이 ui_viz와 같고, 본문 스크롤 Dialog는 3개 viewport 모두 ui_viz와 같은 크기·위치(1280: 580×750 @350,86 / 390: 358×750 @16,86). Tab·Shift+Tab 순환, `aria-labelledby`/`aria-describedby`, scroll lock, Escape·액션·X 닫기 후 trigger focus 복귀 확인(1280/390).
|
||||
- 의도적 차이(유지): Overlay(ui_viz 42% REFERENCE_ONLY), Close X(ui_viz 미정의), 640px 미만 Footer 세로 stack(ui_viz TBD), 초기 focus(Radix 기본 = 첫 focus 가능 요소, ui_viz는 Title/Cancel).
|
||||
- 미확인: Demo(App)의 Dialog 열림 시각 확인, Select를 Dialog 안에 둔 조합.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Drawer
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Drawer 고유의 API·Token·상태·결정만 기록한다. Dialog·Sheet와 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Drawer = **vaul** (`vaul@1.1.2`, 이번에 `@hm-design/ui` dependency로 추가) + Radix Dialog |
|
||||
| 대표 Story | `Components/Overlay/Drawer` → `All` (이름 "Drawer") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Drawer가 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. Dialog의 접근성 원칙과 `asChild` 처리 규칙은 재사용하고 수치는 현재 HM Semantic으로 새로 정했다.
|
||||
- **구현 방식**: shadcn Drawer 구조(vaul)를 유지하고 theme CSS로 HM 스타일 연결. drag·snap·dismiss는 vaul 기본이며 재구현하지 않았다.
|
||||
- **vaul 추가 이유**: shadcn 공식 Drawer가 vaul 기반이다(Radix에 Drawer가 없음). 프로젝트에 없던 dependency라서 추가했다(`npm install vaul --workspace @hm-design/ui`).
|
||||
- **Sheet와의 역할 구분**: Drawer = 손가락/포인터로 끌어 닫는 하단 중심 임시 panel(handle 포함), Sheet = 버튼으로 닫는 edge panel.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Surface / 전경 | `--hm-sem-bg-surface` / `--hm-sem-fg-primary` |
|
||||
| Border | 1px `--hm-sem-stroke-input-default`, 화면을 향한 한 변 |
|
||||
| Radius | 8px, 화면 안쪽 두 모서리(bottom이면 위 두 모서리) |
|
||||
| Shadow | `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| bottom / top | 폭 100%, 높이 내용 기준, 최대 `85dvh` |
|
||||
| left / right | 폭 `min(400px, 100vw − 48px)`, 높이 100% |
|
||||
| Handle(bottom만) | 48 × 4px 알약, `--hm-sem-stroke-default`, 위 12px |
|
||||
| Padding | 24px |
|
||||
| Header | padding `24 24 16`, gap 8px |
|
||||
| Title | 18 / 28 / 700 |
|
||||
| Description | 14 / 20 / 400, `--hm-sem-fg-secondary` |
|
||||
| Body | 14 / 20 / 400, 좌우 padding 24px, 유일한 scroll 영역 |
|
||||
| Footer | padding `16 24 24`, gap 8px, 세로 쌓임(주 action이 위, shadcn 기본) |
|
||||
| Close(X) | 없음(handle drag, Overlay 클릭, Escape, `DrawerClose`) |
|
||||
| Overlay | shadcn 기본(`bg-black/10` + blur) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Closed / Open | vaul `data-state`, 열림 시 modal, 배경 aria-hidden, scroll lock |
|
||||
| Drag | handle 또는 panel을 아래로 끌어 닫음(vaul 기본). 내부 scroll 영역이 있으면 vaul이 scroll과 drag를 구분 |
|
||||
| Focus | 열릴 때 첫 tabbable로 이동, Tab 순환, 닫힐 때 trigger로 복귀 |
|
||||
| Long Content | Header·Footer 고정, Body만 scroll |
|
||||
|
||||
## 5. API
|
||||
|
||||
vaul `Drawer.Root` props 그대로(`direction`, `snapPoints`, `dismissible`, `shouldScaleBackground`, `modal`, `open`/`onOpenChange` …). export: `Drawer`, `DrawerTrigger`, `DrawerPortal`, `DrawerOverlay`, `DrawerClose`, `DrawerContent`, `DrawerHeader`, `DrawerBody`, `DrawerFooter`, `DrawerTitle`, `DrawerDescription`.
|
||||
|
||||
- `Drawer`: **`autoFocus` 기본값을 true로 둔다**(vaul 기본은 false). vaul 기본값은 Radix의 초기 focus를 취소해, 열린 뒤에도 focus가 aria-hidden 처리된 trigger에 남고 Tab이 Drawer 밖으로 나간다(실측). `autoFocus={false}`로 vaul 동작을 되돌릴 수 있다.
|
||||
- `DrawerContent`: 방향은 root의 `direction`(기본 bottom), `data-vaul-drawer-direction`으로 출력.
|
||||
- `DrawerBody`(신규): scroll owner.
|
||||
- `DrawerClose`: `asChild`일 때 자식의 `data-slot`을 유지(DialogClose와 같은 규칙).
|
||||
- 새 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/drawer.css` (`--hm-drawer-*`).
|
||||
|
||||
`background`, `text`, `border-width`, `border-color`, `radius`, `shadow`, `max-height`, `width`, `edge-gap`, `padding`, `header-gap`, `footer-gap`, `handle-width|height|color|margin-top`, `title-*`, `description-*`, `body-*`. Semantic과 Theme palette만 참조하며 `--hm-sheet-*`·`--hm-dialog-*`를 참조하지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / vaul / Radix에서 유지한 항목
|
||||
|
||||
vaul의 drag·snap·dismiss·body 처리와 `data-vaul-*` 속성, Radix의 Portal·focus trap·Escape·ARIA·focus 복귀, 방향별 position class, `data-slot`, `cn(..., className)`, handle 요소(bottom 방향에서만 표시), Overlay.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-background`, 방향별 `rounded-*`/`border-*`/`max-h-[80vh]`, handle `h-2 w-[100px] bg-muted`, Header/Footer `p-4` 제거 → theme CSS.
|
||||
- `Drawer`의 `autoFocus` 기본 true(위 §5).
|
||||
- `DrawerBody` 추가, `DrawerClose asChild` data-slot 처리.
|
||||
- Handle을 `data-slot="drawer-handle"` 요소로 구분해 token으로 스타일.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Drawer"): 트리거 3개(기본, 폼, 긴 내용)가 각각 실제 Drawer를 연다. 핸들을 끌어 닫을 수 있다.
|
||||
- verification: `Open`, `Form`, `Long Content`(처음부터 열림).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(85dvh, 18px Title, handle 크기, 그림자)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Overlay, drag/transition Motion은 vaul·shadcn 기본 유지. `shouldScaleBackground`는 쓰지 않았다.
|
||||
- snap points 사용 예는 만들지 않았다(API는 vaul 그대로 사용 가능).
|
||||
- left/right/top 방향의 시각은 CSS에 두었으나 Story로는 검증하지 않았다(bottom만 사용 예).
|
||||
- Drawer와 Sheet의 사용 경계 가이드는 후속.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/drawer.tsx
|
||||
packages/ui/src/theme/drawer.css
|
||||
packages/ui/package.json (dependency: vaul)
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/drawer.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 클릭 열기, focus가 "확인"으로 이동(`autoFocus` 적용 전에는 trigger에 남음), Tab 3회 후에도 Drawer 안(focus trap), Escape 닫힘과 trigger 복귀, handle을 아래로 drag해 닫힘과 focus 복귀, 바깥(Overlay) 클릭 닫힘.
|
||||
- 390×844 긴 내용: Drawer 높이 717px(85dvh), Body만 scroll, Footer가 viewport 하단, 가로 overflow 없음, Footer 버튼 세로 쌓임.
|
||||
- computed style: radius `8px 8px 0 0`, border-top `rgb(222,219,210)`, handle 48×4 `rgb(154,146,132)`.
|
||||
- 미확인: 실제 터치 drag(마우스 drag만 확인), snap points, scroll 영역 안에서 drag와 scroll 구분, 닫기 animation 시각, left/right/top 방향 시각.
|
||||
@@ -0,0 +1,112 @@
|
||||
# Dropdown Menu
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Dropdown Menu 고유의 API·Token·상태·결정만 기록한다. Floating 계열 공통 사항은 §14.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Dropdown Menu (Radix `DropdownMenu`) |
|
||||
| 대표 Story | `Components/Overlay/Dropdown Menu` → `All` (이름 "Dropdown Menu") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Dropdown Menu 규격이 없다(인벤토리에 "상세 디자인·API 보류"). Select의 option 목록 패턴(높이·hover 면)만 참고했다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계(surface, fg, stroke, destructive)와 Select option의 highlight 면(shadcn `accent`)에 맞춰 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Dropdown Menu 구조를 유지하고 theme CSS로 HM 스타일을 연결. 상태는 Radix 속성(`data-highlighted`, `data-disabled`, `data-state`)만 쓴다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 | 비고 |
|
||||
|---|---|---|
|
||||
| Surface | `--hm-sem-bg-surface` (White) | Semantic |
|
||||
| Border | 1px `--hm-sem-stroke-input-default` | Dialog·Card와 같은 윤곽색 |
|
||||
| Radius | `--hm-radius-container` (4px) | 공통 container radius |
|
||||
| Shadow | `0 0 12px rgb(0 0 0 / .2)` | HM의 기존 floating 그림자 |
|
||||
| Content padding | 4px | |
|
||||
| Item | min-height 36px, padding-x 12px, gap 8px, radius 2px | |
|
||||
| Typography | 14 / 20 / 400 | Label·Shortcut은 12px (Label 12/18/500) |
|
||||
| 전경 | `--hm-sem-fg-primary` / Label·Shortcut `--hm-sem-fg-secondary` | |
|
||||
| Separator | 1px `--hm-sem-stroke-input-default`, Content 폭 전체 | |
|
||||
| Check / Radio | 16px indicator 열(12px 안쪽), shadcn `CheckIcon`, 8px 채운 원 | shadcn 아이콘 유지 |
|
||||
| Sub menu chevron | shadcn `ChevronRightIcon` | |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Default | 위 기본 규격 |
|
||||
| Highlighted (`data-highlighted`) | 배경 Accent 45%(`--hm-sem-surface-highlight`), 전경 `--accent-foreground` (포인터 hover와 키보드 이동 공통) |
|
||||
| Disabled (`data-disabled`) | shadcn `opacity-50`, 포인터 무시, 키보드 이동에서 제외 |
|
||||
| Checked / Selected | `ItemIndicator`가 check 또는 원을 표시 (Radix `data-state`) |
|
||||
| Sub menu open | SubTrigger `data-state="open"`이 Highlighted와 같은 면 유지 |
|
||||
| Destructive (`variant="destructive"`) | 전경 `--hm-sem-action-destructive`, Highlighted 면 `--hm-color-action-destructive-hover` |
|
||||
| Focus | 항목 자체 outline 없음(`outline-hidden`), Highlighted 면이 focus 표시 |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Dropdown Menu props 그대로. export: `DropdownMenu`, `DropdownMenuTrigger`, `DropdownMenuContent`, `DropdownMenuGroup`, `DropdownMenuLabel`, `DropdownMenuItem`, `DropdownMenuCheckboxItem`, `DropdownMenuRadioGroup`, `DropdownMenuRadioItem`, `DropdownMenuSeparator`, `DropdownMenuShortcut`, `DropdownMenuSub`, `DropdownMenuSubTrigger`, `DropdownMenuSubContent`, `DropdownMenuPortal`.
|
||||
|
||||
- `DropdownMenuContent`: `sideOffset` 기본 4, Radix `align`·`side`·collision props.
|
||||
- `DropdownMenuItem`: shadcn `variant="default" | "destructive"`, `inset`. 출력 `data-variant`.
|
||||
- Label·CheckboxItem·RadioItem·SubTrigger: `inset`.
|
||||
- 출력 속성: `data-slot="dropdown-menu-*"`, Radix `data-state`·`data-highlighted`·`data-disabled`·`data-side`.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/dropdown-menu.css` (`--hm-dropdown-menu-*`).
|
||||
|
||||
`background`, `text`, `border-width`, `border-color`, `radius`, `shadow`, `padding`, `item-height`, `item-padding-x`, `item-gap`, `item-radius`, `font-size`, `line-height`, `font-weight`, `highlight-background`, `highlight-text`, `destructive-text`, `destructive-highlight-background`, `label-*`, `shortcut-*`, `separator-color`, `indicator-size`, `radio-dot-size`. Semantic 또는 Theme palette만 참조하고 다른 컴포넌트의 Component Token은 쓰지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`role=menu`, `menuitem*`)·키보드(방향키, Home/End, Enter/Space, 오른쪽 방향키 sub menu, Escape, typeahead)·focus 복귀, Portal·positioning·collision, `data-slot`, `cn(..., className)`, open/close animation(`data-open:`/`data-closed:` class), `z-50`, lucide 아이콘.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn의 색·`rounded`·`shadow`·`p-1`·`px-2 py-1.5`·`text-sm` 등 시각 class와 `focus:bg-accent` 계열을 제거하고 theme CSS + `data-highlighted`로 대체.
|
||||
- Radio 표시를 별도 아이콘 import 대신 CSS 크기 token을 쓰는 원(`data-slot="dropdown-menu-radio-dot"`)으로 구현.
|
||||
- Check / Radio indicator span에 `data-slot="dropdown-menu-item-indicator"` 추가(위치·크기 token용).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Dropdown Menu"): 트리거로 실제 메뉴를 연다(Label, Item + Shortcut, disabled, Separator, Checkbox, Radio Group, Sub menu, destructive).
|
||||
- verification: `Open`(열린 상태, fullscreen).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(36px item, 12px padding, 4px content padding, 그림자 등)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Disabled opacity는 shadcn 기본 50 유지(전역 Disabled 규칙 없음).
|
||||
- Hover/Highlight 면은 Select와 같은 공통 Accent 45% 정책을 쓴다.
|
||||
- icon 크기·check 위치(시작 위치) 등 icon 세부는 shadcn 기본 유지.
|
||||
- Motion 규칙은 정의되지 않아 shadcn 기본 animation을 유지.
|
||||
- 자체 scroll 처리(긴 목록)는 `max-h` available height + overflow로 둔다. 별도 규격 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/dropdown-menu.tsx
|
||||
packages/ui/src/theme/dropdown-menu.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/dropdown-menu.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): Enter로 열기, 방향키 이동(disabled 항목 건너뜀), Enter로 Checkbox 토글, Radio 선택 시 닫힘과 trigger focus 복귀, 오른쪽 방향키로 Sub menu 열림과 첫 항목 focus, Escape로 sub → menu 순서 닫힘과 trigger focus 복귀.
|
||||
- computed style: Surface white, border `rgb(222,219,210)`, radius 4px, shadow `0 0 12px .2`, item 36px/14·20, Highlighted `rgb(189,223,212)`.
|
||||
- 미확인: Home/End·typeahead 입력, 실제 pointer hover, 좁은 viewport collision 시각.
|
||||
|
||||
## 14. Floating 계열 공통
|
||||
|
||||
Popover·Tooltip·Hover Card와 surface(White, 1px pale border, 4px radius, 12px glow) 값이 같지만 각각 `--hm-<component>-*`를 둔다. 공통 Semantic 승격 후보는 `docs/components/popover.md` §14에 함께 기록했다.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Field / Form Field
|
||||
|
||||
**보류 — 컴포넌트를 만들지 않았다.** 현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6.7), 이 문서는 보류 이유와 소비 조합 기준만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | **보류** (코드·Story 없음) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | 해당 없음 |
|
||||
| 대표 Story | 없음 |
|
||||
|
||||
## 2. 적용 근거 (보류 이유)
|
||||
|
||||
- 외부 ui_viz Reference (ZIP 미포함) `docs/components/field-semantics.md`: "COMMON SEMANTIC RELATIONSHIP / NON-GEOMETRY-OWNER … **새 Field Component/API를 정의하지 않는다.**" 높이·gap·inset·색·text role·state paint를 공통 Field에 넣지 않으며 DOM/ARIA 연결도 "공통 Field 구현을 요구하지 않는다".
|
||||
- Field 관계(Label → Control → Message)는 컴포넌트별 owner가 소유한다: Input §5·§13·§15·§17.1, Textarea §1·§3·§4, Select.
|
||||
- `프로젝트-지침.md` §6.7: Field/Label 컴포넌트 도입은 별도 결정이며 Input/Textarea/Select에 Field wrapper를 다시 넣지 않는다.
|
||||
- Checkbox Group의 Label/Heading, error slot 계층, helper/error 우선순위는 ui_viz Pattern §4–5에서 `TBD`다.
|
||||
- 근거 없이 Form 시스템을 설계하지 않는다.
|
||||
|
||||
## 3~10. 해당 없음
|
||||
|
||||
구현이 없으므로 기본/상태 규격, API, Token, 유지·변경 항목, Storybook, 사용자 결정은 없다.
|
||||
|
||||
## 11. 소비 조합 기준 (구현이 아니라 기록)
|
||||
|
||||
현재 Input/Textarea Story가 쓰는 조합이며 ui_viz 값이다.
|
||||
|
||||
| 항목 | 값 | 출처 |
|
||||
|---|---|---|
|
||||
| 순서 | Label → Control → Helper/Error | input.md §5, textarea.md §1 |
|
||||
| Label | `<Label>` 컴포넌트(14/20/500, `fg.secondary`) | `docs/components/label.md` |
|
||||
| Field gap | 8px (`input.field.gap`, `OWNER_CONFIRMED_REVISION`) — `space.*`로 승격하지 않음 | input.md §8 |
|
||||
| Helper | 12 / 18 / 400, `fg.secondary` | input.md §9 |
|
||||
| Error | 11 / 18 / 400, `#D32F2F` + `aria-invalid` + `aria-describedby`, 색 단독 전달 금지 | input.md §9, §15 |
|
||||
| Required | 별표 `#D32F2F`, `aria-hidden`; 의미는 native `required` | input.md, TEXTAREA-07 |
|
||||
| 라벨 위치 | 등록·수정 폼은 상단(gap 8), PC 조회·필터는 좌측 라벨(라벨↔입력 12, 묶음 사이 24) | input.md §17.1 |
|
||||
|
||||
보류 해제 조건: ui_viz가 Field를 Component/Pattern으로 확정하거나 사용자가 별도 결정으로 도입을 지시할 때. 이 경우에도 Input/Textarea/Select 내부에 구조를 넣지 않고 composition 컴포넌트로 추가한다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
없음 (`docs/components/label.md`, `input.md`, `textarea.md` 참조).
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
해당 없음.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Hover Card
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Hover Card 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Hover Card (Radix `HoverCard`) |
|
||||
| 대표 Story | `Components/Overlay/Hover Card` → `All` (이름 "Hover Card") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Hover Card 규격이 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. Popover와 같은 floating surface이며 현재 HM Semantic 체계로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Hover Card 구조를 유지하고 theme CSS로 HM 스타일을 연결.
|
||||
- **역할 구분**: Tooltip = 짧고 즉각적인 설명, Hover Card = 제목·설명·metadata를 가진 정보 미리보기(non-interactive 중심), Popover = click으로 여는 panel/form.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Surface | `--hm-sem-bg-surface` |
|
||||
| 전경 | `--hm-sem-fg-primary` |
|
||||
| Border | 1px `--hm-sem-floating-border-color` |
|
||||
| Radius | `--hm-radius-container` (4px) |
|
||||
| Shadow | `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| Padding | 16px |
|
||||
| Width | 320px (viewport − 16px 이하로 제한), 긴 문자열은 줄바꿈 |
|
||||
| Typography | 14 / 20 / 400 |
|
||||
| Position | `sideOffset` 4, `align` center |
|
||||
| Delay | Radix 기본 유지(open 700ms, close 300ms) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Open / Closed | Radix `data-state`, shadcn open/close animation |
|
||||
| 열림 조건 | trigger hover 또는 keyboard focus |
|
||||
| 유지 | trigger → content로 pointer 이동 시 유지(Radix hover bridge) |
|
||||
| 닫힘 조건 | pointer가 trigger와 content를 떠난 뒤 close delay, blur, Escape |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Hover Card props 그대로. export: `HoverCard`, `HoverCardTrigger`, `HoverCardContent`.
|
||||
|
||||
- `HoverCardContent`: `align` 기본 `center`, `sideOffset` 기본 4. `HoverCard`에 `openDelay`, `closeDelay`.
|
||||
- 출력 속성: `data-slot="hover-card-*"`, Radix `data-state`·`data-side`·`data-align`.
|
||||
- 접근성 주의: Hover Card는 sighted pointer user용 보조 정보이며 keyboard focus로도 열리지만 content 안으로 focus를 옮기지 않는다. 필수 정보나 interactive 요소는 넣지 않는다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/hover-card.css` (`--hm-hover-card-*`).
|
||||
|
||||
`background`, `text`, `border-width`, `border-color`, `radius`, `shadow`, `padding`, `width`, `font-size`, `line-height`, `font-weight`. 다른 컴포넌트의 Component Token을 참조하지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·delay·hover bridge·Escape·focus 열림, Portal·positioning·collision, `data-slot`, `cn(..., className)`, animation class, `z-50`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `w-64 rounded-md border p-2.5 shadow-md bg-popover` 등 시각 class 제거 → theme CSS.
|
||||
- 폭 256 → 320px, padding 10 → 16px(Popover와 구분되는 미리보기 폭).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Hover Card"): 기본 preview(제목 / 설명 / metadata), 긴 콘텐츠 edge case.
|
||||
- verification: `Open`(열린 상태).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(320px, 16px padding, 그림자)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- delay는 Radix 기본. HM delay 규칙은 없다.
|
||||
- arrow 없음(shadcn 기본에 없음).
|
||||
- Motion 규칙은 정의되지 않아 shadcn 기본 animation 유지.
|
||||
- 제목·metadata 타이포 role은 소비 조합(Story는 shadcn Token 유틸 사용). 전용 slot 컴포넌트 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/hover-card.tsx
|
||||
packages/ui/src/theme/hover-card.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/hover-card.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): hover로 열림, trigger → content 이동 시 유지, 영역 이탈 후 닫힘, keyboard focus로 열림, Escape 닫힘.
|
||||
- computed style: width 320px, white, border `rgb(222,219,210)` 1px, padding 16px, shadow `0 0 12px .2`.
|
||||
- 미확인: open/close delay 시간의 정확한 측정, 긴 콘텐츠 시각, 좁은 viewport collision.
|
||||
@@ -0,0 +1,133 @@
|
||||
# Input
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Input 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (`hm-input` class 없음) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Input (native `<input>`) |
|
||||
| 대표 Story | `Components/Form/Input` → `All` (이름 "Input") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **ui_viz Confirmed**: size·padding-x(input.md §24, §16), border/radius/색(§10, §11, D-005), Focus/Error 표현.
|
||||
- **ui_viz owner decision**: size별 value·placeholder 글자(§9, §21 `INPUT_VALUE_ROLE_FOLLOWS_SIZE`).
|
||||
- **HM 사용자 결정**: Disabled opacity `0.6`(2026-10-02, §10).
|
||||
- **shadcn 유지**: native input 동작, `file:` 스타일 class, disabled 포인터/커서 처리.
|
||||
- **ui_viz 미정의**: 아래 §11 참조.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| Size | Height(min) | Padding-x | Font / Line-height |
|
||||
|---|---|---|---|
|
||||
| `small` | 28 | 10 | 12 / 18 |
|
||||
| `medium` | 34 | 12 | 14 / 20 |
|
||||
| `large` (기본) | 42 | 12 | 16 / 24 |
|
||||
|
||||
- 높이는 `min-height`, 한 줄 텍스트는 가운데.
|
||||
- Border 1px(`--hm-sem-stroke-input-default`), radius `--hm-radius-input`, face `--hm-sem-bg-surface`, 글자색 `--hm-sem-fg-primary`, weight 400.
|
||||
- Placeholder: `--hm-sem-fg-placeholder`, `opacity: 1`.
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Default | 위 기본 규격 | ui_viz Confirmed |
|
||||
| Focus (`:focus-visible`) | border 1.5px `--hm-sem-stroke-input-focus`, shadow/glow 없음, padding으로 0.5px 보정해 값이 움직이지 않음 | ui_viz Confirmed |
|
||||
| Error (`[aria-invalid="true"]`) | border 1.5px `--hm-sem-action-destructive`, ring 없음 | ui_viz Confirmed |
|
||||
| Error + Keyboard Focus | Destructive border 유지 + HM Focus Semantic 3px / 50% outer ring, `outline: none` | 2026-10-06 사용자 결정 |
|
||||
| Disabled | White face 유지, `opacity: 0.6`, `cursor: not-allowed`, `pointer-events: none` | face: ui_viz Confirmed / opacity: 사용자 결정 |
|
||||
| Readonly (`[readonly]`) | 박스 없음(border 0, padding-x 0, 투명 face), dim 없음, 항상 Large 치수 | ui_viz |
|
||||
| Hover / Active | 정의 없음(별도 스타일 없음) | ui_viz 미정의 |
|
||||
|
||||
Focus ring chain은 `--ring` → `--hm-sem-stroke-input-focus` → `--hm-color-border-focus`다. Error border는 `--hm-input-error-border-color` → `--hm-sem-action-destructive`를 따른다. 일반 Focus / Readonly의 기존 표현은 유지하며, editable Invalid + Focus에서 UA outline 의존을 제거했다.
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
Input:
|
||||
size?: native <input size> (number)
|
||||
→
|
||||
size?: "small" | "medium" | "large" 기본 "large"
|
||||
```
|
||||
|
||||
- native `size`(글자 수 폭)를 대체한다. native 속성이 CSS 폭과 무관해 쓰임이 없다는 판단(지침 §6.7).
|
||||
- 출력 속성: `data-slot="input"`, `data-size`.
|
||||
- 상태는 새 prop이 아니라 native 속성으로 준다: `aria-invalid`, `disabled`, `readOnly`.
|
||||
- Field(Label, Required 별표, Helper/Error 문구)는 Input에 포함하지 않는다. 소비 조합이다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/input.css` (`--hm-input-*`).
|
||||
|
||||
- 재사용한 공통 Semantic: `--hm-radius-input`, `--hm-sem-bg-surface`, `--hm-sem-fg-primary`, `--hm-sem-fg-placeholder`, `--hm-sem-stroke-input-default`, `--hm-sem-stroke-input-focus`, `--hm-sem-action-destructive`(Semantic 참조).
|
||||
- Input 전용 값: `--hm-input-height-{sm,md,lg}`, `--hm-input-padding-x-*`, `--hm-input-font-size-*`, `--hm-input-line-height-*`, `--hm-input-state-border-width`(1.5px).
|
||||
- Primitive 신규 추가 없음.
|
||||
- Button의 높이 Token(`--hm-button-height-*`)·Select 높이와 값이 겹쳐도 alias하지 않는다. 각 ui_viz 규격이 따로 확정한 값이다.
|
||||
- Disabled opacity는 Token이 아니라 `ui/input.tsx`의 `disabled:opacity-60`으로 적용된다(§11).
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
native `<input>` 동작 전체(`type`, `file:` 스타일 class, `placeholder`), `data-slot`, `cn(..., className)`, `transition-colors`, `outline-none`, disabled의 `pointer-events-none`/`cursor-not-allowed`. Radix 사용 없음.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- `size` API 교체, 기본 Large, `data-size` 추가.
|
||||
- shadcn의 기본 높이/padding/border/ring/dark class 제거 → theme CSS가 담당.
|
||||
- 일반 Focus는 border 1.5px로 표시, Error 단독은 ring 없음. Error + `:focus-visible`만 독립 Focus ring을 표시한다.
|
||||
- Readonly 전용 표현 추가.
|
||||
- `hm-input` class 제거.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Input"): 구역 `sizes`, `default`, `focus`, `error`, `error-message`, `disabled`, `readonly`. Focus는 `play` 함수로 실제 focus를 준다.
|
||||
- verification(`tags: ["verification"]`): `Default`, `Focus`, `Error`, `InvalidFocus`, `ErrorMessage`, `Required`, `Disabled`, `Readonly`, `Sizes`("Compare Sizes"), `Small`, `Medium`, `Large`, `Playground`(HM 전용).
|
||||
- Error Message·Required 표시는 Story 안의 조합 예시이며 Input 컴포넌트의 일부가 아니다.
|
||||
- sidebar는 `All` 하나만 보이는 1뎁스. 운영 규칙은 지침 §6.3, §7 참조.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
**2026-10-02**
|
||||
|
||||
- Input Disabled opacity `0.6` 확정.
|
||||
- ui_viz에서 Temporary로 기록된 이력이 있으나, 현재 HM 구현에서는 사용자 결정이 우선한다. 이전의 "Temporary → 미적용" 판단은 이 결정으로 superseded.
|
||||
- 이 값은 Input에 대한 현재 최종값이다. 전체 디자인 시스템 공통 Disabled opacity로 자동 확대하지 않는다.
|
||||
- 근거: Select 작업에서 확정된 Disabled 시각 intent와의 정합. Select Trigger도 `0.6`(select.md 참조).
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- ui_viz 미정의: Hover, Active 표현, Error 메시지 컴포넌트, Input 내부 아이콘/clear 버튼.
|
||||
- Disabled opacity `0.6`은 Input 한정 사용자 결정이다. 다른 컴포넌트에 적용하지 않는다.
|
||||
- `file:` 타입 스타일은 shadcn 기본이며 HM 규격 없음.
|
||||
- Readonly에서 Disabled 조합 동작은 별도로 확인하지 않았다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/input.tsx
|
||||
packages/ui/src/theme/input.css
|
||||
src/components/input.stories.tsx
|
||||
src/components/story-board.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- TypeScript, `npm run build`: 통과(2026-10-02).
|
||||
- Storybook computed style: `Components/Form/Input` Disabled `opacity` = `0.6` 확인.
|
||||
- Story: `All`의 구역 7개와 verification Story ID 존재 확인.
|
||||
- 2026-10-06 ui_viz Storybook 화면 비교(1280/768/390, Chromium): 대표 Story 7구역의 컨트롤·Label·Error 문구 computed style 일치, placeholder 색, Focus·Error 경계, Error+Focus의 outline 유지, 긴 값 가로 스크롤 일치. Readonly는 ui_viz `<p>` 대신 native `input[readonly]`로 같은 42px·16/24 표시. Error Message·Required Story에 ui_viz처럼 placeholder를 넣음(Story만 변경).
|
||||
- 미확인: 브라우저별 렌더(Chromium 외).
|
||||
|
||||
|
||||
### Invalid + Focus 검증 (2026-10-07)
|
||||
|
||||
- Focus ring: `--ring` → `--hm-sem-stroke-input-focus` → `--hm-color-border-focus`. Invalid border: 기존 `--hm-sem-action-destructive`. 새 Runtime Role / Production Error prop / 메시지 컴포넌트 없음.
|
||||
- Chromium computed style: Invalid border `rgb(211, 47, 47)`, Invalid + Keyboard Focus box-shadow `color(srgb 0.14902 0.4 0.317647 / 0.5) 0px 0px 0px 3px`, `outline-style: none`. Invalid 단독은 `box-shadow: none`.
|
||||
- Theme Designer UI에서 Focus `#c75b00`, Destructive `#d41466`, Active `#007f73`, Accent `#ffd36d`로 분리. Invalid border `rgb(212, 20, 102)` + Focus shadow `color(srgb 0.780392 0.356863 0 / 0.5) 0px 0px 0px 3px` 확인.
|
||||
- `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. 기존 문서 예시 CSS warning 및 Storybook chunk 크기 / plugin timing warning 유지.
|
||||
- 검증 범위: Chromium 환경. 다른 브라우저의 렌더링은 별도 확인이 필요하다.
|
||||
- 대표 `All`과 숨김 `InvalidFocus` 확인. 일반 Focus는 border만 변경, Small/Medium/Large 28/34/42px 및 12/14/16px 유지. Readonly border 0 / padding 0, Disabled opacity 0.6 유지. Border CSS 계약 1.5px, 이 브라우저 환경의 computed border-width는 1px으로 정규화; padding 보정은 11.5px 유지. 기존 non-invalid Readonly의 UA outline 정책은 이번 범위에서 유지.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Label
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Label 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (typography·color만, `hm-label` class 없음) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Label (Radix `Label`) |
|
||||
| 대표 Story | `Components/Form/Label` → `All` (이름 "Label") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- ui_viz에는 **독립 Label 문서가 없다.** Field Label 역할은 Input·Textarea가 소유한다: `docs/components/input.md` §9(14/20/500, `label-md`), §10·§16(`color.fg.secondary` `#6E6759`, Confirmed), `textarea.md` §2.2·§2.
|
||||
- **ui_viz Confirmed (Input·Textarea Field Label 범위)**: 14 / 20 / 500, 색 `color.fg.secondary`. (Source 13/700은 `CANONICAL_OVERRIDE`로 14/20/500이 확정. D-093 원본 실행본 13/700은 `KNOWN_DEVIATION / FOLLOW_UP`.)
|
||||
- **shadcn/Radix 유지**: 라벨 연결(`htmlFor`/감싸기), 클릭 시 컨트롤 focus, `select-none`, Disabled 연동 class.
|
||||
- Required 표시, Checkbox/Radio option label의 별도 규칙은 ui_viz에 없다 → §11.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Typography | 14 / 20 / 500 |
|
||||
| 색 | `--hm-sem-fg-secondary` (`#6E6759`) |
|
||||
| 배치 | `flex w-fit items-center gap-2` (shadcn 기본, 부모 grid에서 시작 정렬) |
|
||||
|
||||
Label ↔ Control 간격 8px(`input.field.gap`)은 Field 조합의 값이며 Label이 소유하지 않는다.
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Default | 위 기본 규격 |
|
||||
| Disabled | shadcn 기본: 연결된 컨트롤이 `peer`이고 disabled면 `opacity-50`, `cursor-not-allowed` (또는 상위 `group[data-disabled=true]`) |
|
||||
|
||||
Hover/Focus/Active 스타일 없음.
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix `Label` props 그대로 (`htmlFor`, `asChild` 등). 출력 속성: `data-slot="label"`.
|
||||
|
||||
- Required 표시(`*`)를 위한 prop은 만들지 않았다(ui_viz `field-semantics.md` §1: Select/Checkbox/Radio 등에 required API·표시를 새로 추가하지 않는다). 소비자가 `aria-hidden` span으로 조합한다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/label.css` (`--hm-label-*`).
|
||||
|
||||
- `--hm-label-font-size`(14px), `--hm-label-line-height`(20px), `--hm-label-font-weight`(500), `--hm-label-text`(→ `--hm-sem-fg-secondary`).
|
||||
- Primitive 신규 추가 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix Label 동작·접근성, `data-slot`, `cn(..., className)`, `select-none`, `group-data-[disabled=true]`·`peer-disabled` 연동.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `leading-none font-medium text-sm` 대신 theme CSS가 typography·색을 정한다.
|
||||
- `flex` 라벨이 한 줄 전체를 차지하지 않도록 `w-fit`을 추가(ui_viz Label `justify-self: start`).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Label"): 구역 `default`(htmlFor), `required`(소비 조합), `disabled`, `long`.
|
||||
- 별도 verification Story 없음(구역이 단순).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 추가 사용자 결정 없음.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 독립 Label 규격 없음. Input·Textarea Field Label 값을 Label 컴포넌트에 적용한 것이다. Checkbox/Radio option label의 전경 role은 ui_viz `TBD`(`field-semantics.md` §3)이므로 해당 컴포넌트 작업에서 따로 확인한다.
|
||||
- Disabled opacity: Input의 "Label·Message 포함 .6"(2026-10-02 Input 한정 결정)은 확대하지 않고 shadcn 기본 0.5를 유지.
|
||||
- Required 표시 색(`#D32F2F`, TEXTAREA-07)과 표시 방식은 소비 조합이다. 컴포넌트 API 없음.
|
||||
- D-093 원본 실행본 13/700: `KNOWN_DEVIATION / FOLLOW_UP`.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/label.tsx
|
||||
packages/ui/src/theme/label.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Label)
|
||||
src/components/label.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- Storybook computed style(`components-form-label--all`): 14px / 20px / 500, color `rgb(110,103,89)`, Disabled 연결 시 opacity 0.5.
|
||||
- 라벨 클릭 시 연결된 input으로 실제 focus 이동 확인.
|
||||
- 2026-10-02 당시 미확인: 사이드바 화면, 390px 폭, Checkbox/Radio와의 조합. 현재 Checkbox/Radio는 구현되어 있으나 이 과거 검증 기록이 조합 검증 완료를 뜻하지는 않는다.
|
||||
@@ -0,0 +1,112 @@
|
||||
# Menubar
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Menubar 고유의 API·Token·상태·결정만 기록한다. Dropdown Menu(`dropdown-menu.md`), Navigation Menu(`navigation-menu.md`)와 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-06 |
|
||||
| 기반 | shadcn/ui Menubar (Radix `Menubar`) |
|
||||
| 대표 Story | `Components/Navigation/Menubar` → `All` (이름 "Menubar") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Menubar 규격이 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계로 새로 정했다. Dropdown Menu에서 확인한 Radix menu 운영 원칙(Radix 상태 속성, highlighted/disabled, submenu, Escape, focus 복귀, Portal/collision, shadcn 아이콘 유지)만 참고하고 token은 참조하지 않았다.
|
||||
- **구현 방식**: shadcn / Radix Menubar 구조 유지, theme CSS로 HM 스타일 연결. 상태는 Radix 속성(`data-highlighted`, `data-disabled`, `data-state`)만 쓴다.
|
||||
- **역할 구분**: 데스크톱 menu bar(좌우 키로 상위 메뉴 이동)다. Navigation Menu(목적지 링크)와 접근성 모델이 다르고(`role=menubar`/`menu`), Dropdown Menu(단일 trigger)와도 다르다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Bar | 폭 내용 기준(`fit-content`), 높이 38px, padding 2px, gap 2px, 1px `--hm-sem-stroke-input-default` border, radius 4px, 흰 면 |
|
||||
| 상위 Trigger | padding `0 12px`, radius 2px, 14 / 20 / 500 |
|
||||
| Content / SubContent | 흰 면, 1px `--hm-sem-stroke-input-default`, radius 4px, shadow `0 0 12px rgb(0 0 0 / .2)`, padding 4px |
|
||||
| Item | min-height 36px, padding-x 12px, gap 8px, radius 2px, 14 / 20 / 400 |
|
||||
| Label | 12 / 18 / 500, `--hm-sem-fg-secondary` |
|
||||
| Shortcut | 12px, `--hm-sem-fg-secondary` |
|
||||
| Separator | 1px `--hm-sem-stroke-input-default`, Content 폭 전체 |
|
||||
| Check / Radio | 16px indicator 열, shadcn `CheckIcon`, 8px 채운 원 |
|
||||
| Sub menu chevron | shadcn `ChevronRightIcon` |
|
||||
| 위치 | Content `sideOffset` 8, `alignOffset` -4, `align` start (shadcn 기본) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| 상위 Trigger Highlighted / Open | 배경 Accent 100%(`--hm-menubar-trigger-highlight-background` → `--accent`), 글자 `--accent-foreground` |
|
||||
| 상위 Trigger Focus (`:focus-visible`) | 기존 강조 면 위에 독립 3px `--ring` 50% box-shadow ring (`--ring` → `--hm-sem-stroke-input-focus`) |
|
||||
| 상위 Trigger Open + Focus | Accent 100% 면 + Focus ring 동시 표시. Focus가 Open 면을 대체하지 않음 |
|
||||
| Item Highlighted (`data-highlighted`) | 배경 Accent 45%(`--hm-sem-surface-highlight`), 글자 `--accent-foreground` (포인터 hover와 키보드 이동 공통) |
|
||||
| Disabled (`data-disabled`) | Item은 shadcn `opacity-50`, 상위 Trigger는 `opacity: 0.6`, 키보드 이동에서 제외 |
|
||||
| Checked / Selected | `ItemIndicator`가 check 또는 원을 표시 (Radix `data-state`) |
|
||||
| Sub menu open | SubTrigger `data-state="open"`이 Highlighted와 같은 면 유지 |
|
||||
| Destructive (`variant="destructive"`) | 글자 `--hm-sem-action-destructive`, Highlighted 면 `--hm-color-action-destructive-hover` |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Menubar props 그대로. export: `Menubar`, `MenubarMenu`, `MenubarTrigger`, `MenubarContent`, `MenubarItem`, `MenubarCheckboxItem`, `MenubarRadioGroup`, `MenubarRadioItem`, `MenubarLabel`, `MenubarSeparator`, `MenubarShortcut`, `MenubarSub`, `MenubarSubTrigger`, `MenubarSubContent`, `MenubarGroup`, `MenubarPortal`.
|
||||
|
||||
- `MenubarItem`: shadcn `variant="default" | "destructive"`(출력 `data-variant`), `inset`. Label·CheckboxItem·RadioItem·SubTrigger도 `inset`.
|
||||
- 새 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/menubar.css` (`--hm-menubar-*`).
|
||||
|
||||
Bar: `background`, `text`, `border-width|color`, `radius`, `height`, `padding`, `gap`. Trigger: `trigger-*`. Content: `content-background|border-color|radius|shadow|padding`. Item: `item-height|padding-x|gap|radius`, `font-size`, `line-height`, `highlight-*`, `destructive-*`, `label-*`, `shortcut-*`, `separator-color`, `indicator-size`, `radio-dot-size`. Semantic과 Theme palette(`--hm-sem-stroke-input-default`)만 참조한다. `--hm-dropdown-menu-*`는 참조하지 않는다(값이 같아 보이는 부분은 후속 token 정리 후보). 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`role=menubar`, `menu`, `menuitem*`)·키보드 모델(Down/Enter/Space로 열기, Up/Down, Home/End, Left/Right, sub menu, typeahead, Escape)·focus 이동/복귀, Portal·positioning·collision, `data-slot`, `cn(..., className)`, open/close animation, lucide 아이콘.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn의 `rounded-lg border bg-background h-9 p-1 shadow-xs`, Trigger `rounded-sm px-2 py-1 text-sm font-medium focus:bg-accent`, Content·Item의 색/radius/padding class를 제거하고 theme CSS + `data-highlighted`로 대체.
|
||||
- Radio 표시를 별도 아이콘 import 대신 CSS 크기 token을 쓰는 원으로 구현(Dropdown Menu와 같은 방식).
|
||||
- Check / Radio indicator span에 `data-slot="menubar-item-indicator"` 추가.
|
||||
- Bar 폭을 `fit-content`로 지정(shadcn의 `flex`는 가로 전체로 늘어난다).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Menubar"): 파일(item+shortcut, disabled, separator, sub menu 내보내기, destructive 종료), 편집, 보기(Label, Checkbox, Radio Group)를 가진 실제 bar.
|
||||
- 숨김 verification: `TriggerFocus`, `OpenFocus`. `OpenFocus`는 검수 목적으로 Story의 `play`에서 실제 열린 menu의 Trigger에 focus를 준다. 일반 Story/Production은 Radix 기본대로 메뉴 항목에 focus를 이동한다.
|
||||
- Focus 검수는 일반 Story의 computed style 기준으로 한다. Designer의 `.hm-designer button:focus-visible` 장식 outline을 컴포넌트 ring의 근거로 사용하지 않는다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(38px bar, 12px padding, 36px item, 그림자)는 현재 HM 규격이며 디자인 확정 시 재검토. Dropdown Menu와 값이 같은 부분은 공통 Semantic 후보이며 이번에 승격·리팩터링하지 않았다.
|
||||
- **모바일**: 데스크톱 중심 컴포넌트다. 390px에서의 구성(상위 메뉴가 넘칠 때)은 검증하지 않았고, 모바일 menu 패턴은 별도로 정한다.
|
||||
- Disabled opacity: Item은 shadcn 50, 상위 Trigger는 0.6으로 서로 다르다(전역 Disabled 규칙 없음).
|
||||
- Motion은 shadcn 기본.
|
||||
- Menu 안 항목을 링크로 쓰는 예는 정하지 않았다(링크 목적지는 Navigation Menu).
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/menubar.tsx
|
||||
packages/ui/src/theme/menubar.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/menubar.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-06 Focus 정합: `typecheck`, `build`, `build-storybook`, `git diff --check` 통과. 기존 문서 예시 CSS(`bg-[var(...)]`)와 Storybook chunk 크기 경고 유지.
|
||||
- Theme Designer에서 Primary `#6837a8`, Active `#007f73`, Focus `#c75b00`, Accent `#ffd36d`를 분리 적용한 뒤 **일반 Story**에서 확인: Open + Focus Trigger 면 `rgb(255,211,109)` + Focus 50% / 3px ring. 실제 `:focus-visible`, `data-state="open"` 동시 성립, Designer wrapper 없음, computed outline `none`으로 장식 CSS 영향 배제.
|
||||
- 단독 override: Focus는 Trigger ring만, Accent는 Top Trigger 면과 Item Highlighted(45%)만 변경. Primary/Active는 Menubar 면·ring에 영향 없음. Disabled Trigger shadow 없음·opacity 0.6, Disabled Item opacity 0.5 유지.
|
||||
- 일반 키보드 Enter는 실제 첫 Item에 focus를 이동하며 Trigger는 Accent 100% Open 면 / ring 없음, Item은 Accent 45% Highlighted를 유지한다. Escape는 닫힌 Trigger로 focus를 복귀시키며 독립 ring을 표시한다. Navigation Menu 일반 Story에서도 Accent 면 + 3px Focus ring·좌우 키 이동을 확인했고 소스는 변경하지 않았다.
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 키보드(Browser pane): 상위 Trigger에서 Enter로 열기(첫 항목에 focus), ArrowDown/Up으로 이동(disabled 항목 건너뜀), Home/End, 오른쪽 방향키로 Sub menu 열림과 왼쪽 방향키로 Sub menu 닫힘(상위 menu 유지), Sub menu 안에서 오른쪽 방향키로 다음 상위 메뉴 Trigger로 이동, 상위 Trigger에서 오른쪽 방향키로 다음 Trigger 이동, ArrowDown으로 보기 menu 열기, Enter로 Checkbox 토글(menu가 닫히고 Trigger로 focus 복귀, 재오픈 시 `눈금자` checked), Escape로 닫힘과 Trigger focus 유지.
|
||||
- computed style: Bar 38px / radius 4px / border `rgb(222,219,210)` / 흰 면, Trigger 14/20/500, Content radius 4px, shadow `0 0 12px .2`, padding 4px, Item 36px, Highlighted `rgb(189,223,212)`.
|
||||
- 미확인: Radio 선택 키보드 동작(Radix 동일 구조, Checkbox만 확인), 실제 pointer hover, typeahead, 좁은 viewport.
|
||||
@@ -0,0 +1,109 @@
|
||||
# Navigation Menu
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Navigation Menu 고유의 API·Token·상태·결정만 기록한다. Dropdown Menu(`dropdown-menu.md`), Menubar(`menubar.md`)와 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Navigation Menu (Radix `NavigationMenu`) |
|
||||
| 대표 Story | `Components/Navigation/Navigation Menu` → `All` (이름 "Navigation Menu") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Navigation Menu 규격이 없다. 현재 위치(Current) 의미와 Hover/Focus는 독립이라는 ui-viz의 navigation 안내만 참고했다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계로 새로 정했다. Dropdown Menu의 운영 원칙(Radix 상태 속성, Escape, focus 복귀, Portal/viewport)만 참고하고 token은 참조하지 않았다.
|
||||
- **구현 방식**: shadcn / Radix NavigationMenu 구조(Root, List, Item, Trigger, Content, Link, Viewport, Indicator) 유지, theme CSS로 HM 스타일 연결.
|
||||
- **역할 구분**: 목적지 이동(링크) 중심이다. Dropdown Menu(명령)·Menubar(메뉴 바)와 접근성 모델이 다르다(`role=menu`가 아니라 disclosure + link).
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 상위 Trigger / Link | 높이 34px, padding `0 12px`, radius 4px, 14 / 20 / 500, `--hm-sem-fg-primary`, 항목 gap 4px |
|
||||
| Trigger indicator | shadcn `ChevronDownIcon` 12px, open 시 180° 회전 |
|
||||
| Content / Viewport 면 | `--hm-sem-bg-surface`, 1px `--hm-sem-floating-border-color`, radius 4px, shadow `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| Content | padding 8px, min-width 240px, 폭은 안의 내용이 정함 |
|
||||
| Content 안 Link(목적지) | 세로 배치, padding `8px 12px`, gap 2px, radius 2px. `data-description` 자식은 12 / 18 / 400 secondary |
|
||||
| Indicator(화살표) | shadcn 회전 사각형, `--hm-sem-floating-border-color` |
|
||||
| 열기 | hover(Radix 기본 delay)·click·keyboard. 이동 animation은 shadcn 기본 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Hover / Focus / Open (Trigger) | 배경 `--accent`, 글자 `--accent-foreground`. Focus는 ring 모양 `0 0 0 3px ring 50%` 추가 |
|
||||
| Link Hover / Focus | Trigger와 같은 면 |
|
||||
| Active Link (`active`) | Radix `data-active` + `aria-current="page"`. 글자 `--hm-sem-action-primary`, weight 700, 배경 `--accent` 45% (hover면 일반 hover 면) |
|
||||
| Disabled Trigger | `opacity: 0.6`(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), `cursor: not-allowed`, native `disabled` |
|
||||
| Open / Closed | Radix `data-state`, 이동 방향은 `data-motion` (shadcn animation) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix NavigationMenu props 그대로. export: `NavigationMenu`, `NavigationMenuList`, `NavigationMenuItem`, `NavigationMenuTrigger`, `NavigationMenuContent`, `NavigationMenuLink`, `NavigationMenuIndicator`, `NavigationMenuViewport`.
|
||||
|
||||
- `NavigationMenu`: `viewport`(기본 true, shadcn). `false`면 각 Content가 자기 Trigger 아래에 열린다. 출력 `data-viewport`.
|
||||
- `NavigationMenuLink`: `active`(현재 위치), `asChild`로 router Link 조합(router에 의존하지 않음). disabled link는 지원하지 않는다.
|
||||
- `NavigationMenuTrigger`: chevron 내장. `disabled`는 native 속성.
|
||||
- `NavigationMenuIndicator`: 선택 사용(Story의 viewport 예시에서 사용).
|
||||
- 2026-10-06 Visual QA: viewport 모드의 Item은 static으로 두어 Radix List wrapper가 Trigger의 `offsetParent`가 되도록 한다. `viewport={false}`에서만 Item의 relative anchor를 유지한다. Indicator에 임의 위치 보정값을 넣지 않는다. `IndicatorAlignment` verification Story는 서로 다른 label width의 첫·중간·마지막 Trigger와 두 viewport 모드를 검증한다.
|
||||
- 대표 Story의 고정 360/300px 내용은 좁은 화면에서 자연 폭을 사용한다. Desktop 폭도 Story frame container에 제한하며, 모바일 inline Content의 absolute 배치는 Story 범위에서만 적용한다(Production CSS 변경 없음).
|
||||
- 새 prop은 만들지 않았다. Content 안 title/description은 소비 조합이며 `data-description` 속성으로 보조 문구를 표시한다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/navigation-menu.css` (`--hm-navigation-menu-*`).
|
||||
|
||||
`height`, `padding-x`, `gap`, `radius`, `font-size|line-height|font-weight`, `text`, `icon-size`, `hover-background|text`, `active-background|text|font-weight`, `background`, `border-width|color`, `surface-radius`, `shadow`, `content-padding`, `content-min-width`, `item-padding|gap|radius`, `description-*`, `disabled-opacity`. Semantic과 Theme palette(`--hm-sem-floating-border-color`)만 참조한다. `--hm-dropdown-menu-*`, `--hm-tab-*`, `--hm-button-*`는 참조하지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·동작(hover open delay, Arrow 키와 Tab 이동, Escape 닫힘, focus 복귀, Viewport 크기 측정, Indicator 위치), ARIA(`aria-expanded`, `aria-controls`, `aria-current`), 이동 animation(`data-motion`), viewport/non-viewport 구조, chevron, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `navigationMenuTriggerStyle`(rounded-md, h-9, bg-background, hover/focus/open `bg-accent/50`)과 Content·Viewport의 `bg-popover text-popover-foreground rounded-md border shadow` 등을 제거하고 theme CSS로 이전.
|
||||
- Content 안 Link는 shadcn에서 별도 class로 꾸미던 것을 선택자(`content` 하위 `link`)로 처리.
|
||||
- Active Link 스타일(색·weight·배경)을 정의(shadcn은 `data-[active=true]:bg-accent/50`).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Navigation Menu"): `Viewport(기본)`과 `viewport={false}` 두 구성을 한 페이지에 둔다. 제품(설명 있는 3 링크), 자료(2열 4 링크), 회사(현재 위치 링크), 문의(일반 링크), 준비 중(disabled Trigger).
|
||||
- verification: `Default`, `No Viewport`.
|
||||
- Story의 Content 폭은 `style` 인라인으로 지정한다(새 Story 파일의 Tailwind 임의값 클래스가 개발 서버에서 생성되지 않는 일이 있었다).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(34px, 12px padding, Content 240px 최소 폭, 그림자, Active 표현)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- **모바일**: Radix NavigationMenu는 데스크톱 GNB 구조다. 390px에서 Viewport의 폭은 Content 폭(Story에서 360px)을 따라가며 `max-width` 제한이 없어 우측 끝에 닿았다(가로 overflow는 없음). 모바일 navigation(햄버거, Sheet/Drawer 조합 등)은 GNB/Layout 단계에서 별도 패턴으로 검토한다.
|
||||
- disabled link 지원 없음. 필요하면 `aria-disabled` 규칙을 Pagination과 같은 방식으로 후속 검토.
|
||||
- Indicator 사용 여부와 색은 현재 선택 사항이며 별도 규격이 없다.
|
||||
- 열림/닫힘 Motion은 shadcn 기본.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/navigation-menu.tsx
|
||||
packages/ui/src/theme/navigation-menu.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/navigation-menu.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-06 Visual QA: 1280/1024/768/390px에서 제품 Trigger/Arrow center 48.8828/49px, 자료 118.6484/119px. 다른 label width(제품/기술 자료/지원 센터)도 Arrow 오차 절댓값 0.45px 이내. inline 모드의 각 Content가 자기 Trigger 아래에 열림 확인. 대표 Story 두 모드의 열린 패널이 viewport 안에 있고 페이지 가로 overflow 없음. `typecheck`, `build`, `build-storybook` 통과.
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 키보드로 Enter 열림(`aria-expanded=true`), Tab으로 Content 안 첫 링크, 다음 링크로 이동, Escape로 닫히고 Trigger로 focus 복귀, ArrowRight로 다음 상위 항목으로 이동, 마우스 hover로 해당 Content가 열림, 현재 위치 링크가 `aria-current="page"`·`data-active`·weight 700.
|
||||
- computed style: Viewport 흰 면, border `rgb(222,219,210)`, radius 4px, shadow `0 0 12px .2`. Content 폭이 안의 목록(360px) 폭에 맞춰 376px.
|
||||
- 390×844: 열린 Viewport 폭 378px, 좌측 16px에서 시작해 오른쪽 끝에 닿음(가로 overflow 없음). 한계는 §11.
|
||||
- 미확인: 닫힘 animation 시각, `viewport={false}` 구성의 열림(코드 경로는 Radix/shadcn 기본), 두 Content 사이 이동 animation 시각, ArrowDown으로 Content 진입.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Pagination
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Pagination 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Pagination (`nav > ul > li > a`, Radix 없음) |
|
||||
| 대표 Story | `Components/Navigation/Pagination` → `All` (이름 "Pagination") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Pagination이 컴포넌트 이름으로만 있고 규격은 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계(fg-primary, action-primary, accent)로 새로 정했다.
|
||||
- **구현 방식**: shadcn Pagination 구조 유지. Presentational / navigation primitive로, page state와 href는 소비자가 관리한다. 링크는 `a`로 유지하고 Button으로 바꾸지 않는다.
|
||||
- **Button과의 관계**: shadcn은 `buttonVariants`를 입혀 Button처럼 그리지만, HM Button은 `data-slot="button"` 기반이라 이를 그대로 쓰면 HM 스타일이 적용되지 않고 Button token도 재사용하면 안 된다. 그래서 `PaginationLink`는 독립 anchor 스타일(`--hm-pagination-*`)로 구현했다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Page link | 34 × 34px(min-width 34), radius 4px(`--hm-radius-control-md`), 14 / 20 / 500, `--hm-sem-fg-primary` |
|
||||
| Previous / Next | 높이 34px, padding `0 12px`, 아이콘 16px, 라벨 `hidden sm:block`("Previous"/"Next", 모바일은 아이콘만) |
|
||||
| 간격 | 목록 gap 4px |
|
||||
| Current Page | 배경 `--hm-sem-action-primary`(Primary Green `#123328`), 글자 `--hm-sem-on-action-primary`(흰색), weight 700 |
|
||||
| Ellipsis | 34 × 34 영역, 아이콘 16px, `--hm-sem-fg-secondary` |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Hover | 배경 `--accent` 45%, 글자 `--accent-foreground` |
|
||||
| Focus (`:focus-visible`) | ring 모양 `0 0 0 3px ring 50%` |
|
||||
| Current (`isActive`) | `aria-current="page"`, 위 Current 스타일(hover에도 `Current`로 보이도록 hover 배경 대신 유지) |
|
||||
| Disabled (`aria-disabled="true"`) | `opacity: 0.6`(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), `cursor: not-allowed`, hover 면 없음, `tabIndex -1`, 클릭 취소(`preventDefault`) |
|
||||
|
||||
## 5. API
|
||||
|
||||
export: `Pagination`, `PaginationContent`, `PaginationItem`, `PaginationLink`, `PaginationPrevious`, `PaginationNext`, `PaginationEllipsis`.
|
||||
|
||||
- `PaginationLink`: `a` props + `isActive`(현재 페이지), `size` `"default" | "icon"`(기본 `icon`; shadcn은 Button size, HM은 `default`/`icon` 둘만). 출력 `data-slot="pagination-link"`, `data-active`, `data-size`.
|
||||
- disabled: 별도 prop 없이 `aria-disabled`를 쓴다. `aria-disabled="true"`이면 컴포넌트가 `tabIndex=-1`로 두고 클릭(onClick 호출 포함)을 취소한다. anchor에는 native disabled가 없어 class만으로는 접근성이 보장되지 않기 때문에 보완했다.
|
||||
- `PaginationPrevious`/`PaginationNext`: 기본 `aria-label`("Go to previous/next page")과 라벨, `children`으로 교체 가능.
|
||||
- 새 상태 관리, `page`/`total` 같은 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/pagination.css` (`--hm-pagination-*`).
|
||||
|
||||
`gap`, `height`, `min-width`, `padding-x`, `radius`, `font-size|line-height|font-weight`, `icon-size`, `text`, `hover-background`, `hover-text`, `active-background`, `active-text`, `active-font-weight`, `ellipsis-color`, `disabled-opacity`. Semantic만 참조하고 `--hm-button-*` 등 다른 컴포넌트 Token을 쓰지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn에서 유지한 항목
|
||||
|
||||
`nav role="navigation" aria-label="pagination"`, `ul`/`li` 구조, `a` 링크, `isActive`→`aria-current="page"`, Previous/Next 아이콘과 `aria-label`, Ellipsis의 `aria-hidden` + sr-only, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- `buttonVariants` 합성을 제거하고 독립 anchor 스타일로 대체(위 §2).
|
||||
- `size`를 shadcn의 Button size 전체에서 `default`/`icon`으로 좁혔다.
|
||||
- `aria-disabled` 처리(tabIndex, 클릭 취소)를 추가했다.
|
||||
- 현재 페이지를 outline 대신 채움(primary)으로 표시.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Pagination"): First page, Middle page, Last page, Long range with ellipsis(1 … 9 10 11 … 20)를 한 페이지에 둔다. 각 Specimen이 local state를 가지고 클릭 시 현재 페이지가 실제로 바뀐다(`#page-N` href + `preventDefault`).
|
||||
- verification: `First Page`, `Last Page`, `Long Range`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
- 2026-10-06 (Pagination Current 원복): 직전 Active Semantic 작업에서 Current를 `--hm-sem-state-active`(Light 2)로 바꾼 변경을 취소하고 이전 값 `--hm-sem-action-primary` / `--hm-sem-on-action-primary`(Primary Green `#123328`, 흰색)로 복원했다. Current + Hover도 Current 색을 유지한다. Hover의 `--accent` 45%는 유지한다. Focus·Disabled·API·구조 변경 없음.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(34px, 4px radius, 12px padding, 채움 current, Disabled opacity 0.6)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- page range(ellipsis 계산)는 소비자 책임이다(Story의 `pageRange`는 예시일 뿐 컴포넌트가 아님).
|
||||
- 언어: Previous/Next 기본 라벨은 shadcn 영어 그대로(`children`으로 교체).
|
||||
- 좁은 화면 compact 표현은 shadcn의 라벨 숨김(`sm` 미만)만 유지. 별도 compact variant 없음.
|
||||
- 페이지 크기 선택, 결과 수 표시는 범위 밖.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/pagination.tsx
|
||||
packages/ui/src/theme/pagination.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/pagination.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane, JS click): page 4 클릭으로 current가 4로 변경, Next로 5, 첫 페이지의 Previous(`aria-disabled`)와 마지막 페이지의 Next 클릭은 상태를 바꾸지 않음. `aria-disabled` 링크는 `tabindex=-1`, opacity 0.6, cursor not-allowed. Current는 `aria-current="page"`, 34×34, 배경 `rgb(18,51,40)`, 글자 white, weight 700.
|
||||
- 구조: `a` 요소, ellipsis 위치(1 … 9 10 11 … 20)와 First/Last 상태 확인.
|
||||
- Responsive: 390×844에서 Previous/Next 라벨이 숨겨져 아이콘만 남고, 20페이지 범위(`1 … 9 10 11 … 20`)가 350px 한 줄에 들어가며 가로 overflow 없음.
|
||||
- 미확인: 실제 키보드 Tab/Enter 순서, pointer hover·focus ring 시각.
|
||||
@@ -0,0 +1,130 @@
|
||||
# Popover
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Popover 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Popover (Radix `Popover`) |
|
||||
| 대표 Story | `Components/Overlay/Popover` → `All` (이름 "Popover") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Popover 규격이 없다(인벤토리 "상세 디자인·API 보류"). 같은 floating surface인 Dialog의 윤곽 의도만 참고했다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Popover 구조를 유지하고 theme CSS로 HM 스타일을 연결. Popover는 일반 floating panel이며 modal이 아니다(overlay·focus trap 없음).
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Surface | `--hm-sem-bg-surface` |
|
||||
| 전경 | `--hm-sem-fg-primary` |
|
||||
| Border | 1px `--hm-sem-floating-border-color` |
|
||||
| Radius | `--hm-radius-container` (4px) |
|
||||
| Shadow | `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| Padding | 16px |
|
||||
| Width | 288px (viewport − 16px 이하로 제한) |
|
||||
| Typography | 14 / 20 / 400 |
|
||||
| Position | `sideOffset` 4, `align` center (기본) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Open / Closed | Radix `data-state`, shadcn open/close animation |
|
||||
| Focus | 열릴 때 Radix가 content 안 첫 focus 가능 요소로 이동, 닫힐 때 trigger로 복귀(Escape/Close) |
|
||||
| 바깥 클릭 | 닫힘. focus는 클릭한 곳에 둔다 |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Popover props 그대로. export: `Popover`, `PopoverTrigger`, `PopoverContent`, `PopoverAnchor`, `PopoverClose`.
|
||||
|
||||
- `PopoverContent`: `align` 기본 `center`, `sideOffset` 기본 4.
|
||||
- `PopoverClose`: `asChild`일 때 자식의 `data-slot`을 유지(DialogClose와 같은 규칙, Button HM 스타일 보존).
|
||||
- 출력 속성: `data-slot="popover-*"`, Radix `data-state`·`data-side`·`data-align`.
|
||||
|
||||
### 접근성 이름
|
||||
|
||||
`PopoverContent`는 `role="dialog"`로 출력된다. 소비자는 패널의 목적을 나타내는 접근성 이름을 `aria-label` 또는 `aria-labelledby`로 지정한다. Trigger의 텍스트는 패널 이름으로 자동 연결되지 않는다.
|
||||
|
||||
```tsx
|
||||
<PopoverContent aria-label="프로젝트 이름 변경">
|
||||
{/* 입력 폼 */}
|
||||
</PopoverContent>
|
||||
```
|
||||
|
||||
보이는 제목이 있다면 해당 제목의 고유 ID를 연결한다. 같은 화면에서 반복되는 패널은 `React.useId()` 등으로 서로 다른 ID를 생성한다.
|
||||
|
||||
```tsx
|
||||
const titleId = React.useId()
|
||||
|
||||
<PopoverContent aria-labelledby={titleId}>
|
||||
<h2 id={titleId}>프로젝트 안내</h2>
|
||||
<p>일정과 담당자 정보를 확인합니다.</p>
|
||||
</PopoverContent>
|
||||
```
|
||||
|
||||
대표 Story는 정보·폼·정렬 예제 각각에 `aria-label`을 제공한다. 이름 지정은 기존 레이아웃과 Radix의 focus 이동·Escape·닫힘 동작을 바꾸지 않는다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/popover.css` (`--hm-popover-*`).
|
||||
|
||||
`background`, `text`, `border-width`, `border-color`, `radius`, `shadow`, `padding`, `width`, `font-size`, `line-height`, `font-weight`. 다른 컴포넌트의 Component Token을 참조하지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`role=dialog`, `aria-expanded`, `aria-controls`)·Escape·바깥 클릭 닫힘·focus 이동/복귀, Portal·positioning·collision, `data-slot`, `cn(..., className)`, animation class, `z-50`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `w-72 rounded-md border p-4 shadow-md bg-popover text-popover-foreground` 등 시각 class 제거 → theme CSS.
|
||||
- `PopoverClose` 추가 export (shadcn 기본 export에 없는 Radix `Close`).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Popover"): 정보 패널, 폼 패널(Input + 취소/저장 `PopoverClose`), `align` start/center/end 비교.
|
||||
- verification: `Open`(열린 상태).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(288px, 16px padding, 그림자)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- arrow 없음(shadcn 기본에 없음).
|
||||
- 크기 variant 없음. 폭은 `className`으로 확장.
|
||||
- Motion 규칙은 정의되지 않아 shadcn 기본 animation 유지.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/popover.tsx
|
||||
packages/ui/src/theme/popover.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/popover.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-08: 대표 Story의 정보·폼·정렬 3종, 총 5개 `role="dialog"` 접근성 이름을 확인했다. 키보드 Enter·Tab·Escape, 폼 자동 focus, 취소/저장 `PopoverClose`의 닫힘·trigger focus 복귀, 바깥 클릭 닫힘을 Browser에서 확인했다. 공개 API 타입 검사 통과, 최종 검증 Console 오류 없음.
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 클릭 열기(focus가 content 안 Input으로 이동), Escape 닫힘과 trigger focus 복귀, 바깥 클릭 닫힘.
|
||||
- computed style: width 288px, white, border `rgb(222,219,210)` 1px, radius 4px, padding 16px, shadow `0 0 12px .2`.
|
||||
- 미확인: 좁은 viewport collision 시각.
|
||||
|
||||
## 14. Floating 계열 공통 (Semantic 승격 완료)
|
||||
|
||||
Dropdown Menu·Popover·Hover Card는 surface(White, 1px pale border, 4px radius, `0 0 12px .2` 그림자)가 같다. 의미(떠 있는 패널의 면·윤곽·그림자)도 같아 공통 Semantic 후보는 아래이며, `default.css`의 `--hm-sem-floating-*`로 승격했고 각 컴포넌트의 `--hm-<component>-*`가 이를 참조한다(Component Token은 유지). Dialog(Dialog 전용 값), Tooltip(dark inverse face)은 같은 면이 아니다.
|
||||
|
||||
- Floating background / border / shadow / radius
|
||||
|
||||
Popover만 다르게 하려면 `--hm-popover-*`만 override한다. Date Picker·Combobox popup은 Popover를 그대로 쓰므로 이 값을 따른다.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Progress
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Progress 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격, 기본 Progress 하나) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Progress (Radix `Progress`) |
|
||||
| 대표 Story | `Components/Data & Display/Progress` → `All` (이름 "Progress") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Progress가 컴포넌트 이름으로만 있고 규격은 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Progress 구조(Root + Indicator)를 유지하고 theme CSS로 연결.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Track | 높이 8px, radius 9999px, 배경 `--hm-sem-palette-warm-neutral-light` |
|
||||
| Indicator | 배경 `--hm-sem-state-active`(현재 Active와 Progress의 색 정책이 같아 같은 Semantic을 쓴다. 의미가 달라지면 별도 Semantic으로 분리), 폭 = value/max 비율(`translateX`), `transition-all`(shadcn 기본) |
|
||||
| 폭 | 부모 폭 100% |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Loading / Complete | Radix `data-state="loading"`(0 ≤ value < max), `"complete"`(value = max) |
|
||||
| Indeterminate | `value`를 주지 않으면 Radix `data-state="indeterminate"`. 현재는 Indicator가 비어 있다(별도 표현 없음) |
|
||||
| Success / Warning / Error | 만들지 않았다(HM 상태색 미확정) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Progress props 그대로. export: `Progress`.
|
||||
|
||||
- `value`, `max`(기본 100), `getValueLabel`, `asChild`. Indicator는 내부에서 렌더하며 별도 export 없음(shadcn 구조).
|
||||
- ARIA: `role="progressbar"`, `aria-valuemin`, `aria-valuemax`, `aria-valuenow`(Radix). 접근 가능한 이름은 소비자가 `aria-label`/`aria-labelledby`로 지정.
|
||||
- `variant`, `size`, `status` 같은 prop은 만들지 않았다. 높이나 색을 바꾸려면 `--hm-progress-*` Token을 덮는다(unlayered theme 규칙이라 `className`의 높이 utility는 이기지 못한다).
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/progress.css` (`--hm-progress-*`).
|
||||
|
||||
`height`, `radius`, `track-background`, `indicator-background`. Semantic과 Theme palette(`--hm-sem-palette-warm-neutral-light`)만 참조한다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA·`data-state`, Indicator의 `translateX` 표현(폭 계산은 HM에서 value/max 비율로 보정), `transition-all`, `overflow-hidden`, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `h-1 rounded-full bg-muted`, Indicator `bg-primary` class를 제거하고 theme CSS로 이전. 높이를 4px에서 8px로 키웠다.
|
||||
- 트랙색을 `muted`가 아닌 `--hm-sem-palette-warm-neutral-light`로 정했다(`muted`는 베이지 배경에서 거의 안 보임).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Progress"): 0%, 50%, 100%, max=200, Interactive value(`-25`/`+25` 버튼, local state)를 한 페이지에 둔다.
|
||||
- verification: `Zero`, `Middle`, `Full`, `NonDefaultMax`(max=200의 0/50/200/indeterminate). 대표 Story에도 동일 specimen을 둔다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(8px, 트랙색)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 상태 variant(success/warning/error)는 HM 상태색 확정 후.
|
||||
- Indeterminate 표현(움직이는 막대)은 없다. Motion 규칙이 정해지면 검토.
|
||||
- 값 label/퍼센트 텍스트는 소비 조합(컴포넌트에 없음).
|
||||
- `prefers-reduced-motion` 대응은 추가하지 않았다(`transition-all`은 shadcn 기본).
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/progress.tsx
|
||||
packages/ui/src/theme/progress.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/progress.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 DOM(Browser pane): `role="progressbar"`, `aria-valuemin=0`, `aria-valuemax=100`, `aria-valuenow`가 0/50/100/30(interactive), `data-state` loading/complete, indicator 채움이 0/50/100%.
|
||||
- value 변경: `+25` 클릭으로 `aria-valuenow`가 30 → 55, 이어서 100으로 클램프되고 indicator transform이 `translateX(0%)`, 렌더 후 채움 100%. 0과 max 경계 확인.
|
||||
- computed style: 높이 8px, radius 9999px, 트랙 `rgb(224,218,206)`, indicator `rgb(18,51,40)`.
|
||||
- Responsive(390×844): 트랙이 부모 폭 358px을 따르고 가로 overflow 없음.
|
||||
- 미확인: indeterminate 표시, `max`를 100이 아닌 값으로 쓴 경우(Indicator 계산이 value를 %로 간주하므로 `max≠100`이면 소비자가 환산해야 함).
|
||||
|
||||
### 2026-10-06 정합성 감사 수정
|
||||
|
||||
위 2026-10-02 검증 이력의 max≠100 제한은 해소했다. Indicator는 Radix와 같은 유효 범위/기본 max를 사용하여 value/max 비율로 채운다. max=200, value=50은 ARIA 50/200과 25% 채움이 일치하며 값 없음은 기존 indeterminate 빈 Track을 유지한다.
|
||||
@@ -0,0 +1,87 @@
|
||||
# Radio Group
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Radio Group 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Radio Group (Radix `RadioGroup`) |
|
||||
| 대표 Story | `Components/Form/Radio Group` → `All` (이름 "Radio Group") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz 기존 테스트 규격에는 Radio 문서가 없다(`RADIO-01` TBD, HOLD, group spacing·전경 role TBD). 확정 규격이 없어 참고만 했다.
|
||||
- **현재 HM 규격**: §3~§4. Checkbox와 같은 HM Semantic 체계로 새로 정했다(Component Token은 따로 둔다).
|
||||
- **구현 방식**: Radix RadioGroup(roving focus, 방향키 선택, `role=radiogroup/radio`, `data-state`, `data-orientation`)을 유지하고 theme CSS로 연결. shadcn `CircleIcon`을 inner dot으로 유지.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Outer | 18 × 18px 원, border 1px `--hm-sem-stroke-default`, face `--hm-sem-bg-surface` |
|
||||
| Inner dot | 8px, `--hm-sem-state-active` (shadcn `CircleIcon`, fill) |
|
||||
| Group gap | vertical 12px, horizontal 16px (wrap) |
|
||||
| Orientation | Radix `orientation` prop → `data-orientation`. 기본 vertical(grid), horizontal은 flex |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Unchecked | 위 기본 규격 |
|
||||
| Checked | border `--hm-sem-state-active` + inner dot |
|
||||
| Hover | border `--hm-sem-action-primary` |
|
||||
| Focus (`:focus-visible`) | shadcn ring 유지 |
|
||||
| Error (`aria-invalid="true"` on item) | border `--hm-sem-action-destructive` |
|
||||
| Disabled | `opacity: 0.6`, `cursor: not-allowed` (항목 또는 그룹 `disabled`) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix `RadioGroup`/`RadioGroupItem` props 그대로 (`value`/`defaultValue`/`onValueChange`, `orientation`, `disabled`, `name`, `required`, `loop`, `value` on item). 추가 prop 없음. 출력 속성: `data-slot="radio-group"`, `"radio-group-item"`, `"radio-group-indicator"`. 그룹 이름은 `aria-label` 또는 `aria-labelledby`로 준다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
`packages/ui/src/theme/radio-group.css` (`--hm-radio-*`): `size`, `border-width`, `dot-size`, `background`, `border-color`, `hover-border-color`, `checked-border-color`, `dot-color`, `error-border-color`, `group-gap`, `group-gap-horizontal`. Semantic 참조, Checkbox 등 다른 컴포넌트 Token 미사용. Primitive 신규 추가 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA·roving tabindex·방향키 선택, `data-slot`, `cn(..., className)`, `transition-colors`, `CircleIcon`, focus ring, disabled 커서.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
shadcn `size-4 border-input text-primary shadow-xs dark:` class 제거 → theme CSS. Group gap `gap-3` 고정 대신 Token + horizontal 레이아웃. Disabled opacity 50 → 60.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
대표 Story `All`: `vertical`, `horizontal`, `hover`, `focus`, `error`, `disabled-item`, `disabled-group`. Focus는 `play`로 실제 focus. verification: `Vertical`, `Horizontal`, `Disabled`, `Error`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서(작업 지시). 개별 수치 결정 없음.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 그룹 Label/Heading, Error·Helper 슬롯의 소유와 우선순위(컴포넌트 없음, 소비 조합).
|
||||
- 수치(18px, 8px dot, gap 12/16)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Hit area 확장 미적용.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/radio-group.tsx
|
||||
packages/ui/src/theme/radio-group.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export RadioGroup, RadioGroupItem)
|
||||
src/components/radio-group.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- computed style: 18×18, 50% radius, border 1px; Checked border `rgb(18,51,40)`, dot 8px; Unchecked `rgb(154,146,132)`; Error `rgb(211,47,47)`; Disabled opacity 0.6; vertical gap 12px, horizontal `flex` gap 16px.
|
||||
- interaction: 항목 click으로 선택 변경, ArrowUp으로 선택·focus가 이전 항목으로 이동, roving tabindex(선택 항목 0, 나머지 -1) 확인.
|
||||
- 미확인: 실제 pointer hover·focus ring 시각, 사이드바 화면, 390px 폭.
|
||||
@@ -0,0 +1,110 @@
|
||||
# Resizable
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Resizable 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태와 성격
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 성격 | **Interactive layout primitive** (panel 크기 조절). Dashboard/Sidebar/persistence를 소유하지 않는다 |
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-03 |
|
||||
| 기반 | shadcn/ui Resizable = **react-resizable-panels v4** (`^4.14.2`) |
|
||||
| 대표 Story | `Components/Disclosure/Resizable` → `All` (이름 "Resizable") |
|
||||
| 보류 | 아래 §12 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Resizable이 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다. Separator Token은 참조하지 않는다.
|
||||
- **구현 방식**: 라이브러리가 drag, 최소/최대, collapsing, layout 계산, 키보드 resize, pointer hit-target 확장, separator ARIA를 소유한다. HM은 divider와 선택형 grip만 스타일한다.
|
||||
- **v4 주의**: 라이브러리 이름과 API가 v3와 다르다. v3의 `PanelGroup`/`PanelResizeHandle`가 아니라 **`Group`/`Panel`/`Separator`**이고, 크기는 **숫자 = 픽셀, 단위 없는 문자열(`"30"`) 또는 `"30%"` = 퍼센트**다. shadcn 이름(`ResizablePanelGroup`/`ResizablePanel`/`ResizableHandle`)은 유지했다.
|
||||
- **보류**: §12.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Group | `flex h-full w-full`. `orientation` `"horizontal"`(기본) / `"vertical"`. Group·Panel의 inline sizing은 라이브러리 것이라 덮을 수 없다 |
|
||||
| Divider(Separator) | 1px, `--hm-sem-stroke-input-default`. 나란한 panel 사이는 세로선(`aria-orientation="vertical"`), 쌓인 panel 사이는 가로선 |
|
||||
| Pointer target | 라이브러리가 1px divider 둘레로 확장한다(DOM 크기가 아니라 hit test). 8px 떨어진 곳은 잡히지 않았다 |
|
||||
| Grip (`withHandle`) | 12 × 16px, 흰 면, 1px pale border, radius 2px, `GripVerticalIcon` 10px `--hm-sem-fg-secondary`, divider 가운데. 쌓인 panel 사이에서는 90° 회전 |
|
||||
| 크기 | 소비자(Group의 `className`/부모)가 정한다. 컴포넌트가 높이/폭을 강제하지 않는다 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Hover (`data-separator="hover"`) | divider와 1px halo가 `--hm-sem-stroke-default` |
|
||||
| Dragging (`data-separator="active"`) | divider와 1px halo가 `--hm-sem-action-primary` |
|
||||
| Focus (`:focus-visible`) | divider와 1px halo가 `--hm-sem-stroke-input-focus` |
|
||||
| Disabled | opacity 0.6(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함) |
|
||||
| Collapsed panel | 라이브러리의 collapse(`collapsible`, `collapsedSize`). `aria-valuenow`가 0 |
|
||||
|
||||
## 5. API
|
||||
|
||||
라이브러리 props 그대로. export: `ResizablePanelGroup`(= `Group`), `ResizablePanel`(= `Panel`), `ResizableHandle`(= `Separator`).
|
||||
|
||||
- `ResizablePanelGroup`: `orientation`, `defaultLayout`, `onLayoutChange`/`onLayoutChanged`, `groupRef`, `disabled`, `resizeTargetMinimumSize`, `id`. 저장(`useDefaultLayout`) 같은 persistence는 라이브러리에 있지만 HM이 래핑하거나 쓰지 않았다.
|
||||
- `ResizablePanel`: `defaultSize`, `minSize`, `maxSize`, `collapsible`, `collapsedSize`, `panelRef`, `onResize`, `id`.
|
||||
- `ResizableHandle`: `withHandle`(shadcn, grip 표시), `disabled`, `disableDoubleClick`, 그리고 div props(`aria-label` 등). `data-slot="resizable-handle"`, grip은 `resizable-handle-grip`.
|
||||
- 접근성(라이브러리): Separator는 `role="separator"`, `aria-orientation`, `aria-valuenow|min|max`(%), `aria-controls`(앞 panel id), `tabindex=0`. 키보드: 가로 배치는 ←/→(5% 단계), 세로 배치는 ↑/↓, Home/End는 최소/최대, collapsible이면 Enter가 접힘을 토글. 의미 있는 이름은 소비자가 `aria-label`로 준다.
|
||||
- Group에는 `role`이 붙지 않는다(라이브러리 구조). 이름은 `aria-label`을 소비자가 준다.
|
||||
|
||||
## 6. Dependency
|
||||
|
||||
- `react-resizable-panels` ^4.14.2 (`packages/ui`에 추가, shadcn Resizable의 기반). 사용: Resizable. CENTER 패키지에는 추가하지 않았다.
|
||||
|
||||
## 7. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/resizable.css` (`--hm-resizable-*`).
|
||||
|
||||
`divider-width|color`, `hover-color`, `active-color`, `focus-color`, `halo-width`, `grip-width|height|radius|background|border-color|icon-size|icon-color`, `disabled-opacity`. Semantic과 Primitive(`--hm-warm-neutral-pale`, `--hm-warm-neutral-mid`)만 참조하며 `--hm-separator-*`는 참조하지 않는다. 신규 Primitive와 Semantic 없음. 규칙은 `@layer components`에 있어 소비자 utility가 이긴다.
|
||||
|
||||
## 8. 라이브러리에서 유지한 항목
|
||||
|
||||
drag(pointer)와 최소/최대 clamp, collapsible, keyboard resize, double click 동작, layout callback, ARIA, 커서 처리, panel inline sizing.
|
||||
|
||||
## 9. 소비자 확장성
|
||||
|
||||
- Group 크기: `className`/`style`(Story는 frame의 `height`/`max-width`)로 정한다.
|
||||
- Divider/Grip 색과 크기: `--hm-resizable-*` token을 덮거나 `ResizableHandle className`의 utility를 쓴다.
|
||||
- Panel 내용과 최소 크기: 소비자가 `minSize`를 정한다(**`minSize` 없이 끌면 panel이 0까지 줄어들 수 있다.** Story는 모든 panel에 `minSize`를 둔다).
|
||||
|
||||
## 10. Storybook
|
||||
|
||||
- 대표 Story `All`("Resizable"): Horizontal(list min 15%), Vertical with grip, Horizontal with grip, Min/max(25%–60%), Collapsible panel을 한 페이지에 둔다. 프레임과 panel 내용은 Story 배치이며 Dashboard가 아니다.
|
||||
- verification: `Horizontal`, `Vertical`, `Min Max`.
|
||||
|
||||
## 11. 사용자 확정 결정
|
||||
|
||||
- 2026-10-03: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). Dashboard state, 저장, preset은 이후 Layout Pattern에서 조합한다(작업 지시).
|
||||
|
||||
## 12. 미확정 / 보류
|
||||
|
||||
- 수치(1px divider, 12×16 grip, hover/active 색)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- **모바일**: 390px에서 가로 분할은 사용 가능하다(panel 107/249px). 하지만 좁은 화면의 가로 분할은 정보 밀도에 부적합할 수 있어, 자동으로 세로로 바꾸는 responsive는 만들지 않았다. 모바일 layout 대체는 Dashboard/Layout 단계에서 검토한다.
|
||||
- 중첩 Group, 3개 이상 panel, Grid(`Grid`/`Cell`/`Gridline`, 2D)는 검증하지 않았다.
|
||||
- 레이아웃 저장(`useDefaultLayout`, localStorage)과 사용자 preference는 만들지 않았다.
|
||||
- 터치 기기에서의 pointer target(`resizeTargetMinimumSize.coarse`)은 라이브러리 기본이다.
|
||||
|
||||
## 13. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/resizable.tsx
|
||||
packages/ui/src/theme/resizable.css
|
||||
packages/ui/package.json (dependency: react-resizable-panels)
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/resizable.stories.tsx
|
||||
```
|
||||
|
||||
## 14. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-03).
|
||||
- 실제 DOM(Browser pane): Separator `role=separator`, `aria-orientation`(가로 배치는 vertical, 세로 배치는 horizontal), `aria-valuenow`가 30 → 50.6(drag 후), `aria-valuemin`/`max`가 panel의 min/max를 반영(예: list min 15, 다른 panel min 20이면 max 80), `aria-controls`, `tabindex=0`, divider 1px `rgb(222,219,210)`.
|
||||
- pointer drag: divider를 오른쪽으로 끌어 30% → 50.6%(panel 282/275), 왼쪽으로 끝까지 끌어 min 15%(84px)에서 멈춤, 오른쪽 끝까지 끌면 다른 panel에 `minSize`가 없어 0까지 줄어듦(Story에 `minSize`를 추가하는 근거). 세로 분할은 아래로 drag해 40% → 67%.
|
||||
- keyboard: 가로에서 ArrowLeft가 5%씩 줄고, Home이 최소(15), End가 최대, 세로에서는 ArrowUp이 줄고 ArrowRight는 무시, collapsible에서 Home이 0으로 접고 Enter가 이전 크기(30%)로 복원. focus 시 divider와 halo가 `rgb(18,51,40)`.
|
||||
- 레이아웃: Group `flex`, panel 크기는 라이브러리 inline style, grip은 12×16px이고 쌓인 panel 사이에서 90° 회전.
|
||||
- Responsive(390×844): 모든 Group이 356px 안에 들어가고 페이지 가로 overflow 없음, 가로 분할 panel 107/249px.
|
||||
- 미확인: 터치 drag, 중첩/3개 이상 panel, hover 상태(`data-separator="hover"`)의 pointer 시각, 포인터 hit target의 정확한 범위(8px 밖은 잡히지 않음만 확인).
|
||||
@@ -0,0 +1,98 @@
|
||||
# Scroll Area
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Scroll Area 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Scroll Area (Radix `ScrollArea`) |
|
||||
| 대표 Story | `Components/Data & Display/Scroll Area` → `All` (이름 "Scroll Area") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Scroll Area가 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3. 현재 HM Primitive로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix ScrollArea 구조를 유지. scroll 동작(wheel, touch, native keyboard, viewport 측정, thumb drag)은 Radix가 소유하고 scrollbar 모양만 정한다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Scrollbar | 두께 10px(`p-px`, 투명 border로 가장자리 여백), 트랙은 투명 |
|
||||
| Thumb | `--hm-sem-stroke-default`, hover/drag 중 `--hm-sem-fg-secondary`, radius 9999px |
|
||||
| 노출 | Radix 기본 `type="hover"`: 마우스가 올라가 있거나 scroll 중에만 scrollbar가 나타난다 |
|
||||
| Orientation | 세로는 기본, 가로는 `<ScrollBar orientation="horizontal" />`를 추가 |
|
||||
| Corner | Radix Corner |
|
||||
| 크기 | 없음. Root에 `className`/`style`로 높이·폭 지정(Story의 크기는 Story 배치) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Scrollbar visible / hidden | Radix `data-state="visible" | "hidden"` |
|
||||
| Thumb hover / active | 색이 한 단계 어두워짐 |
|
||||
| Viewport focus | shadcn ring(`focus-visible:ring-[3px] ring-ring/50`) 유지 |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix ScrollArea props 그대로. export: `ScrollArea`, `ScrollBar`.
|
||||
|
||||
- `ScrollArea`: `type`(`hover` 기본 | `auto` | `always` | `scroll`), `scrollHideDelay`, `dir`. Viewport, 세로 ScrollBar, Corner를 내장한다.
|
||||
- `ScrollBar`: `orientation` `"vertical"`(기본) | `"horizontal"`. 가로 scroll이 필요하면 소비자가 직접 추가한다(shadcn 구조).
|
||||
- 출력 `data-slot="scroll-area|scroll-area-viewport|scroll-area-scrollbar|scroll-area-thumb"`, `data-orientation`.
|
||||
- 새 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/scroll-area.css` (`--hm-scroll-area-*`).
|
||||
|
||||
`scrollbar-size`, `thumb-background`, `thumb-hover-background`, `thumb-radius`. Semantic(`--hm-sem-stroke-default`, `--hm-sem-fg-secondary`)을 참조한다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·scroll 동작·scrollbar `type`, viewport의 focus ring, `touch-none select-none` scrollbar, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-border` thumb과 scrollbar 두께 class(`w-2.5`/`h-2.5`)를 Token으로 이전, thumb 색을 베이지 배경에서 보이는 warm neutral로 지정.
|
||||
- 노출 방식은 변경하지 않았다(Radix 기본 hover).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Scroll Area"): Vertical, Horizontal, Both axes, Table-like content를 한 페이지에 둔다.
|
||||
- verification: `Vertical`, `Horizontal`.
|
||||
- 영역 크기는 Story 배치(`style`)이며 Scroll Area의 크기 API가 아니다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 두께 10px, thumb 색, 항상 노출 vs hover 노출은 현재 HM 규격이며 디자인 확정 시 재검토(지금은 Radix 기본 hover).
|
||||
- 터치 기기에서는 Radix가 native scroll을 쓰지만 scrollbar 노출은 `type`에 따른다.
|
||||
- native 키보드 scroll: Viewport는 `tabindex=-1`이라 Tab 순서에 들어가지 않는다(shadcn 구조 그대로). 키보드 사용자를 위한 focus 가능 scroll 영역 정책(예: `tabIndex={0}`)은 사용처가 정해질 때 결정한다.
|
||||
- 가로 scroll이 필요한 경우 `ScrollBar orientation="horizontal"` 추가를 잊기 쉽다(shadcn 구조).
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/scroll-area.tsx
|
||||
packages/ui/src/theme/scroll-area.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/scroll-area.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 마우스 wheel로 세로 scroll(`scrollTop` 0 → 300), thumb 위에서 아래로 drag해 `scrollTop` 300 → 456, 가로 wheel로 `scrollLeft` 0 → 300, 마우스를 올리면 scrollbar가 `data-state="visible"`로 나타나고 thumb 7×39, 색 `rgb(154,146,132)`, scrollbar 폭 10px.
|
||||
- 구조: 4개 Specimen에서 세로/가로/양방향 overflow가 각각 의도대로(세로만, 가로만, 둘 다) 생김. Viewport는 `focus()` 가능.
|
||||
- Responsive(390×844): 영역이 고정 크기(280×200 등)로 유지되고 페이지 가로 overflow 없음.
|
||||
- 미확인: 화살표 키 native scroll(도구의 합성 키 입력으로 확인되지 않음), 터치 scroll, 가로 scrollbar thumb drag, corner 시각.
|
||||
@@ -0,0 +1,152 @@
|
||||
# Select
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Select 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (`hm-select-trigger` class 없음) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Select, Radix `Select` |
|
||||
| 대표 Story | `Components/Form/Select` → `All` (이름 "Select") |
|
||||
| 보류 | Item Disabled, 일반 Focus 두께 등 (§11) |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **ui_viz Confirmed**: size/높이(select.md §1.1 D-004, §8), 색·border(§10, §1.3, D-005, M06), indicator glyph(§7, 사용자 제공 Source), Trigger/Content 연결 윤곽(§8).
|
||||
- **ui_viz BASELINE_VALUE**: padding-x 12, gap 12, option 높이 42(§8, §17) — "현재 적용 계약"으로 표기된 값.
|
||||
- **HM 사용자 결정**: Trigger Disabled opacity `0.6`(2026-10-02, §10).
|
||||
- **shadcn/Radix 유지**: 구조·접근성·positioning(§7).
|
||||
- **HM 사용자 결정(2026-10-06)**: Error는 Destructive border, Keyboard Focus는 독립 Focus Semantic ring.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| Size | 외곽 | Trigger 내부 높이 | Trigger 글자 |
|
||||
|---|---|---|---|
|
||||
| `medium` | 34 | 32 | 14 / 20 / 400 |
|
||||
| `large` (기본) | 44 | 42 | 16 / 24 / 400 |
|
||||
|
||||
- 높이는 `min-height = 내부 높이 + 1px border × 2`.
|
||||
- Padding-x 12, gap 12, border 1px `--hm-sem-stroke-input-default`, radius `--hm-radius-input`, face `--hm-sem-bg-surface`.
|
||||
- Placeholder(`[data-placeholder]`): `--hm-sem-fg-placeholder`.
|
||||
- Trigger와 Value는 `min-width: 0`; 긴 placeholder/선택값은 한 줄 ellipsis로 줄고 18px indicator를 밀지 않는다. ui_viz 공개 Story에 없던 현재 HM 복원력 규칙이며 parity 판정 근거로 쓰지 않는다.
|
||||
- Icon: HM glyph, 18×18 `viewBox`, `M5 7 L9 11 L13 7`, stroke 1.50542, 색 `#2d2d2d`(`--hm-select-indicator-color`).
|
||||
- Content: Trigger와 같은 폭(`--radix-select-trigger-width`), border 1px focus 색, `bottom`일 때 아래쪽만 radius.
|
||||
- Option: Trigger의 size를 따른다. min-height 42(두 size 공통, ui_viz §8), padding-x 12, 글자 Medium 14/20/400(Medium Trigger와 동일) · Large 16/24/400, radius 0. ui_viz는 Medium Option 글자 Role을 TBD로 남겼고, 2026-10-06 Trigger 값에 맞추기로 했다(사용자 요청). 높이·폭은 size로 바꾸지 않는다(Content = Trigger 외곽 폭).
|
||||
- size 전달: `SelectTrigger size`가 유일한 지정처다. `Select`가 내부 context로 받아 Portal된 `SelectContent`의 `data-size`로 노출하고, `select.css`가 `[data-slot="select-content"][data-size]`로 Option profile을 정한다. `SelectContent`·`SelectItem`에는 size prop이 없다.
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Default | 위 기본 규격 | ui_viz Confirmed |
|
||||
| Focus (`:focus`) | border 색 → `--hm-sem-stroke-input-focus`, 두께 1px 유지 | ui_viz Confirmed (1.5px 두께는 ui_viz FOLLOW_UP으로 보류) |
|
||||
| Open (`[data-state="open"]`) | border 색 → focus 색, 위쪽 모서리만 round(리스트와 이어짐) | ui_viz Confirmed |
|
||||
| Selected (Item `[data-state="checked"]`) | Hover와 같은 배경 면(`--hm-sem-surface-highlight`, Accent 45%), border·check icon 없음 | 표현: ui_viz Confirmed / 색: shadcn Token(`accent`) |
|
||||
| Highlighted (Item `focus:`/`data-highlighted`) | Accent 45%(`--hm-sem-surface-highlight`) | 위와 동일 |
|
||||
| Disabled (Trigger `:disabled`) | Default paint 유지, `opacity: 0.6`, `cursor: not-allowed` | opacity: 사용자 결정 |
|
||||
| Disabled (Item `data-disabled`) | shadcn 기본 `opacity-50`, 포인터 차단 | **미검증**(§11) |
|
||||
| Invalid (`aria-invalid="true"`) | border 1px `--hm-sem-action-destructive`, Error ring 없음 | 2026-10-06 사용자 결정 |
|
||||
| Invalid + Keyboard Focus (`:focus-visible`) | Destructive border 유지 + Focus Semantic 3px / 50% outer ring | 위와 동일 |
|
||||
| Invalid + Open | Trigger Destructive border, 연결 seam은 Neutral. Content의 기존 Surface / Focus outer border / Accent 45% Option 유지 | 위와 동일 |
|
||||
| Disabled Invalid | Destructive border + 기존 opacity 0.6, focus 불가 | 위와 동일 |
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
SelectTrigger size:
|
||||
"sm" | "default"
|
||||
→
|
||||
"medium" | "large" 기본 "large"
|
||||
|
||||
SelectContent sideOffset:
|
||||
shadcn 기본 4
|
||||
→
|
||||
기본 -1 (Trigger와 1px border를 공유)
|
||||
|
||||
SelectItem:
|
||||
children 앞의 ItemIndicator(check icon) 제거. children만 ItemText로 렌더.
|
||||
```
|
||||
|
||||
- `Select`, `SelectValue`, `SelectGroup`, `SelectLabel`, `SelectSeparator`, 스크롤 버튼은 shadcn API 그대로.
|
||||
- 출력 속성: `data-slot="select-trigger"`, `data-size`. HM 전용 `error`/`open`/`focused` prop은 없다. 상태는 Radix 속성(`data-state`, `data-placeholder`, `data-highlighted`, `data-side`)과 `:disabled`로 적용한다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/select.css` (`--hm-select-*`).
|
||||
|
||||
- 재사용한 공통 Semantic: `--hm-radius-input`(radius), `--hm-sem-bg-surface`, `--hm-sem-fg-primary`, `--hm-sem-fg-placeholder`, `--hm-sem-stroke-input-default`, `--hm-sem-stroke-input-focus`.
|
||||
- Select 전용 값: `--hm-select-control-height-{md,lg}`(32/42), `--hm-select-padding-x`, `--hm-select-gap`, `--hm-select-option-height`, `--hm-select-font-size-*`, `--hm-select-disabled-opacity`, `--hm-select-indicator-color`.
|
||||
- indicator는 `--hm-select-indicator-color` → `--hm-sem-palette-indicator`를 참조한다(Default `#2d2d2d`). 신규 Primitive 없음.
|
||||
- Input·Button과 값(42 등)이 겹쳐도 alias하지 않는다. 각 ui_viz 규격이 따로 확정한 값이다.
|
||||
- `--hm-select-disabled-opacity: 0.6`은 Select 전용 Component Token이며 Input의 Disabled와 공유하지 않는다.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix Root/Trigger/Portal/Content/Viewport/Item/ItemText 구조, 키보드 조작·typeahead·focus 관리, Portal, popper positioning(`position="popper"`, `align="start"`), 스크롤 버튼, 열림/닫힘 animation class, `data-slot`, `cn(..., className)`, Trigger의 native `aria-invalid` 전달, Item의 `data-disabled` 처리.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- Size API(`sm|default` → `medium|large`), 기본 `large`, `data-size`.
|
||||
- `sideOffset` 기본 `-1`, Content 폭을 Trigger 폭에 맞춤, Trigger/Content 연결형 윤곽.
|
||||
- chevron 아이콘을 HM glyph(inline SVG)로 교체.
|
||||
- Item check indicator 제거, Selected를 배경 면으로 표현.
|
||||
- shadcn Trigger의 높이·padding·border·focus ring class 제거 → theme CSS가 담당.
|
||||
- `hm-select-trigger` class 제거.
|
||||
- 변경하지 않음: 스크롤 버튼의 lucide chevron(ChevronUp/Down).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Select"): 구역 `default`(Large, size 생략), `selected`, `disabled`, `open`.
|
||||
- verification(`tags: ["verification"]`): `Default`, `Open`, `Selected`, `Disabled`, `Sizes`("Compare Sizes"), `Medium`, `Large`, `LongContent`, `Scroll`, `Playground`(HM 전용).
|
||||
- `Disabled` Story 구성: Trigger 비활성 상태들(placeholder 포함).
|
||||
- `InvalidFocus` verification Story: 앞/뒤 버튼 사이의 Invalid Trigger와 Disabled Invalid Trigger. Tab / Shift+Tab, Enter / Space, Escape 및 focus return을 실제 키보드로 확인한다.
|
||||
- `LongContent`와 `Scroll`은 ui_viz 공개 Story parity가 아니라 좁은 폭·긴 값·Radix scroll 회귀를 확인하는 HM 전용 Story다.
|
||||
- sidebar는 `All` 하나만 보이는 1뎁스. 운영 규칙은 지침 §6.3, §7 참조.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
**2026-10-02**
|
||||
|
||||
- Select Trigger Disabled opacity `0.6` 확정(`select.css`의 `--hm-select-disabled-opacity`).
|
||||
- 같은 날 Input Disabled `0.6`도 확정되었다(input.md 참조). 현재 적용 범위는 Input / Select이며 전체 디자인 시스템 Disabled opacity로 자동 확대하지 않는다.
|
||||
- **Select Item의 Disabled opacity는 이 결정에 포함하지 않는다.** Trigger와 같은 규격이라고 판단하지 않는다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- **Item Disabled**: 현재 shadcn 기본 `opacity-50`. 사용자 결정·ui_viz 근거 없음. 미검증.
|
||||
- **FOLLOW_UP(ui_viz)**: Focus/Open 상태의 border 1.5px 두께.
|
||||
- **ui_viz 미정의**: Group/Label/Separator 규격, 스크롤 버튼 규격, `item-aligned` position. Error 표현은 2026-10-06 HM 사용자 결정이 우선한다.
|
||||
- **BASELINE_VALUE**: padding-x 12, gap 12, option 42는 "현재 적용 계약"이며 별도 Confirmed 결정은 아니다.
|
||||
- Selected/Highlighted는 현재 공통 Accent 45% 정책을 쓴다. ui_viz Temporary 표기는 도입 당시 근거다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/select.tsx
|
||||
packages/ui/src/theme/select.css
|
||||
src/components/select.stories.tsx
|
||||
src/components/story-board.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-05: `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. 기존 generated CSS의 `bg-[var(...)]` 경고와 500kB chunk warning은 유지된다.
|
||||
- Storybook computed style: Medium/Large Trigger 34/44px, padding-x 12px, radius 4px, border 1px, Option 42px·16/24/400, Disabled Trigger opacity `0.6` 확인.
|
||||
- Open: Trigger와 Content 폭 동일, `sideOffset=-1` 접점, Focus/Open `#123328`, Content border/radius/shadow와 Portal listbox 확인.
|
||||
- Interaction: click open, ArrowUp/Down, Home/End, typeahead, Enter select, Escape, focus return, Disabled open 차단, 32개 Option의 wheel/keyboard scroll 확인.
|
||||
- Responsive: 1280/768/390px에서 popup clipping·문서 가로 overflow 없음. 390px의 280px Trigger에서 긴 placeholder/선택값 ellipsis와 indicator 비중첩 확인.
|
||||
- Story: `All`의 공개 구역 4개와 숨김 `LongContent`/`Scroll` verification Story 확인.
|
||||
- 미확인/보류: Item Disabled 시각, collision으로 `side="top"`이 된 연결 윤곽의 별도 디자인(원 ui_viz도 viewport edge flip TBD), 브라우저별 렌더 차이.
|
||||
|
||||
|
||||
### Invalid + Focus 검증 (2026-10-07)
|
||||
|
||||
- Focus ring: `--ring` → `--hm-sem-stroke-input-focus` → `--hm-color-border-focus`. Invalid border: 기존 `--hm-sem-action-destructive`. 새 Runtime Role / Production Error prop / 메시지 컴포넌트 없음.
|
||||
- Chromium computed style: Invalid border `rgb(211, 47, 47)`, Invalid + Keyboard Focus box-shadow `color(srgb 0.14902 0.4 0.317647 / 0.5) 0px 0px 0px 3px`, `outline-style: none`. Invalid 단독은 `box-shadow: none`.
|
||||
- Theme Designer UI에서 Focus `#c75b00`, Destructive `#d41466`, Active `#007f73`, Accent `#ffd36d`로 분리. Invalid border `rgb(212, 20, 102)` + Focus shadow `color(srgb 0.780392 0.356863 0 / 0.5) 0px 0px 0px 3px` 확인.
|
||||
- `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. 기존 문서 예시 CSS warning 및 Storybook chunk 크기 / plugin timing warning 유지.
|
||||
- 검증 범위: Chromium 환경. 다른 브라우저의 렌더링은 별도 확인이 필요하다.
|
||||
- 숨김 `InvalidFocus`에서 Closed Invalid / Tab Focus / Invalid Open / 방향키 강조 / Disabled Invalid 확인. Invalid border computed 1px; Tab 전후 shadow none → 3px Focus ring. Tab은 Disabled를 건너뛰고, Shift+Tab으로 복귀. Enter / Space open, ArrowDown 이동, Escape close 및 Trigger focus return 확인.
|
||||
- Invalid + Open은 Trigger Destructive border와 Neutral seam `rgb(222, 219, 210)`을 동시에 유지. Popup white Surface, 외곽 `rgb(199, 91, 0)`(Focus), Selected / Highlighted `color(srgb 1 0.827451 0.427451 / 0.45)`(Accent) 확인. Popup에 Error 색 없음. 대표 `All` 회귀 확인.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Separator
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Separator 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Separator (Radix `Separator`) |
|
||||
| 대표 Story | `Components/Data & Display/Separator` → `All` (이름 "Separator") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz에는 독립 Separator 규격이 없다(컴포넌트별 divider 값만 있다). 참고만 했다.
|
||||
- **현재 HM 규격**: §3~§4.
|
||||
- **구현 방식**: Radix Separator의 `orientation`과 `decorative`(기본 true, `role=none`) API를 유지하고 theme CSS로 두께·색을 연결.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Thickness | 1px (`--hm-separator-thickness`) |
|
||||
| Color | `--hm-separator-color` → Semantic `--hm-sem-stroke-input-default` (`#DEDBD2`) |
|
||||
| Horizontal | 높이 1px, 폭 100% |
|
||||
| Vertical | 폭 1px, `align-self: stretch`, `min-height: 1em` (flex row 안에서 사용) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
상태 없음. `decorative`(기본 true)는 `role="none"`, `decorative={false}`는 `role="separator"`.
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Separator props 그대로 (`orientation`: `"horizontal"` 기본 / `"vertical"`, `decorative`: 기본 true). 출력 속성: `data-slot="separator"`, `data-orientation`.
|
||||
|
||||
## 6. Token
|
||||
|
||||
`packages/ui/src/theme/separator.css`: `--hm-separator-thickness`, `--hm-separator-color`. 현재 공통 `--hm-sem-stroke-input-default`를 참조한다. Component Token은 독립적으로 유지한다.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 접근성(`decorative`, `aria-orientation`), `data-slot`, `cn(..., className)`, `shrink-0`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
shadcn의 `bg-border`, `h-px`/`w-px` class를 제거하고 theme CSS가 두께·색을 담당.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
대표 Story `All`: `horizontal`, `vertical`, `semantic`(decorative=false). verification: `Horizontal`, `Vertical`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 이 컴포넌트에 대한 개별 수치 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 구분선 전용 Semantic(`stroke.divider` 등) 도입: 미결정. 현재 공통 stroke Semantic과 구분할 필요가 생기면 재검토.
|
||||
- 텍스트가 들어간 구분선("or"), 두께 변형: 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/separator.tsx
|
||||
packages/ui/src/theme/separator.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Separator)
|
||||
src/components/separator.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- computed style: horizontal 높이 1px·폭 100%, vertical 폭 1px·높이 24px(flex row stretch), 색 `rgb(222,219,210)`.
|
||||
- a11y: 기본 `role=none`, `decorative={false}`이면 `role=separator` 확인.
|
||||
- 미확인: 사이드바 화면.
|
||||
@@ -0,0 +1,108 @@
|
||||
# Sheet
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Sheet 고유의 API·Token·상태·결정만 기록한다. Dialog 문서(`dialog.md`)와 값을 공유하지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Sheet (Radix `Dialog`) |
|
||||
| 대표 Story | `Components/Overlay/Sheet` → `All` (이름 "Sheet") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Sheet가 없다(Dialog 문서가 "side sheet"를 범위 밖으로 둠). 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. Dialog에서 검증한 접근성 원칙(Radix modal, `asChild` 처리)은 재사용하고, 면·윤곽·그림자 등 수치는 현재 HM Semantic으로 새로 정했다. `--hm-dialog-*`를 참조하지 않는다.
|
||||
- **구현 방식**: shadcn / Radix Dialog 기반 Sheet 구조와 `side` API 유지, theme CSS로 HM 스타일 연결.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Surface / 전경 | `--hm-sem-bg-surface` / `--hm-sem-fg-primary` |
|
||||
| Border | 1px `--hm-sem-stroke-input-default`, 페이지를 향한 한 변에만 |
|
||||
| Shadow | `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| Radius | 없음(viewport edge에 붙음) |
|
||||
| left / right | 폭 `min(400px, 100vw − 48px)`, 높이 100% |
|
||||
| top / bottom | 폭 100%, 높이 내용 기준, 최대 `100dvh − 48px` |
|
||||
| Padding | 24px (Header·Footer·Body 좌우) |
|
||||
| Header | padding `24 (24+36) 16 24` (오른쪽은 Close 버튼 자리), Title↔Description gap 8px |
|
||||
| Title | 20 / 28 / 700 |
|
||||
| Description | 14 / 20 / 400, `--hm-sem-fg-secondary` |
|
||||
| Body | 14 / 20 / 400, 좌우 padding 24px, 유일한 scroll 영역 |
|
||||
| Footer | padding `16 24 24`, gap 8px, 오른쪽 정렬(좁은 화면은 세로, shadcn `flex-col-reverse sm:flex-row`) |
|
||||
| Close(X) | shadcn 기본(ghost `icon-sm`, 오른쪽 위 12px, sr-only "Close") |
|
||||
| Overlay / animation | shadcn 기본(`bg-black/10` + blur, side별 slide) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Closed / Open | Radix `data-state`, 열림 시 modal, 배경 aria-hidden, scroll lock |
|
||||
| Focus | 열릴 때 Radix 기본(첫 tabbable), 닫힐 때 trigger로 복귀 |
|
||||
| Long Content | Header·Footer 고정, Body만 세로 scroll |
|
||||
| Form | 소비 조합(Body 안 Label + Input) |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Dialog props 그대로. export: `Sheet`, `SheetTrigger`, `SheetClose`, `SheetPortal`, `SheetOverlay`, `SheetContent`, `SheetHeader`, `SheetBody`, `SheetFooter`, `SheetTitle`, `SheetDescription`.
|
||||
|
||||
- `SheetContent`: `side` `"top" | "right" | "bottom" | "left"` (기본 `right`, shadcn API 유지), `showCloseButton`(기본 true). 출력 `data-side`.
|
||||
- `SheetBody`(신규): scroll owner. 없어도 동작하지만 이 경우 Body padding과 내부 scroll이 없다.
|
||||
- `SheetClose`: `asChild`일 때 자식의 `data-slot`을 유지(DialogClose와 같은 규칙, Button HM 스타일 보존).
|
||||
- 새 prop(`size`, `variant`)은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/sheet.css` (`--hm-sheet-*`).
|
||||
|
||||
`background`, `text`, `border-width`, `border-color`, `shadow`, `width`, `edge-gap`, `padding`, `header-gap`, `footer-gap`, `close-clearance`, `title-*`, `description-*`, `body-font-size`, `body-line-height`. Semantic과 Theme palette(`--hm-sem-stroke-input-default`)만 참조한다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`role=dialog`, `aria-labelledby/describedby`)·focus trap·initial focus·focus 복귀·Escape·바깥 클릭 닫힘·scroll lock, Portal·Overlay, position(`inset-*`)·side별 slide animation class, `data-slot`, `cn(..., className)`, Close 버튼.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-popover text-popover-foreground shadow-lg gap-4 border-*`, side별 `w-3/4 sm:max-w-sm`·`h-auto`, Header/Footer `p-4` 제거 → theme CSS.
|
||||
- `SheetBody` 추가(scroll owner), `SheetClose asChild` data-slot 처리.
|
||||
- 폭을 `min(400px, 100vw − 48px)`로 지정(좁은 화면에서 Overlay 띠 48px 유지).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Sheet"): 트리거 6개(right, left, top, bottom, 폼, 긴 내용)가 각각 실제 Sheet를 연다.
|
||||
- verification: `Right`, `Left`, `Top`, `Bottom`, `Form`, `Long Content`(처음부터 열림).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(400px 폭, 24px padding, 그림자, 48px edge gap)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Overlay 색·blur, Motion은 정해진 규칙이 없어 shadcn 기본 유지.
|
||||
- Header/Footer divider 없음(미정의). Close 버튼 규격은 shadcn 기본.
|
||||
- top/bottom side 사용처는 Drawer와 겹친다. 둘의 역할 경계(Drawer = 드래그로 닫는 하단 panel)는 사용 가이드로 정리 예정.
|
||||
- 소비 화면이 쓰는 공통 header/footer 높이 대응은 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/sheet.tsx
|
||||
packages/ui/src/theme/sheet.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/sheet.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 클릭 열기(focus가 "취소"로 이동, `role=dialog`), Escape 닫힘과 trigger focus 복귀, 바깥(Overlay) 클릭 닫힘과 focus 복귀, X 닫기, 네 방향 위치(right 우측 400px, left 좌측 0, top/bottom 전체 폭), 긴 내용에서 Body만 scroll하고 Footer가 화면 안.
|
||||
- 390×844: right Sheet 폭 342px(left 82px), 가로 overflow 없음.
|
||||
- computed style: white, border-left `rgb(222,219,210)`, shadow `0 0 12px .2`.
|
||||
- 미확인: focus trap의 Tab 순환(코드 경로는 Radix 기본), side별 닫힘 animation 시각, top/bottom의 모바일 높이 한계.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Skeleton
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Skeleton 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Skeleton (`div` + `animate-pulse`, Radix 없음) |
|
||||
| 대표 Story | `Components/Data & Display/Skeleton` → `All` (이름 "Skeleton") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Skeleton이 컴포넌트 이름으로만 있고 규격은 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3. 현재 HM Primitive로 새로 정했다.
|
||||
- **구현 방식**: shadcn Skeleton을 유지. loading 로직이 없는 시각 placeholder primitive이며 크기·모양은 `className`으로 정한다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| 배경 | `--hm-sem-palette-warm-neutral-light` |
|
||||
| Radius | `--hm-radius-control-md` (4px). 원형은 `className="rounded-full"` |
|
||||
| Animation | shadcn `animate-pulse`(2s). `prefers-reduced-motion: reduce`에서는 animation 없음 |
|
||||
| 크기 | 없음. `className`으로 `h-*`, `w-*`, `size-*` 지정 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
상태 없음(placeholder). 배경과 radius는 `@layer components`에 있어 `className`의 utility(`rounded-full`, `bg-*`)가 이긴다. 다른 컴포넌트의 theme CSS(레이어 없음)와 달리 Skeleton은 모양을 소비자가 정하는 것이 역할이기 때문이다.
|
||||
|
||||
## 5. API
|
||||
|
||||
export: `Skeleton`. `div` props 그대로. 출력 `data-slot="skeleton"`.
|
||||
|
||||
- `width`/`height`/`variant` prop은 만들지 않았다. Text line, Avatar, Card 등은 Story처럼 `className`으로 조합한다.
|
||||
- 접근성: placeholder 자체에는 ARIA를 두지 않는다. loading 영역의 `aria-busy` 등은 소비자 책임.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/skeleton.css` (`--hm-skeleton-*`).
|
||||
|
||||
`background`, `radius`. Theme palette(`--hm-sem-palette-warm-neutral-light`)와 공통 control radius만 참조한다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn에서 유지한 항목
|
||||
|
||||
`div` 구조, `animate-pulse`, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-muted rounded-md`(또는 `bg-accent`) class를 제거하고 Token으로 연결(배경 `--hm-warm-neutral-light`, radius 4px).
|
||||
- reduced-motion에서 pulse를 끈다(shadcn 기본에는 대응이 없다). 새 Motion token은 만들지 않았다.
|
||||
- 배경·radius 규칙을 `@layer components`에 둔 것은 이 컴포넌트만의 결정이다(§4).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Skeleton"): Text(3줄), Avatar + text, Card/list loading(Card 안 조합)을 한 페이지에 둔다. 데이터 fetch는 없다.
|
||||
- verification: `Text`, `Card List`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 배경색과 radius는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- Pulse 속도·형태(shimmer 등)는 Motion 규칙이 정해질 때 다룬다.
|
||||
- Text line 높이 등 사전 정의된 크기 preset은 만들지 않았다.
|
||||
- 전체 loading 영역의 ARIA(`aria-busy`, live region) 가이드는 Data Table Pattern 등 조합 단계에서 정한다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/skeleton.tsx
|
||||
packages/ui/src/theme/skeleton.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/skeleton.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 렌더(Browser pane): 배경 `rgb(224,218,206)`, radius 4px, animation `pulse 2s`. `className="size-10 rounded-full"`이 radius override를 받아 원형으로 표시(처음에는 unlayered CSS가 `rounded-full`을 덮는 문제가 있어 `@layer components`로 수정).
|
||||
- Responsive(390×844): Text, Avatar+text, Card 구성이 부모 폭 안에 들어가고 가로 overflow 없음.
|
||||
- 미확인: 실제 `prefers-reduced-motion` 환경에서의 animation 정지(CSS만 작성).
|
||||
@@ -0,0 +1,116 @@
|
||||
# Slider
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Slider 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-06 |
|
||||
| 기반 | shadcn/ui Slider (Radix `Slider`) |
|
||||
| 대표 Story | `Components/Form/Slider` → `All` (이름 "Slider") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Slider가 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Slider 구조(Root, Track, Range, Thumb)를 유지하고 theme CSS로 연결. Slider는 값 입력 컨트롤이며 Toggle/Button 계열과 역할이 다르다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Track | 두께 6px, radius 9999px, 배경 `--hm-sem-palette-warm-neutral-light` |
|
||||
| Range | `--hm-sem-state-active` |
|
||||
| Thumb | 18px 원, 배경 `--hm-sem-bg-surface`, 2px `--hm-sem-state-active` border |
|
||||
| Thumb halo | Hover 4px / Drag(`:active`) 6px Primary 20% 후광. 키보드 Focus는 같은 크기의 Focus Semantic 20% 후광, 커서 grab/grabbing 유지 |
|
||||
| 폭/높이 | Root에 `className`. 기본은 부모 폭 100%, vertical은 높이 100%·최소 176px(shadcn 기본) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Default | 후광 없음, 커서 grab |
|
||||
| Hover | 4px `--hm-sem-action-primary` 20% 후광 |
|
||||
| Focus (`:focus-visible`) | 4px `--hm-sem-stroke-input-focus` 20% 후광 |
|
||||
| Drag (`:active`) | 6px Primary 20% 후광, 커서 grabbing |
|
||||
| Focus + Hover | Focus 색 우선, 4px Focus 20% 후광 |
|
||||
| Focus + Drag | Focus 색 우선, Drag의 6px 크기와 grabbing 커서 유지 |
|
||||
| Disabled (`data-disabled`) | Root `opacity: 0.6`(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), 후광 없음, 커서 not-allowed, thumb이 focus를 받지 않음 |
|
||||
| Orientation | `data-orientation` horizontal / vertical |
|
||||
|
||||
우선순위: Disabled > Focus 색 > Hover/Drag interaction 색. Drag는 halo 크기만 6px로 확대한다. Radix의 pointer focus 정책은 유지한다(pointer로 thumb을 잡으면 `focusVisible: false`로 focus하므로 일반 pointer drag는 Primary halo). Focus + Drag 규칙은 실제로 `:focus-visible`과 `:active`가 겹칠 때 적용하며, interaction 규칙이 Focus 표시를 덮지 않는다.
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Slider props 그대로. export: `Slider`.
|
||||
|
||||
- `value`/`defaultValue`/`onValueChange`/`onValueCommit`, `min`/`max`/`step`, `minStepsBetweenThumbs`, `orientation`, `dir`, `inverted`, `disabled`, `name`.
|
||||
- 값이 배열이면 항목 수만큼 thumb을 렌더한다(두 개면 range). Range용 별도 컴포넌트는 없다.
|
||||
- `value`와 `defaultValue`를 모두 생략하면 초기값은 `[min]`이고 thumb은 하나다(`min` 기본값 0). 숨겨진 추가 thumb이나 빈 FormData 항목을 만들지 않는다.
|
||||
- Uncontrolled의 `defaultValue`는 마운트 시 초기값이다. 이후 `defaultValue` 배열이나 항목 수를 바꿔도 내부 값과 thumb 수는 초기 배열을 유지한다. 초기화가 필요하면 새 `key`로 재마운트하고, 동적 값 변경에는 Controlled `value`를 사용한다. Controlled/Uncontrolled 모드는 컴포넌트 수명 동안 유지한다.
|
||||
- Form 안에서 `name`을 지정하면 Enabled Slider는 현재 값을 제출한다(단일값은 `name`, 범위값은 `name[]`으로 여러 항목). `form="id"`로 외부 Form을 연결할 수도 있다.
|
||||
- Disabled Slider는 FormData와 native Form Submit에서 제외한다. 설치된 Radix Slider 1.4.7의 내부 input에 `disabled`가 전달되지 않아 HM은 disabled일 때 Radix에 넘기는 `name`을 생략한다. 다시 Enabled로 바꾸면 기존 `name`으로 현재 값 제출을 재개한다. Controlled/Uncontrolled에 동일하게 적용하며 값·callback·키보드·ARIA 계약은 유지한다.
|
||||
- **`aria-label`**: `role="slider"`는 Root가 아니라 Thumb이므로 Slider가 `aria-label`을 모든 thumb에 전달한다(thumb이 둘 이상이면 `"label 1/2"`, `"label 2/2"`). `aria-label`이 없으면 Radix 기본 thumb 이름(Minimum/Maximum)이 남는다.
|
||||
- `variant`, `status`, `appearance` 같은 prop은 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/slider.css` (`--hm-slider-*`).
|
||||
|
||||
`track-size`, `track-background`, `range-background`, `thumb-size`, `thumb-background`, `thumb-border-width|color`, `thumb-halo-color|size|size-active`, `thumb-focus-halo-color`, `disabled-opacity`. Interaction halo는 Primary, Focus halo는 기존 Focus Semantic을 참조한다. 다른 컴포넌트 Token을 쓰지 않는다. 신규 Primitive 없음. Track/Range/Thumb은 내부 부품이라 theme CSS(레이어 밖)로 정하고, Slider Root의 폭·높이는 `className` 영역이다.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·동작(pointer drag, track 클릭 이동, 키보드, thumb 정렬/교차 방지), ARIA(`role=slider`, `aria-valuemin|max|now`, `aria-orientation`), 배열 value에서 thumb 수를 계산하는 shadcn 로직, `touch-none select-none`, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-muted`, `h-1`, `bg-primary`, thumb `size-3 border-ring ring-*` class를 제거하고 Token으로 이전. thumb을 12px에서 18px로 키웠다.
|
||||
- `aria-label`을 thumb으로 전달(shadcn은 Root에만 붙어 thumb에 이름이 없었다).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Slider"): Single(값 label), Range(두 thumb, `minStepsBetweenThumbs` 5), Step(25), Disabled, Vertical을 한 페이지에 둔다. 값 label은 Story 조합(local state)이다.
|
||||
- verification: `Single`, `Range`, `Disabled`, `Focus`(실제 thumb focus). 기본 Sidebar에서 숨긴다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(6px track, 18px thumb, halo 4/6px)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 값 tooltip/label 표시, tick mark, 범위 밖 구간 표현은 정의하지 않았다.
|
||||
- 상태색 range(예: 경고)는 HM 상태색 확정 후.
|
||||
- hit area: thumb 18px이 터치 타깃(44px 권장)보다 작다. 터치 기기 정책은 미정.
|
||||
- 대표 Story의 form 예시는 만들지 않았다. 기본값 및 Enabled/Disabled 제출 계약은 §5를 따른다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/slider.tsx
|
||||
packages/ui/src/theme/slider.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/slider.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-08 기능 개선: 프로젝트 외부 UI 복사본에서 타입 검사와 패키지 빌드 통과. 실제 프로젝트 Slider 소스와 복사본의 SHA-256이 일치하며, 브라우저는 해당 패키지 빌드 결과를 사용했다.
|
||||
- 브라우저 Form 검증: 기본값 생략/`min=10`은 thumb 하나·정상 `aria-valuenow`·빈 제출 항목 없음. 명시적 단일/범위, Controlled 값·배열 길이 변경, Uncontrolled `defaultValue` 배열 길이 변경 시 초기 thumb 수 유지 확인. Controlled/Uncontrolled 단일·범위의 Disabled → Enabled → Disabled 전환에서 FormData와 실제 native POST 제출 결과 정상. 키보드·callback·ARIA와 외부 `form` 연결의 FormData 확인.
|
||||
- 디자인 보존: Slider CSS·Theme JSON·대표 Story 변경 없음. 수정 전후 computed style에서 thumb 18px/2px border, track 6px, Disabled opacity 0.6, Hover/Focus 4px 후광 일치. 검증 예제와 POST 수집 서버는 외부 임시 환경에서만 실행했다.
|
||||
- 2026-10-06 Focus 정합: `typecheck`, `build`, `build-storybook`, `git diff --check` 통과. 기존 문서 예시 CSS(`bg-[var(...)]`)와 Storybook chunk 크기 경고 유지.
|
||||
- Theme Designer에서 Primary `#6837a8`, Active `#007f73`, Focus `#c75b00`, Accent `#ffd36d`를 분리 적용한 뒤 **일반 verification Story**의 computed style로 확인: Range·Thumb border `rgb(0,127,115)`, Hover/Drag는 Primary 20% / 4·6px, Focus/Focus + Hover는 Focus 20% / 4px, Focus + Drag는 Focus 20% / 6px, Default/Disabled는 shadow 없음. Hover/Drag 중첩은 기존 Storybook pseudo-state mirror로 고정하고 Focus는 실제 DOM focus다.
|
||||
- 각 역할을 단독 override해도 Primary는 interaction halo, Active는 Range·Thumb border, Focus는 Focus halo만 변경하며 Accent는 Slider에 영향 없음. 실제 ArrowRight로 값·ARIA·label 40 → 41 확인. Focus Semantic 이름·값·Runtime/JSON 계약 변경 없음.
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 DOM(Browser pane): thumb `role="slider"`, `aria-valuemin=0`, `aria-valuemax=100`, `aria-valuenow`가 실제 값과 일치, vertical은 `aria-orientation=vertical`, disabled thumb은 focus 불가. `aria-label`이 단일 thumb에 `볼륨`, range에 `가격 범위 1/2`·`2/2`로 반영됨.
|
||||
- keyboard: ArrowRight 40 → 41, ArrowLeft → 40, End 100, Home 0, step 25에서 ArrowRight 50 → 75, vertical ArrowUp 30 → 31. 값 label과 ARIA가 함께 변함.
|
||||
- pointer: thumb을 오른쪽으로 drag해 40 → 79(range 폭 382/484), track 클릭으로 23, 왼쪽 경계 밖으로 drag해 0에 고정. Range에서 첫 thumb을 두 번째 thumb 너머로 drag해도 값이 정렬 상태(75, 100)로 유지됨.
|
||||
- computed style: track 6px `rgb(224,218,206)`, range `rgb(18,51,40)`, thumb 18px·2px border·흰 면, disabled Root opacity 0.6.
|
||||
- Responsive(390×844): Slider 폭 358px(좌우 16px), thumb이 잘리지 않고 페이지 가로 overflow 없음.
|
||||
- 미확인: PageUp/PageDown, 터치 drag, `inverted`/RTL, thumb halo의 시각(hover/active 상태 pointer 시각).
|
||||
@@ -0,0 +1,120 @@
|
||||
# Sonner (Toast)
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Toast 고유의 API·Token·상태·결정만 기록한다. public export가 `Toaster` / `toast`이고 shadcn 컴포넌트명이 Sonner이므로 문서명은 `sonner.md`로 하며 `toast.md`는 만들지 않는다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-03 |
|
||||
| 기반 | shadcn/ui Sonner (`sonner` 2.0.8) |
|
||||
| 대표 Story | `Components/Feedback/Sonner` → `All` (이름 "Toast") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz에는 Toast 규격이 없고 `color.status.success|warning|information`은 TBD다. 새 Status Palette를 만들지 않았다.
|
||||
- **현재 HM 규격**: surface·text·radius·shadow는 현재 HM Semantic(surface, fg, stroke, destructive)에서 정했다.
|
||||
- **구현 방식**: Sonner가 queue·stack·timeout·hover pause·swipe·promise·`aria-live`를 소유한다. 오래된 Radix Toast는 만들지 않는다(Toast는 Sonner 하나).
|
||||
|
||||
## 3. Alert / Dialog와 차이
|
||||
|
||||
| | 성격 | 닫힘 | focus |
|
||||
|---|---|---|---|
|
||||
| Alert | 화면 안에 지속적으로 존재하는 in-page feedback | 사용자가 처리하거나 화면이 바뀔 때까지 | 흐름을 막지 않음 |
|
||||
| Toast | 일시적인 global notification (저장 완료, 오류, 비동기 진행) | timeout 또는 닫기 / action | 흐름을 막지 않음 |
|
||||
| Alert Dialog | 사용자의 응답이 필요한 modal 확인 | 응답으로만 | focus trap |
|
||||
|
||||
Toast는 anchored floating panel이 아니라 notification surface이므로 Floating Surface Semantic(`--hm-sem-floating-*`)을 쓰지 않고 `--hm-toast-*`를 별도로 둔다.
|
||||
|
||||
## 4. 기본 규격 / 상태 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Width | 356px (Sonner 기본 유지), 좁은 화면(≤600px)은 Sonner가 좌우 16px offset으로 줄임 |
|
||||
| Surface | `--hm-sem-bg-surface`, 1px `--hm-sem-stroke-input-default`, radius `--hm-radius-container`(4px), 그림자 `0 0 12px rgb(0 0 0 / .2)` |
|
||||
| Padding / gap | 16px / 12px |
|
||||
| Title | 14 / 20 / 500, Description 13 / 18 / 400 `--hm-sem-fg-secondary` |
|
||||
| Icon | 16px, shadcn(lucide) `CircleCheck`·`Info`·`TriangleAlert`·`OctagonX`·`LoaderCircle(spin)` |
|
||||
| Default / Success / Info / Warning / Loading | 중립 면. 의미는 icon과 문구가 전달한다 |
|
||||
| Error (`data-type="error"`) | 윤곽·icon·title `--hm-sem-action-destructive` (Alert destructive와 같은 방침) |
|
||||
| Action | 높이 28px, radius 4px, 12/500, `--hm-sem-action-primary` 면 + `--hm-sem-on-action-primary` 글자 |
|
||||
| Cancel | `--hm-sem-bg-subtle` 면 + `--hm-sem-fg-secondary` 글자 |
|
||||
| Close button | Sonner `closeButton` 옵션. 면·윤곽은 Toast와 같고 글자 `--hm-sem-fg-secondary` |
|
||||
|
||||
Action은 HM `Button`을 넣지 않고 Sonner의 `action` / `cancel` 버튼을 Toast 전용 token으로 스타일한다(Button token 상속 없음).
|
||||
|
||||
## 5. API
|
||||
|
||||
```tsx
|
||||
import { Toaster, toast } from "@hm-design/ui"
|
||||
// 앱 root에 한 번: <Toaster />
|
||||
toast("저장했습니다")
|
||||
toast.success("..."); toast.error("..."); toast.warning("..."); toast.info("..."); toast.loading("...")
|
||||
toast.promise(promise, { loading, success, error })
|
||||
toast("삭제했습니다", { action: { label: "실행 취소", onClick }, cancel: { label, onClick }, closeButton: true, duration })
|
||||
toast.dismiss()
|
||||
```
|
||||
|
||||
- `Toaster`는 Sonner `Toaster`에 shadcn icon set만 주입한 것이다. `position`, `closeButton`, `expand`, `richColors`, `duration`, `offset` 등 Sonner props를 그대로 받는다. HM 전용 prop과 position system은 없다.
|
||||
- `toast`는 `sonner`의 `toast`를 re-export한다. 소비자는 `sonner`를 직접 import하거나 설치할 필요가 없다. 타입 `ToasterProps`, `ExternalToast`도 export한다.
|
||||
- Light/Dark 전환(`next-themes`)은 쓰지 않는다. `theme`은 Sonner 기본값이다.
|
||||
- `richColors`를 켜면 Sonner의 자체 상태 색이 적용되어 HM 규격 밖이 되므로 쓰지 않는다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/sonner.css` (`--hm-toast-*`).
|
||||
|
||||
`width`, `background`, `text`, `border-width`, `border-color`, `radius`, `shadow`, `padding`, `gap`, `font-family`, `title-*`, `description-*`, `icon-size`, `icon-color`, `error-border-color`, `error-text`, `action-*`, `cancel-*`, `close-*`. Semantic 또는 Theme palette만 참조한다. 신규 Primitive·Semantic 없음.
|
||||
|
||||
Sonner 변수(`--width`, `--border-radius`, `--normal-bg|border|text`)는 `[data-sonner-toaster]`에서 이 token으로 연결한다. 상태 hook은 Sonner의 자체 속성(`[data-sonner-toast]`, `[data-type]`, `[data-button]`, `[data-cancel]`, `[data-close-button]`)이다. Sonner CSS가 런타임에 주입되므로 selector에 속성을 겹쳐 specificity를 확보했다.
|
||||
|
||||
**Notification Semantic 후보(미승격)**: Toast 표면 값이 Floating surface와 현재 같지만 역할이 달라 별도 Semantic으로 승격하지 않았다. Status(success·warning·information) 색이 확정되면 `--hm-sem-status-*` 후보와 함께 검토한다.
|
||||
|
||||
## 7. shadcn / Sonner에서 유지한 항목
|
||||
|
||||
queue·stack·expand·hover pause·swipe·timeout·promise lifecycle, `section[aria-live="polite"]`(label `Notifications alt+T`, `aria-relevant`), 단축키(Alt+T로 focus), toast의 `tabindex=0`, 기본 position(`bottom-right`), 모바일 offset·폭 처리, shadcn icon set.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn Sonner의 `next-themes`·`--popover` 계열 inline style을 제거하고 theme CSS로 대체.
|
||||
- Sonner 기본 font(`ui-sans-serif`)를 HM font family로, 기본 radius 8px를 4px로 변경.
|
||||
- action 버튼의 Sonner 기본 반전색(`--normal-bg`/`--normal-text`)을 HM primary로 변경.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Sonner"): 버튼으로 실제 Toast를 생성한다 — Types(Default·Success·Error·Warning·Info·Loading), Action / Action + Cancel / Close button, Promise(→ Success / → Error, 로컬 timeout), Stack ×3 / Long message / Dismiss all.
|
||||
- `Toaster`는 `.storybook/preview.tsx`에 한 번만 둔다(Story별 중복 없음). 정적 Toast specimen은 없다.
|
||||
- Root Demo에는 추가하지 않았다(필수 아님).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-03: Toast는 Sonner 하나로 통일한다(Radix Toast 미사용). Floating Semantic과 분리된 Notification UI로 설계한다. 새 Status Palette를 임의로 만들지 않는다(작업 지시).
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- Success / Warning / Info의 상태 색은 ui-viz에서 TBD라 중립 면으로 두었다. 확정되면 `--hm-toast-<status>-*`를 추가한다.
|
||||
- 수치(356px, 16px padding, 28px action 등)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- 한국어 문장이 좁은 폭에서 어절 중간에서 줄바꿈될 수 있다(Sonner 기본 `word-break`). 규격이 정해지면 `keep-all` 검토.
|
||||
- `position` 비교, 키보드 focus 이동(Alt+T → Tab), 스크린 리더 낭독, swipe 닫기는 확인하지 못했다.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/sonner.tsx
|
||||
packages/ui/src/theme/sonner.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
packages/ui/package.json (dependency: sonner)
|
||||
.storybook/preview.tsx (Toaster 1회)
|
||||
src/components/sonner.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-03).
|
||||
- 실제 interaction(Browser pane): Default·Success·Error·Warning·Info·Loading 생성, Stack ×3(겹침과 hover 펼침), Action + Cancel 표시와 Action 클릭 시 닫힘, Close button 표시, Dismiss all, Promise → Error(loading → error 전환, 윤곽·icon·title destructive), timeout 자동 닫힘.
|
||||
- DOM: `section[aria-live=polite][aria-relevant="additions text"]`, label `Notifications alt+T`, toast `tabindex=0`.
|
||||
- computed style: surface white, border `rgb(222,219,210)`, radius 4px, shadow `0 0 12px .2`, width 356px, padding 16px, title 14px, action `rgb(18,51,40)`·28px.
|
||||
- 390px: 폭 358px(좌우 16px), 가로 스크롤 없음, 긴 title·description·action·close button 모두 폭 안에서 줄바꿈.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Switch
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Switch 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Switch (Radix `Switch`) |
|
||||
| 대표 Story | `Components/Form/Switch` → `All` (이름 "Switch") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz 기존 테스트 규격에는 Switch 규격이 없다(후보 목록뿐). 참고할 수치가 없어 현재 HM Semantic으로 새로 정했다.
|
||||
- **현재 HM 규격**: §3~§4.
|
||||
- **구현 방식**: Radix Switch(`role=switch`, `data-state`, Space/Enter, 폼 동작)와 shadcn의 transition(`transition-all`, 썸 `transition-transform`)을 유지하고 theme CSS로 HM 스타일을 연결. 새 motion은 정의하지 않았다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Track | 36 × 20px, pill radius |
|
||||
| Thumb | 16px 원, `--hm-sem-bg-surface`, 좌우 offset 2px (On: translate `36 − 16 − 2`) |
|
||||
| Off 색 | track `--hm-sem-stroke-default` |
|
||||
| On 색 | track `--hm-sem-state-active` |
|
||||
| Label | `<Label htmlFor>` 조합 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Off / On | 위 기본 규격 (`data-state="unchecked"` / `"checked"`) |
|
||||
| Hover | Black 20% overlay (`--hm-color-action-overlay-hover`) |
|
||||
| Focus (`:focus-visible`) | shadcn ring 유지 |
|
||||
| Disabled | `opacity: 0.6`, `cursor: not-allowed` |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Switch props 그대로 (`checked`/`defaultChecked`/`onCheckedChange`, `disabled`, `required`, `name`, `value`). 추가 prop 없음. 출력 속성: `data-slot="switch"`, `"switch-thumb"`, Radix `data-state`.
|
||||
|
||||
## 6. Token
|
||||
|
||||
`packages/ui/src/theme/switch.css` (`--hm-switch-*`): `width`, `height`, `thumb-size`, `thumb-offset`, `off-background`, `on-background`, `thumb-color`, `hover-overlay`. Semantic 참조. Primitive 신규 추가 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA·키보드, `data-slot`, `cn(..., className)`, shadcn transition, focus ring, disabled 커서.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
shadcn의 `h-[1.15rem] w-8`, `border`, `shadow-xs`, `dark:` class 제거 → theme CSS. Off 색을 `input`(옅음) 대신 stroke 색으로, Disabled opacity 50 → 60.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
대표 Story `All`: `off`, `on`, `interactive`(controlled), `hover`, `focus`, `disabled`. Focus는 `play`로 실제 focus. verification: `Off`, `On`, `Disabled`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 이 컴포넌트에 대한 개별 수치 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- size 변형(sm/md), 라벨 위치 규칙: 미결정.
|
||||
- 수치(36×20, 썸 16)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- motion은 shadcn 기본 유지.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/switch.tsx
|
||||
packages/ui/src/theme/switch.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Switch)
|
||||
src/components/switch.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- computed style: track 36×20, Off `rgb(154,146,132)`, On `rgb(18,51,40)`, 썸 16px White, translate 2px / 18px, Disabled opacity 0.6.
|
||||
- interaction: controlled click으로 `data-state`·`aria-checked` 토글 확인.
|
||||
- 미확인: 실제 pointer hover·focus ring 시각, transition 실제 재생, 사이드바 화면.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Table
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Table 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Table (semantic HTML table, Radix 없음) |
|
||||
| 대표 Story | `Components/Data & Display/Table` → `All` (이름 "Table") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격(`docs/components/table.md`)을 참고했다: 연한 녹색 header 띠, 1px 행 구분선, 셀 padding 14/12, 기본 최소 폭 520px, radius 없음, 행 hover는 면만 변경, header 13/20/700, 셀 14/20/400, 숫자 tabular numerals. 값은 현재 HM Token(Primitive 조합)으로 다시 연결했다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. ui-viz에 없던 selected row와 footer는 새로 정했다.
|
||||
- **구현 방식**: shadcn Table 구조(container div + table 요소들)를 유지하고 theme CSS로 연결. **정적 표시 primitive**이며 이후 Data Table Pattern(정렬·필터·선택 state·페이지·열 표시)의 기반이다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Table | min-width 520px(기본값, 덮어쓸 수 있음), 폭 100%, 배경 `--hm-sem-bg-surface`, 글자 `--hm-sem-fg-primary`, radius 없음, `border-collapse: collapse` |
|
||||
| Header cell | 13 / 20 / 700, 글자 `--hm-sem-palette-green-light-1`, 배경 흰색에 `--hm-sem-palette-green-tint` 30% 섞은 연한 면, 위 1px `--hm-sem-palette-green-light-2`, 아래 1px 같은 색 20% |
|
||||
| Cell | 14 / 20 / 400, padding `14px 12px`, 위 1px `--hm-sem-stroke-input-default` 구분선(첫 body 행은 header 경계가 대신함), `tabular-nums` |
|
||||
| Footer | 배경 `--hm-sem-bg-subtle`, weight 500, 위 1px 구분선 |
|
||||
| Caption | 12 / 18, `--hm-sem-fg-secondary`, 왼쪽 정렬, 표 아래(caption-bottom), 위 간격 12px |
|
||||
| 긴 텍스트 | Cell은 줄바꿈(`overflow-wrap: anywhere`), Header는 `nowrap` |
|
||||
| 숫자 열 | 소비자가 `className="text-right"` 지정 |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Row Hover | body 행 배경 `--accent` 45%(`color-mix`, 면만 변경, header 제외) |
|
||||
| Selected (`data-state="selected"`) | 행 배경 `--accent` 45%. 소비자가 속성을 붙인다 |
|
||||
| Selected + Hover | `--accent` 45% 유지(농도 변화 없음) |
|
||||
| Focus / Disabled / Loading | Table 자체에는 없음(셀 안 content가 각자 가진다) |
|
||||
|
||||
## 5. API
|
||||
|
||||
shadcn 구조 그대로. export: `Table`, `TableHeader`, `TableBody`, `TableFooter`, `TableHead`, `TableRow`, `TableCell`, `TableCaption`.
|
||||
|
||||
- `Table`: `table` props. `div[data-slot=table-container]`(overflow-x auto)로 감싼다.
|
||||
- `TableRow`: `data-state="selected"`를 지원하는 스타일(shadcn 규칙). 선택 state 관리는 소비자.
|
||||
- `density`, `variant`, `striped`, `interactive` 같은 prop은 만들지 않았다. 정렬·필터·선택·column visibility·페이지·resize·server loading은 넣지 않았다(Data Table Pattern에서 조합).
|
||||
- Checkbox 등 셀 안 요소는 소비 조합이며 Table은 runtime 의존이 없다. `[role=checkbox]` 포함 셀은 오른쪽 padding을 줄이고 checkbox를 2px 내리는 shadcn 규칙만 유지.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/table.css` (`--hm-table-*`).
|
||||
|
||||
`min-width`, `background`, `text`, `font-size|line-height|font-weight`, `cell-padding-y|x`, `divider-width|color`, `header-background|text|font-size|font-weight|border-top|border-bottom`, `row-hover-background`, `row-selected-background`, `footer-background|font-weight`, `caption-*`. Semantic과 Theme palette(`--hm-sem-palette-green-*`, `--hm-sem-stroke-input-default`, `--hm-sem-palette-white`)를 참조하고 다른 컴포넌트 Token을 쓰지 않는다. 신규 Primitive 없음(ui-viz의 `#EDF4F1`은 기존 Primitive의 `color-mix`로 표현). row hover / selected는 shadcn `--accent`의 `color-mix` 45%(Select·Menu·Pagination hover와 같은 Soft Surface 규칙, 공통 `--hm-sem-surface-highlight`를 Component Token이 참조).
|
||||
|
||||
## 7. shadcn에서 유지한 항목
|
||||
|
||||
container(`overflow-x-auto`)와 table 구조, `caption-bottom`, Head/Cell의 checkbox 보정 class, `data-slot`, `cn(..., className)`, `data-state="selected"` 규칙.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn의 `text-sm`, `h-10 px-2 font-medium`, `p-2`, `border-b`, `hover:bg-muted/50`, `bg-muted/50`, Caption `mt-4 text-muted-foreground` class를 제거하고 theme CSS로 이전.
|
||||
- Cell의 `whitespace-nowrap`을 제거해 긴 텍스트가 줄바꿈되게 했다(Header는 nowrap 유지).
|
||||
- Header 띠 스타일과 숫자 `tabular-nums`, 최소 폭 520px은 ui-viz를 참고한 신규 규격.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Table"): Basic(Badge, 숫자 열, footer, caption), Selected row(Checkbox, local state), Long text, Horizontal overflow(1100px 표)를 한 페이지에 둔다.
|
||||
- verification: `Basic`, `Selected`, `Long Text`, `Horizontal Overflow`.
|
||||
- Checkbox·Badge는 Story 조합이다. Story의 폭 wrapper에는 `minWidth: 0`이 필요하다(grid item의 `min-width:auto`가 Table 최소 폭만큼 부모를 넓힘).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
- 2026-10-06 (컬러 역할 정리): 행은 넓은 Soft Surface이므로 Hover·Selected·Selected+Hover 모두 `--accent` 45%를 쓴다(이전 `--hm-green-tint` 18% / 40%). Active State Semantic(Light 2)은 쓰지 않는다. 선택 구분은 면 색이 아니라 행 안의 Checkbox(Active Semantic)가 담당한다. `--accent` 값 자체는 바꾸지 않았다. Table row highlight surface는 Select Highlight / Selected와 동일한 Accent 45% 정책을 사용한다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(520px, 14/12 padding, header 띠 색)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- zebra, sticky header, 정렬/필터/선택/페이지, column resize, empty/loading 표현: 이후 Data Table Pattern.
|
||||
- 모바일: Table을 카드로 자동 변환하지 않는다. container 가로 scroll만 제공하며 모바일용 Data Table 패턴은 Layout/Data Table 단계에서 정한다.
|
||||
- header 수치 13px는 ui-viz 참고값이며 공통 typography role과 연결하지 않았다.
|
||||
- 말줄임(ellipsis) 셀 규칙 없음.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/table.tsx
|
||||
packages/ui/src/theme/table.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/table.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 구조/스타일(Browser pane): `th`/`td` 구조, header 13/700 `rgb(32,82,65)`, 위 border `rgb(38,102,81)`, cell 14/20·padding 14/12, 표 폭 100%·최소 520px, caption bottom·왼쪽 정렬.
|
||||
- 선택 행: Checkbox Space로 선택하면 해당 행이 `data-state="selected"`가 되고 header Checkbox가 `mixed`, header Checkbox 클릭으로 전체 선택. Tab이 행 Checkbox로 이동.
|
||||
- Responsive(390×844): container 폭 358px에 표 520px(1100px 표)이 container 안에서 가로 scroll, 페이지(`scrollWidth` 390) 가로 overflow 없음, 긴 텍스트는 셀 안에서 줄바꿈(89px 행).
|
||||
- 2026-10-06 ui_viz Storybook 화면 비교(1280/768/390, Chromium): header·cell padding/typography/색, divider, 텍스트 행 49px, 실제 pointer hover 면 `rgba(189,223,212,.18)` 일치. header/첫 행 경계는 ui_viz가 td 위 선을 두고 HM은 없애지만 border-collapse에서 header 하단선이 이기므로 렌더 픽셀이 같음(197,215,208 / 196,216,209). header 면 ±2 차이는 §6 `color-mix` 근사. 390px에서 ui_viz Story는 페이지가 544px로 넘치고 HM은 container scroll만 생김.
|
||||
- 2026-10-06 Accent 45% 전환(실제 pointer hover): Hover·Selected·Selected+Hover 모두 `color(srgb 0.741 0.875 0.831 / 0.45)`(= `#bddfd4` 45%). Data Table 선택 행 동일 + 선택 Checkbox `rgb(38,102,81)` + focus ring 기존 그대로. Dialog Wide Content: header·cell typography·padding·행 높이 49px 유지, hover 동일 값, 좁은 Dialog에서 container만 가로 scroll(1600 / 750).
|
||||
- 미확인: 가로 scroll을 사용자가 끝까지 끌었을 때의 clipping, Dropdown Menu·Pagination 조합(Story에 없음).
|
||||
@@ -0,0 +1,218 @@
|
||||
# Tabs
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Tabs 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 기본 사용: Primary (2026-10-08)
|
||||
|
||||
앱 진입점에서 `@hm-design/ui/theme.css`를 로드한다. `TabsList`의 기본값은 `fill=true`, `panelSurface="content"`이며 기본 API만으로 Primary가 적용된다.
|
||||
|
||||
```tsx
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@hm-design/ui"
|
||||
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList aria-label="업무 보기">
|
||||
<TabsTrigger value="overview">개요</TabsTrigger>
|
||||
<TabsTrigger value="details">상세</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview" className="border bg-background rounded-b p-4">개요 콘텐츠</TabsContent>
|
||||
<TabsContent value="details" className="border bg-background rounded-b p-4">상세 콘텐츠</TabsContent>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
- **Primary**: `<TabsList>`, `<TabsList fill>`, `<TabsList fill panelSurface="content">`는 같은 디자인이다. 선택 면은 White → `--hm-sem-bg-content` Gradient다.
|
||||
- **Secondary 명시적 사용**: 위 예제의 List를 `<TabsList fill panelSurface="surface" aria-label="업무 보기">`로 바꾸고 **각** `TabsContent`에 `border bg-card rounded-b p-4`를 사용한다. 선택 면은 기존 White surface다.
|
||||
- **Legacy non-fill 명시적 사용**: `<TabsList fill={false} aria-label="업무 보기">`와 **각** `TabsContent`의 `border bg-card rounded-b p-4` 조합으로 이전 스타일을 유지한다. `<TabsList fill={false} panelSurface="surface">`도 같은 시각이다. OLD_TEST_ONLY / REMOVE_CANDIDATE 이력은 유지하며 별도 승인 없이 삭제하지 않는다.
|
||||
|
||||
`TabsContent`는 패널 배경·테두리를 자동으로 지정하지 않는다. 소비자가 Primary에는 `bg-background`(content), Secondary / Legacy에는 `bg-card`(surface)를 연결한다. `panelSurface`는 List의 선택 면·연결 곡선에만 적용되며 콘텐츠 색을 바꾸지 않는다. Bar와 패널 사이에 gap이나 별도 구분선을 넣지 않는다. 상태·Variant는 §4.1, 외부 소비자 변경 안내는 §15를 따른다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 기본 Primary Fill + 명시적 Secondary Fill + Legacy non-fill + 넘침 이동 버튼(D-090) 구현 (`hm-tab` class 없음) |
|
||||
| 기준일 | 2026-10-08 (Primary 기본값 승인 반영) |
|
||||
| 기반 | shadcn/ui Tabs (Radix `Tabs`) |
|
||||
| 대표 Story | `Components/Navigation/Tabs` → `All` (이름 "Tabs"): Primary(Fill + content) / Secondary(Fill + surface) |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: ui-viz 기존 테스트 규격(`docs/components/tabs.md`, `brand/hanmac/components/tab.md`)의 기본 profile, `fill` 콘텐츠 전환 profile, 넘침 이동 버튼의 디자인 의도와 수치.
|
||||
- **현재 HM 규격**: Primary / Secondary는 §4.1, Legacy non-fill은 §3~§4. 수치는 ui-viz에서 가져왔지만 HM 구현에서 다시 검토해 유지한 것이다(아래 검토 결과).
|
||||
- **구현 방식**: shadcn / Radix Tabs 구조(roving focus, 방향키·Home/End, 자동 활성화, ARIA)를 유지하고 theme CSS로 HM 스타일을 연결. `TabsList`가 frame과 이동 버튼을 렌더.
|
||||
- **보류**: §11.
|
||||
|
||||
**ui-viz 비확정 값 검토 (2026-10-02)**: Default 면(`green-tint` 45%)·Default 테두리(`green-light-2` 35%)는 ui-viz에서 비확정 표기였으나 HM Primitive 팔레트의 값을 투명도로 합성한 것이고 Selected(White)와의 대비가 분명해 HM 방향에 맞는다고 판단, 현재 HM 규격으로 유지한다. 곡선 반경 8px도 같은 이유로 유지한다. 단 값은 디자인 확정 시 조정될 수 있다. ui-viz의 볼록 gradient(미구현), 13px 라벨(미채택)은 가져오지 않았다.
|
||||
|
||||
## 3. Legacy non-fill 규격 (`fill={false}`)
|
||||
|
||||
이 표의 배치·인접 경계는 이전 non-fill profile이다. 높이·padding·radius·Typography·Bar 규격은 Fill에서도 공유하지만, 균등 폭·인접 경계·선택 면·연결 곡선은 §4.1을 따른다.
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Tab 높이 | 34px (`height` + `min-height`), Button·radius token과 alias 없음 |
|
||||
| Padding | `0 16px` |
|
||||
| Radius | `4px 4px 0 0` |
|
||||
| Border | 1px. 인접 탭은 `border-left: 0`으로 단일 경계 |
|
||||
| Typography | 16 / 24 / 700 (Selected 동일, 구분은 색·면·테두리로) |
|
||||
| 배치 | `flex-end` 정렬, gap 0, nowrap, `flex: none`, 각 탭 `position: relative` |
|
||||
| Bar | 자체 표면·padding·border·radius 없음, `margin-bottom: -1px`, `overflow-x: auto`, scrollbar 숨김, `max-width: 100%` |
|
||||
|
||||
크기·variant 축 없음 (ui_viz Out of Scope). 방향은 horizontal 하나.
|
||||
|
||||
## 4. Legacy non-fill 상태 규격 (`fill={false}`)
|
||||
|
||||
`panelSurface="surface"` / `"content"`는 이 상태표에 시각적 영향을 주지 않는다. 아래 Default는 비선택 상태를 뜻하며 기본 Variant를 뜻하지 않는다.
|
||||
|
||||
| 상태 | 면 | 테두리 | 전경 |
|
||||
|---|---|---|---|
|
||||
| Default (`data-state="inactive"`) | `rgba(189,223,212,.45)` | top/right `rgba(38,102,81,.35)`, bottom `#DEDBD2` | `#266651` |
|
||||
| Selected (`data-state="active"`) | `#FFFFFF` (`--hm-sem-bg-surface`) | top/right/left `#DEDBD2`, bottom 1px 표면색(기준선을 덮어 콘텐츠와 연결), `z-index: 2` | `#123328` |
|
||||
| Hover (비선택만, `:hover`) | 기본 면 + Black 20% overlay (`linear-gradient`) | 변화 없음 | `#123328` |
|
||||
| Hover (선택) | 적용 안 함 | — | — |
|
||||
| Focus (`:focus-visible`) | shadcn ring 유지 | — | — |
|
||||
| Disabled | shadcn 기본 `opacity-50`, 클릭 불가 | — | — |
|
||||
|
||||
전환: 면·전경 0.15s ease (테두리·geometry는 전환하지 않음). Selected 하단 stroke는 indicator가 아니라 연결 수단이다.
|
||||
|
||||
### 4.1 현재 Fill 디자인: Primary / Secondary State · Variant
|
||||
|
||||
- **Primary (기본값)**: `fill=true`, `panelSurface="content"`. Selected는 White → bg-content Gradient, top / left / right는 Green Light 3(`--hm-tab-fill-line-color`), bottom은 content 패널색이다.
|
||||
- **Secondary**: `fill=true`, `panelSurface="surface"`. Selected는 기존 White surface 면, 같은 Green Light 3 테두리, bottom은 surface 패널색이다.
|
||||
- **공통 Geometry**: 기존 높이 34px, Typography 16/24/700, 좌우 padding 16px, 상단 radius 4px, 선택 탭의 8px 곡선 연결을 유지한다. Overflow가 없으면 균등 폭, 모든 Fill 이웃은 1px left border와 `margin-left: -1px`로 경계를 공유한다. Overflow이면 라벨 필요 폭을 유지한다.
|
||||
- **Inactive**: 면·top / vertical border·전경은 §4의 기존 비선택 값, bottom line만 Green Light 3다.
|
||||
- **Hover**: 비선택·활성화 가능한 탭에만 기존 Black 20% overlay와 전경 전환을 적용한다. Selected Gradient / White 면은 유지한다.
|
||||
- **Focus / Disabled**: 기존 shadcn focus-visible ring / Disabled opacity 0.5·클릭 불가·Radix 키보드 건너뛰기를 유지한다. 선택·ARIA·키보드 동작은 §7, Overflow 이동은 §5·§8을 따른다.
|
||||
|
||||
## 5. API
|
||||
|
||||
shadcn/Radix 그대로. `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` (Radix `Tabs.Root/List/Trigger/Content` props). 추가(기존 API 무변경, 모두 선택):
|
||||
|
||||
```
|
||||
TabsList:
|
||||
fill?: boolean 기본 true. Primary / Secondary Fill; false는 Legacy non-fill
|
||||
panelSurface?: "surface" | "content" 기본 "content". Fill 선택 면이 이어지는 panel 바탕 (임의 색 미지원 = TABS_PANEL_SURFACE_ENUM)
|
||||
```
|
||||
|
||||
- `TabsList`는 Radix list를 감싸는 frame(`data-slot="tabs-frame"`)을 렌더한다(ui_viz `.hm-tabs-frame`과 같은 구조). `role`·`aria-*`·`className`·ref·나머지 props는 tablist로 간다. frame에 `data-overflow`, `data-fill`, `data-panel-surface`.
|
||||
- `panelSurface`는 `fill=false`이면 시각적으로 무시한다. Props 이름·타입·선택 여부와 공개 export는 유지하며 기본 렌더링만 변경한다(§15).
|
||||
- 넘칠 때만 frame 안에 이동 버튼 2개(`data-slot="tabs-arrow"`, `aria-label` 「이전 탭 보기」「다음 탭 보기」, `aria-controls`=tablist id)를 tablist 밖에 렌더한다. Tab 순서: 이전 버튼 → 선택 tab → 다음 버튼, 끝 방향 버튼은 `disabled`.
|
||||
|
||||
- `value`/`defaultValue`/`onValueChange`, `disabled`, `activationMode`, `orientation` 등은 Radix 동작 그대로다. ui_viz의 controlled-only `<Tabs value onValueChange><Tab/></Tabs>` API는 복제하지 않는다.
|
||||
- 출력 속성: `data-slot="tabs|tabs-list|tabs-trigger|tabs-content"`, Radix `data-state`·`data-orientation`.
|
||||
- `TabsList`에는 `aria-label` 또는 `aria-labelledby`를 넘겨야 한다(ui_viz §6).
|
||||
- 이동 버튼은 기존 `Button`을 쓰지 않는 Tabs 전용 control이다.
|
||||
- Selected가 이어지는 콘텐츠 표면은 소비자의 panel이다. Tabs가 자체 표면·구분선을 만들지 않는다. Tab Bar와 콘텐츠 사이에 구분선을 넣으면 연결이 깨진다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/tabs.css` (`--hm-tab-*`).
|
||||
|
||||
- Tabs 전용: `--hm-tab-{height,padding-x,radius-top,border-width}`, `--hm-tab-font-{size,weight}`, `--hm-tab-line-height`, `--hm-tab-{background,border-color,text}`, `--hm-tab-selected-{background,border-color,text}`, `--hm-tab-hover-{overlay,text}`, `--hm-tab-transition-duration`.
|
||||
- 재사용한 공통 값: `--hm-sem-bg-surface`(Selected 면), 역할 Semantic `--hm-sem-action-primary`, `--hm-sem-stroke-input-default`, Theme palette `--hm-sem-palette-green-tint`, `--hm-sem-palette-green-light-2`, `--hm-sem-palette-black`. Component Token은 독립적으로 유지한다. Primitive 직접 참조 설명은 초기 구현 이력이다.
|
||||
- 이동 버튼: `--hm-tab-arrow-{size,icon-size,background,border-color,icon-color,disabled-opacity}`. fill: `--hm-tab-fill-line-color`(→ `--hm-sem-palette-green-light-3`), `--hm-tab-fill-curve-radius`(8px, REVIEW VALUE), 내부 `--hm-tab-fill-{panel,face}`.
|
||||
- **초기 Primitive 추가 이력**: `--hm-green-light-3: #249473` (ui-viz 팔레트의 Green Light 3). 현재 Tabs fill은 공개 Theme palette `--hm-sem-palette-green-light-3`를 통해 참조한다.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix Tabs 구조·ARIA(`role=tablist/tab/tabpanel`, `aria-selected`)·roving tabindex·방향키 순환·Home/End·자동 활성화, `data-slot`, `cn(..., className)`, trigger의 focus ring, Disabled 처리.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn의 list `bg-muted`·rounded·padding, trigger의 흰색 pill/dark 스타일, root `gap-2`, list variant(`default`/`line`) 제거 → theme CSS가 담당.
|
||||
- Bar는 콘텐츠와 gap 없이 붙는다.
|
||||
- `TabsList`가 frame wrapper와 이동 버튼을 렌더(ui_viz 구조 반영, 프로젝트 지침상 필요한 wrapper).
|
||||
- 선택 tab을 보이게 유지: click/keyboard 선택, resize·글꼴 로딩·항목 변경 시 list `scrollLeft` 보정(ResizeObserver/MutationObserver).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Tabs"): **Overview**는 fill / panelSurface를 생략한 기본 Primary, **Default API**는 `fill`만 명시한 Primary와 `fill` + `panelSurface="content"`를 명시한 Primary를 비교한다. **Variants**는 명시적 Secondary, **States**는 기본 Primary + 8개 탭 + 260px wrapper Overflow다. 기존 대표 Story ID와 메뉴를 유지한다. Hover는 실제 포인터로 확인한다.
|
||||
- verification(`tags: ["verification"]`): `Secondary`, `SelectedFirst`, `SelectedLast`, `Focus`, `Disabled`, `Overflow`, `SecondaryOverflow`, `LongLabel`. Primary 기준이며 Secondary는 같은 CSS 경로를 쓴다. Focus는 `play`로 실제 focus를 준다.
|
||||
- `LongLabel`은 짧은 라벨, 긴 라벨 1개, 긴 라벨 여러 개, 260px 좁은 container를 함께 검증한다. 라벨은 `nowrap`·무말줄임이며 실제 필요 폭이 넘을 때 이동 버튼으로 전환한다.
|
||||
- non-fill Story(`Selected`, `SelectedMiddle`, `Fill`, `FillContent`, `FillOverflow`와 대표의 non-fill 구역)는 2026-10-06에 제거했다. API(`fill=false`, Legacy profile CSS)는 유지한다(§10).
|
||||
- 2026-10-08 숨김 `Compatibility`(`verification` 태그 + **VERIFICATION ONLY** 표시)는 Legacy `fill={false}` / `fill={false} panelSurface="surface"` 동일성과 `panelSurface="surface"`만 명시한 경우의 Secondary 전환을 확인한다. 일반 Story 메뉴를 추가하지 않는다.
|
||||
- Story의 콘텐츠 panel은 Primary `TabsContent` + `border bg-background`, Secondary / Legacy `TabsContent` + `border bg-card` 소비자 조합 예시다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-06: Tabs의 최종 디자인 언어는 **Primary = Fill + `panelSurface="content"`**(기본형), **Secondary = Fill + `panelSurface="surface"`**(서브형) 두 가지다. 이 결정이 ui_viz보다 우선한다. non-fill 기본 profile은 OLD_TEST_ONLY로 분류해 대표 Story에서 내렸다.
|
||||
- **2026-10-06 당시 범위**는 Storybook 대표형에 한정했다. 당시 Public API 기본값(`fill=false`, `panelSurface="surface"`)과 non-fill CSS branch는 유지하고 기본값 변경·branch 삭제를 별도 승인 대상으로 남겼다. 당시 저장소 내 소비처가 없다는 관찰은 외부 소비자의 무영향을 보장하지 않는다.
|
||||
- **2026-10-08 추가 개선 E 승인**: `TabsList` 기본값을 `fill=true`, `panelSurface="content"`로 변경한다. Primary / Secondary / Legacy의 기존 CSS 디자인 값·선택자·동작·Theme / Token을 유지한다. non-fill의 OLD_TEST_ONLY / REMOVE_CANDIDATE 이력과 명시적 사용 API는 유지하며 삭제 승인은 포함하지 않는다. `panelSurface`는 Fill일 때만 효과가 있는 List prop이다(`Tabs` Root prop이 아님).
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 이동 후 가장자리 정렬 세부 (다음 버튼은 오른쪽에 걸친 첫 항목의 끝을 보이는 영역 끝에 맞춤), 이동 버튼 포커스 수치(global focus TBD, box-shadow 3px 링 사용), fill에서도 Bar −1px 겹침 유지, Brown 콘텐츠 탭·dark mode TBD.
|
||||
- `panelSurface`는 `surface`/`content`만 지원(현재 제한).
|
||||
- Default 면·테두리 합성색과 곡선 반경 8px는 디자인 확정 시 재검토.
|
||||
- Focus 수치: global focus `TBD`. shadcn ring 유지.
|
||||
- Disabled: ui_viz P0 제외. shadcn 기본.
|
||||
- 볼록 Gradient: `TABS_CONVEX_GRADIENT_KNOWN_DEVIATION`, 평면으로 출하(구현도 평면).
|
||||
- Typography: expanded Source의 13px는 `TABS_SOURCE_LABEL_SIZE_13_UNADOPTED`로 16px 유지.
|
||||
- 앞 탭 right border가 2번째 이후 Selected의 좌측을 대신 그리는 관계는 ui_viz `BASELINE_BEHAVIOR_ACCEPTABLE`이며 그대로 구현.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/tabs.tsx
|
||||
packages/ui/src/theme/tabs.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Tabs, TabsContent, TabsList, TabsTrigger)
|
||||
src/components/tabs.stories.tsx
|
||||
src/components/story-board.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-08 추가 개선 E: 기존 6024 Storybook 재사용. 대표 `All`에서 기본 Props / `fill`만 명시 / 두 Props 명시가 모두 Primary(`data-fill="true"`, `data-panel-surface="content"`)이며 같은 Gradient·Green Light 3 경계·34px·16/24/700·8px 곡선을 확인했다. Secondary의 White 면·패널과 Bar / panel 1px 겹침도 유지했다.
|
||||
- 숨김 `Compatibility`: `fill={false}`의 content / surface 조합은 같은 White 선택 면·Legacy 경계·라벨 필요 폭이고 곡선이 없다. `panelSurface="surface"`만 명시하면 Secondary다. CSS 주석을 제외한 모든 규칙·값과 Production의 기본값 두 개를 제외한 코드가 작업 시작 상태와 동일함을 비교했다.
|
||||
- 실제 포인터 / 키보드: 클릭·ArrowLeft / Right·Home / End 선택과 콘텐츠 전환, 비선택 Hover Black 20% / 선택 Gradient 유지, focus-visible 3px 링, Disabled opacity 0.5·pointer-events none·tabIndex -1·방향키 건너뛰기를 확인했다. 독립 Overflow Story는 260px frame / 192px list에서 다음 이동 `scrollLeft 0→33.33`, 이전 이동 `→0`, 끝 버튼 Disabled 전환과 선택 유지가 정상이다.
|
||||
- 1280px / 390px: 대표 Story의 문서 가로 넘침 없음. 390px의 기본·명시적 Fill frame은 약 342.67px, Overflow specimen은 260px. 390px Compatibility도 문서 가로 넘침 없이 Legacy list 약 226px를 유지했다. HM Theme의 content / surface / Green Light 3 연결은 유지한다.
|
||||
- `npm run build:ui`(UI 패키지·선언 빌드), `node node_modules/typescript/bin/tsc -p tsconfig.app.json`(Story 포함) 통과. Tabs 변경 파일의 diff whitespace 검사 통과. 전체 회귀·전체 Storybook 정적 빌드는 이번 범위에서 실행하지 않았다.
|
||||
- Console: 패키지 dist 재생성 중 기존 브라우저에 Vite `src/index.css` reload 오류가 한 번 기록됐으나 복구됐다. 빌드 후 새 브라우저에서 대표 / Compatibility를 확인한 Console error / warning은 0건이다. 외부 소비자 전체 호환성은 §15의 한계를 따른다.
|
||||
- 2026-10-04: `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. build의 기존 Tailwind 문서 예시 경고(`bg-[var(...)]`)와 Storybook 500kB chunk 경고 외 신규 경고 없음.
|
||||
- Storybook computed style(`components-navigation-tabs--all`): 높이 34px, padding `0 16px`, radius `4px 4px 0 0`, 16/24/700, Default/Selected 색과 연결선 일치. fill 선택 경계 `rgb(36,148,115)`, 선택 면과 8×8 곡선 확인.
|
||||
- 실제 pointer + keyboard: 비선택 hover Black 20% overlay·전경 전환, 선택 hover 비적용, 클릭·ArrowRight/Left·Home/End 자동 선택, 3px focus-visible ring, `tabpanel` 내용 전환과 54px panel 높이 유지.
|
||||
- Disabled: `disabled=true`, `tabIndex=-1`, opacity 0.5, 활성화 불가.
|
||||
- Overflow: 260px frame에서 192px list / 605px scroll width, 이동 버튼 1회 `scrollLeft 0→36`, 선택값 유지, 이전 버튼 활성화.
|
||||
- Long Label: `nowrap`, `text-overflow: clip`, 단일/복수 긴 라벨에서 실제 필요 폭 기준 overflow. 가시 영역보다 넓은 선택 탭은 시작점을 목록 왼쪽에 맞춘다.
|
||||
- Responsive: 1280 / 768 / 390px 모두 문서 가로 overflow 없음(`scrollWidth == clientWidth`). 390px에서 기본·fill은 frame 343px, 명시적 narrow specimen은 260px로 유지.
|
||||
- 2026-10-06 smoke check(ui_viz Storybook 나란히): fill 인접 경계(`margin-left:-1px` + 1px 경계)·선택 탭 8×8 곡선 ±8px 접점·34px·16/24/700·색 유지. Overflow/Fill Overflow/Long Label은 3개 viewport에서 이동 버튼 전환, 선택 탭 시작점 왼쪽 정렬, 페이지 넘침 없음. Click·ArrowRight·Home 동작 확인. 새 회귀 없음.
|
||||
- 2026-10-08 저장소 소스 검색: Production / Dashboard에는 TabsList 소비처가 없으며 `src/components/tabs.stories.tsx`만 소비한다. 외부 프로젝트는 이 확인 범위에 포함하지 않는다.
|
||||
|
||||
## 14. ui-viz 직접 비교 (2026-10-04)
|
||||
|
||||
2026-10-06 비교는 외부 ui_viz의 보존된 두 원본 사본의 Tabs JSX/CSS SHA-256 일치를 확인한 뒤 동일 소스와 당시 캡처를 참고했다. 외부 자료는 ZIP에 포함되지 않으며 현재 Source of Truth는 HM 구현·대표 Story와 이 문서다. 당시 백업 Storybook은 dependency가 제거되어 실행하지 못했으므로, 비교 범위는 보존된 소스·캡처와 HM computed value였다.
|
||||
|
||||
| 항목 | ui-viz | 수정 전 HM | 판정 / 최종 처리 |
|
||||
|---|---|---|---|
|
||||
| 구조·상태 | 자체 controlled Tabs/Tab + roving focus | Radix Root/List/Trigger/Content | `IMPLEMENTATION_CHANGE`; Radix 유지 |
|
||||
| 기본 geometry·색·type | 34px, 0 16px, 4px, 16/24/700, 연결형 border | computed value 일치 | `MATCHED` |
|
||||
| hover / selected | inactive만 Black 20%, selected surface 연결 | 일치 | `MATCHED` |
|
||||
| focus / disabled | 전역 수치 미정 / P0 제외 | 명확한 Radix focus ring / opacity 0.5 | `INTENTIONAL_CHANGE` |
|
||||
| fill 인접 경계 | 모든 이웃이 `margin-left:-1px` + left border 복원 | active 이웃만 부분 복원 | `REGRESSION`; 원본 겹침 관계 복구 |
|
||||
| fill 곡선 접점 | active edge에서 정확히 ±8px | edge 밖으로 1px 이격 | `REGRESSION`; ±8px 접점 복구 |
|
||||
| overflow | 실제 필요 폭, 34px 양끝 control, 한 항목 이동 | 수치·동작 일치 | `MATCHED` |
|
||||
| 매우 긴 선택 탭 | item이 viewport보다 넓으면 시작점 left align | right align으로 라벨 앞부분이 가려짐 | `REGRESSION`; left align 복구 |
|
||||
| Long Label 검증 | 실제 사용 specimen 존재 | 전용 Story 없음 | `MISSING`; 숨김 verification Story와 대표 구역 추가 |
|
||||
| controlled-only API·13px 실험 라벨 | ui-viz Web mapping/미채택 값 | Radix 양방향 API·16px | `OLD_TEST_ONLY`; 미도입 |
|
||||
|
||||
### Before / After
|
||||
|
||||
| 항목 | Before | After | ui-viz |
|
||||
|---|---|---|---|
|
||||
| fill 이웃 경계 | 후속 inactive left border 0 | left border 1px + margin-left -1px | 동일 |
|
||||
| fill 곡선 | active edge와 1px gap | edge 기준 `-8px` | 동일 |
|
||||
| viewport보다 넓은 선택 tab | 오른쪽 끝 정렬 | 왼쪽 시작점 정렬 | 왼쪽 시작점 정렬 |
|
||||
| Long Label Story | 없음 | 단일/복수/260px 검증 | 사용 specimen 존재 |
|
||||
|
||||
## 15. 외부 소비자 영향 / 호환성 (2026-10-08)
|
||||
|
||||
- **변경 전**: `<TabsList>` → Legacy non-fill (`fill=false`, `panelSurface="surface"`).
|
||||
- **변경 후**: `<TabsList>` → Primary Fill (`fill=true`, `panelSurface="content"`).
|
||||
- **이전 스타일 유지**: `<TabsList fill={false}>`. 이전 White 패널 조합(`bg-card`)도 유지한다. `panelSurface="surface"`를 함께 명시해도 non-fill 시각은 같다.
|
||||
- **`fill`만 명시한 기존 소비자**: 이전에는 Secondary(surface), 이제는 Primary(content)가 된다. 이전 Secondary를 유지하려면 `<TabsList fill panelSurface="surface">`와 surface 패널을 사용한다.
|
||||
- **`panelSurface`만 명시한 기존 소비자**: 이전에는 non-fill이라 시각적으로 무시했지만 이제 Fill이 적용된다. `"surface"`는 Secondary, `"content"`는 Primary다. non-fill을 유지하려면 `fill={false}`를 추가한다.
|
||||
- **두 Props 모두 명시**: 기존 조합의 시각은 유지한다. Props 이름·타입·Radix props·export·DOM 구조는 변경하지 않는다. 타입 호환성과 기본 디자인 호환성은 구분한다.
|
||||
- **레이아웃 차이**: non-fill은 라벨 필요 폭, Fill은 Overflow가 없을 때 컨테이너 전체를 균등 분할한다. 높이 34px은 같으나 탭 폭·공유 경계·선택 연결 곡선·패널색이 달라질 수 있다. 좁은 폭에서는 기존 Overflow 이동 버튼이 나타날 수 있다.
|
||||
- **CSS Override 영향**: frame의 기본 attribute가 `data-fill="false"` / `data-panel-surface="surface"`에서 `"true"` / `"content"`로 바뀐다. non-fill 조건 selector, list width / flex, active background / border, 곡선 pseudo-element override를 사용하는 소비자는 검토가 필요하다. `className`과 ref는 계속 tablist에 전달된다.
|
||||
- **확인 범위**: 현재 저장소의 TabsList 사용은 Story만 확인했다. 외부 소비자 전체의 명시적 Props·패널색·레이아웃·CSS Override는 실제 확인하지 못했으므로 위 내용은 영향 가능성과 이행 안내이며 외부 호환성 전체 검증을 뜻하지 않는다.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Textarea
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Textarea 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | HM 스타일 적용 완료 (`hm-textarea` class 없음) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Textarea (native `<textarea>`) |
|
||||
| 대표 Story | `Components/Form/Textarea` → `All` (이름 "Textarea") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
원본: 외부 ui_viz Reference (ZIP 미포함)의 `docs/components/textarea.md` (+ `brand/hanmac/foundation/{color,border}.md`, `foundation/token-inventory.md`의 입력 계열 Semantic).
|
||||
|
||||
- **ui_viz Confirmed**: border 1px / radius 4px(§2, §2.2), 면·값·Placeholder 색(§2.2), 기본 테두리 `color.stroke.input.default`, Focus `color.stroke.input.focus`, Error `color.stroke.input.error`, Focus/Error 두께 1.5px(D-005 상속, §2.2), Hover = Default(§3.3 사용자 결정 TEXTAREA-05, Confirmed), Readonly 표현(§3.1), 값 typography 16/24/400(§2).
|
||||
- **SCOPED_DESIGN_APPROVAL** (2026-09-17 owner, Textarea 한정): min-height `112px`, padding `8px 12px`(§8.1, TEXTAREA-01/02). D-093 디자인 확정값이 아니며 ui_viz Canonical은 `TBD`다. Textarea 범위에서만 사용하고 `size.*`/`space.*`로 승격하지 않는다.
|
||||
- **Component-local**: `resize` 정책(vertical 기본, none/both 선택, Readonly는 none 고정).
|
||||
- **shadcn 유지**: native textarea 동작, `transition-colors`, `outline-none`, Disabled opacity와 커서.
|
||||
- **HM 사용자 결정(2026-10-06)**: Error border와 Keyboard Focus ring을 독립 Semantic으로 표시한다.
|
||||
|
||||
## 3. 기본 규격
|
||||
|
||||
| 항목 | 값 | 상태 |
|
||||
|---|---|---|
|
||||
| Width | 부모 폭 100% (`w-full`) | Canonical |
|
||||
| Min-height | 112px | SCOPED_DESIGN_APPROVAL |
|
||||
| Padding | 8px (block) / 12px (inline) | SCOPED_DESIGN_APPROVAL |
|
||||
| Border / Radius | 1px `--hm-sem-stroke-input-default` / `--hm-radius-input`(4px) | Confirmed |
|
||||
| Face / 값 색 | `--hm-sem-bg-surface` / `--hm-sem-fg-primary` | Confirmed |
|
||||
| Typography | 16 / 24 / 400 | Existing role |
|
||||
| Placeholder | `--hm-sem-fg-placeholder`, `opacity: 1` | Confirmed |
|
||||
| Resize | `vertical` 기본 | Component-local |
|
||||
|
||||
Size 체계 없음. Input의 Small/Medium/Large와 공유하지 않는다(ui_viz Textarea에 size 정의 없음). `field.gap` 8px 등 Field 관계는 Textarea 밖(소비 조합)이다.
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 | 근거 |
|
||||
|---|---|---|
|
||||
| Default | 위 기본 규격 | Confirmed |
|
||||
| Hover | Default와 동일 (hover rule 없음) | Confirmed (사용자 결정 TEXTAREA-05) |
|
||||
| Focus (`:focus-visible`) | border 1.5px `--hm-sem-stroke-input-focus`, shadow/glow 없음, padding으로 0.5px 보정해 글자 위치 유지 | Confirmed (D-005 상속) |
|
||||
| Error (`[aria-invalid="true"]`) | border 1.5px `--hm-sem-action-destructive`, ring 없음, 동일 padding 보정 | Confirmed (D-005 상속) |
|
||||
| Error + Keyboard Focus | Destructive border 유지 + HM Focus Semantic 3px / 50% outer ring, `outline: none` | Input과 동일, 2026-10-06 사용자 결정 |
|
||||
| Disabled | surface face 유지, `cursor: not-allowed`, `opacity: 0.6` | opacity: 사용자 결정(§10) |
|
||||
| Readonly (`[readonly]`) | Default와 같은 면·테두리·typography, `opacity: 1`, `resize: none` 고정 | Component-local (§3.1) |
|
||||
|
||||
Chromium은 1.5px 테두리를 `getComputedStyle` 기준 1px로 계산한다(ui_viz도 같은 관측을 기록). padding 보정(7.5px/11.5px)은 선언 1.5px 기준으로 적용된다.
|
||||
|
||||
## 5. API
|
||||
|
||||
```
|
||||
Textarea:
|
||||
resize?: "vertical" | "none" | "both" 기본 "vertical"
|
||||
```
|
||||
|
||||
- 나머지는 native `<textarea>` props 그대로(`rows`, `placeholder`, `disabled`, `readOnly`, `aria-invalid` 등).
|
||||
- 출력 속성: `data-slot="textarea"`, `data-resize`.
|
||||
- 상태는 새 prop이 아니라 native 속성으로 준다.
|
||||
- Field(Label, Required 별표, Helper/Error 문구)는 Textarea에 포함하지 않는다. 소비 조합이다.
|
||||
- auto-grow, 글자 수 카운터는 범위 밖이다. shadcn의 `field-sizing-content`(auto-grow)는 제거했다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/textarea.css` (`--hm-textarea-*`).
|
||||
|
||||
- 재사용한 공통 Semantic: `--hm-radius-input`, `--hm-sem-bg-surface`, `--hm-sem-fg-primary`, `--hm-sem-fg-placeholder`, `--hm-sem-stroke-input-default`, `--hm-sem-stroke-input-focus`, `--hm-sem-action-destructive`(Semantic 참조).
|
||||
- Textarea 전용 값: `--hm-textarea-min-height`, `--hm-textarea-padding-{block,inline}`, `--hm-textarea-font-{size,weight}`, `--hm-textarea-line-height`, `--hm-textarea-border-width`, `--hm-textarea-state-border-width`(1.5px).
|
||||
- Primitive 신규 추가 없음.
|
||||
- Input의 `--hm-input-*`와 값이 겹쳐도 alias하지 않는다(ui_viz도 Input·Textarea를 별도 owner로 두고 값을 각각 소비).
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
native `<textarea>` 동작 전체, `data-slot`, `cn(..., className)`, `transition-colors`, `outline-none`, `disabled:cursor-not-allowed`. Radix 사용 없음.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- `resize` prop, `data-resize` 추가.
|
||||
- shadcn의 기본 min-height/padding/border/ring/shadow/`dark:`/`field-sizing-content` class 제거 → theme CSS가 담당.
|
||||
- 일반 Focus는 border 1.5px로 표시, Error 단독은 ring 없음. Error + `:focus-visible`만 독립 Focus ring을 표시한다.
|
||||
- Readonly 전용 표현(resize 고정) 추가.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Textarea"): 구역 `default`, `hover`, `focus`, `error`, `error-message`, `readonly`, `disabled`, `multiline`, `resize`. Focus는 `play` 함수로 실제 focus를 준다. Hover는 별도 pseudo 복제 없이 Default와 같다는 설명만 둔다.
|
||||
- verification(`tags: ["verification"]`): `Default`, `Focus`, `Error`, `InvalidFocus`, `ErrorMessage`, `Required`, `Readonly`, `Disabled`, `LongContent`, `Playground`(HM 전용).
|
||||
- Error Message·Required 표시는 Story 안의 조합 예시이며 Textarea 컴포넌트의 일부가 아니다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
**2026-10-02**: Textarea Disabled opacity `0.6` 확정. ui_viz에서 `Temporary`지만 현재 HM 구현에서는 이 결정이 우선한다. Textarea 한정이며 전역 Disabled opacity로 확대하지 않는다 (Input·Select와 같은 값). Token이 아니라 `ui/textarea.tsx`의 `disabled:opacity-60`으로 적용.
|
||||
- ui_viz에 기록된 결정을 따른다: 2026-09-17 Hover = Default(TEXTAREA-05), min-height 112 / padding 8·12 Textarea 한정 승인(TEXTAREA-01/02). 2026-10-01 필수 별표 `#D32F2F`(TEXTAREA-07, Textarea에 별표 없음 — Story 조합 예시는 shadcn `text-destructive`).
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- **min-height 112 / padding 8·12**: Textarea 한정 owner 승인이지만 ui_viz Canonical은 `TBD`, D-093 미확정(TEXTAREA-01/02/06, `NO_SOURCE_EVIDENCE`). 변경 가능성이 있다.
|
||||
- **Label 14/20/500 · Value 16/24 · Error 11/18 vs D-093 원본 실행본 13/700 등**: `KNOWN_DEVIATION / FOLLOW_UP`(TEXTAREA-08). 확정 근거 없어 ui_viz 문서값을 사용.
|
||||
- **Error + Focus**: HM 사용자 결정(2026-10-06)으로 Input과 같은 3px / 50% Focus Semantic ring을 적용한다.
|
||||
- auto-grow, 글자 수 카운터(TEXTAREA-03): 범위 밖. max length 관련 표현 없음.
|
||||
- 전역 focus ring 수치: TBD.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/textarea.tsx
|
||||
packages/ui/src/theme/textarea.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export Textarea, TextareaResize)
|
||||
src/components/textarea.stories.tsx
|
||||
src/components/story-board.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- `npm run typecheck`(root + ui + center-graphic), `npm run build`: 통과(2026-10-02).
|
||||
- Storybook computed style(`components-form-textarea--all`): Default min-height 112px / padding 8px 12px / border 1px rgb(222,219,210) / radius 4px / bg white / 16·24·400 / resize vertical / Placeholder rgb(153,153,153), Error border rgb(211,47,47) + padding 7.5px 11.5px, Readonly resize none · opacity 1, `resize="none"` 적용, Disabled opacity 0.6.
|
||||
- 실제 클릭 focus: `:focus-visible` 매칭, border rgb(18,51,40), padding 7.5px 11.5px 확인.
|
||||
- Story ID: `All` + verification 8개 존재 확인(index.json). `@hm-design/ui`에서 `import { Textarea }`로 Story가 빌드됨.
|
||||
- 2026-10-06 ui_viz Storybook 화면 비교(1280/768/390, Chromium): 컨트롤 computed style 일치(ui_viz Story의 114px은 `rows=4` 때문), Error+Focus outline 유지, 긴 내용은 내부 세로 scroll·가로 넘침 없음, Readonly 편집 불가·선택 가능, Disabled focus 불가. ui_viz는 Disabled에서 Field 전체(라벨 포함)를 흐리지만 HM은 컨트롤만 흐리고 라벨은 `Label`의 disabled 연동(소비 조합)이 맡는다 — 의도적 차이. 대표 Story는 ui_viz 5구역을 모두 포함한 상위 집합(Hover = Default, Error Message, Resize none 추가)이라 변경하지 않음.
|
||||
- 미확인: 브라우저별 렌더(Chromium 외).
|
||||
|
||||
|
||||
### Invalid + Focus 검증 (2026-10-07)
|
||||
|
||||
- Focus ring: `--ring` → `--hm-sem-stroke-input-focus` → `--hm-color-border-focus`. Invalid border: 기존 `--hm-sem-action-destructive`. 새 Runtime Role / Production Error prop / 메시지 컴포넌트 없음.
|
||||
- Chromium computed style: Invalid border `rgb(211, 47, 47)`, Invalid + Keyboard Focus box-shadow `color(srgb 0.14902 0.4 0.317647 / 0.5) 0px 0px 0px 3px`, `outline-style: none`. Invalid 단독은 `box-shadow: none`.
|
||||
- Theme Designer UI에서 Focus `#c75b00`, Destructive `#d41466`, Active `#007f73`, Accent `#ffd36d`로 분리. Invalid border `rgb(212, 20, 102)` + Focus shadow `color(srgb 0.780392 0.356863 0 / 0.5) 0px 0px 0px 3px` 확인.
|
||||
- `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. 기존 문서 예시 CSS warning 및 Storybook chunk 크기 / plugin timing warning 유지.
|
||||
- 검증 범위: Chromium 환경. 다른 브라우저의 렌더링은 별도 확인이 필요하다.
|
||||
- 대표 `All`과 숨김 `InvalidFocus` 확인. 일반 Focus / Invalid의 padding 7.5px 11.5px, 기본 min-height 112px / 16px 글자 / Readonly resize none / Disabled opacity 0.6 유지. Border CSS 계약 1.5px, 이 브라우저 환경의 computed border-width는 1px으로 정규화. Input과 같은 Invalid + Focus ring.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Toggle Group
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Toggle Group 고유의 API·Token·상태·결정만 기록한다. Toggle(`toggle.md`)과 역할 비교는 거기 §14.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Toggle Group (Radix `ToggleGroup`) |
|
||||
| 대표 Story | `Components/Form/Toggle Group` → `All` (이름 "Toggle Group") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Toggle Group이 없다. 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다. Toggle과 같은 선택 언어(On = accent 면)를 공유하지만 Token은 `--hm-toggle-group-*`로 따로 두고 Toggle의 CSS에 의존하지 않는다.
|
||||
- **구현 방식**: shadcn / Radix ToggleGroup 구조(Root + Item)와 `variant`·`size`·`spacing` API(context로 Item에 전달)를 유지한다. Item은 Toggle 컴포넌트가 아니라 독립 요소다.
|
||||
- **Tabs와의 구분**: Tabs는 content 전환(navigation), Toggle Group은 상태 선택이다.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Item | 높이 `sm` 28 / `default` 34 / `lg` 42px, padding-x 8 / 12 / 16px, radius 4px, 14 / 20 / 500, icon + text gap 8px |
|
||||
| Icon-only | 단일 React element + 직계 `svg` 하나인 경우 `data-icon-only`와 CSS로 padding 0, 높이와 같은 정사각형. SVG + text node는 size별 padding 8/12/16px 유지 |
|
||||
| Off / On | Off는 투명 배경과 `--hm-sem-fg-primary`, On은 `--accent` 배경과 `--accent-foreground` |
|
||||
| `outline` variant | 1px `--hm-sem-stroke-input-default` border |
|
||||
| Connected (`spacing={0}`, 기본) | 안쪽 모서리 직각, 바깥 모서리 4px, outline border는 1px씩 겹침(`-1px`), On Item은 위로 올림(`z-index`) |
|
||||
| Separated (`spacing > 0`) | 각 Item이 독립 radius, 간격 = `spacing × 4px` |
|
||||
| 방향 | horizontal(기본), vertical(`orientation="vertical"`, 세로 연결 모서리) |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Off / On | Radix `data-state`. single은 Item `role="radio"` + `aria-checked`(Root `role="radiogroup"`), multiple은 `aria-pressed`(Root `role="toolbar"`) |
|
||||
| Hover (off) | 배경 `--accent` 45% |
|
||||
| Selected + Hover (on) | 배경 `--accent` 100%, 글자·아이콘 `--accent-foreground` 유지 |
|
||||
| Focus (`:focus-visible`) | shadcn ring, focus된 Item이 인접 Item 위로(`z-10`) |
|
||||
| Disabled (그룹 또는 Item) | `opacity: 0.6`(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), 클릭·focus 불가, 방향키 이동에서 제외 |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix ToggleGroup props 그대로. export: `ToggleGroup`, `ToggleGroupItem`.
|
||||
|
||||
- `ToggleGroup`: `type="single" | "multiple"`, `value`/`defaultValue`/`onValueChange`, `orientation`, `disabled`, `rovingFocus`, `loop`, `dir`. shadcn: `variant`, `size`, `spacing`(기본 0). 출력 `data-variant`, `data-size`, `data-spacing`, `data-orientation`(`orientation`을 줄 때만).
|
||||
- `ToggleGroupItem`: `value`(필수), `disabled`. `variant`/`size`는 그룹에서 내려온다.
|
||||
- single에서 선택된 Item을 다시 누르면 선택이 해제되어 `onValueChange("")`가 호출된다(Radix 동작). 항상 하나를 유지하려면 소비자가 빈 값을 무시한다(Story 참고).
|
||||
- icon-only Item은 `aria-label`을 준다.
|
||||
- 새 selection state 관리는 만들지 않았다.
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/toggle-group.css` (`--hm-toggle-group-*`).
|
||||
|
||||
`height-sm|md|lg`, `padding-x-sm|md|lg`, `radius`, `gap`, `font-size|line-height|font-weight`, `off-background|off-text`, `hover-background`, `on-background|on-text`, `border-width|color`, `disabled-opacity`. Semantic과 Theme palette(`--hm-sem-stroke-input-default`)만 참조한다. `--hm-toggle-*`, `--hm-button-*`는 참조하지 않는다. 신규 Primitive와 Semantic 없음. `--hm-toggle-group-gap`은 `spacing` 값으로 인라인 style이 덮어쓴다.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA·roving focus(방향키, Home/End, disabled 건너뜀, `loop`)·Enter/Space·controlled/uncontrolled, shadcn context 방식의 `variant`/`size`/`spacing`, focus ring class, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn의 `data-[spacing=0]:*` 계열 class와 Toggle 색 class를 제거하고 theme CSS로 이전. 연결형 모서리·border 겹침을 CSS 선택자로 처리.
|
||||
- Radix가 `orientation`을 주지 않으면 `data-orientation`을 출력하지 않으므로 가로 연결형은 `:not([data-orientation="vertical"])`로 선택한다.
|
||||
- 기본 모양을 `@layer components`에 두어 소비자의 `className`이 이긴다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Toggle Group"): Single(local state, 값 표시), Multiple(icon-only, local state), Disabled group / item, Vertical(icon-only), `spacing={2}`, Many items(`spacing={1}` + `flex-wrap`)을 한 페이지에 둔다.
|
||||
- verification: `Single`, `Multiple`, `Vertical`.
|
||||
- meta에 `component`를 지정하지 않는다(props가 `single | multiple` union이라 args 없는 Story가 타입을 통과하지 못함).
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). Toggle Group은 Button과 독립 Component Token을 쓰며 Tabs와 같은 모양으로 만들지 않는다(작업 지시).
|
||||
- 2026-10-06 Selected 원복: Git baseline `d101530`의 On 매핑 `--accent` / `--accent-foreground`를 복원했다. Selected + Hover도 Accent 100%와 동일 전경을 유지하고 아이콘은 `currentColor`를 따른다. Border는 기존 default 투명 / outline `--hm-sem-stroke-input-default`를 유지한다. Active Semantic은 참조하지 않고 Focus는 `--ring` → `--hm-sem-stroke-input-focus`(Light 2), `ring-ring/50`을 유지한다. Hover·Disabled·Icon-only / Icon+Text padding·Theme JSON·Runtime 계약 변경 없음.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(28/34/42px, 연결형 기본, On 면 accent, Disabled opacity 0.6)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- **줄바꿈**: 연결형(`spacing={0}`) 그룹이 `flex-wrap`으로 여러 줄이 되면 두 번째 줄 첫 Item의 border 겹침(`-1px`)과 모서리가 어색하다. 항목이 많거나 폭이 좁으면 `spacing > 0`을 쓰고 `className="flex-wrap"`을 소비자가 선택한다. 자동 responsive 배치는 만들지 않았다.
|
||||
- vertical 그룹의 폭: Item 폭이 서로 다르면 가장 넓은 Item에 맞춰 늘어난다(`items-stretch`).
|
||||
- 선택 해제 가능 정책(single에서 재클릭)은 Radix 기본.
|
||||
- 스크린리더 안내 문구(그룹 이름)는 소비자의 `aria-label`.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/toggle-group.tsx
|
||||
packages/ui/src/theme/toggle-group.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/toggle-group.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-06 Selected 원복 검증: Storybook `All` / `ContentNodes`에서 Default·실제 Hover·Selected·Selected + Hover·키보드 Focus·Disabled·Icon-only·Icon+Text 확인. Selected 배경 `rgb(189, 223, 212)` / 전경·아이콘 `rgb(18, 51, 40)`, outline border `rgb(222, 219, 210)`, Focus는 Light 2 `#266651` 50%의 3px ring. Runtime API의 Active override는 Selected에 영향 없음, Accent/on Accent override는 면·전경만 변경, Focus override는 ring만 변경. 1280/390px에서 sm/default/lg Icon-only 28/34/42px square·padding 0, Icon+Text/Text-only padding-x 8/12/16px 및 Array/Fragment 조합 유지. `typecheck`, `build`, `build-storybook`, `git diff --check` 통과(기존 CSS 예시·Storybook chunk 경고 유지).
|
||||
|
||||
- 2026-10-06 Visual QA: Icon+Text item도 기존 `:has(> svg:only-child)`에 실제 매칭됨을 확인하고 Toggle과 함께 수정. 1280/1024/768/390px, sm/default/lg에서 Icon-only PL/PR 0/0px·28/34/42px square, Icon+Text와 Text-only PL/PR 각각 8/12/16px, pressed 상태 정상. Array/Fragment 조합 확인. `typecheck`, `build`, `build-storybook` 통과.
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 DOM(Browser pane): single은 `radiogroup`/`radio`(`aria-checked`), multiple은 `toolbar`와 `aria-pressed`.
|
||||
- single keyboard: 포커스 후 ArrowRight로 다음 Item, Enter로 선택(`value = center`), End로 마지막, Space로 선택(`value = right`), Home으로 처음. multiple: Enter/Space로 각각 토글되어 `[bold, italic, underline]`. vertical: ArrowDown으로 이동, ArrowRight는 이동하지 않음. disabled: ArrowRight가 비활성 Item을 건너뛰어 A → C, 비활성 그룹은 클릭해도 상태 불변. (ArrowLeft는 첫 입력이 한 번 무시되는 경우가 있었고 두 번째 입력에서 A로 이동. 입력 도구의 타이밍으로 보이며 ArrowRight 건너뜀은 확인됨.)
|
||||
- computed style: 연결형에서 첫/끝 Item만 바깥 모서리 4px, 가운데 0, outline Item margin `-1px`, vertical은 위/아래 모서리, `spacing={2}`는 각 Item 4px radius.
|
||||
- Responsive(390×844): 모든 그룹이 부모 폭 안에서 페이지 가로 overflow 없음, `spacing={1}` + `flex-wrap` 그룹은 358px 안에서 두 줄로 줄바꿈.
|
||||
- 미확인: 실제 pointer hover 면 시각, focus ring 시각, `loop={false}`·`rovingFocus={false}`, RTL.
|
||||
@@ -0,0 +1,119 @@
|
||||
# Toggle
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Toggle 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Toggle (Radix `Toggle`) |
|
||||
| 대표 Story | `Components/Form/Toggle` → `All` (이름 "Toggle") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Toggle이 없다(인벤토리에 Switch 계열 기능 이름으로만 언급). 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Toggle 구조와 `variant`(default | outline), `size`(default | sm | lg) API를 유지하고, 모양은 HM 독립 Token(`--hm-toggle-*`)으로 정한다.
|
||||
- **Button과의 관계**: 이 문서 §14.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Size | `sm` 28px, `default` 34px, `lg` 42px (높이). padding-x 8 / 12 / 16px |
|
||||
| Icon-only | 단일 React element + 직계 `svg` 하나인 경우 `data-icon-only`와 CSS로 padding 0. SVG + text node는 size별 padding 8/12/16px 유지 |
|
||||
| Radius | `--hm-radius-control-md` (4px) |
|
||||
| Typography | 14 / 20 / 500, icon + text는 gap 8px |
|
||||
| Off | 배경 투명, 글자 `--hm-sem-fg-primary` |
|
||||
| On | 배경 `--accent`, 글자 `--accent-foreground` (full Accent; Select 선택 항목의 Accent 45% 면과는 별도 정책) |
|
||||
| `outline` variant | 1px `--hm-sem-stroke-input-default` border(`default`는 border 없음) |
|
||||
| 커서 | pointer |
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Off / On | Radix `data-state="off" | "on"`, `aria-pressed` |
|
||||
| Hover (off) | 배경 `--accent` 45% (on 상태는 그대로 On 면) |
|
||||
| Selected + Hover (on) | 배경 `--accent` 100%, 글자·아이콘 `--accent-foreground` 유지. Hover 45% 면으로 내려가지 않음 |
|
||||
| Focus (`:focus-visible`) | shadcn ring (`ring-3 ring-ring/50`) 유지 |
|
||||
| Disabled | `opacity: 0.6`(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), `cursor: not-allowed`, 클릭·hover 무반응 |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Toggle props 그대로. export: `Toggle`, `toggleVariants`.
|
||||
|
||||
- `pressed`/`defaultPressed`/`onPressedChange`, `disabled`, `asChild`.
|
||||
- `variant` `"default" | "outline"`, `size` `"default" | "sm" | "lg"`(shadcn 호환). 출력 `data-variant`, `data-size`, `data-slot="toggle"`.
|
||||
- icon-only Toggle은 소비자가 `aria-label`을 준다(Story 참고).
|
||||
- 새 variant는 만들지 않았다(`default`, `outline` 두 개만 유지).
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/toggle.css` (`--hm-toggle-*`).
|
||||
|
||||
`height-sm|md|lg`, `padding-x-sm|md|lg`, `radius`, `gap`, `font-size|line-height|font-weight`, `off-background|off-text`, `hover-background`, `on-background|on-text`, `outline-border-width|color`, `disabled-opacity`. Semantic과 Theme palette(`--hm-sem-stroke-input-default`)만 참조한다. `--hm-button-*`와 Button의 role 체계는 참조하지 않는다. 신규 Primitive와 Semantic 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·`aria-pressed`·`data-state`·Enter/Space·controlled/uncontrolled·disabled·`asChild`, shadcn `variant`/`size` API와 `toggleVariants`(cva), focus ring class, svg 정렬 class, `data-slot`, `cn(..., className)`.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `toggleVariants`의 색·크기 class(`h-8 min-w-8 px-2.5`, `hover:bg-muted`, `data-[state=on]:bg-muted`, outline `border-input shadow-xs`)를 제거하고 theme CSS로 이전. On 면은 `muted`가 아닌 accent.
|
||||
- 기본 모양을 `@layer components`에 두어 소비자의 `className`(폭, radius, 레이아웃 utility)이 이긴다. 다른 theme CSS(레이어 밖)와 달리 Toggle은 Story에서 `className="w-40 rounded-full"`이 적용되는 것을 확인했다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Toggle"): Off/On(default·outline), Icon-only와 icon + text(`BoldIcon`, `ItalicIcon`, `StarIcon`), Sizes, Disabled(off/on), Controlled(`pressed` 상태 표시), className 확장을 한 페이지에 둔다.
|
||||
- verification: `States`, `Icon Only`, `Disabled`.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). Toggle은 Button과 독립 Component Token을 쓴다(작업 지시).
|
||||
- 2026-10-06 Selected 원복: Git baseline `d101530`의 `--accent` / `--accent-foreground` 매핑을 복원했다. 아이콘은 `currentColor`로 전경을 따른다. Border는 baseline대로 default 투명 / outline `--hm-sem-stroke-input-default`이며 Selected 전용 border는 없다. Active Semantic은 참조하지 않고 Focus는 기존 `--ring` → `--hm-sem-stroke-input-focus`(Light 2)와 `ring-ring/50`을 유지한다. Hover·Disabled·Icon-only / Icon+Text padding·Theme JSON·Runtime 계약 변경 없음.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(28/34/42px, padding, On 면 accent, Disabled opacity 0.6)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- On 상태 hover는 On 면 그대로이다(별도 hover 면을 정하지 않음).
|
||||
- On 상태를 Primary(진한 녹색 면)로 강하게 표시하는 variant는 만들지 않았다.
|
||||
- 아이콘 크기는 shadcn 기본(16px) 유지.
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/toggle.tsx
|
||||
packages/ui/src/theme/toggle.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/toggle.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-06 Selected 원복 검증: Storybook `All` / `ContentNodes`에서 Default·실제 Hover·Selected·Selected + Hover·키보드 Focus·Disabled·Icon-only·Icon+Text 확인. Selected 배경 `rgb(189, 223, 212)` / 전경·아이콘 `rgb(18, 51, 40)`, Focus는 Light 2 `#266651` 50%의 3px ring. Runtime API의 Active override는 Selected에 영향 없음, Accent/on Accent override는 면·전경만 변경, Focus override는 ring만 변경. 1280/390px에서 sm/default/lg Icon-only 28/34/42px square·padding 0, Icon+Text/Text-only padding-x 8/12/16px 및 Array/Fragment 조합 유지. `typecheck`, `build`, `build-storybook`, `git diff --check` 통과(기존 CSS 예시·Storybook chunk 경고 유지).
|
||||
|
||||
- 2026-10-06 Visual QA: 1280/1024/768/390px, sm/default/lg에서 Icon-only PL/PR 0/0px·28/34/42px square, Icon+Text와 Text-only PL/PR 각각 8/12/16px, pressed 상태 정상. Array/Fragment icon과 Fragment/element label 조합도 확인. `typecheck`, `build`, `build-storybook` 통과.
|
||||
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): 클릭으로 off → on → off(`aria-pressed`, `data-state` 일치), 키보드 focus 후 Enter로 on, Space로 off, disabled는 클릭과 focus 불가, controlled는 클릭으로 `pressed = true`와 `data-state="on"` 동기화.
|
||||
- computed style: default 34px / On 배경 `rgb(189,223,212)`·글자 `rgb(18,51,40)`, outline border `rgb(222,219,210)`, `sm` 28px, `lg` 42px, icon-only 34×34, disabled opacity 0.6, `className="w-40 rounded-full"` 적용(폭 160px, radius 최대).
|
||||
- 미확인: 실제 pointer hover 면 시각, focus ring 시각, 390px(단일 버튼이라 폭 문제 없음).
|
||||
|
||||
## 14. 역할 구분 (Button · Checkbox · Radio Group · Switch와의 차이)
|
||||
|
||||
| 컴포넌트 | 역할 |
|
||||
|---|---|
|
||||
| Button | **Action 실행**(일회성). `variant`가 역할(primary/secondary/destructive…), `appearance`가 treatment |
|
||||
| Toggle | **pressed 상태 유지**(툴바식 on/off). `aria-pressed` |
|
||||
| Toggle Group | **선택 그룹**(단일/다중 선택, roving focus) |
|
||||
| Checkbox | 폼의 boolean / 다중 선택 |
|
||||
| Radio Group | 폼의 배타적 선택 |
|
||||
| Switch | 설정 on/off(즉시 적용되는 설정 값) |
|
||||
|
||||
Toggle은 Button과 모양이 비슷해 보여도 Button의 action 중심 variant 체계(role/appearance)와 `--hm-button-*` Token을 상속하지 않는다. 역할이 다르고, Button 규격이 바뀔 때 Toggle이 함께 바뀌면 안 되기 때문이다. 공통 의미(선택 면의 accent, control radius)만 Semantic으로 공유한다.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Tooltip
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Tooltip 고유의 API·Token·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-02 |
|
||||
| 기반 | shadcn/ui Tooltip (Radix `Tooltip`) |
|
||||
| 대표 Story | `Components/Overlay/Tooltip` → `All` (이름 "Tooltip") |
|
||||
| 보류 | 아래 §11 |
|
||||
|
||||
## 2. 적용 근거
|
||||
|
||||
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Tooltip 규격이 없다(인벤토리 "상세 동작·디자인 보류"). 참고할 수치가 없었다.
|
||||
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic 체계(fg-primary, text-inverse)로 새로 정했다.
|
||||
- **구현 방식**: shadcn / Radix Tooltip 구조(Provider, Root, Trigger, Content, Arrow)를 유지하고 theme CSS로 HM 스타일을 연결.
|
||||
- **보류**: §11.
|
||||
|
||||
## 3. 기본 규격 (현재 HM 규격)
|
||||
|
||||
| 항목 | 값 |
|
||||
|---|---|
|
||||
| Background | `--hm-sem-fg-primary` (진한 녹색) |
|
||||
| Text | `--hm-color-text-inverse` (White) |
|
||||
| Radius | `--hm-radius-container` (4px) |
|
||||
| Padding | 6px 10px |
|
||||
| Max width | 240px, 긴 문장은 줄바꿈(`text-balance`, `overflow-wrap: anywhere`) |
|
||||
| Typography | 12 / 18 / 400 |
|
||||
| Arrow | shadcn 회전 사각형, 면과 같은 색(배경·fill) |
|
||||
| Position | `sideOffset` 4, shadcn 기본 side(top) |
|
||||
| Delay | `TooltipProvider delayDuration` 기본 0 (shadcn 기본 유지) |
|
||||
|
||||
Tooltip은 짧고 즉각적인 설명만 담는다. 긴 설명·interactive 콘텐츠는 Popover / Hover Card를 쓴다. 그림자와 border는 없다.
|
||||
|
||||
## 4. 상태 규격
|
||||
|
||||
| 상태 | 값 |
|
||||
|---|---|
|
||||
| Open / Closed | Radix `data-state`(`delayed-open` 포함), shadcn open/close animation |
|
||||
| 열림 조건 | trigger hover 또는 keyboard focus |
|
||||
| 닫힘 조건 | pointer leave, blur, Escape, trigger 클릭 |
|
||||
|
||||
## 5. API
|
||||
|
||||
Radix Tooltip props 그대로. export: `TooltipProvider`, `Tooltip`, `TooltipTrigger`, `TooltipContent`.
|
||||
|
||||
- `Tooltip`은 상위 HM `TooltipProvider`가 없을 때만 자체 Provider를 만든다. 단독 사용은 그대로 지원하며 기본 `delayDuration`은 0이다.
|
||||
- 그룹에는 `@hm-design/ui`의 `TooltipProvider`를 둔다. 내부 Provider를 추가하지 않아 가장 가까운 Provider의 `delayDuration`, `skipDelayDuration`, `disableHoverableContent`와 그룹의 skip-delay 상태를 공유한다. Radix Provider를 직접 사용하는 대신 HM Provider를 사용한다.
|
||||
- 개별 `Tooltip delayDuration`은 Provider의 delay를 override한다. `skipDelayDuration`은 Provider에서 지정하며, skip 구간의 다음 Tooltip은 즉시 열린다. `skipDelayDuration={0}`이면 각 hover에 delay가 적용된다. keyboard focus는 delay 없이 열리고 Escape로 닫힌다.
|
||||
- `TooltipContent`: `sideOffset` 기본 4. 출력 속성: `data-slot="tooltip-*"`, Radix `data-state`·`data-side`. Arrow는 `data-slot="tooltip-arrow"`.
|
||||
|
||||
```tsx
|
||||
<TooltipProvider delayDuration={700} skipDelayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild><button>저장</button></TooltipTrigger>
|
||||
<TooltipContent>변경 사항을 저장합니다</TooltipContent>
|
||||
</Tooltip>
|
||||
{/* 같은 Provider 아래의 Tooltip은 skip-delay 상태를 공유한다. */}
|
||||
</TooltipProvider>
|
||||
```
|
||||
|
||||
## 6. Token
|
||||
|
||||
Component Mapping 위치: `packages/ui/src/theme/tooltip.css` (`--hm-tooltip-*`).
|
||||
|
||||
`background`, `text`, `radius`, `padding-x`, `padding-y`, `max-width`, `font-size`, `line-height`, `font-weight`. 다른 컴포넌트의 Component Token을 참조하지 않는다. 신규 Primitive 없음.
|
||||
|
||||
## 7. shadcn / Radix에서 유지한 항목
|
||||
|
||||
Radix 구조·ARIA(`role=tooltip`, `aria-describedby`)·Escape·hover/focus 동작, Provider 구조와 delay, Portal·positioning·collision, `data-slot`, `cn(..., className)`, animation class, `z-50`, Arrow.
|
||||
|
||||
## 8. HM에서 변경한 항목
|
||||
|
||||
- shadcn `bg-foreground text-background rounded-md px-3 py-1.5 text-xs` 및 arrow의 `bg-foreground fill-foreground` class 제거 → theme CSS.
|
||||
- 최대 폭 240px 추가(shadcn은 `w-fit`만 있어 긴 문장 한계가 없었다).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- 대표 Story `All`("Tooltip"): 기본, 긴 텍스트, 비활성 버튼(wrapper trigger), 4방향 placement 비교.
|
||||
- verification: `Open`(열린 상태).
|
||||
- interactive control은 넣지 않는다.
|
||||
|
||||
## 10. 사용자 확정 결정
|
||||
|
||||
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
|
||||
|
||||
## 11. 미확정 / 보류
|
||||
|
||||
- 수치(240px, padding, 12px 글자)는 현재 HM 규격이며 디자인 확정 시 재검토.
|
||||
- delay: shadcn 기본 0 유지. HM delay 규칙은 없다.
|
||||
- Motion 규칙은 정의되지 않아 shadcn 기본 animation 유지.
|
||||
- 비활성 control에 붙이는 방식은 소비 조합(wrapper element).
|
||||
|
||||
## 12. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/components/ui/tooltip.tsx
|
||||
packages/ui/src/theme/tooltip.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/components/tooltip.stories.tsx
|
||||
```
|
||||
|
||||
## 13. 검증 상태
|
||||
|
||||
- 2026-10-08: 공개 패키지 빌드·타입 검사 통과. Browser에서 mouse `PointerEvent`로 단독 사용, 상위/중첩 HM Provider delay, Root override, 그룹 skip-delay·만료·0 설정과 pointer leave를 검증했다. 실제 키보드로 focus·blur·Escape, controlled `open`/`onOpenChange`, `asChild`·ref, 비활성 wrapper를 확인했다. 주요 computed style은 수정 전과 동일하다. 실제 마우스 이동을 통한 hover는 별도 미검증이다.
|
||||
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
|
||||
- 실제 interaction(Browser pane): keyboard focus로 열림(`role=tooltip` 확인), Escape 닫힘.
|
||||
- computed style: bg `rgb(10,34,29)`, text white, radius 4px, padding 6px 10px, 12/18, arrow fill 같은 색.
|
||||
- 미확인: 실제 pointer hover 열림·pointer leave 닫힘, 긴 텍스트 줄바꿈 시각, 4방향 placement 시각, 비활성 wrapper trigger.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Component / Pattern Reference Index
|
||||
|
||||
AI 화면 구현 Quick Start의 Component·Pattern 검색에 사용하는 파일 인덱스다. 오전 `HMDesignTest.zip`과 현재 개발 저장소는 별도 스냅샷이다. 현재 검증된 최신 테스트 배포 기준 `HMDesignTest-Portable-Test-r3.zip`에는 아래 44개 Component와 8개 Pattern 모듈, Dashboard Status 소스/export/CSS/문서/정적 예제와 사전 빌드된 Storybook을 모두 포함한다. `HMDesignTest-Portable-Test-r2.zip`, 기존 `HMDesignTest-Portable-Test.zip` 및 오전 ZIP은 이전 스냅샷이며, 아래 오전 ZIP 수량은 역사적 비교로 표시한다. 공개 심볼의 Source of Truth는 [`packages/ui/src/index.ts`](../../packages/ui/src/index.ts)이며, Props와 상태는 각 연결 문서 및 구현을 확인한다. 파일 수, 공개 API 심볼 수, Story 그룹 수는 서로 다른 집계 단위다.
|
||||
|
||||
## Component
|
||||
|
||||
현재 `packages/ui/src/components/ui/`에는 구현 TSX 44개가 있다. 각 구현 파일은 `packages/ui/src/index.ts`의 공개 export를 통해 소비한다. 아래 Story는 실제 Story 파일의 대표 그룹이다.
|
||||
|
||||
| Component | 구현·공개 API (`@hm-design/ui`) | 사용 문서 | 대표 Story |
|
||||
|---|---|---|---|
|
||||
| Accordion | `Accordion`, `AccordionContent`, `AccordionItem`, `AccordionTrigger`<br>[구현](../../packages/ui/src/components/ui/accordion.tsx) | [문서](../components/accordion.md) | [Disclosure/Accordion](../../src/components/accordion.stories.tsx) |
|
||||
| Alert | `Alert`, `AlertDescription`, `AlertTitle`<br>[구현](../../packages/ui/src/components/ui/alert.tsx) | [문서](../components/alert.md) | [Feedback/Alert](../../src/components/alert.stories.tsx) |
|
||||
| Alert Dialog | `AlertDialog`, `AlertDialogAction`, `AlertDialogCancel`, `AlertDialogContent`, `AlertDialogDescription`, `AlertDialogFooter`, `AlertDialogHeader`, `AlertDialogOverlay`, `AlertDialogPortal`, `AlertDialogTitle`, `AlertDialogTrigger`<br>[구현](../../packages/ui/src/components/ui/alert-dialog.tsx) | [문서](../components/alert-dialog.md) | [Overlay/Alert Dialog](../../src/components/alert-dialog.stories.tsx) |
|
||||
| Aspect Ratio | `AspectRatio`<br>[구현](../../packages/ui/src/components/ui/aspect-ratio.tsx) | [문서](../components/aspect-ratio.md) | [Data & Display/Aspect Ratio](../../src/components/aspect-ratio.stories.tsx) |
|
||||
| Avatar | `Avatar`, `AvatarFallback`, `AvatarImage`<br>[구현](../../packages/ui/src/components/ui/avatar.tsx) | [문서](../components/avatar.md) | [Data & Display/Avatar](../../src/components/avatar.stories.tsx) |
|
||||
| Badge | `Badge`, `badgeVariants`<br>[구현](../../packages/ui/src/components/ui/badge.tsx) | [문서](../components/badge.md) | [Data & Display/Badge](../../src/components/badge.stories.tsx) |
|
||||
| Breadcrumb | `Breadcrumb`, `BreadcrumbEllipsis`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbList`, `BreadcrumbPage`, `BreadcrumbSeparator`<br>[구현](../../packages/ui/src/components/ui/breadcrumb.tsx) | [문서](../components/breadcrumb.md) | [Navigation/Breadcrumb](../../src/components/breadcrumb.stories.tsx) |
|
||||
| Button | `Button`, `buttonVariants`, `ButtonAppearance`<br>[구현](../../packages/ui/src/components/ui/button.tsx) | [문서](../components/button.md) | [Form/Button](../../src/components/button.stories.tsx) |
|
||||
| Calendar | `Calendar`<br>[구현](../../packages/ui/src/components/ui/calendar.tsx) | [문서](../components/calendar.md) | [Search & Date/Calendar](../../src/components/calendar.stories.tsx) |
|
||||
| Card | `Card`, `CardAction`, `CardContent`, `CardDescription`, `CardFooter`, `CardHeader`, `CardTitle`, `CardSurface`<br>[구현](../../packages/ui/src/components/ui/card.tsx) | [문서](../components/card.md) | [Data & Display/Card](../../src/components/card.stories.tsx) |
|
||||
| Carousel | `Carousel`, `CarouselContent`, `CarouselItem`, `CarouselNext`, `CarouselPrevious`, `useCarousel`, `CarouselApi`<br>[구현](../../packages/ui/src/components/ui/carousel.tsx) | [문서](../components/carousel.md) | [Data & Display/Carousel](../../src/components/carousel.stories.tsx) |
|
||||
| Checkbox | `Checkbox`<br>[구현](../../packages/ui/src/components/ui/checkbox.tsx) | [문서](../components/checkbox.md) | [Form/Checkbox](../../src/components/checkbox.stories.tsx) |
|
||||
| Collapsible | `Collapsible`, `CollapsibleContent`, `CollapsibleTrigger`<br>[구현](../../packages/ui/src/components/ui/collapsible.tsx) | [문서](../components/collapsible.md) | [Disclosure/Collapsible](../../src/components/collapsible.stories.tsx) |
|
||||
| Combobox | `Combobox`, `ComboboxOption`, `ComboboxSize`<br>[구현](../../packages/ui/src/components/ui/combobox.tsx) | [문서](../components/combobox.md) | [Search & Date/Combobox](../../src/components/combobox.stories.tsx) |
|
||||
| Command | `Command`, `CommandEmpty`, `CommandGroup`, `CommandInput`, `CommandItem`, `CommandList`, `CommandSeparator`, `CommandShortcut`<br>[구현](../../packages/ui/src/components/ui/command.tsx) | [문서](../components/command.md) | [Search & Date/Command](../../src/components/command.stories.tsx) |
|
||||
| Context Menu | `ContextMenu`, `ContextMenuCheckboxItem`, `ContextMenuContent`, `ContextMenuGroup`, `ContextMenuItem`, `ContextMenuLabel`, `ContextMenuPortal`, `ContextMenuRadioGroup`, `ContextMenuRadioItem`, `ContextMenuSeparator`, `ContextMenuShortcut`, `ContextMenuSub`, `ContextMenuSubContent`, `ContextMenuSubTrigger`, `ContextMenuTrigger`<br>[구현](../../packages/ui/src/components/ui/context-menu.tsx) | [문서](../components/context-menu.md) | [Overlay/Context Menu](../../src/components/context-menu.stories.tsx) |
|
||||
| Date Picker | `DatePicker`, `DatePickerSize`<br>[구현](../../packages/ui/src/components/ui/date-picker.tsx) | [문서](../components/date-picker.md) | [Search & Date/Date Picker](../../src/components/date-picker.stories.tsx) |
|
||||
| Dialog | `Dialog`, `DialogBody`, `DialogClose`, `DialogContent`, `DialogDescription`, `DialogFooter`, `DialogHeader`, `DialogOverlay`, `DialogPortal`, `DialogTitle`, `DialogTrigger`<br>[구현](../../packages/ui/src/components/ui/dialog.tsx) | [문서](../components/dialog.md) | [Overlay/Dialog](../../src/components/dialog.stories.tsx) |
|
||||
| Drawer | `Drawer`, `DrawerBody`, `DrawerClose`, `DrawerContent`, `DrawerDescription`, `DrawerFooter`, `DrawerHeader`, `DrawerOverlay`, `DrawerPortal`, `DrawerTitle`, `DrawerTrigger`<br>[구현](../../packages/ui/src/components/ui/drawer.tsx) | [문서](../components/drawer.md) | [Overlay/Drawer](../../src/components/drawer.stories.tsx) |
|
||||
| Dropdown Menu | `DropdownMenu`, `DropdownMenuCheckboxItem`, `DropdownMenuContent`, `DropdownMenuGroup`, `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuPortal`, `DropdownMenuRadioGroup`, `DropdownMenuRadioItem`, `DropdownMenuSeparator`, `DropdownMenuShortcut`, `DropdownMenuSub`, `DropdownMenuSubContent`, `DropdownMenuSubTrigger`, `DropdownMenuTrigger`<br>[구현](../../packages/ui/src/components/ui/dropdown-menu.tsx) | [문서](../components/dropdown-menu.md) | [Overlay/Dropdown Menu](../../src/components/dropdown-menu.stories.tsx) |
|
||||
| Hover Card | `HoverCard`, `HoverCardContent`, `HoverCardTrigger`<br>[구현](../../packages/ui/src/components/ui/hover-card.tsx) | [문서](../components/hover-card.md) | [Overlay/Hover Card](../../src/components/hover-card.stories.tsx) |
|
||||
| Input | `Input`, `InputSize`<br>[구현](../../packages/ui/src/components/ui/input.tsx) | [문서](../components/input.md) | [Form/Input](../../src/components/input.stories.tsx) |
|
||||
| Label | `Label`<br>[구현](../../packages/ui/src/components/ui/label.tsx) | [문서](../components/label.md) | [Form/Label](../../src/components/label.stories.tsx) |
|
||||
| Menubar | `Menubar`, `MenubarCheckboxItem`, `MenubarContent`, `MenubarGroup`, `MenubarItem`, `MenubarLabel`, `MenubarMenu`, `MenubarPortal`, `MenubarRadioGroup`, `MenubarRadioItem`, `MenubarSeparator`, `MenubarShortcut`, `MenubarSub`, `MenubarSubContent`, `MenubarSubTrigger`, `MenubarTrigger`<br>[구현](../../packages/ui/src/components/ui/menubar.tsx) | [문서](../components/menubar.md) | [Navigation/Menubar](../../src/components/menubar.stories.tsx) |
|
||||
| Navigation Menu | `NavigationMenu`, `NavigationMenuContent`, `NavigationMenuIndicator`, `NavigationMenuItem`, `NavigationMenuLink`, `NavigationMenuList`, `NavigationMenuTrigger`, `NavigationMenuViewport`<br>[구현](../../packages/ui/src/components/ui/navigation-menu.tsx) | [문서](../components/navigation-menu.md) | [Navigation/Navigation Menu](../../src/components/navigation-menu.stories.tsx) |
|
||||
| Pagination | `Pagination`, `PaginationContent`, `PaginationEllipsis`, `PaginationItem`, `PaginationLink`, `PaginationNext`, `PaginationPrevious`<br>[구현](../../packages/ui/src/components/ui/pagination.tsx) | [문서](../components/pagination.md) | [Navigation/Pagination](../../src/components/pagination.stories.tsx) |
|
||||
| Popover | `Popover`, `PopoverAnchor`, `PopoverClose`, `PopoverContent`, `PopoverTrigger`<br>[구현](../../packages/ui/src/components/ui/popover.tsx) | [문서](../components/popover.md) | [Overlay/Popover](../../src/components/popover.stories.tsx) |
|
||||
| Progress | `Progress`<br>[구현](../../packages/ui/src/components/ui/progress.tsx) | [문서](../components/progress.md) | [Data & Display/Progress](../../src/components/progress.stories.tsx) |
|
||||
| Radio Group | `RadioGroup`, `RadioGroupItem`<br>[구현](../../packages/ui/src/components/ui/radio-group.tsx) | [문서](../components/radio-group.md) | [Form/Radio Group](../../src/components/radio-group.stories.tsx) |
|
||||
| Resizable | `ResizableHandle`, `ResizablePanel`, `ResizablePanelGroup`<br>[구현](../../packages/ui/src/components/ui/resizable.tsx) | [문서](../components/resizable.md) | [Disclosure/Resizable](../../src/components/resizable.stories.tsx) |
|
||||
| Scroll Area | `ScrollArea`, `ScrollBar`<br>[구현](../../packages/ui/src/components/ui/scroll-area.tsx) | [문서](../components/scroll-area.md) | [Data & Display/Scroll Area](../../src/components/scroll-area.stories.tsx) |
|
||||
| Select | `Select`, `SelectContent`, `SelectGroup`, `SelectItem`, `SelectLabel`, `SelectScrollDownButton`, `SelectScrollUpButton`, `SelectSeparator`, `SelectTrigger`, `SelectValue`, `SelectSize`<br>[구현](../../packages/ui/src/components/ui/select.tsx) | [문서](../components/select.md) | [Form/Select](../../src/components/select.stories.tsx) |
|
||||
| Separator | `Separator`<br>[구현](../../packages/ui/src/components/ui/separator.tsx) | [문서](../components/separator.md) | [Data & Display/Separator](../../src/components/separator.stories.tsx) |
|
||||
| Sheet | `Sheet`, `SheetBody`, `SheetClose`, `SheetContent`, `SheetDescription`, `SheetFooter`, `SheetHeader`, `SheetOverlay`, `SheetPortal`, `SheetTitle`, `SheetTrigger`<br>[구현](../../packages/ui/src/components/ui/sheet.tsx) | [문서](../components/sheet.md) | [Overlay/Sheet](../../src/components/sheet.stories.tsx) |
|
||||
| Skeleton | `Skeleton`<br>[구현](../../packages/ui/src/components/ui/skeleton.tsx) | [문서](../components/skeleton.md) | [Data & Display/Skeleton](../../src/components/skeleton.stories.tsx) |
|
||||
| Slider | `Slider`<br>[구현](../../packages/ui/src/components/ui/slider.tsx) | [문서](../components/slider.md) | [Form/Slider](../../src/components/slider.stories.tsx) |
|
||||
| Sonner | `Toaster`, `toast`, `ExternalToast`, `ToasterProps`<br>[구현](../../packages/ui/src/components/ui/sonner.tsx) | [문서](../components/sonner.md) | [Feedback/Sonner](../../src/components/sonner.stories.tsx) |
|
||||
| Switch | `Switch`<br>[구현](../../packages/ui/src/components/ui/switch.tsx) | [문서](../components/switch.md) | [Form/Switch](../../src/components/switch.stories.tsx) |
|
||||
| Table | `Table`, `TableBody`, `TableCaption`, `TableCell`, `TableFooter`, `TableHead`, `TableHeader`, `TableRow`<br>[구현](../../packages/ui/src/components/ui/table.tsx) | [문서](../components/table.md) | [Data & Display/Table](../../src/components/table.stories.tsx) |
|
||||
| Tabs | `Tabs`, `TabsContent`, `TabsList`, `TabsTrigger`, `TabsPanelSurface`<br>[구현](../../packages/ui/src/components/ui/tabs.tsx)<br>기본 `<TabsList>` = Primary (`fill=true`, `panelSurface="content"`). Secondary: `fill panelSurface="surface"`. Legacy: `fill={false}` (panelSurface 시각 무시). 패널은 Primary `bg-background`, Secondary / Legacy `bg-card`로 조합한다 | [문서·기본값 변경 영향](../components/tabs.md) | [Navigation/Tabs](../../src/components/tabs.stories.tsx) |
|
||||
| Textarea | `Textarea`, `TextareaResize`<br>[구현](../../packages/ui/src/components/ui/textarea.tsx) | [문서](../components/textarea.md) | [Form/Textarea](../../src/components/textarea.stories.tsx) |
|
||||
| Toggle | `Toggle`, `toggleVariants`<br>[구현](../../packages/ui/src/components/ui/toggle.tsx) | [문서](../components/toggle.md) | [Form/Toggle](../../src/components/toggle.stories.tsx) |
|
||||
| Toggle Group | `ToggleGroup`, `ToggleGroupItem`<br>[구현](../../packages/ui/src/components/ui/toggle-group.tsx) | [문서](../components/toggle-group.md) | [Form/Toggle Group](../../src/components/toggle-group.stories.tsx) |
|
||||
| Tooltip | `Tooltip`, `TooltipContent`, `TooltipProvider`, `TooltipTrigger`<br>[구현](../../packages/ui/src/components/ui/tooltip.tsx) | [문서](../components/tooltip.md) | [Overlay/Tooltip](../../src/components/tooltip.stories.tsx) |
|
||||
|
||||
`packages/ui/src/index.ts`에는 위 44개 Component 모듈의 named export 222개(값과 type 포함)가 연결되어 있다. 사용 시 API·Props·Variant·주요 상태·Theme 연결은 해당 Component 문서의 API/상태/Token/Storybook 절을 따른다. 문서 45개 중 `field.md`는 별도 `field.tsx` 공개 Component를 나타내지 않는 Field 소비 가이드다.
|
||||
|
||||
## Pattern
|
||||
|
||||
현재 개발 저장소와 포터블 r3의 `packages/ui/src/patterns/`에는 8개 디렉터리와 62개 named export(값과 type 포함)가 있다. 역사적 오전 배포 기준 `HMDesignTest.zip`에는 7개 Pattern 모듈과 50개 named export가 있었다. Pattern 소스 디렉터리, Story 파일, Story 그룹 수는 별도 집계다.
|
||||
|
||||
| Pattern / 분류 | 공개 API 위치 | 사용 문서 | 대표 Reference / 계약 |
|
||||
|---|---|---|---|
|
||||
| Dashboard Layout — Layout Pattern | 운영: `DashboardLayout`, `DashboardHeader`, `DashboardMain`, `DashboardContent`, `DashboardLayoutProps`.<br>기존 호환성·검증용: `DashboardSidebar`, 선택적 `sidebar` slot. 공개 Export 존재는 운영 허용을 뜻하지 않음 | [dashboard-layout.md](../patterns/dashboard-layout.md) | [Patterns/Layout/Dashboard → Dashboard](../../src/patterns/dashboard-layout.stories.tsx) — Sidebar 없는 48px Header·업무 상태 필터·960px 최소 폭 |
|
||||
| Dashboard Status — Runtime 상태 계약 | `countDashboardFilter`, `dashboardFilterDefinitions`, `dashboardFilterIds`, `dashboardStatusAttributes`, `dashboardStatusDefinitions`, `matchesDashboardFilter`, `DashboardFilterDefinition`, `DashboardFilterId`, `DashboardStatusDefinition`, `DashboardStatusId`, `DashboardStatusRecord`, `DashboardStatusTone` | [Dashboard 문서의 상태 계약 절](../patterns/dashboard-layout.md#3-storybook) | **최신 포터블 테스트 ZIP에 export·소스·CSS·Static HTML·빌드 결과 포함. 오전 ZIP에는 없음.** [현재 contract.ts](../../packages/ui/src/patterns/dashboard-status/contract.ts), [Runtime-neutral CSS](../../src/patterns/dashboard-status-contract.css), [React 대표 Story](../../src/patterns/dashboard-layout.stories.tsx), [Static HTML 예제](../patterns/dashboard-status-static.html) |
|
||||
| Data Table — 여러 Component 조합 Pattern | `DataTable`, `DataTableColumnHeader`, `DataTablePagination`, `DataTableViewOptions`, `dataTableSelectColumn`, `useDataTable`, `DataTableColumnDef`, `DataTableInstance`, `DataTableOptions`, `DataTableProps` | [data-table.md](../patterns/data-table.md) | [Patterns/Data/Data Table → Data Table](../../src/patterns/data-table.stories.tsx) |
|
||||
| Filter Bar — 여러 Component 조합 Pattern | `FilterBar`, `FilterBarActions`, `FilterBarSearch` 및 `FilterBarSearchProps` | [filter-bar.md](../patterns/filter-bar.md) | [Patterns/Data/Filter Bar → Filter Bar](../../src/patterns/filter-bar.stories.tsx) |
|
||||
| GNB — Navigation Pattern | `GNB`, `GNBBrand`, `GNBMobileGroup`, `GNBMobileLink`, `GNBMobileMenu`, `GNBNav`, `GNBUser`, `GNBUtilities`, `GNBMobileMenuProps`, `GNBUserProps` | [gnb.md](../patterns/gnb.md) | [Patterns/Layout/GNB](../../src/patterns/gnb.stories.tsx) |
|
||||
| LNB — Navigation Pattern | `LNB`, `LNBBrand`, `LNBGroup`, `LNBItem`, `LNBNav`, `LNBProfile`, `LNBMobileHeader`, `LNBMobileBrand`, `LNBMobileMenu`, `LNBMobileLink`, `LNBMobileGroup`, `LNBMobileMenuProps`, `LNBGroupProps`, `LNBProfileProps` | [lnb.md](../patterns/lnb.md) | [Patterns/Layout/LNB](../../src/patterns/lnb.stories.tsx) |
|
||||
| Loading Screen — 화면 UI Pattern | `LoadingScreen` | [loading-screen.md](../patterns/loading-screen.md) | [Patterns/Layout/Loading Screen](../../src/patterns/loading-screen.stories.tsx) |
|
||||
| Page Header — 화면 구성 Pattern | `PageHeader`, `PageHeaderActions`, `PageHeaderContent`, `PageHeaderDescription`, `PageHeaderTitle` | [page-header.md](../patterns/page-header.md) | [Patterns/Layout/Page Header → Page Header](../../src/patterns/page-header.stories.tsx) (`patterns-layout-page-header--all`). Breadcrumb + Title + Description + 단일 Brand Action. [Dashboard 조합](../../src/patterns/dashboard-layout.stories.tsx)은 Dashboard Layout 계약을 따름 |
|
||||
|
||||
Data Table은 기존 Table·Pagination·Checkbox·Select·Dropdown Menu 등을 조합한다. 연동 예제가 필요하면 Data Table Story 안의 `With Filter Bar`를 참고하되 숨김 `verification` Story이므로 일반 화면의 단독 기본 Reference로 삼지 않는다. Filter Bar는 단독으로도 사용 가능하며 검색·Select·Combobox·Date Picker 등 조회 조건의 layout이다. Dashboard 빠른 상태 필터는 Filter Bar API가 아니라 Dashboard Status 계약을 따른다.
|
||||
|
||||
오전 소스 ZIP을 받은 AI는 Dashboard Status 행의 현재 개발 경로를 사용할 수 있다고 가정하지 않는다. 최신 포터블 ZIP에서는 해당 상대경로와 공개 API를 사용할 수 있다. ZIP에 실제 포함된 Dashboard 구현·Story와 해당 ZIP 안의 Dashboard 문서를 확인하고, Status 공개 API가 없으면 없다고 보고한다.
|
||||
|
||||
## Reference 읽는 순서
|
||||
|
||||
### 화면 생성 시 놓치기 쉬운 소비 계약
|
||||
|
||||
- Navigation 미지정: [선택 근거](navigation-single-mode.md#선택-근거)를 기록한다. GNB/LNB의 목적과 대표 화면 구조로 판단하며 일반 목록의 기본 모드를 임의 확정하지 않는다.
|
||||
- GNB ONLY: [Desktop·Mobile 구성](../patterns/gnb.md#8-desktop--mobile)을 함께 구현한다. 대표 `patterns-layout-gnb--all`의 Overview·Usage · GNB Sidebar를 확인하고 [Sidebar 예제](../patterns/gnb.md#15-gnb-only-sidebar-composition-2026-10-08)도 Sheet 하나만 사용한다.
|
||||
- LNB ONLY: 기존 독립 LNB 구조와 [LNB Mobile Header/Menu](../patterns/lnb.md#3b-lnb-mobile-header-2026-10-07)를 유지한다.
|
||||
- DASHBOARD ONLY: Sidebar를 생성하지 않는다. 대표 `patterns-layout-dashboard-layout--all`의 전용 Header(48px)·업무 상태 필터 + Content, 최소 폭 960px·내부 가로 스크롤을 따른다. `DashboardSidebar`와 `sidebar` slot은 호환성·검증용이며 과거 Sidebar verification Story는 신규 운영 화면 구현 기준이 아니다.
|
||||
- Dashboard Status: 공개 `@hm-design/ui/dashboard-status.css`는 변수만 제공한다. [Theme → Status CSS → 기존 Outline Badge mapping·React 소비 예제](../patterns/dashboard-layout.md#dashboard-업무-상태-공통-계약)를 사용한다. `issue`는 독립 flag다. [Static HTML Reference](../patterns/dashboard-status-static.html)도 같은 계약을 소비한다.
|
||||
- FilterBar: [입력 draft / 조회 applied·초기화 예제](../patterns/filter-bar.md#61-조회-버튼으로-적용하는-소비-예제)를 확인한다. 대표 `patterns-data-filter-bar--all`은 조회 callback 없는 조합 예시이며 DataTable의 `patterns-data-data-table--with-filter-bar-story`는 즉시 적용 verification이다.
|
||||
- React 소비 CSS: [Tailwind v4 검색 범위와 HM source 유지](runtime-portability.md#21-tailwind-v4-소비자-검색-범위), [Runtime Theme 연결](runtime-theme.md)을 확인한다.
|
||||
- 실행 위치별 Asset: [Noto Sans KR·폰트 URL·단일 HTML 내장](runtime-portability.md#41-noto-sans-kr-경로-실행-환경별-확인)을 확인한다.
|
||||
|
||||
### 탐색 절차
|
||||
|
||||
1. 이 인덱스에서 후보를 찾고 실제 파일·공개 API가 존재하는지 확인한다.
|
||||
2. 사용 문서에서 Props/Variant, 상태, Theme Token, 조합 규칙을 확인한다.
|
||||
3. 대표 Story에서 시각 언어와 실제 composition을 확인한다. Storybook을 실행할 수 없으면 Story 소스에서 확인한다.
|
||||
4. `verification` Story는 계약·상태 검사로, Compatibility 예제는 호환성 확인으로, Fixture는 시각 테스트 데이터로 분류한다. 이들을 Production API나 운영 화면 예제로 일반화하지 않는다.
|
||||
5. Dashboard는 GNB/LNB와 함께 사용하지 않는다. 허용되는 페이지 모드와 Responsive 규칙은 [Navigation Single Mode Policy](navigation-single-mode.md)를 따른다.
|
||||
6. Theme Source of Truth는 `themes/default.json` 및 공개 Runtime 계약이다. Component Token/상태 mapping은 연결된 Theme 문서를 확인한다.
|
||||
|
||||
정적 Storybook 빌드가 없거나 실행할 수 없는 환경에서는 확인된 Story 소스와 문서만 근거로 삼고, 검증하지 않은 시각 동작은 미확인으로 보고한다.
|
||||
@@ -0,0 +1,93 @@
|
||||
# HMDesignSystem 전체 정합성 감사 — 2026-10-06
|
||||
|
||||
> HISTORICAL_RECORD: 2026-10-06 감사 시점의 저장소 상태·실측·보류 판단을 보존한다. 이후 상태 정책과 구현을 이 기록으로 대체하지 않는다. 현재 Component/Pattern 상태는 각 [Component 문서](../components/)/[Pattern 문서](../patterns/), Runtime 적용은 [Runtime Theme](runtime-theme.md), Designer 사용은 [Theme 가이드](designer-theme-guide.md)를 따른다.
|
||||
|
||||
현재 Production 코드와 실제 Storybook을 1차 기준으로 새로 대조했다. 문서/CLAUDE.md는 비교 대상이며 과거 작업 보고서의 검증 수치를 이번 결과로 재사용하지 않았다. 수정 전 분류를 먼저 보고한 뒤 명백한 불일치만 수정했다.
|
||||
|
||||
## 범위와 현재 구조
|
||||
|
||||
- `packages/ui`: private source export 패키지. Component, 7개 Pattern, CSS Theme와 `@hm-design/ui/theme` Runtime API를 소유한다.
|
||||
- `packages/center-graphic`: UI와 서로 의존하지 않는 private source export 패키지. 통합 Storybook에서 별도 그룹으로 제공한다.
|
||||
- 루트 `src`, `.storybook`: Demo와 Component/Pattern/Theme/CENTER Story. UI 44개 그룹, Pattern 6개 그룹(Page Header는 Dashboard에서 조합), 전체 56개 Story 파일을 확인했다.
|
||||
- 대조 문서: Component 45개(Field 소비 가이드 포함), Pattern 7개, 기존 guideline 9개 및 CLAUDE.md/README.md. 현재 API·상태·Token·Story 경로를 비교하고 날짜가 있는 과거 검증 기록은 이력으로 읽었다.
|
||||
- 현재 루트에 `.git`이 없다. Git diff/커밋 검증 대신 수정 전 확보한 파일과 로컬 변경 기록을 사용했다. 과거 기준 화면 snapshot과 보고서/CSV를 재생성하지 않았다.
|
||||
|
||||
Theme 흐름은 `primitives.css` → `default.css`의 Semantic/Theme palette 및 shadcn adapter → Component Mapping → CSS selector/utility → 실제 UI다. `theme.css`가 Default를 먼저 import한다. Component Mapping은 `:root, [data-hm-theme]`에서 다시 계산된다.
|
||||
|
||||
Runtime 계약은 `contract.ts`의 73개 입력이며 `runtime.ts`가 unknown 입력 전체를 검증한다. 색상은 6자리 불투명 HEX, input/container radius는 2–12px 정수, gradient는 세트 전체 또는 미지정이다. Partial/empty, atomic invalid fallback, 교체/cleanup, host inline value/priority 복원을 지원한다. Global 아래 명시적 Scoped Theme는 독립 HM Default + 자기 override이며 미지정 값이 Global override를 이어받지 않는다. 네트워크 Loader와 운영 Publish는 아직 없다.
|
||||
|
||||
## MATCHED — 유지한 구현
|
||||
|
||||
- **Active / Focus 분리**: Checkbox/Radio/Switch/Slider/Progress/Calendar selected·range edge는 Active, 일반 focus border/ring은 Focus를 사용한다. Runtime에서 Primary `#6837a8`, Active `#007f73`, Focus `#c75b00`을 분리해 기존 자동 검증을 새로 실행했고 실제 Checkbox/Calendar 채움과 Focus shadow 검사를 통과했다. Default에서는 Active/일반 Focus 모두 `#266651`이나 서로 alias하지 않는다.
|
||||
- **Calendar / Date Picker**: selected와 range edge는 Active, hover/range middle은 Accent 45%, today는 Subtle Surface다. Global External에서 실제 Date Picker를 열면 body Portal의 selected day가 `rgb(0,127,115)`이고, 10/16 선택 후 닫히며 트리거로 Focus가 복귀했다. Wrapper Scoped popup의 제한은 아래 FOLLOW_UP이다.
|
||||
- **Badge / Pagination**: Default Badge와 Pagination Current는 Primary Green(Default `#123328`). Global External에서는 둘 다 Primary purple로 바뀌고 Active teal과 구분된다. 실제 Pagination 일반 링크 hover는 Accent yellow 45%, Current는 Primary를 유지했다.
|
||||
- **Table / Data Table**: row hover/selected는 공통 Accent 45%. Global Table에서 실제 hover/selected를 같은 `color(srgb 1 0.827451 0.427451 / 0.45)`로 확인했다. Data Table에서 행 선택 후 선택 면은 Default Accent 45%, Checkbox는 전환 종료 후 `rgb(38,102,81)`(Active), Current 페이지는 `rgb(18,51,40)`(Primary), 선택 요약은 `1개 선택됨 · 전체 47개`였다. Header 팔레트 장식은 별도 정책이다.
|
||||
- **LNB 3Depth**: 기존 LNBGroup 중첩/Collapsible 조합을 유지한다. 3Depth 행 48px, padding-start 88px, weight 500/current 700, connector 40%와 current 배경·상위 노랑을 코드 및 실제 DOM으로 대조했다. Enter/Space 접기·펼치기와 소비자가 제어하는 중첩 open 상태가 유지된다.
|
||||
- **Brand / Mobile / Scroll**: GNB+LNB는 GNBBrand 1개와 LNBBrand 0개. Standalone은 desktop LNBBrand, mobile Local bar의 서비스명으로 대체한다. 1023px에서는 GNB 56px/LNB hidden, 1024px에서는 GNB 70px/LNB 표시, 문서 폭은 viewport와 같다. 높이 700에서 LNB nav는 y=98→610, client 512/scroll 526; 실제 scrollTop=14에도 Profile y=610, GNB y=0을 유지했다. nav RTL/내용 LTR로 scrollbar를 시작 쪽에 배치한다. Standalone Mobile Sheet의 지역 3Depth 메뉴, Escape 닫힘과 `서비스 메뉴` 트리거 Focus 복귀를 확인했다.
|
||||
- **Dashboard composition**: header/sidebar/children slot과 main scroll 소유권, desktop GNB/LNB 접점 및 mobile 전환이 현재 문서와 일치한다. KPI/Filter Bar/Data Table은 공개 primitive/pattern 조합이다. GNB+LNB 조립은 이미 구현되어 있다.
|
||||
- **Dialog Wide Table**: 390px viewport에서 Dialog 358px, 문서 scrollWidth 390px, Table container client 308px/scrollWidth 1600px/overflow-x auto. 가로 스크롤은 내부 Table container가 소유한다. Header 13/20, Cell 14/20, padding 14/12를 유지하고 Escape 후 `넓은 표` 트리거 Focus 복귀를 확인했다.
|
||||
- **Filter Bar Search Input**: 기본 padding-start 36px, focus 상태 35.5px 보정으로 아이콘 뒤 텍스트 시작점을 유지한다. 실제 검색 입력/조건 수/지우기와 좁은 화면 overflow를 확인했다. Input 구현/CSS는 수정하지 않았다.
|
||||
- **Storybook**: favicon override는 실제 Manager DOM에서 icon link 1개, href `data:,`로 확인했다. 최종 빌드 index는 254개 Story/56개 그룹이며 UI 44개와 Pattern 6개 그룹은 각각 대표 Story 하나다. Theme 4개 Story는 verification으로 기본 숨김. 새 Progress NonDefaultMax도 숨김 verification이며 대표 Story에 같은 specimen이 있다.
|
||||
|
||||
## DOC_FIX — 수정 완료
|
||||
|
||||
- Component/Pattern의 오래된 Story 경로를 현재 category/title로 갱신했다. Page Header는 별도 Story 없이 `Patterns/Layout/Dashboard Layout`에서 사용한다.
|
||||
- Primitive 직접 참조 설명을 현재 Semantic/Theme palette 연결에 맞췄다. Input/Textarea/Radio의 Destructive, Table/Subtle, Slider/Progress track, Tabs/Separator/Scroll Area, GNB/LNB/Loading Screen을 대조했다.
|
||||
- Button의 로컬 HEX 잔존 설명, Select indicator 리터럴 설명, Select/Menu/Command의 full Accent 설명을 실제 palette/Accent 45% 연결로 고쳤다. Toggle on/full Accent와 Select selected/45%의 차이를 명시했다.
|
||||
- Runtime API 미구현/Preview 전용, root 아래 Production 경로, toolbar 전환, 초기 5개 Component 상태를 현재 workspaces/API/Story 구조로 정정했다. Library build/registry/Loader/Publish 목표 절차와 현재 구현을 구분했다.
|
||||
- CLAUDE.md의 Portal container API 설명을 실제 HM popup API 제한으로 바로잡았다. general Focus 설명에 기존 Slider/GNB/LNB 예외를 명시했다.
|
||||
- Data Table의 With Filter Bar를 두 번째 대표 Story라고 한 설명을 숨김 verification으로 정정했다. Filter Bar 문서의 연동 대상도 Data Table로 유지했다.
|
||||
- 과거 검증 절의 당시 색상·측정·실패/미확인 기록은 현재 결과로 덮지 않았다. Progress의 이전 max 제한에는 이번 수정 기록을 추가했다.
|
||||
|
||||
## CODE_FIX — 수정 완료
|
||||
|
||||
**Progress의 max 비율 계산 한 곳**을 수정했다(`packages/ui/src/components/ui/progress.tsx`). 이전에는 value를 백분율로 간주하므로 max=200/value=50에서 ARIA는 50/200인데 채움은 50%였다. 이제 유효 value/max 비율로 25% 채우며 Radix 기본 max/유효 범위 및 값 없음 상태를 따른다. 디자인/CSS/공개 API는 유지했다.
|
||||
|
||||
`src/components/progress.stories.tsx`에 max=200의 0/50/200/값 없음 specimen과 ARIA/Indicator 회귀 검증을 추가했다. 실제 DOM은 각각 transform -100%/-75%/0%/-100%, ARIA 0/50/200/valuenow 없음으로 일치했다. 기존 max=100 대표 상태와 interactive specimen도 유지했다.
|
||||
|
||||
Production 수정은 위 파일 1개, Story 수정은 Progress 파일 1개다. 기존 문서는 58개 파일에 경로/Token/현재 상태를 정정했으며 Component·Pattern 구현을 재작성하지 않았다.
|
||||
|
||||
## STALE — 현재 설명에서 정리
|
||||
|
||||
- 초기 5개 Component/Medium Preview만 구현된 상태.
|
||||
- Storybook toolbar Default/Draft 전환과 Runtime API 공개 여부를 미래 결정으로 적은 설명.
|
||||
- Dashboard가 다음 작업이거나 GNB+LNB 조립 자체가 아직 없는 상태.
|
||||
- Alert Dialog 미구현, Dialog 이후 구조 안정화 대기, Select의 근거 없는 동시 작업 추측.
|
||||
|
||||
날짜가 있는 이전 작업 보고서, CSV, ui-viz 비교표는 폐기하지 않았다. 현재 구현 증거로 재사용하지 않고 당시 이력으로 구분했다.
|
||||
|
||||
## AMBIGUOUS — 이번에 변경하지 않음
|
||||
|
||||
- Slider Primary 20% halo, desktop GNB/LNB inverse outline(current는 Primary)까지 일반 Focus Semantic으로 통합할지. 현재 component 문서와 Runtime 문서는 예외를 명시하지만 일반 Focus 표현을 한 가지로 확대하면 디자인 결정이 필요하다. 일반 Focus의 Light 2 자체도 가이드에 시험 적용값으로 남아 있다.
|
||||
- GNB+LNB gradient 이음새/콘텐츠 모서리의 최종 시각 승인. 조립과 Brand ownership은 구현/확정되었고, 이 부분은 Dashboard/GNB 문서에서 확정 대기로 남아 있다.
|
||||
- 선택 경로 전체 connector 강조와 모바일 LNB의 14px 지역 메뉴/상위 current 표현 등 기존 미확정 Layout 항목. 현재 문서에 없는 최종 정책을 추정해 바꾸지 않았다.
|
||||
|
||||
## FOLLOW_UP — 별도 작업
|
||||
|
||||
- HM SelectContent/PopoverContent/DatePicker의 wrapper Scoped Theme 전달 API. 현재 body Portal은 Global Theme를 따르며 자동 Scoped 전달을 제공하지 않는다.
|
||||
- Date Picker/Combobox를 undefined로 비우는 controlled reset 제약(현재 문서대로 key remount 사용). 새 API/상태 모델 작업으로 분리한다.
|
||||
- FilterBarSearch에 disabled/readOnly를 전달할 때 별도 지우기 버튼의 활성 정책. 현재 input props는 Input에 전달되고 clear callback은 독립적으로 실행된다. 이번에 새 상태 계약을 만들지 않았다.
|
||||
- Center Graphic/Facility의 7개 노출 Story는 서로 다른 시설물 export를 묶은 그룹이다. UI와 같은 단일 집계 대표 Story로 정리할지 검수 구조를 별도로 결정한다. 기존 CENTER 구현은 이번에 변경하지 않았다.
|
||||
- 문서 예시를 자동 탐색해 생기는 CSS warning, Storybook chunk size와 docgen callback 성능 안내. 빌드 실패는 아니며 source 탐색/번들 최적화는 별도 범위다.
|
||||
- Library 산출물/Registry, 네트워크 Loader, Theme Publish 운영은 아직 미구현이다.
|
||||
|
||||
## 최종 통합 검증
|
||||
|
||||
- `npm run typecheck`: PASS / exit 0. 루트와 두 workspace 포함.
|
||||
- `npm run build`: PASS / exit 0. CSS 예시 warning 1개 유지.
|
||||
- `npm run build-storybook`: PASS / exit 0. CSS 예시 warning, 500kB chunk 안내 및 PLUGIN_TIMINGS 성능 안내가 출력되었다.
|
||||
- 현재 실제 Storybook Runtime 자동 검증: **PASS — 136 checks; 63 rendered parts**. Default parity, 73개 공개 입력, 의미 독립성, partial/empty/invalid, switching/cleanup, Global/Scoped, geometry를 새로 실행했다.
|
||||
- CSS 변수 의존성/현재 문서 Token·Story 경로/73개 계약표를 정적으로 대조했다. Loading spinner index는 TSX inline style로 선언되며 LNB collapsed-width는 문서에서 명시적으로 없는 변수다.
|
||||
- 최종 빌드 index에서 대표/verification 태그와 신규 NonDefaultMax 포함을 확인했다. 전체 254개 Story의 모든 play/interaction을 실행했다는 의미는 아니다. 실제 동작 검수는 위 핵심 대상에 집중했으며 다른 브라우저/실기기는 미검증이다.
|
||||
|
||||
로그/변경 기록: `_local/consistency-typecheck.log`, `consistency-build.log`, `consistency-storybook-build.log`, `consistency-audit-data.json`, `consistency-audit-diff.txt`. 로컬 비교용 파일은 `_local/consistency-audit-before/`에 확보 시점 기준으로 보관했다. Page Header 경로 및 Loading Screen Token의 단일 문구 수동 수정은 위 수정 범위에 별도로 기록했다.
|
||||
|
||||
화면 증거: `_local/consistency-progress.png`, `_local/consistency-progress-final.png`(실제 Manager에서 대표 Story 하나와 max=200), `_local/consistency-dialog-mobile.png`. Runtime 검증의 PASS 수치는 현재 브라우저의 result output에서 직접 읽었다.
|
||||
|
||||
## 작업 시간 (KST)
|
||||
|
||||
- Started At: 2026-10-06 15:03:01
|
||||
- Estimated Duration: 40–60분(분석 20–30 / 최소 수정 10–15 / 통합 검증 10–15)
|
||||
- Completed At: 2026-10-06 15:25:46
|
||||
- Actual Duration: 22분 45초
|
||||
- Variance: 예상보다 17분 15초–37분 15초 단축
|
||||
@@ -0,0 +1,52 @@
|
||||
# 디자인팀 컬러 Theme 사용 방법
|
||||
|
||||
Storybook에서 **Theme → Designer Playground → Editor**를 엽니다.
|
||||
로컬 기본 주소: http://localhost:6007/?path=/story/theme-designer-playground--editor
|
||||
|
||||
1. 처음 열면 `themes/default.json` 기반 HM Default 컬러가 표시됩니다. **Load Default JSON**, **Load Service Example JSON**으로 repository JSON을 URL에서 불러오거나 **Theme JSON URL**에 CDN URL을 입력한 뒤 **Load URL**을 누릅니다. 같은 탭에 저장값이 있다면 마지막 유효 Theme를 다시 표시합니다. **Reset to HM Default**로 기본값을 다시 불러옵니다.
|
||||
2. Primary / Active / Focus / Accent / Destructive 등 역할의 컬러 피커 또는 HEX 입력을 수정합니다. 불투명 6자리 HEX(`#123abc`)를 사용합니다. 유효한 값은 입력 즉시 적용되며 별도의 Apply 버튼이 필요 없습니다. 미완성 입력은 마지막 유효 색을 유지합니다.
|
||||
3. 오른쪽 Live Preview에서 여러 컴포넌트를 함께 확인합니다. 화면이 좁으면 편집기 아래에 Preview가 이어집니다. Focus 확인은 Tab 키로 Checkbox/Input 등에 이동합니다. Select, Date Picker, Combobox 및 메뉴를 열면 Global Theme가 팝업에도 적용됩니다.
|
||||
4. 다른 Component / Pattern Story로 이동해도 같은 Theme를 확인할 수 있습니다. 값 변경 자체에는 reload, 파일 저장, Vite HMR, build, Storybook 재시작이 필요 없습니다. 기존 Runtime Contract 검증 Story는 독립 검증을 위해 편집 Theme를 적용하지 않습니다.
|
||||
5. **Reset to HM Default**는 현재 override를 해제하고 기본값을 즉시 복원합니다. 컴포넌트 API와 레이아웃은 바꾸지 않습니다.
|
||||
6. **Show JSON**으로 현재 데이터를 표시하고 **Copy JSON**으로 복사하거나 **Export JSON**으로 `hm-color-theme.json`을 내려받습니다. 클립보드 접근이 막히면 JSON 입력란에서 직접 복사합니다. 수정한 키만 저장하므로 나머지는 HM Default입니다.
|
||||
7. JSON을 입력란에 붙여 넣고 **Import JSON**을 누르거나 파일 입력으로 JSON을 선택하면 바로 적용됩니다. 이전 Theme와 병합하지 않고 새 Theme로 교체합니다. 잘못된 JSON/키/값/불완전한 gradient 그룹은 거부하고 마지막 유효 Theme를 유지합니다. 이 편집기는 컬러 전용이므로 radius 입력을 거부합니다.
|
||||
8. Export한 JSON을 `themes/<서비스명>.json`에 저장하고 검증·Git diff/review·commit/push 후 Theme artifact를 CDN에 별도 배포합니다. 개발팀은 설정한 Theme URL을 `loadRuntimeTheme`에 전달하거나 API/로컬 JSON을 `applyRuntimeTheme`에 전달하면 됩니다. 이미 지원하는 Theme JSON의 color-only 변경에는 UI package 재빌드·version up·재배포가 필요 없습니다. Component CSS 규칙이나 Runtime 계약 변경은 package 릴리스 경로를 따릅니다. 자세한 배포 절차: [Theme delivery](theme-delivery.md).
|
||||
|
||||
```ts
|
||||
import "@hm-design/ui/theme.css"
|
||||
import { applyRuntimeTheme, resetRuntimeTheme, isRuntimeTheme } from "@hm-design/ui/theme"
|
||||
|
||||
// 설정/API/사용자 입력에서 받아온 공식 v1 Theme JSON.
|
||||
const theme = { schemaVersion: 1, tokens: { "color.state.active": "#007f73" } }
|
||||
if (isRuntimeTheme(theme)) applyRuntimeTheme(document.documentElement, theme)
|
||||
// 같은 root에 새로운 Theme를 전달하면 즉시 교체된다.
|
||||
// isRuntimeTheme은 DOM을 건드리지 않는 사전 검증이다.
|
||||
resetRuntimeTheme(document.documentElement)
|
||||
```
|
||||
|
||||
## 컬러 역할과 적용 범위
|
||||
|
||||
- Primary는 Badge default, Pagination current, Button outline 및 내비게이션 강조 글자에 적용합니다. Brand Button/GNB의 gradient와 일부 palette 장식은 별도 기존 계약을 유지합니다. Primary 하나로 모든 gradient stop을 재설계하지 않습니다.
|
||||
- Active는 Checkbox, Radio, Switch, Slider, Progress 및 Calendar/Date Picker의 선택 면을 바꿉니다. Focus와 독립적입니다. On Active는 그 위 아이콘/텍스트입니다.
|
||||
- Focus는 일반 입력 테두리와 Checkbox/Calendar 등의 ring, Slider Keyboard Focus의 Focus Semantic 20% halo입니다. Slider Hover/Drag halo는 Primary 20%이며 GNB/LNB는 contextual navigation focus 예외를 유지합니다.
|
||||
- Accent 45%는 Select/메뉴 Item/Calendar range middle·hover/Pagination hover/Table hover·selected의 Soft Highlight 면입니다. Combobox는 Command를 조합하므로 같은 강조 역할을 사용합니다. Toggle/Toggle Group Selected, Menubar 상위 Highlighted/Open 및 Navigation Menu Trigger/Open은 Accent 100% + On Accent를 사용합니다. Navigation Current는 Primary + contextual surface로 Control Active와 구분합니다.
|
||||
- Destructive는 오류, invalid 및 destructive 표현입니다. Primary/Active/Focus와 독립적입니다.
|
||||
- 각 입력란은 Default JSON + partial override의 실제 계산된 역할 값을 표시합니다. Export는 표시값 전체가 아니라 명시적으로 수정·Import한 데이터를 저장합니다. Primary와 On Accent는 sibling Semantic이며 Default JSON과 CSS-only package fallback 모두 독립값을 유지합니다. 기본값이 같아도 Primary override가 On Accent를 함께 바꾸지는 않습니다.
|
||||
|
||||
## 저장과 알려진 제한
|
||||
|
||||
- 서버/DB/Publish 기능은 없습니다. 같은 브라우저 탭의 Storybook preview origin에 `sessionStorage`로 보관하며, 세션 저장을 사용할 수 없는 환경에서도 현재 화면의 편집/Export는 동작합니다. 장기 보관은 JSON Export를 사용하세요.
|
||||
- Global Theme는 `document.documentElement`에 적용하며 body Portal도 같은 Theme를 사용합니다. wrapper 밖으로 Portal된 popup은 Scoped Theme를 자동 전달받지 않습니다. 이 제한은 기존 FOLLOW_UP입니다.
|
||||
- 전체 Runtime Contract는 73개 입력을 유지합니다. 배포하는 Default JSON은 71개 컬러이며 radius/geometry는 package에서 관리합니다. 편집 화면은 상위 컬러 역할 17개만 노출하며 Component Token, radius, palette/gradient 개별 입력 UI를 만들지 않습니다. 기존 컬러 계약의 palette/gradient는 유효 JSON으로 Import할 수 있으며 gradient는 세트 전체를 제공해야 합니다.
|
||||
- 명시적 `[data-hm-theme]` scope는 독립 HM preset이므로 Global Theme를 이어받지 않습니다. Production에서 같은 root는 한 owner가 Apply/Reset을 관리합니다.
|
||||
- Storybook 편집기 설치는 이번 코드 변경을 적용하는 최초 빌드가 필요합니다. 설치 후 컬러 값을 바꾸는 운영에는 재빌드가 필요 없습니다.
|
||||
|
||||
## 개발팀의 Default 유지보수
|
||||
|
||||
Default 컬러의 사람이 관리하는 원본은 `themes/default.json`입니다. `contract.ts`는 공개 key/property/type을 정의하고 Default literal을 중복 소유하지 않습니다. `scripts/theme-templates/`는 컬러 literal 없이 alias/geometry만 유지합니다.
|
||||
|
||||
Storybook의 `src/theme/designer-preview.tsx`는 `themes/default.json`을 직접 import합니다. 디자인팀이 `default.json`을 수정하면 개발 서버의 Storybook HMR로 기본값이 갱신됩니다. Preview는 이 Default와 partial override를 병합하며, Reset은 override를 비워 현재 Default를 확인합니다.
|
||||
|
||||
Theme-only 배포는 `npm run build:themes`로 독립 JSON artifact만 생성합니다. UI package의 fallback snapshot까지 새 버전에서 갱신할 때 `npm run generate:theme-defaults`로 `primitives.css`, `default.css`, `default-values.generated.ts`를 생성합니다. `typecheck`, `build`, `build-storybook`의 pre hook은 생성 결과의 일치를 검사합니다. 이미 배포한 UI package를 그대로 두고 CDN JSON만 바꿀 수 있습니다.
|
||||
|
||||
공식 전체 입력표: [Runtime Theme Contract](runtime-theme-contract.md). Runtime 적용·제한: [Runtime Theme](runtime-theme.md).
|
||||
@@ -0,0 +1,150 @@
|
||||
# 디자인팀 External Runtime Color Theme 운영 결과
|
||||
|
||||
> HISTORICAL_RECORD: 작성·검증 시점(2026-10-06)의 기록이다. 아래 baseline·실측·당시 Slider/On Accent 설명을 보존한다. 현재 Slider Focus, 독립 On Accent fallback, Default JSON 직접 import/Preview/Reset 흐름은 [디자인팀 Theme 가이드](designer-theme-guide.md), Runtime 계약은 [Runtime Theme](runtime-theme.md), 배포는 [Theme delivery](theme-delivery.md)를 따른다.
|
||||
|
||||
기준: `main` / `d1015309ab43fba69c8959a3dda660358e0e8fa1`. 시작 시 HEAD 일치, working tree clean을 실제 확인했다. 이전 감사 문서의 수치는 이번 증거로 재사용하지 않았다.
|
||||
|
||||
## 1. 기존 Runtime Theme 구조
|
||||
|
||||
`packages/ui/src/theme/runtime.ts`의 framework-independent Apply/Reset은 이미 `@hm-design/ui/theme`에서 공개되어 있었다. `src/theme/preview.ts`는 이 API를 재수출하는 Storybook 호환 adapter였다. Production API와 Preview를 동일시하지 않고 실제 package export를 확인했다. CSS의 Primitive → Default/Semantic → Component Mapping을 유지한다.
|
||||
|
||||
## 2. 기존에 가능했던 디자인팀 작업
|
||||
|
||||
공식 v1 JSON과 CSS variable override, Default preset 데이터, 부분/빈 Theme, invalid fallback, Global/Scoped, 원래 inline 값/priority cleanup이 가능했다. Comparison/GlobalExternal/Verification/Dashboard는 개발·검증용 숨김 Story였다. 디자인팀의 직접 편집·전달 workflow는 없었다.
|
||||
|
||||
## 3. 부족했던 것과 구현
|
||||
|
||||
JSON을 컬러 원본으로 연결하고 URL fetch/검증·Apply 분리, Default/partial resolve, CDN 실패 fallback, Theme JSON 독립 산출을 추가했다. 기존 편집기를 JSON URL/파일 Preview 소비자로 재사용하고 Default 표시/같은 세션의 여러 Story 확인/즉시 Reset/Copy·Export·Import를 제공한다. DOM을 변경하지 않는 `isRuntimeTheme`은 기존 validator에서 분리했다. 새 Theme engine/React Production Provider/서버/DB/Portal API는 만들지 않았다.
|
||||
|
||||
## 4. Default HM Theme Source of Truth
|
||||
|
||||
운영 컬러의 원본은 **`themes/default.json` / 서비스별 `themes/*.json`**이다. Default는 기존 계약의 71개 컬러 값을 포함하며 Component Token은 없다. `contract.ts`의 반복된 Default literal을 제거했다. `scripts/theme-defaults.mjs`가 JSON + 컬러 literal 없는 alias/geometry template에서 package `primitives.css`, `default.css`, `default-values.generated.ts`를 생성한다. `defaultRuntimeTheme`는 이 데이터로 구성된다. 2개 radius/geometry는 package template에 유지한다. 현재 생성 CSS의 실제 선언값/alias는 baseline과 동일하며 diff는 생성 설명 주석뿐이다.
|
||||
|
||||
`npm run build:themes`는 Theme-only JSON 산출이며 UI/Graphic build·version up을 수행하지 않는다. 새 UI package fallback snapshot을 갱신할 때만 `npm run generate:theme-defaults`; `typecheck/build/build-storybook` pre hook은 stale 생성 데이터를 거부한다. 이미 배포한 package fallback은 해당 버전의 Default snapshot이고 최신 Default JSON은 CDN으로 독립 적용할 수 있다. 자세한 저장/배포/캐시 정책은 [Theme delivery](theme-delivery.md)에 정리했다.
|
||||
|
||||
## 5. Public Runtime Theme Contract
|
||||
|
||||
전체 공식 계약은 기존 73개 그대로이며 [전체 표](runtime-theme-contract.md)를 유지한다. 편집기는 아래 17개 상위 역할만 노출한다. Component Token은 노출하지 않는다. 각 입력에는 실제 계산된 컬러를 표시한다.
|
||||
|
||||
| Theme Role | Token | Default | Runtime Editable | 영향 |
|
||||
|---|---|---|---|---|
|
||||
| Primary / Brand | `color.action.primary` | `#123328` | Yes | Badge, Pagination current, Button outline, navigation current ink |
|
||||
| Active / Selected | `color.state.active` | `#266651` | Yes | Checkbox, Radio, Switch, Slider, Progress, Calendar/Date Picker selected |
|
||||
| On Active | `color.on.state.active` | `#ffffff` | Yes | 선택 아이콘/텍스트 |
|
||||
| Focus | `color.stroke.input.focus` | `#266651` | Yes | 일반 입력/Checkbox/Calendar ring |
|
||||
| Accent | `color.accent` | `#bddfd4` | Yes | 45% soft highlight |
|
||||
| On Accent | `color.on.accent` | `#123328` | Yes | 강조 면 글자; JSON Default에서는 독립값 |
|
||||
| Destructive | `color.action.destructive` | `#d32f2f` | Yes | 오류, invalid, destructive |
|
||||
| Surface | `color.bg.surface` | `#ffffff` | Yes | Input, Card, Popup |
|
||||
| Content Surface | `color.bg.content` | `#f5f0e5` | Yes | 콘텐츠 면 |
|
||||
| Subtle Surface | `color.bg.subtle` | `#f4f1ea` | Yes | 은은한 면, Calendar today |
|
||||
| Text Primary | `color.fg.primary` | `#0a221d` | Yes | 주요 텍스트 |
|
||||
| Text Secondary | `color.fg.secondary` | `#6e6759` | Yes | 보조 텍스트 |
|
||||
| Placeholder | `color.fg.placeholder` | `#999999` | Yes | 입력 안내 |
|
||||
| Inverse Text | `color.fg.inverse` | `#ffffff` | Yes | 반전 텍스트 |
|
||||
| Default Border | `color.stroke.default` | `#9a9284` | Yes | 컨트롤/Badge/Scroll Area |
|
||||
| Input Border | `color.stroke.input.default` | `#dedbd2` | Yes | 입력/패널 |
|
||||
| Cancel | `color.action.cancel` | `#6e6759` | Yes | 취소 동작 |
|
||||
|
||||
## 6. Live Theme Apply 경로
|
||||
|
||||
Designer JSON URL/file/HEX → 공식 v1 `RuntimeTheme` → Storybook 세션 store → `resolveRuntimeTheme(theme, defaultJson)` → Global decorator → Production `applyRuntimeTheme(document.documentElement, theme)` → inline CSS Custom Properties → 기존 Semantic/Derived/Component Mapping → Components/body Portal.
|
||||
|
||||
Production은 서비스 설정 URL → `fetchRuntimeTheme` → 기존 validation → 선택적 Default/partial resolve → Apply이며, 편의 함수 `loadRuntimeTheme`도 같은 경로를 사용한다. Fetch와 Apply는 각각 따로 사용할 수 있다. 회사 CDN URL은 하드코딩하지 않는다.
|
||||
|
||||
Storybook 전용 UI/저장과 Production 계약/주입 함수를 분리한다. Component는 Theme 출처를 알지 못한다.
|
||||
|
||||
## 7. Default Theme Load
|
||||
|
||||
처음에는 `themes/default.json` + 빈 override를 같은 Runtime 경로로 적용하고 실제 계산값으로 입력을 채운다. 같은 탭 저장값은 Default JSON 위 partial로 사용한다. Reset으로 JSON Default를 다시 적용한다. Load Default JSON 버튼은 `/themes/default.json`을 실제 fetch/검증하여 적용한다. CSS minifier의 HEX3 결과는 편집기의 공식 HEX6 표현으로 정규화한다.
|
||||
|
||||
## 8. Live Update
|
||||
|
||||
유효한 컬러 입력을 받은 순간 Global CSS 변수가 바뀐다. 값 변경에는 페이지 reload, 파일 저장, Vite HMR, build, Storybook 재시작/재빌드가 필요 없다. 같은 세션의 다른 Story에서도 적용한다. 기존 `Theme/Runtime Contract` 검증군은 고정 baseline과 분리하기 위해 editor override를 사용하지 않는다.
|
||||
|
||||
## 9. Reset
|
||||
|
||||
`Reset to HM Default`는 session override를 비우고 JSON Default를 즉시 복원한다. invalid HEX draft와 pending URL 요청도 초기화한다. Production `resetRuntimeTheme`은 원래 host inline 값/priority와 package CSS fallback을 복원하며 pending load를 무효화한다.
|
||||
|
||||
## 10. Partial Override
|
||||
|
||||
Export/Import는 입력 문서/명시한 key만 포함하며 부분 JSON을 지원한다. 새 Theme로 교체하여 이전 key를 남기지 않는다. Storybook과 supplied Default를 사용하는 Production resolve는 JSON Default + partial을 합친다. 별도 Default를 전달하지 않는 URL Loader는 package CSS fallback + partial을 사용한다. Primary/Active 두 역할만 전달할 수 있으며 전체 73개 값은 필요하지 않다.
|
||||
|
||||
## 11. Export / Import
|
||||
|
||||
Show JSON, Copy JSON(클립보드 제한 시 직접 복사), JSON 파일 다운로드, JSON 텍스트/파일 Import를 제공한다. 모두 동일한 `{schemaVersion:1,tokens:{...}}` 계약을 사용한다. 유효하지 않은 Import는 현재 Theme를 유지한다. 컬러 작업 범위의 layout 회귀를 막기 위해 편집기에서는 radius를 거부한다. Production의 기존 radius 계약은 유지한다. 서버/DB 없음; 임시 보관은 sessionStorage, 전달/장기 보관은 Export다.
|
||||
|
||||
## 12. Production 적용 방법
|
||||
|
||||
`import "@hm-design/ui/theme.css"` 후 `loadRuntimeTheme(document.documentElement, serviceConfig.themeUrl)`을 호출한다. 별도로 받은 Default JSON + 서비스 partial은 `resolveRuntimeTheme(serviceTheme, defaults)` 후 Apply한다. JSON fetch/validation은 `fetchRuntimeTheme`, 데이터 Apply/Reset은 기존 API로 분리한다. HTTP/JSON/validation/abort 실패 시 package HM fallback이 유지된다. 오래된 요청 응답은 새로운 Apply/Load/Reset을 덮지 않는다. 상세 실제 API 예제와 갱신/캐시 정책은 [Theme delivery](theme-delivery.md)에 기록했다. CDN 인프라/credentials/publish는 이번 범위에서 만들지 않았다.
|
||||
|
||||
## 13. Active / Focus / Primary / Accent 독립성
|
||||
|
||||
Verification 컬러는 Primary `#6837a8`, Active `#007f73`, Focus `#c75b00`, Accent `#ffd36d`, Destructive `#d41466`로 서로 다르다. 다섯 역할을 하나씩 입력하며 다른 네 역할이 유지되는지 확인한다. 기존 일반 Focus 검증은 실제 Checkbox/Calendar shadow의 색을 검사한다. 기존 Slider Primary 20% halo와 GNB/LNB inverse outline 예외는 보존한다.
|
||||
|
||||
## 14. Component 검증
|
||||
|
||||
공유 Specimens는 Button/Input/Checkbox/Radio/Switch/Slider/Progress/Select/Calendar single·range/DatePicker/Pagination/Badge/Table/GNB/LNB/Command를 기존 API 그대로 렌더링한다. Designer extras에 실제 DataTable, Combobox, DropdownMenu, ContextMenu, Menubar를 추가했다. 기존 Specimens 내용과 고정 `verification-baseline.json`은 변경하지 않고 파일로 추출했다.
|
||||
|
||||
새 LiveVerification은 실제 input/Import/Reset 버튼으로 Default 17개 표시, 핵심 역할 독립성, 실제 주요 컴포넌트 색, Accent 45%의 10개 Mapping, Export→Import, invalid 보존, partial 교체, 17개 입력, geometry 및 동일 document를 검사한다. Loader 검증은 실제 정적 `/themes/*.json` HTTP 요청, 71개 Default와 생성 데이터 일치, partial/merge/reset/invalid/HTTP 실패/abort와 delayed response race simulation을 검사한다. 기존 Runtime Verification은 모든 73개 입력/Default visual parity/실제 Focus/cleanup/Global·Scoped 등을 다시 검사한다. 정적 빌드 HEX3 정규화만 검증 코드에 추가했다. baseline snapshot은 변경하지 않았다.
|
||||
|
||||
## 15. Scoped Portal 제한
|
||||
|
||||
Global document Theme는 body Portal에도 적용한다. 명시적 scope는 독립 HM Default + 자기 override이며 wrapper 밖으로 Portal된 UI는 Global을 사용한다. scope/Portal 자동 전달은 기존 FOLLOW_UP으로 유지한다.
|
||||
|
||||
## 16. 수정 파일
|
||||
|
||||
- Production: `packages/ui/src/theme/contract.ts`, `default-values.generated.ts`, `runtime.ts`, `requests.ts`, `loader.ts`, `index.ts`, 생성 `default.css`/`primitives.css`(현재 선언은 baseline과 동일).
|
||||
- 원본/산출: `themes/default.json`, `service-example.json`; `scripts/theme-contract.mjs`, `theme-defaults.mjs`, `theme-artifacts.mjs`, `theme-templates/`; 루트 `package.json`/`.gitignore`.
|
||||
- Storybook: `.storybook/main.ts`/`preview.tsx`, `src/theme/designer-preview.tsx`, `designer-theme.stories.tsx`, `designer-theme.css`, `designer-extras.tsx`, `specimens.tsx`, `runtime-loader.stories.tsx`, 기존 `runtime-theme.stories.tsx`(Specimens 추출/검증 색 정규화).
|
||||
- 문서: 이 보고서/사용법/Theme delivery, README/CLAUDE, 기존 contract/report/테마 토큰 가이드의 후속 섹션 및 runtime/개발 적용/운영 배포/프로젝트 지침의 현재 상태.
|
||||
|
||||
## 17. Git diff 요약
|
||||
|
||||
baseline에서 Theme 운영 관련 변경만 관리했다. Component/Pattern 구현, Component API, Primitive 값/Default CSS 선언, viewport/breakpoint 및 layout 토큰을 변경하지 않았다. 새 editor만 자신의 responsive layout CSS를 갖는다. Git commit/push는 수행하지 않았다. 추적되지 않은 신규 파일도 검토 범위에 포함한다.
|
||||
|
||||
## 18. 문서 및 감사 이력
|
||||
|
||||
[디자인팀 사용법](designer-theme-guide.md)과 [Theme delivery](theme-delivery.md)에 JSON 원본/Preview/Runtime/CDN 배포/캐시/롤백/개발팀 workflow/제한을 정리했다. 기존 Runtime 계약/보고서/테마 토큰 가이드에는 추가 결정을 기록했다. 기존 hardcode audit CSV와 consistency audit는 당시 이력이며 Component 색상 Mapping을 바꾸지 않아 재작성하지 않았다.
|
||||
|
||||
## 19. Build / 최종 브라우저 검증
|
||||
|
||||
- `npm run typecheck`: PASS / exit 0, 루트와 UI/CENTER workspaces.
|
||||
- `npm run build`: PASS / exit 0.
|
||||
- `npm run build-storybook`: PASS / exit 0.
|
||||
- `npm run build:themes`: PASS, 독립 JSON 2개를 `theme-dist/themes/`에 산출.
|
||||
- **1280 / 768 / 390px 각각**: 기존 Runtime **136 checks / 63 rendered parts**, Designer workflow **96 checks**, URL Loader **119 checks** 모두 PASS. 정적 빌드를 사용하여 Vite HMR이 없는 환경에서 검증했다. 해상도 간 이동 시 문서를 새로 열었고, 각 검증 안의 Theme 변경은 동일 document에서 reload 0으로 수행했다.
|
||||
- Default baseline 색상/geometry parity, 전체 73개 기존 계약, 실제 Checkbox/Calendar Focus, 독립 핵심 역할, partial/reset/invalid/cleanup, JSON Export→Import, HTTP Default/External load, Abort와 응답 race를 확인했다.
|
||||
- 실제 Editor의 Load Default JSON / Load Service Example JSON 버튼을 클릭해 즉시 반영을 확인했다. 기존 Badge Story로 이동한 뒤에도 Primary `rgb(104,55,168)`과 Destructive `rgb(212,20,102)`가 세션에서 유지되었다.
|
||||
- 실제 body Select Portal의 selected 면은 Accent yellow 45% `color(srgb 1 0.827451 0.427451 / 0.45)`, Date Picker selected는 Active `rgb(0,127,115)`였다. Combobox/Dropdown/Context/MenuBar의 실제 highlight와 Table/DataTable selected도 같은 Accent 45%였다.
|
||||
- 별도 임시 fixture의 **8개 generation/artifact checks** PASS: stale fallback 검사, JSON 역할→CSS 생성, 나머지 geometry/Default 유지, Theme-only 산출 시 UI 파일 불변, 최신 운영 JSON 포함, 삭제된 source의 stale artifact 제거, 실제 workspace 원본 미변경.
|
||||
- JSON 텍스트 Export→Import는 검증했다. 파일 Export는 DOM download 링크와 Blob 수명 처리를 구현했으나 내장 브라우저가 download 완료 이벤트를 반환하지 않아 파일 저장 완료는 확인하지 못했다. UI는 다운로드 요청으로 표시하고 Show JSON 직접 복사 경로를 제공한다. 클립보드 권한도 환경에 따라 제한될 수 있다.
|
||||
- 실제 회사 CDN/CORS/캐시·registry publish는 미검증이다. HTTP 검증은 localhost의 정적 Theme JSON으로 CDN delivery를 모사했다. 브라우저/실기기 전체 호환성을 검증했다는 의미는 아니다.
|
||||
|
||||
기존 CSS 예시 최적화 warning 1개, Storybook chunk 크기/성능 안내는 비차단으로 유지한다. 처음 sandbox에서 esbuild spawn EPERM이 발생한 빌드는 승인된 실행 환경에서 다시 수행하여 통과했다. 검증 중 HEX3 직렬화/검사 대기 문제는 검증 코드에 보완했고 최종 PASS 결과만 기록했다.
|
||||
|
||||
증거: `_local/designer-typecheck.log`, `designer-build.log`, `designer-storybook-build.log`, `designer-runtime-verification.json`, `theme-generation-verification.json`, `designer-external-preview.jpg`. 이전 감사 CSV/136 checks 결과를 복사한 것이 아니라 현재 정적 빌드 output에서 직접 읽었다.
|
||||
|
||||
## 20. 최종 판정
|
||||
|
||||
**COMPLETE — 요청한 Runtime/JSON/독립 artifact/Preview 구현 범위.**
|
||||
|
||||
- External override: PASS.
|
||||
- 디자인팀 실시간 JSON Preview/편집/Reset/공식 JSON 전달: PASS. JSON 텍스트 Export/Import로 전달 가능하다. 파일 다운로드 완료는 위 환경 제한을 구분한다.
|
||||
- A. 한 Git Repository인가: **YES**, baseline main/commit 확인.
|
||||
- B. UI와 Theme 배포 단위가 분리되는가: **YES**, `build:themes`는 JSON만 산출한다. UI/CENTER는 독립 workspaces.
|
||||
- C. 컬러 Source of Truth: **`themes/default.json` / 서비스별 top-level `themes/*.json`**. package CSS/Default 데이터는 생성물이고 template는 alias/geometry만 소유한다.
|
||||
- D. UI package 재배포 없이 Theme 변경 가능한가: **YES**, 변경 JSON을 독립 배포하고 Loader를 다시 호출한다.
|
||||
- E. CDN JSON으로 적용 가능한가: **YES**, 소비 서비스의 설정 URL을 공식 fetch/load API에 전달한다. 실제 회사 CDN은 미정/미배포.
|
||||
- F. CDN 실패 시 Default 유지되는가: **YES**, 기존 host override가 없는 통상 root에서 package의 HM Default snapshot을 복원한다. host 소유 inline 값이 있었다면 기존 정책대로 그 값을 복원한다.
|
||||
- G. Default/External이 같은 계약/Loader를 사용하는가: **YES**, 동일 v1 JSON과 fetch/validation/Apply 경로; supplied Default + partial resolve도 제공한다.
|
||||
|
||||
실제 CDN 인프라/업로드, registry release, Git commit/push는 실행하지 않았다. 이러한 운영 배포는 이번 범위와 분리하며 Theme-only artifact와 절차는 준비했다. Scoped Portal 자동 전달은 기존 FOLLOW_UP을 유지한다.
|
||||
|
||||
## 작업 시간 (KST)
|
||||
|
||||
- Started At: 2026-10-06 15:41:55 KST
|
||||
- Completed At: 2026-10-06 16:16:53 KST
|
||||
- Actual Duration: 34분 58초
|
||||
- Estimated Duration: 45~70분 (범위 수정 후 50~75분)
|
||||
- Variance: 최초 예상보다 10분 2초~35분 2초 단축
|
||||
@@ -0,0 +1,270 @@
|
||||
# HM Custom Implementation Inventory
|
||||
|
||||
> 조사 기준일: 2026-10-06. 실제 코드(`packages/ui/src`) 기준의 현황 기록이며 확정 규격이 아니다. 구현은 바꾸지 않았다.
|
||||
> 신규 구현은 [AI/Component 구현 원칙](shadcn-기반-구현-원칙.md)과 [Navigation Single Mode Policy](navigation-single-mode.md)의 2026-10-07 확정 정책을 따른다. 아래 조사 이력은 운영 예제나 정책 예외를 허용하는 근거가 아니다.
|
||||
> Select의 `UNDER_REVIEW`는 2026-10-06 조사 당시 설계 평가이며 현재 작업 진행 상태를 뜻하지 않는다. 현재 API·동작은 `docs/components/select.md`와 실제 구현·대표 Story를 따른다.
|
||||
|
||||
> **현재 탐색 기준 (2026-10-08 개발 저장소)**: UI 구현 TSX 44개, 공개 Component 모듈 44개(222 named export), Component Story 파일/그룹 44개, Component 문서 45개(`field.md`는 독립 구현이 아닌 소비 가이드)다. Pattern은 소스 디렉터리/공개 모듈 8개(62 named export), Story 파일/그룹 6개, Pattern 문서 7개다. 확정 배포 기준 `HMDesignTest.zip`에는 Component 구성은 동일하고 Pattern 모듈 7개(50 named export)가 포함되며 Dashboard Status 모듈은 없다. 파일·심볼·그룹은 서로 다른 집계 단위다. 현재 경로 연결은 [Component / Pattern Reference Index](component-pattern-reference.md)를 따른다. 아래 표와 분석은 2026-10-06 당시의 분류 기록이다.
|
||||
|
||||
## 0. 한 줄 요약
|
||||
|
||||
shadcn/Radix를 그대로 쓰지 않고 **실제로 API·State·Behavior·Structure가 추가된 Component는 44개 중 15개**다. 나머지 29개는 shadcn 구조를 유지하고 CSS/Token만 얹었다. Pattern 7종은 모두 기존 Component의 조합이다.
|
||||
|
||||
| 질문 | 답 |
|
||||
|---|---|
|
||||
| 꼭 필요한가? | Component 기준 15개 중 `REQUIRED` 13, `CAN_SIMPLIFY` 1(Textarea), `UNDER_REVIEW` 1(Select). 항목 단위로는 `CAN_SIMPLIFY` 3건, `LEGACY` 1건(Button alias) (§4, §11, §12) |
|
||||
| Public API에 영향을 주는가? | 12건이 consumer에게 노출된다 (§8). 내부 전용은 6건 (§7) |
|
||||
| shadcn 업데이트 시 관리해야 하는가? | **Tabs(HIGH)** 우선, 이어서 Button·Select(MEDIUM). 나머지는 LOW |
|
||||
|
||||
> 위 숫자는 아래 표의 합이다. 표를 고치면 이 요약도 함께 고친다.
|
||||
|
||||
## 1. 목적
|
||||
|
||||
- shadcn 업데이트·재생성 시 "무엇을 손으로 지켜야 하는가"를 한 문서에서 확인한다.
|
||||
- HM 스타일이 들어갔다는 이유만으로 Custom으로 부르지 않는다. Behavior / Structure를 유지하고 CSS/Token만 바꾼 것은 `HM_THEME`다.
|
||||
- 이 문서는 가시화·분류·유지보수 기준만 다룬다. 제거·단순화 판단은 별도 작업이다.
|
||||
- Visual 값(height, radius, color 등)은 각 `docs/components/<name>.md`, `docs/patterns/<name>.md`가 담당하므로 여기에 나열하지 않는다.
|
||||
|
||||
## 2. 분류 기준
|
||||
|
||||
| 분류 | 정의 |
|
||||
|---|---|
|
||||
| `SHADCN_BASE` | shadcn/Radix 구조·API 그대로. `.tsx`에 HM 전용 코드가 없고 Theme CSS도 얇은 Token 매핑 수준 |
|
||||
| `HM_THEME` | Behavior / Structure는 shadcn/Radix 그대로, Visual만 HM CSS / Token. 독립 Custom 아님 |
|
||||
| `HM_CUSTOM` | shadcn에 없는 Public API, 상태/behavior, Radix 동작 확장, 추가 Context/state, helper logic, 구조 확장 중 하나 이상 |
|
||||
| `HM_COMPOSITION` | 기존 HM/shadcn Component를 조합한 Pattern |
|
||||
| `STORY_ONLY` | 검증용 Story, StateBoard 등. Production Custom 아님 |
|
||||
|
||||
조사 한계(반드시 읽을 것):
|
||||
|
||||
- 현재 저장소는 Git으로 관리한다. 2026-10-06 조사는 shadcn 원본 사본과의 Git diff를 수행하지 않았다. 따라서 당시 분류는 "shadcn 원본과의 diff"가 아니라 **현재 코드와 코드 주석을 읽고 shadcn v4(Radix) 구조에 대한 지식과 대조**해 판정했다. 경계 항목은 `shadcn add`로 원본을 받아 diff하면 확정할 수 있다.
|
||||
- `SHADCN_BASE`와 `HM_THEME`의 경계는 휴리스틱이다: `.tsx`가 shadcn 그대로이면서 `packages/ui/src/theme/<name>.css`가 약 70줄 이하이면 `SHADCN_BASE`, 그보다 크면 `HM_THEME`. 둘 다 "Custom 아님"이므로 유지보수 판단에는 영향이 없다.
|
||||
- Story는 전수 검토하지 않고 공용 helper와 파일 구성만 확인했다.
|
||||
|
||||
## 3. 전체 요약
|
||||
|
||||
| Category | Count | 비고 |
|
||||
|---|---:|---|
|
||||
| Component 총계 | 44 | `packages/ui/src/components/ui/` |
|
||||
| `SHADCN_BASE` | 16 | §5 |
|
||||
| `HM_THEME` | 13 | §5 |
|
||||
| `HM_CUSTOM` | 15 | §4 (Public 12건 + Internal 6건이 이 15개에 걸쳐 있음) |
|
||||
| `HM_COMPOSITION` | 7 | Pattern (§6, Component 44개와 별도) |
|
||||
| `STORY_ONLY` | 4 묶음 | §9 |
|
||||
|
||||
## 4. Components — HM_CUSTOM
|
||||
|
||||
한 Component 안에 Visual(HM_THEME)과 구조 Custom이 함께 있을 수 있다. 아래는 구조·API·상태 부분만 적었다. 평가: `REQUIRED` 현재 필요 / `CAN_SIMPLIFY` shadcn 방식으로 단순화 가능 / `LEGACY` 필요성 불명확 / `UNDER_REVIEW` 작업 중·미결정.
|
||||
|
||||
| Component | Base | Custom 종류 | Scope | 평가 | Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| Button | shadcn + Radix Slot + CVA | API, Structure | Public | REQUIRED (+ alias는 LEGACY) | MEDIUM |
|
||||
| Input | shadcn | API | Public | REQUIRED | LOW |
|
||||
| Textarea | shadcn | API | Public | CAN_SIMPLIFY | LOW |
|
||||
| Card | shadcn | API | Public | REQUIRED | LOW |
|
||||
| Select | Radix Select | API, State, Structure | Public + Internal | UNDER_REVIEW | MEDIUM |
|
||||
| Tabs | Radix Tabs | API, State, Behavior, Structure | Public + Internal | REQUIRED | HIGH |
|
||||
| Dialog | Radix Dialog | API(Structure), Behavior | Public + Internal | REQUIRED | LOW |
|
||||
| Sheet | Radix Dialog | API(Structure), Behavior | Public + Internal | REQUIRED | LOW |
|
||||
| Drawer | vaul | API(Structure), Behavior | Public + Internal | REQUIRED | LOW |
|
||||
| Alert Dialog | Radix Alert Dialog | API | Public | REQUIRED | LOW |
|
||||
| Combobox | Popover + Command (cmdk) | API, State, Composition | Public | REQUIRED | LOW |
|
||||
| Date Picker | Popover + Calendar (react-day-picker) | API, State, Behavior | Public + Internal | REQUIRED | LOW |
|
||||
| Popover | Radix Popover | Behavior | Internal | REQUIRED | LOW |
|
||||
| Collapsible | Radix Collapsible | Behavior | Internal | REQUIRED | LOW |
|
||||
| Slider | Radix Slider | Behavior | Internal | REQUIRED | LOW |
|
||||
|
||||
### 4.1 항목별 기록
|
||||
|
||||
**Button** — `button.tsx`, `theme/button.css`
|
||||
- 추가: `variant`를 역할(`primary|secondary|modify|destructive|cancel`)로 교체, `appearance`(`brand|solid|outline`) 추가, `data-variant/data-appearance/data-size`. `default`·`outline`은 shadcn 호환 alias로 `primary`/`primary+outline`에 매핑, `ghost`·`link`는 shadcn 기본 유지.
|
||||
- 이유: ui_viz 역할/처리(treatment) 2축 규격. `<Button />`만으로 HM 디자인 적용.
|
||||
- Public API 영향: 있음(`variant` 값 집합, `appearance`). 기존 shadcn `variant="default|outline"` 코드는 alias로 동작.
|
||||
- 구조 주의: `data-slot="button"`을 `{...props}` **뒤에** 둔다. 앞에 두면 `<XTrigger asChild><Button/></XTrigger>`가 trigger의 `data-slot`으로 덮어써 HM 스타일이 사라진다(§7 I3).
|
||||
- 대체 가능한 shadcn 방식: shadcn `variant` 값에 HM 역할을 추가하는 방식은 가능하나 `appearance` 2축은 표현 불가.
|
||||
- 리스크 사유: `buttonVariants` CVA 정의를 통째로 다시 썼다. shadcn이 `buttonVariants`를 바꾸면 수동 병합이 필요하다. 코드에 `dark:` 잔재 2곳(§11).
|
||||
- 유지 필요성: REQUIRED. 단, `default`/`outline` alias는 LEGACY(호환 목적이며 소비처가 정리되면 제거 후보).
|
||||
|
||||
**Input** — `input.tsx`
|
||||
- 추가: `size="small|medium|large"`(기본 large). 네이티브 `size`(문자 폭) 속성은 `Omit`으로 가려져 **사용 불가**.
|
||||
- Public API 영향: 있음, 네이티브 속성 가림. 대체: `className` 높이 지정(2축 상태 규칙은 불가). 유지 REQUIRED. Risk LOW.
|
||||
|
||||
**Textarea** — `textarea.tsx`
|
||||
- 추가: `resize="vertical|none|both"`(기본 vertical)를 `data-resize`로 노출. readonly→none은 CSS에서 처리.
|
||||
- 대체: `className="resize-none"` 등. 기본 정책을 컴포넌트가 갖게 하려는 목적이라 단순화 가능. CAN_SIMPLIFY, Risk LOW.
|
||||
|
||||
**Card** — `card.tsx`
|
||||
- 추가: `surface="surface|content"`(`data-surface`). `size="sm"`은 shadcn 기존 API.
|
||||
- 유지 REQUIRED(ui_viz Content Card 면 구분). Risk LOW.
|
||||
|
||||
**Select** — `select.tsx`, `theme/select.css` (size 계약 구현됨; 구조 선택은 아래 UNDER_REVIEW)
|
||||
- 추가(조사 시점 코드):
|
||||
- `SelectTrigger`의 `size="medium|large"`(shadcn `sm|default` 대체), `data-size`.
|
||||
- `SelectSizeContext` + `Select` Root를 Provider로 감싸 `useState`로 size 보관, Trigger가 `useLayoutEffect`로 등록. Portal 안의 `SelectContent`가 `data-size`를 읽는다. 즉 **Root가 shadcn처럼 순수 `SelectPrimitive.Root`가 아니라 Provider 래퍼**다.
|
||||
- 아이콘을 Radix 기본 대신 18×18 SVG로 교체, `SelectItem`의 check indicator 제거(선택 표시는 배경 면), `SelectContent` 기본값 `position="popper"`, `sideOffset=-1`(border 1px 공유 seam).
|
||||
- long text ellipsis와 seam(`--hm-select-seam-color`)은 CSS 처리(`select.css`).
|
||||
- Public API 영향: `size` prop(Public). Context·`setSize`는 노출되지 않는 Internal.
|
||||
- 대체 가능한 shadcn 방식: shadcn은 Content가 `size`를 몰라도 되게 Trigger 쪽만 `data-size`를 둔다. Content까지 size 연동이 필요한 이유가 Portal 때문이므로 대안은 (a) Content에도 `size` prop을 직접 전달, (b) CSS만으로 처리, (c) 현 Context 유지. **어느 쪽이 맞는지 결정되지 않았다.**
|
||||
- 유지 필요성: UNDER_REVIEW. Risk MEDIUM(Root 래퍼 + 렌더 후 `useLayoutEffect`로 state를 갱신하는 구조라, 첫 렌더에서 Content가 기본 `large`로 보일 수 있는 타이밍 의존이 있음).
|
||||
- 이 항목은 Final Spec이 아니다.
|
||||
|
||||
**Tabs** — `tabs.tsx`, `theme/tabs.css`
|
||||
- 추가:
|
||||
- `TabsList`에 `fill`(동일 폭·초록 선택 border·곡선 연결)과 `panelSurface="surface|content"` prop.
|
||||
- `TabsList`가 Radix List를 `<div data-slot="tabs-frame">`로 **한 겹 감싼다**(DOM 구조 변경).
|
||||
- 넘침 감지(`ResizeObserver`, `MutationObserver`, scroll 리스너, `document.fonts.ready`), 이전/다음 이동 버튼(`aria-label` 한국어 고정), 선택 탭 가시 유지(`keepSelectedVisible`), `prefers-reduced-motion` 분기, `useId`로 List id 생성, 외부 `ref` 병합.
|
||||
- 연결 곡선은 `tabs.css`의 `::before/::after` + `radial-gradient`(HM 고유 구조 CSS).
|
||||
- Public API 영향: `fill`, `panelSurface`(Public). 나머지는 Internal.
|
||||
- 대체: shadcn Tabs는 위 기능이 전혀 없다. 넘침 버튼 없이 가로 스크롤만 허용하는 단순화는 가능하나 ui_viz D-090 규격을 포기해야 한다.
|
||||
- 유지 REQUIRED. **Risk HIGH**: 원본 `TabsList` 구조를 크게 바꿨고 Observer 기반 로직과 한국어 문구가 컴포넌트 안에 있다.
|
||||
- 2026-10-06: 대표 형태를 Primary(`fill` + `panelSurface="content"`)·Secondary(`fill` + `panelSurface="surface"`) 두 가지로 정리했다. Story 노출만 줄였고 코드·API·CSS branch는 그대로라 **Custom complexity 변화 없음**. non-fill branch는 REMOVE_CANDIDATE(사용자 확정 대기).
|
||||
- 2026-10-08 추가 개선 E 승인: 당시 `fill=false`, `panelSurface="surface"` 기본값을 현재 `fill=true`, `panelSurface="content"`(Primary)로 변경했다. Props 이름·타입·기존 CSS 디자인 값·선택자·Observer 동작은 유지하므로 **Custom complexity 변화 없음**. Legacy non-fill은 `fill={false}`로 유지하고 OLD_TEST_ONLY / REMOVE_CANDIDATE 삭제 대기 이력도 보존한다. 외부 기본 렌더링·명시적 Props 조합·CSS Override 영향은 [Tabs 문서 §15](../components/tabs.md#15-외부-소비자-영향--호환성-2026-10-08)를 따른다. 외부 소비자 전체 호환성은 미검증이다.
|
||||
|
||||
**Dialog / Sheet / Drawer** — `dialog.tsx`, `sheet.tsx`, `drawer.tsx`
|
||||
- 추가(Public): `DialogBody`, `SheetBody`, `DrawerBody` — 스크롤 소유 영역(Header/Footer 고정, Body만 스크롤). shadcn에는 없는 구조 API.
|
||||
- 추가(Internal): `*Close`가 `asChild`일 때 `data-slot`을 넘기지 않는 규칙(§7 I3).
|
||||
- Drawer 추가: `autoFocus` 기본값을 `true`로(vaul 기본은 `false`라 modal에서 포커스가 trigger에 남음). 소비자가 `autoFocus={false}`로 되돌릴 수 있다 → Public 동작 차이.
|
||||
- `showCloseButton`, `side`는 shadcn 기존 API(Custom 아님). Radius·shadow 등은 HM_THEME.
|
||||
- 대체: Body는 `className`이 붙은 `div`로 대체 가능하나 `data-slot` 기반 CSS 선택이 사라진다. REQUIRED, Risk LOW.
|
||||
|
||||
**Alert Dialog** — `alert-dialog.tsx`
|
||||
- 추가: `AlertDialogAction`/`AlertDialogCancel`이 `variant|appearance|size`를 받고 `Button asChild`로 합성. 기본값 Action=`primary/solid`, Cancel=`cancel/solid`. shadcn도 Button 스타일을 입히지만 `appearance`는 HM 전용.
|
||||
- REQUIRED, Risk LOW.
|
||||
|
||||
**Combobox** — `combobox.tsx`
|
||||
- shadcn에는 파일이 아닌 recipe(Popover+Command)만 있고 HM은 이를 **`options` 기반 컴포넌트**로 만들었다. Public: `options`(value/label/disabled), `value/defaultValue/onValueChange`, `placeholder/searchPlaceholder/emptyText`, `size="medium|large"`. State: `open`, 비제어용 `inner`(제어/비제어 분기 직접 구현). 한국어 기본 문구 포함.
|
||||
- 대체: Popover+Command를 소비자가 직접 조합(shadcn 방식). REQUIRED(서비스 공통 필요), Risk LOW(원본 파일이 없어 업스트림 충돌 없음).
|
||||
- 내부: `PopoverContent`에 인라인 `--hm-popover-padding/width` 주입 → CAN_SIMPLIFY(§11).
|
||||
|
||||
**Date Picker** — `date-picker.tsx`
|
||||
- 같은 성격. Public: `value/defaultValue/onValueChange`, `placeholder`, `size`, `format`, `calendarProps`(Calendar 옵션 일부). 제어/비제어 상태 직접 구현. Behavior: `onOpenAutoFocus`에서 Calendar의 tab stop 날짜로 포커스 이동(Internal). 한국어 기본 포맷.
|
||||
- 대체: Popover+Calendar 직접 조합. REQUIRED, Risk LOW.
|
||||
|
||||
**Popover / Collapsible** — `popover.tsx`, `collapsible.tsx`
|
||||
- `PopoverClose`, `CollapsibleTrigger`의 `asChild` data-slot 규칙만 추가(§7 I3). Risk LOW.
|
||||
|
||||
**Slider** — `slider.tsx`
|
||||
- 추가: `aria-label`을 Root가 아니라 **각 Thumb**에 전달(Range는 `"라벨 1/2"`). `role="slider"`가 Thumb에 있기 때문. Internal. shadcn 원본에는 없음. REQUIRED(접근성), Risk LOW.
|
||||
|
||||
## 5. Components — HM_THEME / SHADCN_BASE
|
||||
|
||||
`.tsx`에 HM 구조·API·State 추가가 없는 Component. 시각 값은 각 Component 문서가 관리한다.
|
||||
|
||||
**SHADCN_BASE (16)**: Accordion, Alert, Aspect Ratio, Avatar, Badge, Breadcrumb, Checkbox, Hover Card, Label, Progress, Radio Group, Scroll Area, Separator, Skeleton, Switch, Tooltip
|
||||
|
||||
**HM_THEME (13)**: Calendar, Carousel, Command, Context Menu, Dropdown Menu, Menubar, Navigation Menu, Pagination, Resizable, Sonner, Table, Toggle, Toggle Group
|
||||
|
||||
참고(Custom으로 보지 않은 이유):
|
||||
|
||||
- Alert·Badge: 사용 가능한 `variant`를 HM Theme에 있는 의미로 **줄였다**(Alert=default/destructive, Badge=4종). API 축소이며 추가가 아니다.
|
||||
- Calendar: 기본 `locale`을 한국어(`ko`)로 두었다. shadcn도 `Chevron` 컴포넌트를 교체한다. 기본값 하나의 차이라 THEME로 둔다.
|
||||
- Command: shadcn의 `CommandDialog`가 **없다**(의도적 제외, 코드 주석 명시). 기능 제거이며 추가 Custom 아님.
|
||||
- Toggle Group: shadcn 그대로 Context를 쓰며 `spacing`을 CSS 변수(`--hm-toggle-group-gap`)로 넘기는 한 줄이 추가됐다.
|
||||
- Carousel: shadcn 원본 로직(Context, Embla, 키보드)이 그대로다.
|
||||
- Tooltip: 원본처럼 `Tooltip`이 자체 `TooltipProvider`를 가진다(shadcn v4).
|
||||
|
||||
## 6. Patterns — HM_COMPOSITION
|
||||
|
||||
`packages/ui/src/patterns/`. 모두 기존 HM Component의 조합이며 별도 primitive가 아니다. 전부 shadcn에 없는 **Public API**다(Pattern 이름 자체).
|
||||
|
||||
| Pattern | Composition | Custom Logic | Public API |
|
||||
|---|---|---|---|
|
||||
| GNB | Navigation Menu, Dropdown Menu, Avatar, Sheet, Button | 있음: `GNBMobileMenu`(open state, Context, Sheet 합성), `GNBMobileLink`가 `preventDefault`된 클릭에도 Sheet를 닫음, `GNBUser` 기본 `aria-label` | `GNB`(`sticky`), `GNBBrand`, `GNBNav`, `GNBUtilities`, `GNBUser`, `GNBMobileMenu`, `GNBMobileLink`, `GNBMobileGroup` |
|
||||
| LNB | Collapsible, Slot | 없음(`LNBGroup`은 Collapsible 합성) | `LNB`, `LNBBrand`, `LNBNav`, `LNBItem`, `LNBGroup`, `LNBProfile` |
|
||||
| Dashboard Layout | 현재는 Dashboard 내부 Header/Menu/Sidebar 조합만 허용 | 없음(레이아웃) | `DashboardLayout`(`header`, `sidebar`), `DashboardMain`, `DashboardContent` |
|
||||
| Page Header | Breadcrumb·Button을 child로 받음 | 없음 | `PageHeader`, `PageHeaderContent/Title/Description/Actions` |
|
||||
| Loading Screen | 없음(자체 요소) | 24개 막대 렌더, CSS 애니메이션·reduced-motion, `role="status"` | `LoadingScreen`(`fullscreen`) |
|
||||
| Filter Bar | Input, Select·Combobox·Date Picker·Button을 child로 받음 | 있음: `FilterBarSearch`(검색 아이콘 + Input + 지우기 버튼, `aria-label` 기본값) | `FilterBar`, `FilterBarSearch`, `FilterBarActions` |
|
||||
| Data Table | Table, Checkbox, Dropdown Menu, Pagination, Select, Skeleton, Button + TanStack Table v9 | 있음: 기능(feature) 등록, `useDataTable`, `dataTableSelectColumn`, `DataTableColumnHeader`(정렬 순환·`aria-sort`), `DataTableViewOptions`, `DataTablePagination`(`pageRange`), loading/empty 상태 | `DataTable`, `useDataTable`, `dataTableSelectColumn`, `DataTableColumnHeader`, `DataTableViewOptions`, `DataTablePagination` |
|
||||
|
||||
CSS 자체가 구조를 구현하는 항목(HM 고유): GNB 아래쪽 concave join과 current 항목 연결 곡선(`gnb.css`), LNB와 콘텐츠 join(`lnb.css`, `--hm-lnb-join`), Loading Screen 막대 애니메이션(`loading-screen.css`). 각 Navigation Mode는 독립 레이아웃을 소유하며 Dashboard에 외부 GNB/LNB를 넣지 않는다.
|
||||
|
||||
과거 조사에서는 GNB+LNB 동시 조립과 Brand ownership을 기록했으나 현재 단일 Navigation 정책으로 대체되었다. 혼합 Story는 기술 검증 목적의 `verification` 태그와 **VERIFICATION ONLY** 표시가 있을 때만 유지할 수 있으며 운영 Reference로 사용하지 않는다(`docs/patterns/gnb.md`, `lnb.md`).
|
||||
|
||||
## 7. Internal Custom
|
||||
|
||||
consumer에게 노출되지 않는 내부 구현. 6건.
|
||||
|
||||
| # | 항목 | 위치 | 평가 | Risk |
|
||||
|---|---|---|---|---|
|
||||
| I1 | Select size Context + `useLayoutEffect` 등록 (**작업 중일 수 있음**) | `select.tsx` | UNDER_REVIEW | MEDIUM |
|
||||
| I2 | Tabs 넘침 측정(Observer·scroll·state) 과 이동 버튼 | `tabs.tsx` | REQUIRED | HIGH |
|
||||
| I3 | `asChild` 시 `data-slot`을 child에 남기는 규칙(Dialog/Sheet/Drawer/Popover/Collapsible의 Close·Trigger, Button의 `data-slot` 후순위 배치) | 여러 파일 | REQUIRED | LOW |
|
||||
| I4 | Slider thumb별 `aria-label` 전달 | `slider.tsx` | REQUIRED | LOW |
|
||||
| I5 | Date Picker `onOpenAutoFocus` 날짜 포커스 | `date-picker.tsx` | REQUIRED | LOW |
|
||||
| I6 | Combobox·Date Picker의 `PopoverContent` 인라인 `--hm-popover-*` 변수 | `combobox.tsx`, `date-picker.tsx` | CAN_SIMPLIFY | LOW |
|
||||
|
||||
## 8. Public API Custom
|
||||
|
||||
shadcn 원본에 없는 consumer-facing 확장. Component 12건 + Pattern 전체(§6).
|
||||
|
||||
| # | Component | HM API | shadcn 원본 | Risk |
|
||||
|---|---|---|---|---|
|
||||
| P1 | Button | `variant`(역할 5종+alias), `appearance` | `variant` 값이 다름 | MEDIUM |
|
||||
| P2 | Input | `size="small|medium|large"` | 네이티브 `size` 가림 | LOW |
|
||||
| P3 | Textarea | `resize` | 없음 | LOW |
|
||||
| P4 | Card | `surface` | 없음(`size`는 있음) | LOW |
|
||||
| P5 | SelectTrigger | `size="medium|large"` (**작업 중일 수 있음**) | `sm|default` | MEDIUM |
|
||||
| P6 | TabsList | `fill`, `panelSurface` (+ 프레임 DOM) | 없음 | HIGH |
|
||||
| P7 | Dialog | `DialogBody` | 없음 | LOW |
|
||||
| P8 | Sheet | `SheetBody` | 없음 | LOW |
|
||||
| P9 | Drawer | `DrawerBody`, `autoFocus` 기본 `true` | 없음 / vaul 기본 `false` | LOW |
|
||||
| P10 | AlertDialogAction / Cancel | `appearance` (`variant`·`size` 포함) | `appearance` 없음 | LOW |
|
||||
| P11 | Combobox | `options` 등 전체 API | 파일 없음(recipe) | LOW |
|
||||
| P12 | Date Picker | `format`, `calendarProps` 등 전체 API | 파일 없음(recipe) | LOW |
|
||||
|
||||
## 9. Storybook-only Custom (`STORY_ONLY`)
|
||||
|
||||
Production Custom 목록에 포함하지 않는다. 4묶음.
|
||||
|
||||
| 묶음 | 위치 | 내용 |
|
||||
|---|---|---|
|
||||
| StateBoard helper | `src/components/story-board.tsx` | `BoardState`(`note`, `noteAfter`, `wide`), `StateBoard`. 39개 Story 파일이 사용 |
|
||||
| 고정 상태·검증 Story | `src/components/*.stories.tsx`, `src/patterns/*.stories.tsx` | `States`, `StandardStates`, `ListSizeComparison` 등 비교·검증용 export. 전수 검토하지 않았음 |
|
||||
| 샘플 데이터 | `src/patterns/sample-users.ts` | Data Table Story용 |
|
||||
| Demo / CENTER Story | `src/hm-theme.css`, `src/theme/preview.ts`, `src/center/*` | Demo 스타일, preview 설정, `center-graphic` 전용 Story(UI 규격과 분리) |
|
||||
|
||||
## 10. External Library Base
|
||||
|
||||
| Library | 사용처 | 구분 |
|
||||
|---|---|---|
|
||||
| `radix-ui` | 대부분의 primitive | Base. Custom은 §4 |
|
||||
| `class-variance-authority` | Button, Badge, Alert, Toggle | shadcn 구조. Button만 CVA 정의를 HM 값으로 교체 |
|
||||
| `cmdk` | Command, Combobox | Library Base + HM Theme (Combobox는 HM_CUSTOM 합성) |
|
||||
| `vaul` | Drawer | Library Base + HM Custom(`DrawerBody`, `autoFocus` 기본값) |
|
||||
| `embla-carousel-react` | Carousel | Library Base + HM Theme (shadcn 로직 그대로) |
|
||||
| `react-resizable-panels` | Resizable | Library Base + HM Theme |
|
||||
| `react-day-picker` | Calendar, Date Picker | Library Base + HM Theme (Date Picker는 HM_CUSTOM 합성) |
|
||||
| `sonner` | Sonner(Toast) | Library Base + HM Theme |
|
||||
| `@tanstack/react-table` v9 | Data Table | Library Base + HM Composition (v9 feature 등록 방식) |
|
||||
| `lucide-react` | 아이콘 전반 | shadcn 기본 |
|
||||
|
||||
## 11. CAN_SIMPLIFY 후보
|
||||
|
||||
평가만 한다. 변경은 하지 않았다.
|
||||
|
||||
| 항목 | 단순화 방향 | 비고 |
|
||||
|---|---|---|
|
||||
| Textarea `resize` (P3) | `className`(`resize-none` 등)로 대체 가능 | 기본 정책을 컴포넌트에 두려는 의도라면 유지 |
|
||||
| Combobox·Date Picker 인라인 `--hm-popover-*` (I6) | CSS 선택자·`data-slot`으로 이동 | |
|
||||
| `dark:` 잔재 | 현재 `button.tsx` 2개 행에만 남아 있고 `select.tsx`에는 없다. 기존 upstream `dark:` 분기는 보존할 수 있으며 새 HM Dark 분기는 추가하지 않는다 | HM_CUSTOM이 아니라 shadcn 원본 잔재. 정리 후보 |
|
||||
|
||||
## 12. UNDER_REVIEW / LEGACY
|
||||
|
||||
| 항목 | 상태 | 비고 |
|
||||
|---|---|---|
|
||||
| Select `size` Context (P5 + I1) | UNDER_REVIEW | 조사 당시 미결 설계 평가. 대안은 §4.1 Select 참조. 현재 작업 중이라는 뜻이 아니며 현재 API는 Select 문서·코드를 따른다 |
|
||||
| Button `default`/`outline` alias (P1) | LEGACY | shadcn 호환용. 소비처 사용 여부 미확인 |
|
||||
|
||||
## 13. 유지 원칙
|
||||
|
||||
1. HM_THEME·SHADCN_BASE는 CSS/Token만 바꾼다. `.tsx`에 구조·상태를 추가하려면 `shadcn-기반-구현-원칙.md` §6의 Custom 허용 조건과 기록 요건을 확인하고 이 문서에 행을 추가한다.
|
||||
2. shadcn 재생성·업데이트 시 우선 확인 순서: **Tabs → Button → Select → 나머지 Body/Close 규칙(I3)**. 이 파일들은 덮어쓰지 않고 수동 병합한다.
|
||||
3. `data-slot`을 child에 남기는 규칙(I3)은 새 `asChild` 지원 컴포넌트에도 같은 방식으로 적용한다.
|
||||
4. 새 Pattern은 §6 표에 "Composition / Custom Logic / Public API"를 한 행으로 기록한다.
|
||||
5. 이 문서의 수치는 조사 시점(2026-10-06) 기준이다. Select 설계 평가가 변경되면 §4·§7·§8·§12를 함께 갱신한다.
|
||||
|
||||
## 2026-10-06 Theme infrastructure
|
||||
|
||||
Default/External 동일 Runtime 계약, 패키지 theme API, Semantic/팔레트 연결과 Accent derived 공통화는 HM_THEME / Theme infrastructure다. Component DOM/props/state/Portal/breakpoint를 바꾸지 않았으므로 위 HM_CUSTOM Inventory 수를 늘리지 않는다. 상세: [Runtime Theme](runtime-theme.md).
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# Navigation Single Mode Policy
|
||||
|
||||
2026-10-07 사용자 확정 정책. 이전 GNB/LNB/Dashboard 조합 기준을 대체한다. 2026-10-08 GNB ONLY 내부 Sidebar 허용 및 DASHBOARD ONLY Sidebar 운영 사용 금지를 명확히 했다.
|
||||
|
||||
**ONE PAGE = ONE NAVIGATION SYSTEM = ONE HEADER/NAVIGATION EXPERIENCE**
|
||||
|
||||
## 적용 범위
|
||||
|
||||
AI 기반 코드 생성과 제안, 신규 페이지 구현, 기존 화면 수정, 프로토타입, Storybook 대표/PRIMARY Story, 실제 Production 구현 전체에 동일하게 적용한다. AI는 별도 요청이 없는 한 Navigation 조합을 생성하거나 제안하지 않는다. 별도 요청도 혼합 운영 구조를 자동 허용하지 않으며 정책 변경 또는 기술 검증의 목적을 먼저 구분한다.
|
||||
|
||||
## 모드 선택
|
||||
|
||||
페이지 구현 전에 `navigationMode = GNB | LNB | DASHBOARD` 중 정확히 하나를 결정한다. 이 표기는 페이지 설계 계약이며 현재 공통 Page 컴포넌트의 prop이 아니다.
|
||||
|
||||
| 선택 | 허용 구조 | 함께 사용하지 않는 체계 |
|
||||
|---|---|---|
|
||||
| GNB | Page → GNB Header + Content, 또는 GNB Header + 내부 Sidebar + Content | 독립 LNB Mode, Dashboard |
|
||||
| LNB | Page → LNB + Content | GNB, Dashboard |
|
||||
| DASHBOARD | Page → Dashboard 전용 Header(48px)·업무 상태 필터 + Content, Sidebar 없음 | 모든 Sidebar, 외부 GNB, 일반 LNB |
|
||||
|
||||
독립 Navigation Mode를 결합하는 GNB + LNB, GNB + Dashboard, LNB + Dashboard, GNB + LNB + Dashboard는 모두 금지한다. GNB ONLY 내부 Sidebar의 구현 부품 재사용은 아래 범위에서 허용한다. 콘텐츠의 Breadcrumb, Filter Bar, 사용자 Dropdown 등은 새로운 페이지 Navigation Mode가 아니다.
|
||||
|
||||
## 선택 근거
|
||||
|
||||
1. 사용자 요청에 모드가 있으면 우선 적용한다. 요청이 독립 모드 혼합을 포함하면 이 정책과의 충돌을 기록하고 정책 변경인지 기술 검증인지 확인한다.
|
||||
2. 미지정이면 제공된 화면의 Navigation 구조와 현재 대표 Story를 대조한다. 서비스 전역 상단 메뉴·브랜드·사용자 영역이 중심이면 [GNB](../patterns/gnb.md), 현재 영역의 계층 메뉴·독립 Sidebar와 모바일 Header가 중심이면 [LNB](../patterns/lnb.md), Dashboard 내부 Header/상태 메뉴·최소 폭 프레임이 중심이면 [Dashboard](../patterns/dashboard-layout.md)를 선택한다.
|
||||
3. 일반 업무 목록이나 DataTable 사용 여부만으로 GNB/LNB를 확정하지 않는다. GNB에서도 내부 Sidebar Composition을 사용할 수 있고, Dashboard Header는 GNB가 아니다.
|
||||
4. 선택 근거가 부족하면 가정한 모드와 이유를 보고한다. 기존 화면과의 일관성을 판단할 정보가 없으면 사용자 확인 필요 사항으로 남긴다. 모든 일반 화면에 적용하는 기본 모드는 없다.
|
||||
|
||||
## GNB ONLY 내부 Sidebar
|
||||
|
||||
- GNB Sidebar는 GNB ONLY의 하위 화면 구성이다. 공개 `LNB / LNBNav / LNBGroup / LNBItem`과 필요한 모바일 부품을 재사용해도 독립 LNB ONLY Mode를 추가한 것이 아니다.
|
||||
- 메뉴 DOM/data-slot, 폭, 폰트, 아이콘, 간격, Depth, Hover/Current, 그룹 펼침·접힘과 메뉴 스크롤은 기존 LNB 계약을 유지한다. Sidebar 범위의 `--hm-lnb-background`만 `var(--hm-sem-palette-green-light-2)`로 지정한다. 공통 Theme 값과 LNB 기본 스타일은 변경하지 않는다.
|
||||
- 브랜드·사용자 영역은 GNB가 담당한다. 기본 Sidebar에는 `LNBBrand / LNBProfile`을 넣지 않는다. Desktop/Mobile 모두 현재 경로는 하나로 관리하며, Sidebar 그룹과 GNB 팝업의 열림 상태는 독립적이다. 선택에 따른 새로운 자동 펼침·접힘은 추가하지 않는다.
|
||||
- 이 허용은 GNB 내부 Sidebar에 한정한다. 독립 LNB Header/브랜드 체계의 동시 추가, Dashboard와의 조합, 기존 혼합 verification Story의 운영 승격은 허용하지 않는다. Dashboard Header는 GNB가 아니다.
|
||||
|
||||
## Responsive
|
||||
|
||||
- GNB는 자체 Responsive / Mobile Header를 사용한다.
|
||||
- GNB Sidebar는 ≥1024px에서 표시하고 ≤1023px에서 숨긴다. 모바일은 기존 GNB Header의 메뉴 버튼 하나와 Sheet 하나에 LNB 모바일 링크/그룹을 재사용한다. `LNBMobileHeader`나 두 번째 Sheet를 추가하지 않는다. Sheet 기본 배경, 링크 높이·폰트·선택 동작, Escape·포커스 복귀를 유지한다. 그룹은 기존 모바일 링크 목록이며 새 disclosure 동작을 만들지 않는다.
|
||||
- LNB는 ≥1024px에서 Desktop LNB만, ≤1023px에서 LNB Mobile Header만 표시한다. Mobile Header와 Sheet는 같은 메뉴·선택 상태를 공유하는 LNB의 모바일 표현이며 별도의 GNB가 아니다.
|
||||
- 독립 LNB ONLY를 Desktop LNB → Mobile GNB Mode로 전환하거나 모바일 두 종류를 동시에 노출하지 않는다. GNB 내부 Sidebar의 모바일 표현은 처음부터 끝까지 GNB ONLY다.
|
||||
- Dashboard 운영 화면은 모든 viewport에서 Sidebar 없이 전용 Header(48px)·업무 상태 필터·Content를 유지한다. 최소 폭은 960px이며 그 이하에서는 Dashboard 내부에서 가로 스크롤한다. 일반 LNB의 모바일 숨김이나 GNB breakpoint에 의존하지 않는다.
|
||||
|
||||
## Layout 및 Storybook
|
||||
|
||||
Navigation 간 height/width 계산이나 CSS 변수·breakpoint·spacing 교차 의존성을 만들지 않는다. Dashboard 운영 화면의 `header`에는 전용 DashboardHeader를 넣고 `sidebar`는 전달하지 않는다. 각각의 모드는 자기 변수와 공통 Semantic Token을 사용한다.
|
||||
|
||||
`DashboardSidebar` 공개 Export와 선택적 `sidebar` slot은 기존 소비자 호환성·검증을 위해 유지하는 API다. 존재가 운영 허용을 뜻하지 않으며 신규 Dashboard 운영 화면에는 사용하지 않는다. Sidebar 포함 1232px 규칙은 해당 호환성·검증 구성에만 남는 값으로 현재 운영 기준이 아니다. 기능 제거·런타임 차단은 소비처 영향 조사와 사용자 결정을 거치는 별도 작업이다.
|
||||
|
||||
GNB 내부 Sidebar의 Composition은 기존 LNB 구현을 재사용하기 위해 `--hm-lnb-width / --hm-lnb-join`과 LNB 부품의 토큰을 소비할 수 있다. 이는 독립 Navigation 간 결합이 아니다. 높이는 소비자 프레임의 Grid 행으로 배분하고 GNB/LNB/Dashboard의 토큰 정의나 breakpoint를 서로 수정하지 않는다. 사용 구조는 [GNB §15](../patterns/gnb.md#15-gnb-only-sidebar-composition-2026-10-08)를 따른다.
|
||||
|
||||
대표/PRIMARY Story는 선택된 모드의 운영 예제만 보여준다. 기존 혼합 Story와 Dashboard Sidebar Story가 기술 검증에 필요한 경우에만 `verification` 태그와 화면·Story 이름의 **VERIFICATION ONLY — 운영 사용 금지** 표시로 유지한다. 운영 예제, 신규 코드 생성의 근거로 사용하지 않는다. 과거 검증 기록은 HISTORICAL로 보존하며 현재 운영 계약으로 재해석하지 않는다.
|
||||
|
||||
## 구현·리뷰 완료 조건
|
||||
|
||||
- 페이지 모드가 하나로 결정되어 있고 다른 Navigation 체계를 추가하지 않았는가?
|
||||
- 모바일도 같은 모드의 Responsive Variant이며 Navigation 두 종류가 동시에 보이지 않는가?
|
||||
- LNB의 1024/1023 경계와 Sidebar 없는 Dashboard의 48px Header·업무 상태 필터·960px 최소 폭·내부 스크롤 계약을 지키는가?
|
||||
- Navigation 간 CSS 변수 및 조합 전용 breakpoint/spacing 의존성을 추가하지 않았는가?
|
||||
- 대표 Story 및 AI 생성 지침이 같은 정책을 따르는가?
|
||||
- GNB Sidebar가 기존 LNB 부품·단일 현재 경로를 재사용하며 모바일 Header/Sheet/메뉴 버튼이 중복되지 않는가?
|
||||
|
||||
관련 구현: [GNB](../patterns/gnb.md), [LNB](../patterns/lnb.md), [Dashboard](../patterns/dashboard-layout.md).
|
||||
@@ -0,0 +1,39 @@
|
||||
# 설치 없는 Windows 테스트 배포
|
||||
|
||||
현재 검증된 테스트 배포 기준은 `HMDesignTest-Portable-Test-r3.zip`이며 내부 테스트용이다. 이하 실행 절차·포함 범위는 r3 기준이다. 루트 폴더는 `HMDesignTest/`이며 기존 오전 `HMDesignTest.zip`을 대체하거나 덮어쓰지 않는다.
|
||||
|
||||
## 테스트 사용자 실행
|
||||
|
||||
1. ZIP을 압축 해제한다.
|
||||
2. `HMDesignTest/start-preview.bat`을 더블클릭한다.
|
||||
3. 콘솔에 표시된 `http://127.0.0.1:<port>/` 주소를 브라우저에서 연다.
|
||||
|
||||
Windows x64용 공식 Node.js 22.23.3 런타임을 동봉한다. Node.js/npm 설치, npm install/ci/run, 패키지/Storybook 빌드, Git Clone/Pull은 필요 없다. 서버는 127.0.0.1에만 바인딩하며 기본 포트는 6007이고(개발 저장소 Storybook 6024와 별개) 6007이 사용 중이면 다음 포트 또는 OS가 배정한 사용 가능한 포트를 안내한다. 기존 프로세스를 종료하지 않는다. 한글·공백 경로를 지원한다. 콘솔에서 Ctrl+C로 종료한다.
|
||||
|
||||
## 포함 범위와 Reference
|
||||
|
||||
최신 UI Component 44개, 8개 Pattern 모듈(Dashboard Status 포함), 공개 API 및 declarations, UI/CENTER 패키지 dist, Theme/Token/Runtime, Story 소스/설정, 완전한 정적 Storybook, 문서/AI Reference, 기존 화면 예제, 폰트/Asset 및 라이선스를 포함한다. 소스의 저장소 상대 구조를 보존한다.
|
||||
|
||||
Dashboard Status의 `active | hold | done`, 필터 `all | active | hold | done | issue`, 독립 issue flag와 CSS/export/문서를 함께 제공한다. 대표 Dashboard Story는 Header 48px, 상태 필터, DASHBOARD ONLY 기준이다. `docs/patterns/dashboard-status-static.html`은 상태 계약 정적 예제이며 전체 Dashboard 대체 실행본이 아니다.
|
||||
|
||||
독립 Dashboard export는 포터블 ZIP의 필수 구성이 아니다. 기존 `dist-dashboard/`는 H-1(독립 단일 HTML 미리보기 POC, PASS) 성공 산출물과 동일하지 않다. STEP 5에서 UI Preview를 표시한 미검증 `dist-dashboard/`는 제외하고 검증 대상은 정적 Storybook의 대표 Dashboard로 한정한다. export 소스와 개발자 계약은 보존한다.
|
||||
|
||||
`.git`, 인증 정보, node_modules, 캐시, 로그, 로컬 설정, 오래된 중복 ZIP/배포 결과를 제외한다. `FILE-INVENTORY.json`에는 포함 파일의 상대경로·크기·SHA-256이 있다(인벤토리 자신은 제외). 외부 ZIP 해시 파일과 검증 보고서를 함께 제공한다.
|
||||
|
||||
## 현재 ZIP의 시점
|
||||
|
||||
현재 검증된 기준은 `HMDesignTest-Portable-Test-r3.zip`이다. 아래 r2와 기존 `HMDesignTest-Portable-Test.zip`은 과거 기록으로만 보존하며 현재 기준이 아니다. r3의 정확한 내용·해시는 r3의 BUILD-INFO, Inventory 및 Verification 자료를 기준으로 확인한다.
|
||||
|
||||
과거 기록(r2): `HMDesignTest-Portable-Test-r2.zip`은 2026-10-08 최종 통합 PASS 빌드를 재사용하여 Button·Tabs·DatePicker/Combobox·Slider·Checkbox·Tooltip·Popover 개선, GNB 내부 Sidebar, Dashboard Status CSS 공개 경로, PageHeader 대표 Story를 반영한다. 당시 스냅샷·산출물 해시와 독립 실행 결과는 r2의 BUILD-INFO, Inventory 및 Verification 자료를 기준으로 확인한다. 사용자 실행 절차(위 3단계)와 AI/개발자 내부 빌드 절차(README-DEVELOPER의 `npm ci`, `build:packages`)는 서로 다르며 테스트 사용자는 후자를 수행하지 않는다.
|
||||
|
||||
## 개발자와 운영 문서의 구분
|
||||
|
||||
ZIP 루트 `README.md`는 위 세 단계만 테스트 실행 필수 절차로 안내한다. `README-DEVELOPER.md`와 기존 개발·운영 문서는 소스 수정, 재빌드, 장기 운영용이다. 개발자 절차의 npm 설치/빌드와 Registry/CDN/운영 업로드는 테스트 사용자의 실행 조건이 아니다. 이번 배포에서 운영 서버 업로드, STEP 7 AI 재현 테스트는 수행하지 않는다.
|
||||
|
||||
## AI 구현 재현성 검증 상태
|
||||
|
||||
STEP 7에서 ZIP 파일 접근 정책으로 실행하지 못한 모델 테스트는 실행 환경의 접근 제한으로 기록한다. 이를 AI의 HM 구현 실패나 지침 불이행 결과로 판정하지 않는다. 실제 모델별 화면 구현·재현성 비교 검증은 완료되지 않았으며, STEP 6의 ZIP 실행·Storybook 검증 통과가 모델별 구현 성공을 보증하지 않는다. STEP 8에서는 모델 테스트를 반복하지 않고 이 미검증 상태만 명시한다.
|
||||
|
||||
## 라이선스
|
||||
|
||||
공식 런타임의 전체 LICENSE와 README, 다운로드 URL 및 공식 SHA-256 증거를 `runtime/node/`에 보존한다. Node 라이선스는 저작권·허가 고지 보존 조건으로 배포를 허용한다. 원문: https://raw.githubusercontent.com/nodejs/node/v22.23.3/LICENSE . 검증 목록: https://nodejs.org/download/release/v22.23.3/SHASUMS256.txt . 폰트 OFL과 번들 의존성의 라이선스/NOTICE도 포함한다. HM/CENTER의 기존 내부 테스트 범위를 유지하며 별도 오픈소스 라이선스를 부여하지 않는다.
|
||||
@@ -0,0 +1,172 @@
|
||||
# Runtime / AI Independent Portability Policy
|
||||
|
||||
이 문서는 HM Design System 결과물을 React 외 실행 형식으로 옮길 때 지켜야 하는 공통 계약이다. 특정 AI 모델, IDE, 유료 기능, 프레임워크 또는 빌드 도구를 전제로 하지 않는다.
|
||||
|
||||
## 1. Source of Truth와 Target Runtime
|
||||
|
||||
디자인 시스템의 기준은 다음 순서다.
|
||||
|
||||
1. Design Token과 `themes/*.json`
|
||||
2. CSS Custom Properties와 Runtime Theme 계약
|
||||
3. Component visual state 및 interaction contract
|
||||
4. Component implementation
|
||||
5. Storybook/Preview
|
||||
|
||||
작업 시작 전에 Target Runtime을 기록한다. Source of Truth 우선순위는 `themes/default.json`/공개 Theme contract → Runtime-neutral Component/Pattern contract → CSS Custom Properties → Runtime adapter → Storybook/Preview 순서다.
|
||||
|
||||
- `REACT_VITE`: 현재 React/Vite workspace와 `@hm-design/ui` 공개 API를 사용한다.
|
||||
- `STATIC_HTML`: HTML을 `file://`로 직접 열 수 있어야 한다. JSX/TSX/TypeScript, Vite 전용 기능, bare package import를 사용하지 않는다. 저장소 내 샘플은 승인된 상대 Theme artifact와 상대 font/compatibility CSS를 사용하며, `file://`에서 `/src/...` 절대 경로를 사용하지 않는다.
|
||||
- `VANILLA_WEB`: 일반 웹 서버 또는 정적 호스팅에서 HTML/CSS/JavaScript로 실행한다.
|
||||
- `OTHER_RUNTIME`: Vue, Svelte, Angular, Web Component 등으로 변환하더라도 HM Token·Theme·Component Contract를 유지한다.
|
||||
|
||||
Runtime이 달라져도 디자인을 재해석하거나 React 구현을 삭제하지 않는다. 기존 구현을 Reference로 삼아 같은 Design Contract와 Theme Contract를 소비하는 adapter/standalone implementation을 추가한다.
|
||||
|
||||
## 2. 실제 HM 계약과 파일 경로
|
||||
|
||||
다음 파일을 추측으로 복제하거나 별도 값으로 재작성하지 않는다.
|
||||
|
||||
- `themes/default.json`: HM Default 컬러의 공식 원본
|
||||
- `packages/ui/src/theme/contract.ts`: 공개 Runtime Token, CSS property, 타입·검증 계약
|
||||
- `packages/ui/src/theme/runtime.ts`: Global/Scoped 적용·해제와 atomic validation
|
||||
- `packages/ui/src/theme/loader.ts`: URL fetch/resolve/load 경로
|
||||
- `packages/ui/src/theme.css`: package Theme CSS 진입점
|
||||
- `packages/ui/src/theme/default.css`, `primitives.css`, `<component>.css`: fallback, Semantic, Component Mapping
|
||||
- `packages/ui/package.json`: 공개 export인 `@hm-design/ui/theme`, `@hm-design/ui/theme.css`, `@hm-design/ui/tailwind.css`
|
||||
- `docs/guidelines/runtime-theme-contract.md`: Token별 CSS variable, 기본값, 영향 범위
|
||||
|
||||
React 소비 앱은 다음 순서로 HM CSS를 로드한다.
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "@hm-design/ui/tailwind.css";
|
||||
@import "@hm-design/ui/theme.css";
|
||||
```
|
||||
|
||||
Standalone 결과물은 승인된 Theme CSS artifact를 상대 경로로 연결하거나, 프로젝트가 생성한 standalone artifact를 포함한다. AI가 `#...` 색상, 임의 spacing/radius, Theme variable을 추측해 복제하지 않는다. 실제 변수명과 허용 입력은 Runtime 계약표를 기준으로 한다.
|
||||
|
||||
### 2.1 Tailwind v4 소비자 검색 범위
|
||||
|
||||
현재 저장소의 `package.json`과 lockfile은 `tailwindcss / @tailwindcss/vite` **4.3.3**을 사용하며, [Vite 설정](../../vite.config.ts)은 `react()`와 `tailwindcss()` plugin을 연결한다. 앱은 [src/index.css](../../src/index.css)의 Tailwind/HM Tailwind와 [src/hm-theme.css](../../src/hm-theme.css)의 HM Theme를 [main.tsx](../../src/main.tsx)에서 로드한다. 외부 React 소비 앱도 CSS entry를 실제 bootstrap에서 import해야 한다.
|
||||
|
||||
Tailwind는 기본적으로 실행 cwd를 기준으로 자동 검색하고 `node_modules`, `.gitignore` 대상 등을 제외한다. 외부 프로젝트의 junction 또는 넓은 실행 루트에서 검색 지연이 발생하면 실제 CSS 위치·실행 cwd·연결 폴더를 확인한 뒤 앱 소스 범위만 지정한다. `source("./")`는 CSS 파일의 위치를 기준으로 자동 검색 기준 경로를 정하는 지원 구문이며, 모든 junction 지연을 해결한다고 보증하지 않는다. 아래는 **소비 앱 CSS가 `src/index.css`이고 앱 코드가 `src/`에 있는 경우**의 예다.
|
||||
|
||||
```css
|
||||
@import "tailwindcss" source("./");
|
||||
@import "@hm-design/ui/tailwind.css";
|
||||
@import "@hm-design/ui/theme.css";
|
||||
/* Dashboard 상태를 표시하는 화면만 Theme 다음에 추가 */
|
||||
@import "@hm-design/ui/dashboard-status.css";
|
||||
@source "../index.html";
|
||||
```
|
||||
|
||||
CSS entry가 루트에 있으면 같은 `source("./")`는 루트 전체를 뜻하므로 제한 효과가 다르다. `src/` 밖 공유 UI가 있다면 그 실제 경로를 `@source`로 추가한다. 명시 목록으로 제한할 때는 `source(none)` + 필요한 `@source`도 지원하지만, 앱·공유 UI·HM source를 빠짐없이 지정해야 한다. [공식 Tailwind 소스 검색 문서](https://tailwindcss.com/docs/detecting-classes-in-source-files).
|
||||
|
||||
**HM Component 클래스 보존**: 공개 `@hm-design/ui/tailwind.css`를 생략하지 않는다. [패키지 소스](../../packages/ui/src/tailwind.css)는 `./components / ./patterns`를 검색하고, [기존 package build](../../scripts/build-package.mjs)는 배포 CSS에서 이를 `@source "./index.js"`로 바꾼다. 따라서 앱 자동 검색을 `src/`로 좁혀도 HM compiled 클래스는 해당 CSS의 명시 source를 통해 검색한다. 소비자가 소스 alias로 직접 연결한다면 배포 CSS와 경로가 같다고 가정하지 말고 실제 source 파일 존재를 확인한다. HM package 전체나 외부 `node_modules` 전체를 추가 검색 대상으로 삼지 않는다.
|
||||
|
||||
완료 시 앱 CSS import 순서, HM `@source` 대상 존재, 필요한 utility 생성 여부(예: GNB 모바일 trigger의 `lg:hidden`, Badge의 flex·크기 class)를 실제 소비 빌드에서 확인한다. 이 문서 작업에서는 버전·설정·구문 지원·source 연결만 대조했다. junction 지연 재현과 외부 앱 utility 생성은 미검증이며, 새 설정 파일·의존성·빌드는 추가하지 않았다.
|
||||
|
||||
## 3. 변환 시 보존해야 하는 계약
|
||||
|
||||
layout, spacing, typography, border, radius, icon 크기·정렬, responsive behavior와 hover/focus/active/selected/disabled/checked/error/loading 상태를 유지한다. 접근성 semantic element, keyboard behavior, ARIA 속성도 Target Runtime에 맞는 방식으로 보존한다.
|
||||
|
||||
프레임워크 기능은 Target Runtime의 최소 구현으로 변환한다.
|
||||
|
||||
- React state/props → 명시적 DOM state와 Vanilla JavaScript state
|
||||
- event handler → `addEventListener`
|
||||
- conditional className → class/attribute toggle
|
||||
- component composition → semantic HTML structure
|
||||
- package icon → 배포 가능한 SVG 또는 허용된 icon artifact
|
||||
|
||||
단순 layout·typography·color·responsive 표현은 JavaScript에 의존하지 않는다. JavaScript는 accordion, dropdown, modal, tabs 등 실제 interaction에만 추가한다.
|
||||
|
||||
### 3.1 Dashboard 상태 계약
|
||||
|
||||
Dashboard 업무 상태의 Runtime-neutral 기준은 `packages/ui/src/patterns/dashboard-status/contract.ts`다. 별도 Runtime은 다음을 동일하게 유지한다.
|
||||
|
||||
- 상태 ID: `active`, `hold`, `done`; 필터 ID: `all`, `active`, `hold`, `done`, `issue`
|
||||
- `issue`는 독립 Boolean flag이며 상태 값과 동시에 존재할 수 있음
|
||||
- `done` label은 `완료`
|
||||
- `all`은 모든 record, `issue`는 `issue === true`, 나머지는 `record.status === filter`로 매칭
|
||||
- 상태 표시에는 `data-status`, 이슈에는 `data-issue="true"`, 필터 컨트롤에는 `data-filter`, 선택에는 `aria-pressed="true|false"` 사용
|
||||
- CSS 원본은 `packages/ui/src/patterns/dashboard-status/dashboard-status.css`이며, 패키지 소비자는 `@hm-design/ui/theme.css` 다음에 `@hm-design/ui/dashboard-status.css`를 로드한다. 기존 `src/patterns/dashboard-status-contract.css`는 같은 원본을 import하는 호환 경로다. 상태 CSS는 Custom Property만 정의하므로 Badge와 필터 숫자 등의 색상 mapping은 Runtime adapter에서 연결하며 색상 literal을 복제하지 않는다. active/hold/issue 값은 기존 Story 시각 보존용 compatibility mapping이며 HM Theme JSON을 변경하지 않는다. React/HTML의 `done`은 모두 `--hm-dashboard-status-done`을 사용하고, `:root` 및 `[data-hm-theme]`에서 해당 scope의 HM white token으로 재계산한다. [적용 순서와 Badge mapping](../patterns/dashboard-layout.md#dashboard-업무-상태-공통-계약).
|
||||
|
||||
React는 이 계약을 `Badge`와 `Button`/기존 `FilterBar` 조합으로 표현한다. Static HTML은 같은 attribute를 가진 semantic button/span과 `addEventListener` 기반 필터링으로 표현하며, `aria-controls`와 결과 count의 갱신도 유지한다. `selected`, `hover`, `focus-visible`, `disabled`는 CSS와 native button semantics로 보존한다.
|
||||
|
||||
### 3.2 Static HTML 실행 체크리스트
|
||||
|
||||
1. `docs/patterns/dashboard-status-static.html`을 브라우저에서 직접 `file:///...`로 연다.
|
||||
2. `../../packages/ui/dist/theme.css`, `../../src/patterns/dashboard-status-contract.css`, `../../public/fonts/NotoSansKR-variable.ttf`가 모두 로드되는지 확인한다.
|
||||
3. 전체/진행/보류/완료/이슈 버튼의 `aria-pressed`와 결과 count를 확인한다.
|
||||
4. 이슈 record가 상태 Badge와 독립 이슈 flag를 동시에 표시하는지 확인한다.
|
||||
5. 결과가 0건인 필터에서 Empty State와 `aria-live` 갱신을 확인한다.
|
||||
6. DevTools Console에 오류가 없는지 확인한다. 자동화 환경이 `file://`를 차단하면 PASS로 기록하지 않고 미검증으로 남긴다.
|
||||
|
||||
### 3.3 AI 독립 재현 체크리스트
|
||||
|
||||
이전 대화 없이도 다음 파일만으로 재현 가능해야 한다: `AGENTS.md`, `packages/ui/src/patterns/dashboard-status/contract.ts`, `src/patterns/dashboard-status-contract.css`, `src/patterns/dashboard-layout.stories.tsx`, `docs/patterns/dashboard-status-static.html`, `packages/ui/dist/theme.css`, `docs/guidelines/runtime-theme-contract.md`. 구현 후 상태 ID/label, matching/count, DOM attributes, Theme artifact 상대 경로, React/Static HTML 차이, `file://` 검증 결과를 각각 보고한다.
|
||||
|
||||
### 3.2 Button 사용 계약
|
||||
|
||||
Button의 공통 기준은 [Button 문서](../components/button.md)의 역할·조합 규칙을 따른다. `brand`는 실제 화면당 최대 1개이고 팝업에는 사용하지 않으며, 일반 확인/작성은 같은 `size`의 Solid 세트, 삭제 확인과 반복·보조 작업은 같은 `size`의 Outline 세트를 사용한다. Static HTML과 다른 프레임워크는 React API를 복제하지 않아도 되지만, semantic button, `data-slot`/`data-variant`/`data-appearance`/`data-size` 계약, 기존 Theme CSS variable, native disabled 및 keyboard/modal 동작을 보존해야 한다.
|
||||
|
||||
## 4. Asset과 실행 위치
|
||||
|
||||
`STATIC_HTML`은 Vite asset resolution을 사용하지 않는다. HTML과 함께 이동 가능한 상대 경로를 사용한다.
|
||||
|
||||
```text
|
||||
./assets/
|
||||
./styles/
|
||||
./fonts/
|
||||
./icons/
|
||||
```
|
||||
|
||||
CSS, JavaScript, SVG/image/icon, font, theme stylesheet를 모두 확인한다. package의 compiled CSS가 사용하는 font 경로와 source 개발 서버의 경로가 다를 수 있으므로, package 산출물 사용 시 package `dist`와 export 계약을 우선한다. 현재 source `packages/ui/src/theme.css`의 개발용 font 경로는 `/fonts/NotoSansKR-variable.ttf`이며 App/Storybook은 `public/fonts/`를 사용한다.
|
||||
|
||||
### 4.1 Noto Sans KR 경로: 실행 환경별 확인
|
||||
|
||||
- **HMDesignSystem 개발 저장소**: 원본은 `public/fonts/NotoSansKR-variable.ttf`, 라이선스는 같은 폴더의 `NotoSansKR-OFL.txt`다. Theme source의 `@font-face`는 `/fonts/NotoSansKR-variable.ttf`를 사용한다. Vite는 `public/`을 루트에서 제공하고 Storybook은 [.storybook/main.ts](../../.storybook/main.ts)의 `staticDirs`로 `/fonts`에 연결한다. 이 경로는 해당 서버 환경의 계약이다.
|
||||
- **포터블 테스트 ZIP(현재 기준 r3, 아래는 r2 시점 확인 기록)**: r2 ZIP 안 `HMDesignTest/public/fonts/`, `HMDesignTest/packages/ui/dist/fonts/`, `HMDesignTest/storybook-static/fonts/`에 기존 폰트와 라이선스가 모두 있다. 포터블 실행기는 사전 빌드된 Storybook을 제공하므로 실행 화면은 그 안의 font/CSS asset을 사용한다. 개발 폴더의 `/fonts` 경로를 다른 실행 위치에 그대로 적용한다고 가정하지 않는다. r2는 문서 개선 이전 과거 스냅샷으로 보존하며 현재 기준이 아니다. r3의 폰트 포함 여부는 r3 Inventory로 별도 확인한다.
|
||||
- **별도 React 소비자**: `@hm-design/ui/theme.css`는 공개 export인 `dist/theme.css`다. 기존 build가 원본 font-face URL을 `./fonts/NotoSansKR-variable.ttf`로 바꾸고 폰트·OFL 파일을 `dist/fonts/`로 복사한다. 정상 package를 CSS import로 소비할 때 별도 앱 `/fonts` 복사에 의존하지 않는다. Vite가 CSS `url()` 자산을 처리하므로 최종 배포 `base`와 출력 URL을 확인한다. Theme 소스 파일만 복사하거나 CSS만 분리하면 이 계약이 사라지므로 실제 폰트와 상대 경로도 함께 배치한다. [Vite 공식 Asset 문서](https://vite.dev/guide/assets.html).
|
||||
- **단일 `preview.html`**: `file://`에서는 `/fonts/...`가 HTML 옆 폴더를 뜻하지 않는다. 파일과 폴더를 함께 전달한다면 상대 CSS/font URL을 사용하고, CSS 안 URL은 HTML이 아닌 해당 CSS 파일 위치 기준으로 계산한다. [기존 Static HTML](../patterns/dashboard-status-static.html)은 상대 package Theme와 별도 상대 font-face를 사용하는 **다중 파일** Reference다. HTML 하나만 전달하려면 승인된 Theme·소비 CSS의 import를 모두 펼쳐 inline하고 폰트·아이콘·이미지 등 외부 자산도 내장한다. Tailwind 입력 CSS의 `@source / @apply`나 bare package import를 브라우저에 그대로 전달하지 않는다.
|
||||
|
||||
단일 HTML의 font-face는 기존 TTF를 data URL로 내장할 수 있다. 아래는 저장소 루트에서 기존 폰트로 **삽입할 CSS 문자열만 만드는** PowerShell 예이며, Theme/다른 Asset의 내장을 대신하지 않는다.
|
||||
|
||||
```powershell
|
||||
$fontPath = Join-Path (Get-Location) 'public/fonts/NotoSansKR-variable.ttf'
|
||||
$fontData = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($fontPath))
|
||||
$fontRule = '@font-face { font-family: "Noto Sans KR"; src: url("data:font/ttf;base64,' + $fontData + '") format("truetype"); font-style: normal; font-weight: 100 900; font-display: swap; }'
|
||||
# 기존 font-face를 $fontRule로 교체해 preview.html의 <style>에 삽입한다.
|
||||
```
|
||||
|
||||
Font Family `Noto Sans KR`, 기존 variable weight `100 900`, 폰트 파일·라이선스를 유지한다. 폴더 배포에는 OFL을 함께 전달하고 단일 HTML 배포에는 기존 OFL 전문을 HTML comment 등으로 포함한다. base64는 파일 크기를 늘리므로 내장 여부는 전달 형식에 따라 선택한다. 최종 font 요청·fallback 여부·다른 Asset 로드와 `file://` 실행은 소비 결과물에서 검증하며, 이번 문서 작업에서 새 HTML 생성·실행은 하지 않았다.
|
||||
|
||||
## 5. Target Runtime별 최소 검증
|
||||
|
||||
### 5.1 React Dashboard standalone export
|
||||
|
||||
설치 없는 테스트 ZIP은 사전 빌드된 Storybook의 Dashboard 대표 Story를 제공한다. 독립 Dashboard export는 기존 배포 필수 구성이 아니며, STEP 5에서 UI Preview로 표시된 미검증 `dist-dashboard/`는 포터블 ZIP에서 제외한다. 아래는 개발자용 별도 export 계약이며 해당 빌드 결과의 성공을 보증하지 않는다. [포터블 테스트 안내](portable-test-distribution.md). 이 `dist-dashboard/` export는 H-1(독립 단일 HTML 미리보기 POC, PASS)의 성공 산출물과 동일하지 않다. H-2(AI 생성 화면의 자동 빌드·캡처·검증)는 Storybook 배포와 별도 기능이며 구현 완료 여부를 이 문서가 보증하지 않는다.
|
||||
|
||||
Dashboard Production 화면은 Storybook Story를 import하지 않는 독립 React composition을 소비하는 Vite entry로 생성한다. Storybook은 구조·상태의 Reference이며 Production export의 CSS 또는 bootstrap source가 아니다.
|
||||
|
||||
- Source: `src/patterns/dashboard-composition.tsx`의 `DashboardScreen`
|
||||
- Entry: `src/exports/dashboard/index.html` + `src/exports/dashboard/main.tsx`
|
||||
- Build: `npm run build:dashboard-export`
|
||||
- Output: `dist-dashboard/`
|
||||
- CSS entry 순서: `@/index.css` → `@/hm-theme.css` → `@hm-design/ui/dashboard-status.css` → `./dashboard-export.css`. `@/index.css`가 Tailwind 본체와 HM Tailwind CSS를 로드하므로 `@hm-design/ui/tailwind.css`만 단독 import하지 않는다.
|
||||
- Runtime: React compiled IIFE bundle, classic `defer` script, dynamic import disabled, relative asset base
|
||||
|
||||
이 export는 Storybook manager·Story CSS·개발 서버에 의존하지 않는다. `DashboardScreen`은 Dashboard Production Composition을 직접 사용하며, `dist-dashboard/index.html`은 HTTP 정적 서버와 `file://` 직접 실행을 각각 별도로 검증한다. HTTP 성공은 `file://` 성공을 의미하지 않는다. 빌드 성공 또는 CSS 텍스트의 선택자 존재만으로 화면 검증을 통과시키지 말고, 실제 렌더링에서 `.flex`·`.grid` 등의 utility, Theme CSS Variables, Dashboard Status CSS 및 Header/Layout/Card/FilterBar 레이아웃 적용을 확인한다.
|
||||
|
||||
- `REACT_VITE`: Vite 실행, Theme 적용, component rendering, interaction, responsive behavior를 확인한다.
|
||||
- `STATIC_HTML`: 실제 HTML 파일을 `file://`로 직접 열어 CSS/JavaScript/font/SVG/image/icon 로드, HM CSS Custom Properties, responsive behavior, interaction, console runtime error를 확인한다. Vite dev server 성공만으로 완료 처리하지 않는다.
|
||||
- `VANILLA_WEB`/`OTHER_RUNTIME`: 실제 실행 환경에서 Theme Contract, visual/state/interaction/responsive parity와 asset resolution을 확인한다.
|
||||
|
||||
검증은 전체 회귀보다 변환된 범위의 최소 실행 검증을 우선한다. 확인이 불가능한 항목은 성공으로 간주하지 말고 차이와 제한을 기록한다.
|
||||
|
||||
## 6. 구현 및 AI 독립성
|
||||
|
||||
작업에 필요한 정보는 저장소의 코드, Theme artifact, 문서, package export와 Reference Story에 있어야 한다. 이전 AI 대화, Memory, 특정 IDE/Agent 전용 기능을 전제로 하지 않는다. 구현 편의를 위해 Theme이나 Component visual contract를 변경하지 않는다.
|
||||
|
||||
우선순위는 `HM Theme/Token 정확성 → visual parity → state parity → responsive behavior → accessibility → interaction → runtime 편의성`이다.
|
||||
|
||||
관련 문서: [Runtime Theme](runtime-theme.md), [Runtime 계약표](runtime-theme-contract.md), [Theme delivery](theme-delivery.md), [Theme/Token 가이드](테마-토큰-가이드.md), [개발 적용 가이드](개발-적용-가이드.md).
|
||||
@@ -0,0 +1,98 @@
|
||||
# Public Runtime Theme Contract
|
||||
|
||||
계약 Source of Truth: `packages/ui/src/theme/contract.ts`. 모든 입력은 선택적이며 외부 override 가능. Component/derived 변수는 공개 입력이 아니다.
|
||||
|
||||
| Runtime JSON Token | CSS Token | Default | 의미 | 외부 override | 주요 영향 |
|
||||
|---|---|---|---|---|---|
|
||||
| `color.bg.content` | `--hm-sem-bg-content` | `#f5f0e5` | 콘텐츠 면 | Yes | gnb, lnb |
|
||||
| `color.bg.surface` | `--hm-sem-bg-surface` | `#ffffff` | 기본/카드/입력 면 | Yes | checkbox, radio, switch, slider, badge, table, input, select, button, gnb |
|
||||
| `color.fg.primary` | `--hm-sem-fg-primary` | `#0a221d` | 주요 텍스트 | Yes | calendar, pagination, badge, table, input, select, gnb |
|
||||
| `color.fg.secondary` | `--hm-sem-fg-secondary` | `#6e6759` | 보조 텍스트 | Yes | calendar, pagination, badge, table, gnb |
|
||||
| `color.fg.placeholder` | `--hm-sem-fg-placeholder` | `#999999` | Placeholder | Yes | input, select |
|
||||
| `color.action.primary` | `--hm-sem-action-primary` | `#123328` | Strong / Primary ink | Yes | checkbox/radio hover, slider Hover/Drag halo, pagination current, badge default, button outline, navigation current ink |
|
||||
| `color.action.cancel` | `--hm-sem-action-cancel` | `#6e6759` | 공개 취소 역할 | Yes | Button `variant="cancel"` + `appearance="outline"` |
|
||||
| `color.action.destructive` | `--hm-sem-action-destructive` | `#d32f2f` | 오류/위험 역할 | Yes | checkbox, radio, badge, input, button |
|
||||
| `color.stroke.default` | `--hm-sem-stroke-default` | `#9a9284` | 기본 border | Yes | checkbox, radio, switch, badge |
|
||||
| `color.stroke.input.default` | `--hm-sem-stroke-input-default` | `#dedbd2` | 입력/패널 divider border | Yes | table, input, select |
|
||||
| `color.stroke.input.focus` | `--hm-sem-stroke-input-focus` | `#266651` | 공통 Focus border/ring | Yes | input, select, button, checkbox, calendar, slider Keyboard Focus halo 등; GNB/LNB는 contextual 예외 |
|
||||
| `color.bg.brand.stop.0` | `--hm-sem-brand-bg-stop-0` | `#0a221d` | 기존 gradient stop: color.bg.brand.stop.0 | Yes | Theme palette / adapter |
|
||||
| `color.bg.brand.stop.12` | `--hm-sem-brand-bg-stop-12` | `#071e18` | 기존 gradient stop: color.bg.brand.stop.12 | Yes | Theme palette / adapter |
|
||||
| `color.bg.brand.stop.42` | `--hm-sem-brand-bg-stop-42` | `#123328` | 기존 gradient stop: color.bg.brand.stop.42 | Yes | Theme palette / adapter |
|
||||
| `color.bg.brand.stop.78` | `--hm-sem-brand-bg-stop-78` | `#266651` | 기존 gradient stop: color.bg.brand.stop.78 | Yes | Theme palette / adapter |
|
||||
| `color.bg.brand.stop.100` | `--hm-sem-brand-bg-stop-100` | `#205241` | 기존 gradient stop: color.bg.brand.stop.100 | Yes | Theme palette / adapter |
|
||||
| `surface.action.primary.stop.0` | `--hm-sem-action-surface-stop-0` | `#1f8265` | 기존 gradient stop: surface.action.primary.stop.0 | Yes | button, gnb |
|
||||
| `surface.action.primary.stop.30` | `--hm-sem-action-surface-stop-30` | `#165c47` | 기존 gradient stop: surface.action.primary.stop.30 | Yes | button, gnb |
|
||||
| `surface.action.primary.stop.65` | `--hm-sem-action-surface-stop-65` | `#114838` | 기존 gradient stop: surface.action.primary.stop.65 | Yes | button, gnb |
|
||||
| `surface.action.primary.stop.100` | `--hm-sem-action-surface-stop-100` | `#08241c` | 기존 gradient stop: surface.action.primary.stop.100 | Yes | button, gnb |
|
||||
| `surface.action.primary.border.stop.0` | `--hm-sem-action-border-stop-0` | `#0a221d` | 기존 gradient stop: surface.action.primary.border.stop.0 | Yes | button |
|
||||
| `surface.action.primary.border.stop.50` | `--hm-sem-action-border-stop-50` | `#34daaa` | 기존 gradient stop: surface.action.primary.border.stop.50 | Yes | button |
|
||||
| `surface.action.primary.border.stop.100` | `--hm-sem-action-border-stop-100` | `#08241c` | 기존 gradient stop: surface.action.primary.border.stop.100 | Yes | button |
|
||||
| `radius.input.default` | `--hm-radius-input` | `4px` | Input/Select radius | Yes | input, select |
|
||||
| `radius.container.default` | `--hm-radius-container` | `4px` | Container radius | Yes | Theme palette / adapter |
|
||||
| `color.state.active` | `--hm-sem-state-active` | `#266651` | Checked / Selected / ON fill | Yes | checkbox, radio, switch, slider, progress, calendar |
|
||||
| `color.on.state.active` | `--hm-sem-on-state-active` | `#ffffff` | Active 위 텍스트/indicator | Yes | checkbox, calendar |
|
||||
| `color.fg.inverse` | `--hm-sem-on-action-primary` | `#ffffff` | 반전 텍스트 | Yes | pagination, badge, button, gnb, lnb |
|
||||
| `color.bg.subtle` | `--hm-sem-bg-subtle` | `#f4f1ea` | 은은한 면 | Yes | calendar, badge, table |
|
||||
| `color.accent` | `--hm-sem-accent` | `#bddfd4` | Soft Highlight 45% / Selected·Open surface 100% 원색 | Yes | select, combobox, menu item, table, calendar, pagination hover / toggle, menubar, navigation-menu trigger |
|
||||
| `color.on.accent` | `--hm-sem-on-accent` | `#123328` | Accent 위 텍스트; Primary와 독립 sibling | Yes | select, combobox, menu item, calendar, pagination hover, toggle, menubar, navigation-menu |
|
||||
| `color.palette.green-light-4` | `--hm-sem-palette-green-light-4` | `#34daaa` | 선택적 Theme 팔레트: green-light-4 | Yes | button |
|
||||
| `color.palette.green-light-3` | `--hm-sem-palette-green-light-3` | `#249473` | 선택적 Theme 팔레트: green-light-3 | Yes | data-table Sort Direction Accent, tabs 장식, lnb gradient |
|
||||
| `color.palette.green-light-2` | `--hm-sem-palette-green-light-2` | `#266651` | 선택적 Theme 팔레트: green-light-2 | Yes | checkbox, radio, switch, slider, progress, calendar, table, input, select, gnb, lnb |
|
||||
| `color.palette.green-light-1` | `--hm-sem-palette-green-light-1` | `#205241` | 선택적 Theme 팔레트: green-light-1 | Yes | table, button, lnb |
|
||||
| `color.palette.green-default` | `--hm-sem-palette-green-default` | `#123328` | 선택적 Theme 팔레트: green-default | Yes | checkbox, radio, slider, calendar, pagination, badge, select, button, gnb, lnb |
|
||||
| `color.palette.green-dark-1` | `--hm-sem-palette-green-dark-1` | `#0a221d` | 선택적 Theme 팔레트: green-dark-1 | Yes | calendar, pagination, badge, table, input, select, button, gnb |
|
||||
| `color.palette.green-dark-2` | `--hm-sem-palette-green-dark-2` | `#071e18` | 선택적 Theme 팔레트: green-dark-2 | Yes | Theme palette / adapter |
|
||||
| `color.palette.brown-light-2` | `--hm-sem-palette-brown-light-2` | `#897445` | 선택적 Theme 팔레트: brown-light-2 | Yes | button |
|
||||
| `color.palette.brown-light-1` | `--hm-sem-palette-brown-light-1` | `#604f32` | 선택적 Theme 팔레트: brown-light-1 | Yes | button |
|
||||
| `color.palette.brown-default` | `--hm-sem-palette-brown-default` | `#3e3523` | 선택적 Theme 팔레트: brown-default | Yes | button |
|
||||
| `color.palette.brown-dark-1` | `--hm-sem-palette-brown-dark-1` | `#352d1d` | 선택적 Theme 팔레트: brown-dark-1 | Yes | button |
|
||||
| `color.palette.brown-light-4` | `--hm-sem-palette-brown-light-4` | `#ecd4a2` | 선택적 Theme 팔레트: brown-light-4 | Yes | Theme palette / adapter |
|
||||
| `color.palette.orange-light-4` | `--hm-sem-palette-orange-light-4` | `#feba88` | 선택적 Theme 팔레트: orange-light-4 | Yes | button |
|
||||
| `color.palette.orange-default` | `--hm-sem-palette-orange-default` | `#cc5200` | 선택적 Theme 팔레트: orange-default | Yes | button |
|
||||
| `color.palette.warm-neutral-light` | `--hm-sem-palette-warm-neutral-light` | `#e0dace` | 선택적 Theme 팔레트: warm-neutral-light | Yes | slider, progress |
|
||||
| `color.palette.warm-neutral-mid` | `--hm-sem-palette-warm-neutral-mid` | `#9a9284` | 선택적 Theme 팔레트: warm-neutral-mid | Yes | checkbox, radio, switch, badge |
|
||||
| `color.palette.warm-neutral-dark` | `--hm-sem-palette-warm-neutral-dark` | `#6e6759` | 선택적 Theme 팔레트: warm-neutral-dark | Yes | calendar, pagination, badge, table, gnb |
|
||||
| `color.palette.beige-light` | `--hm-sem-palette-beige-light` | `#f5f0e5` | 선택적 Theme 팔레트: beige-light | Yes | gnb, lnb |
|
||||
| `color.palette.red-danger` | `--hm-sem-palette-red-danger` | `#d32f2f` | 선택적 Theme 팔레트: red-danger | Yes | checkbox, radio, badge, input, button |
|
||||
| `color.palette.white` | `--hm-sem-palette-white` | `#ffffff` | 선택적 Theme 팔레트: white | Yes | checkbox, radio, switch, slider, calendar, pagination, badge, table, input, select, button, gnb, lnb |
|
||||
| `color.palette.black` | `--hm-sem-palette-black` | `#000000` | 선택적 Theme 팔레트: black | Yes | checkbox, switch, select, button, gnb, lnb |
|
||||
| `color.palette.neutral-gray-500` | `--hm-sem-palette-neutral-gray-500` | `#999999` | 선택적 Theme 팔레트: neutral-gray-500 | Yes | input, select |
|
||||
| `color.palette.green-tone-1` | `--hm-sem-palette-green-tone-1` | `#1f8265` | 선택적 Theme 팔레트: green-tone-1 | Yes | button, gnb |
|
||||
| `color.palette.green-tone-2` | `--hm-sem-palette-green-tone-2` | `#165c47` | 선택적 Theme 팔레트: green-tone-2 | Yes | button, gnb |
|
||||
| `color.palette.green-tone-3` | `--hm-sem-palette-green-tone-3` | `#114838` | 선택적 Theme 팔레트: green-tone-3 | Yes | button, gnb |
|
||||
| `color.palette.green-tone-4` | `--hm-sem-palette-green-tone-4` | `#08241c` | 선택적 Theme 팔레트: green-tone-4 | Yes | button, gnb |
|
||||
| `color.palette.warm-neutral-pale` | `--hm-sem-palette-warm-neutral-pale` | `#dedbd2` | 선택적 Theme 팔레트: warm-neutral-pale | Yes | table, input, select |
|
||||
| `color.palette.warm-neutral-secondary` | `--hm-sem-palette-warm-neutral-secondary` | `#ede8db` | 선택적 Theme 팔레트: warm-neutral-secondary | Yes | Theme palette / adapter |
|
||||
| `color.palette.warm-neutral-muted` | `--hm-sem-palette-warm-neutral-muted` | `#f4f1ea` | 선택적 Theme 팔레트: warm-neutral-muted | Yes | calendar, badge, table |
|
||||
| `color.palette.green-tint` | `--hm-sem-palette-green-tint` | `#bddfd4` | 선택적 Theme 팔레트: green-tint | Yes | calendar, pagination, table, select, gnb, lnb |
|
||||
| `color.palette.yellow-accent` | `--hm-sem-palette-yellow-accent` | `#ffc600` | 선택적 Theme 팔레트: yellow-accent | Yes | lnb |
|
||||
| `color.palette.brown-deep` | `--hm-sem-palette-brown-deep` | `#221d13` | 선택적 Theme 팔레트: brown-deep | Yes | button |
|
||||
| `color.palette.brown-highlight` | `--hm-sem-palette-brown-highlight` | `#a98d53` | 선택적 Theme 팔레트: brown-highlight | Yes | button |
|
||||
| `color.palette.orange-light` | `--hm-sem-palette-orange-light` | `#d35a00` | 선택적 Theme 팔레트: orange-light | Yes | button |
|
||||
| `color.palette.orange-mid` | `--hm-sem-palette-orange-mid` | `#ce5600` | 선택적 Theme 팔레트: orange-mid | Yes | button |
|
||||
| `color.palette.orange-dark` | `--hm-sem-palette-orange-dark` | `#b44b00` | 선택적 Theme 팔레트: orange-dark | Yes | button |
|
||||
| `color.palette.orange-deep` | `--hm-sem-palette-orange-deep` | `#783402` | 선택적 Theme 팔레트: orange-deep | Yes | button |
|
||||
| `color.palette.orange-border` | `--hm-sem-palette-orange-border` | `#bc4c00` | 선택적 Theme 팔레트: orange-border | Yes | button |
|
||||
| `color.palette.orange-border-deep` | `--hm-sem-palette-orange-border-deep` | `#5e2902` | 선택적 Theme 팔레트: orange-border-deep | Yes | button |
|
||||
| `color.palette.gray` | `--hm-sem-palette-gray` | `#767676` | 선택적 Theme 팔레트: gray | Yes | button |
|
||||
| `color.palette.gray-dark` | `--hm-sem-palette-gray-dark` | `#5a5a5a` | 선택적 Theme 팔레트: gray-dark | Yes | button |
|
||||
| `color.palette.indicator` | `--hm-sem-palette-indicator` | `#2d2d2d` | 선택적 Theme 팔레트: indicator | Yes | select |
|
||||
|
||||
총 73개. 기존 25개 + 핵심 역할 6개 + 선택적 팔레트 42개. 팔레트는 컴포넌트별 계약이 아니며 Primary / Active / Focus를 개별 지정하면 팔레트 alias보다 우선한다. Focus와 Active, Primary와 On Accent는 각각 sibling이며 서로 alias하지 않는다.
|
||||
|
||||
`color.action.cancel`은 Button의 `variant="cancel"` Outline ink를 위한 Public Runtime role로 사용한다. Solid 취소는 기존 gray Component mapping을 유지하고, Outline 취소는 이 semantic role을 소비한다. 외부 소비 가능성이 있으므로 deprecated로 간주하지 않는다. 적용·병합·fallback·scope 정책은 [Runtime Theme](runtime-theme.md)를 따른다.
|
||||
# 디자인팀 Runtime 운영 추가 결정 — 2026-10-06
|
||||
|
||||
기존 v1 입력 73개와 CSS property/type 계약을 유지한다. 공개 API `isRuntimeTheme(unknown): value is RuntimeTheme`를 제공한다. 이 사전 검증은 DOM을 변경하지 않는다. 기존 `applyRuntimeTheme`의 invalid 입력 시 reset 정책은 유지한다. 편집기는 사전 검증으로 invalid Import를 차단해 마지막 유효 Theme를 보존한다.
|
||||
|
||||
Default 컬러의 운영 원본은 `themes/default.json`(71개 컬러)이다. `scripts/theme-defaults.mjs`가 JSON과 컬러 literal 없는 alias/geometry template에서 package `primitives.css`, `default.css`, `default-values.generated.ts`를 생성한다. `contract.ts`의 defaultValue와 `defaultRuntimeTheme`는 생성 데이터를 읽는다. 2개 radius/geometry는 template에서 유지한다. CSS fallback/현재 색을 유지하고 세 가지 최종 명령의 pre hook에서 stale 생성 데이터를 검사한다. Theme-only 산출은 `npm run build:themes`로 분리한다.
|
||||
|
||||
Storybook `Theme/Designer Playground/Editor`는 17개 상위 컬러 역할과 JSON URL/파일 Preview를 제공하는 소비자다. Global decorator에서 `resolveRuntimeTheme(theme, defaultJson)` 후 Production `applyRuntimeTheme(document.documentElement, theme)` → CSS Custom Properties → 기존 Mapping으로 적용한다. JSON Export/Import는 동일 v1 계약이다. 컬러 artifact/편집기만 radius를 거부한다. Production API의 기존 radius/gradient 계약은 유지한다.
|
||||
|
||||
추가 Production API는 `fetchRuntimeTheme(url, request?)`, `resolveRuntimeTheme(theme, defaults?)`, `loadRuntimeTheme(root,url,options?)`다. fetch는 DOM과 분리되며 validation은 기존 `isRuntimeTheme`을 재사용한다. Loader 실패는 HM package fallback, partial은 같은 계약으로 교체/병합하며 superseded 요청은 새 Theme를 덮거나 reset하지 않는다. 회사 CDN URL은 하드코딩하지 않는다.
|
||||
|
||||
사용법: [디자인팀 Theme 가이드](designer-theme-guide.md), [Theme 운영/배포](theme-delivery.md). 적용·fallback·scope 정책: [Runtime Theme](runtime-theme.md).
|
||||
|
||||
## Dashboard 상태 색상 범위
|
||||
|
||||
Dashboard의 `active / hold / done / issue` 업무 상태 계약은 `packages/ui/src/patterns/dashboard-status/contract.ts`에서 ID·label·tone을 제공한다. 상태 변수 원본은 `packages/ui/src/patterns/dashboard-status/dashboard-status.css`이며 `@hm-design/ui/dashboard-status.css`로 배포한다. `src/patterns/dashboard-status-contract.css`는 기존 Story/HTML 경로의 호환 import다. Theme CSS 다음에 상태 CSS를 로드하고 `--hm-dashboard-status-active|hold|done|issue`를 화면 요소의 색상에 별도로 연결한다. 상태 CSS 자체는 Badge나 `data-status`를 자동으로 꾸미지 않는다. active/hold/issue는 기존 Story 색상을 보존하는 compatibility mapping이고 done은 `:root` 및 `[data-hm-theme]`에서 해당 scope의 기존 HM white token을 참조한다. 기존 HM Theme 및 Public Runtime Token/JSON 73개는 변경하지 않으며, 실제 Production 소비처가 반복되어 공통 색상 의미가 확정될 때만 Semantic Token 승격을 검토한다.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,193 @@
|
||||
# External Runtime Theme 구조 점검·전환 결과
|
||||
|
||||
> HISTORICAL_RECORD: 작성·검증 시점(2026-10-06)의 기록이다. 아래 리터럴·실측·당시 예외를 보존한다. 현재 apply/merge/fallback/scope와 Slider Focus는 [Runtime Theme](runtime-theme.md), key/property/type은 [Runtime 계약표](runtime-theme-contract.md), CDN 배포는 [Theme delivery](theme-delivery.md)를 따른다.
|
||||
|
||||
2026-10-06 · HM_THEME / Theme infrastructure
|
||||
|
||||
## 1. 현재 Theme Architecture
|
||||
|
||||
Primitive(`packages/ui/src/theme/primitives.css`) → HM Default preset / Semantic / shadcn adapter(`default.css`) → Component Mapping(`theme/<name>.css`) → CSS selector / Tailwind → UI. `theme.css`가 Default부터 import한다. Tailwind namespace 매핑은 `tailwind.css`, Demo artwork는 `src/hm-theme.css`, Storybook 설정은 `.storybook/preview.tsx`다.
|
||||
|
||||
대표 체인을 실제 파일에서 확인했다. 모든 Component Token의 기존 이름을 유지했다.
|
||||
|
||||
| Component | 실제 대표 체인 |
|
||||
|---|---|
|
||||
| Checkbox | green-light-2 → sem-state-active → checkbox-checked-background → checked background |
|
||||
| Radio | green-light-2 → sem-state-active → radio-checked-border-color / radio-dot-color |
|
||||
| Switch | green-light-2 → sem-state-active → switch-on-background |
|
||||
| Slider | green-light-2 → sem-state-active → slider-range-background / thumb-border-color |
|
||||
| Progress | green-light-2 → sem-state-active → progress-indicator-background |
|
||||
| Calendar | green-light-2 → sem-state-active → calendar-selected-background / range-edge-background |
|
||||
| Pagination | green-default → sem-action-primary → pagination-active-background |
|
||||
| Badge | green-default → sem-action-primary → badge-default-background |
|
||||
| Table | sem-accent → sem-surface-highlight(45%) → table-row-hover/selected-background |
|
||||
| Input | sem-stroke-input-focus → input-focus-border-color; sem-action-destructive → input-error-border-color |
|
||||
| Select | sem-accent → sem-surface-highlight → select-option-state-background; sem-stroke-input-focus → focus border |
|
||||
| Button | sem-action-primary → button-outline-green-ink; primary gradient stops → button-brand-green-background |
|
||||
| GNB | primary surface stop group → gnb-background; sem-action-primary → gnb-current-text |
|
||||
| LNB | Primary + palette shades → lnb-background; sem-action-primary → lnb-current-text |
|
||||
|
||||
## 2. Runtime Theme 기존 방식
|
||||
|
||||
기존 `src/theme/preview.ts`는 schemaVersion 1 / 25개 allowlist / 6자리 HEX / radius 2–12px / gradient 전체 세트 검증을 제공했다. 전체 문서를 검증한 뒤 HTMLElement inline 변수에 적용하고 이전 주입 키를 제거했다. **실제 호출처는 없었고 서비스 패키지에서 import할 수 없었다.** 새 엔진 없이 이 방식을 UI 패키지로 이동·보강했다.
|
||||
|
||||
## 3. 발견된 문제와 처리
|
||||
|
||||
- Active, on Active, Accent, Subtle Surface, Inverse Text 계약 누락 → 공개 계약에 추가.
|
||||
- 일부 Button / Input / 패널 / navigation / palette 참조가 공개 Semantic을 우회 → 동일 Default 값을 유지하면서 Semantic/palette로 연결.
|
||||
- Button·Select의 직접 HEX와 shadow 고정 RGB → Default preset palette / token 기반 derived mix로 이동.
|
||||
- root에서 계산된 Component 별칭을 wrapper가 상속해 Scoped override가 전달되지 않음 → 명시적 `data-hm-theme` scope에서 별칭·Mapping 재계산.
|
||||
- cleanup이 host 기존 inline 값과 우선순위를 잃을 수 있음 → 값/priority/기존 scope를 복원.
|
||||
- Tailwind upstream root/dark preset과 HM preset 중복 → HM 색상 preset은 Default에 단일 소유, chart/sidebar fallback은 base layer 유지.
|
||||
- Accent 45% 반복 → 동일 Soft Highlight 정책 사용처를 내부 Semantic으로 승격.
|
||||
|
||||
## 4. 확정 Runtime Theme Contract
|
||||
|
||||
전체 표: [runtime-theme-contract.md](runtime-theme-contract.md).
|
||||
|
||||
공개 입력 총 **73개**: 기존 25개, 핵심 역할 추가 6개, 선택적 Theme palette 42개. 모두 partial 입력 가능하며 Component Token 수백 개를 요구하지 않는다. CSS property 중복 매핑은 없다. 색은 불투명 6자리 HEX, radius는 기존 2–12px 정수다. Typography/spacing/layout/shadow geometry는 공개하지 않았다.
|
||||
|
||||
| Runtime Token | Default | 의미 | 외부 override | 주요 영향 |
|
||||
|---|---|---|---|---|
|
||||
| color.action.primary | #123328 | Strong/Primary | Yes | Badge, Pagination current, Button outline, navigation ink |
|
||||
| color.state.active | #266651 | Checked/Selected/ON | Yes | Checkbox, Radio, Switch, Slider, Progress, Calendar |
|
||||
| color.stroke.input.focus | #266651 | 일반 Focus | Yes | Input, Select, Checkbox, Calendar ring 등 |
|
||||
| color.accent | #bddfd4 | Highlight 원색 | Yes | Select/Menu/Calendar/Table/Pagination soft state |
|
||||
| color.bg.content | #f5f0e5 | 콘텐츠 면 | Yes | Dashboard, Card content, navigation current face |
|
||||
| color.bg.surface | #ffffff | 기본 면 | Yes | Input, Select, Card, Floating/Modal |
|
||||
| color.bg.subtle | #f4f1ea | 은은한 면 | Yes | Badge secondary, Table footer, Calendar today |
|
||||
| color.fg.primary | #0a221d | 주요 텍스트 | Yes | 일반 UI 텍스트 |
|
||||
| color.fg.inverse | #ffffff | 반전 텍스트 | Yes | Button, Badge, GNB/LNB, Tooltip |
|
||||
| color.stroke.default | #9a9284 | 기본 border | Yes | Checkbox, Radio, Badge outline, Scroll Area |
|
||||
| color.action.destructive | #d32f2f | 오류/위험 | Yes | Input error, destructive Badge/Button/Menu |
|
||||
|
||||
## 5. Default Theme 구조
|
||||
|
||||
Default CSS preset은 최초 렌더의 fallback이다. `defaultRuntimeTheme`는 같은 contract의 기본 Theme Set이다. 이를 주입한 실제 화면과 주입하지 않은 화면의 parity를 검사했다. 외부 JSON 값은 동일 CSS property를 override한다. Primitive 유지, Semantic 유지, Component Mapping 유지. Component는 외부 객체를 읽지 않는다.
|
||||
|
||||
Default → External 우선순위는 scope inline CSS로 보장한다. 기존 Component/Pattern import 순서는 유지하며 Filter Bar padding 순서도 바꾸지 않았다.
|
||||
|
||||
## 6. Active / Focus 독립성
|
||||
|
||||
Verification Theme는 Active `#007f73`, Focus `#c75b00`이다. Checkbox checked와 Calendar selected의 실제 배경은 `rgb(0,127,115)`, 실제 focus-visible shadow는 orange였다. 두 역할을 각각만 변경해 다른 core 역할의 Default 값이 유지되는지 검사했다. Default에서는 두 값 모두 Light 2여도 서로 alias하지 않는다.
|
||||
|
||||
## 7. Primary / Active 독립성
|
||||
|
||||
Primary `#6837a8`, Active `#007f73`. Badge default / Pagination current / Button outline / GNB·LNB current ink는 Primary, 선택 Control과 Calendar edge는 Active다. Checkbox/Radio unchecked hover border와 Slider halo, Accent foreground는 기존 Primary 정책을 유지하므로 Primary 변경의 실제 영향표에 포함했다.
|
||||
|
||||
## 8. Accent 및 영향표
|
||||
|
||||
결정: **PROMOTE_TO_SEMANTIC**. `--hm-sem-surface-highlight = color-mix(in srgb, var(--hm-sem-accent) 45%, transparent)`는 내부 derived다. 외부 사용자는 Accent만 지정한다. Highlight 비율은 바꾸지 않았다. Table header와 Tab의 과거 팔레트 장식, Toggle on의 full Accent는 각각 기존 정책을 유지한다.
|
||||
|
||||
| Component | Primary | Active | Focus | Accent |
|
||||
|---|---|---|---|---|
|
||||
| Checkbox | unchecked hover border | checked | ring | — |
|
||||
| Radio | unchecked hover border | selected border/dot | ring | — |
|
||||
| Switch | — | ON | ring | — |
|
||||
| Slider | hover/focus halo(기존) | range/thumb border | Primary halo(기존) | — |
|
||||
| Progress | — | fill | — | — |
|
||||
| Calendar | highlight text | selected/edge | ring | hover/middle |
|
||||
| Pagination | current/highlight text | — | ring | hover |
|
||||
| Badge | default | — | — | — |
|
||||
| Table | — | — | — | row hover/selected |
|
||||
| Input | — | — | border/ring | — |
|
||||
| Select | highlight text | — | border/ring | selected/highlight |
|
||||
| Button | outline/ghost/link | — | ring | — |
|
||||
| GNB | current/open ink | — | mobile ring; desktop inverse outline | mobile highlight |
|
||||
| LNB | current/gradient first stop | — | inverse outline(기존) | — |
|
||||
|
||||
Accent를 단독 변경해 Primary/Active/Focus/Destructive가 변하지 않음을 검사했다. Global Select Portal의 선택 옵션은 yellow Accent 45%(`color(srgb 1 0.827451 0.427451 / 0.45)`)로 실제 렌더됐다. DatePicker Portal의 selected day도 Global Active를 받았다.
|
||||
|
||||
## 9. Hardcode Audit
|
||||
|
||||
전체 파일·줄·값·분류·조치는 [CSV](runtime-theme-hardcode-audit.csv)에 있다. 변경 전 UI Theme CSS의 하드코딩 후보 25개 발생 지점을 처리했고 변경 후 Component CSS의 후보는 0이다. 주석은 실행 리터럴에서 제외했다. Primitive, Default preset, 문서 예제, Story, Demo, 독립 CENTER를 구분했다.
|
||||
|
||||
| 파일 | 값 | 분류 | 조치 |
|
||||
|---|---|---|---|
|
||||
| theme/primitives.css | #266651 / #123328 등 | VALID | 기존 Primitive 유지 |
|
||||
| theme/button.css | #221d13 / #a98d53 / orange / gray 등 | HARDCODE_CANDIDATE | 공개 Theme palette 기본값으로 이동 |
|
||||
| theme/select.css | #2d2d2d | HARDCODE_CANDIDATE | palette-indicator 연결 |
|
||||
| theme/dialog.css / sheet.css 등 | rgb(0 0 0 / 0.2) | HARDCODE_CANDIDATE | palette-black 기반 mix, shadow geometry 유지 |
|
||||
| theme/dashboard-layout.css | rgb(7 30 24 / 0.18) | HARDCODE_CANDIDATE | palette-green-dark-2 기반 mix |
|
||||
| theme/lnb.css | rgb black 20%/30% | HARDCODE_CANDIDATE | palette-black 기반 depth gradient |
|
||||
| theme/checkbox.css 등 | #0000 / transparent | INTENTIONAL | ring sentinel / 투명면 유지 |
|
||||
| Select/Menu/Table 등 | Accent 45% color-mix | DERIVED | 같은 의미의 내부 surface-highlight로 승격 |
|
||||
| components/ui/* overlay | bg-black/10 | INTENTIONAL | upstream class 유지; 실제 색은 overlay.css의 derived Semantic |
|
||||
| src/*stories.tsx / .storybook | specimen HEX | STORY_ONLY | 검수 specimen 유지 |
|
||||
| src/hm-theme.css | artwork HEX | INTENTIONAL | Demo 고정 artwork 유지 |
|
||||
| packages/center-graphic | 그래픽 palette | INTENTIONAL | 독립 CENTER 도메인 유지 |
|
||||
|
||||
## 10. Component Mapping 변경
|
||||
|
||||
기존 Mapping 이름/size/props를 삭제하거나 rename하지 않았다. Primary/Destructive/Surface/Text/Border 참조를 공개 Semantic으로 연결하고, 별도 브랜드 shade는 선택적 Theme palette로 연결했다. 모든 preset Mapping 선언에 명시적 Theme scope를 추가했다. Accent mix와 shadow chroma를 derived로 정리했다. UI 컴포넌트·Pattern `.tsx`는 수정하지 않았다.
|
||||
|
||||
## 11. Storybook Verification Theme
|
||||
|
||||
`src/theme/runtime-theme.stories.tsx`의 숨김 verification Story: Comparison, GlobalExternal, Verification, Dashboard. 요청한 15종과 Command를 함께 렌더링한다. Production 기본값에 검증색을 넣지 않았다.
|
||||
|
||||
Verification은 브라우저에서 **136개 검사, 63개 렌더된 data-slot 부품**을 확인한다. 1280 / 768 / 390px에서 모두 PASS. Default는 변경 전 토큰 그래프와 색상/geometry parity, full Default preset 주입 parity를 검사한다. 공개 core/palette는 개별 주입, gradient는 complete-group 주입을 검사한다. 포커스 ring은 실제 focus-visible 및 색 공간을 고려해 확인한다. Theme switching 중간 transition 프레임을 제외하기 위해 이 검사 화면만 transition-duration을 0으로 고정한다. Production transition은 유지한다.
|
||||
|
||||
추가로 `_local/check-theme-geometry.mjs`는 변경 전 snapshot과 최종 CSS 구문을 비교한다. **50개 파일, 1,633개 geometry 선언 변경 0**. Default/External 비교만으로 검사 로직을 대체하지 않았다.
|
||||
|
||||
## 12. Partial Override / Scope
|
||||
|
||||
Partial은 미지정 Default 유지. 빈 tokens는 Default. invalid 문서는 이전 override를 원자적으로 해제해 host/Default 복귀. 교체 때 stale key 제거. 기존 host inline 값·priority·scope 복원.
|
||||
|
||||
Global A 아래 Scoped B는 독립 HM preset에서 시작한다. B가 지정하지 않은 값은 HM Default이며 A를 상속하는 합성 정책은 아니다. Portal은 DOM inheritance를 따른다. 현재 HM SelectContent/PopoverContent/DatePicker는 container API를 노출하지 않으므로 wrapper Theme만으로 body popup을 scoped하게 만들 수 없다. Global popup 전달은 검증했으며 이 제한을 숨기거나 새 Portal API로 우회하지 않았다.
|
||||
|
||||
## 13. 수정 파일
|
||||
|
||||
- `packages/ui/package.json`: `./theme` 진입점 추가.
|
||||
- `packages/ui/src/theme/{contract,runtime,index}.ts`: 단일 계약, 기존 검증/주입 확장, Default preset.
|
||||
- `packages/ui/src/theme/default.css`, `theme.css`, `tailwind.css`: preset/derived/scope/import ownership.
|
||||
- `packages/ui/src/theme/*.css`: 기존 50개 Component/Pattern Mapping의 scope / 색상 의존 관계. `overlay.css` 추가.
|
||||
- `src/theme/preview.ts`: 기존 이름 호환 어댑터.
|
||||
- `src/theme/runtime-theme.stories.tsx`, `verification-baseline.json`: Story 전용 검증.
|
||||
- `docs/guidelines/{runtime-theme,runtime-theme-contract,runtime-theme-report}.md`, audit CSV, 테마 토큰 가이드, HM inventory, `CLAUDE.md`: 계약/결과 문서.
|
||||
|
||||
변경 전 CSS/Preview는 `_local/theme-before/`에 보존했다. Git 저장소가 없으므로 git diff 대신 snapshot 비교를 사용했다. Dependency 추가 없음.
|
||||
|
||||
## 14. API 변화
|
||||
|
||||
**Component API 변화 없음.** Theme infrastructure에 필요한 최소 공개 경로 `@hm-design/ui/theme`와 `applyRuntimeTheme`, `resetRuntimeTheme`, `runtimeThemeContract`, `defaultRuntimeTheme`, 타입 2개를 제공한다. 기존 applyPreviewTheme/resetPreviewTheme 이름은 같은 구현의 어댑터다. 네트워크 fetch·캐시·auth·hydration·Publish는 추가하지 않았다.
|
||||
|
||||
## 15. 문서
|
||||
|
||||
[Runtime 사용·정책](runtime-theme.md), [전체 계약표](runtime-theme-contract.md), [감사 CSV](runtime-theme-hardcode-audit.csv)를 추가하고 기존 테마 가이드/CLAUDE/HM inventory에 구현 상태를 연결했다. Default/External, 계층, partial/invalid, 적용 예제, scope/Portal, 초기 렌더 책임을 실제 API 기준으로 설명했다.
|
||||
|
||||
## 16. Build / 통합 검증
|
||||
|
||||
- `npm run typecheck`: PASS.
|
||||
- `npm run build`: PASS.
|
||||
- `npm run build-storybook`: PASS.
|
||||
- Storybook browser: 1280/768/390px 각 136 checks PASS.
|
||||
- CSS geometry snapshot 비교: PASS (1,633 declarations).
|
||||
- 기존 Dashboard: 1280px GNB 70px / LNB 272px / main padding 34px, 390px GNB 56px / LNB display none / main padding 20px. 문서 가로 overflow 없음.
|
||||
- LNB Standalone Mobile: 390px 서비스 메뉴 Sheet 열기/닫기, 중첩 메뉴 표시, Sheet width 342px, 문서 가로 overflow 없음.
|
||||
- Global Select/DatePicker Portal: 실제 Accent/Active 전달 확인.
|
||||
- 기존 CSS 예제 `bg-[var(...)]` warning, Storybook 500kB chunk warning 유지. Sandbox에서 Vite/esbuild EPERM이 발생해 동일 명령을 허용된 정상 실행 권한으로 재실행했다.
|
||||
|
||||
## 17. FOLLOW_UP
|
||||
|
||||
- 서비스 네트워크 Theme Loader/API/CDN/Publish는 별도 단계다. Bootstrap 전에 동기 Theme 적용은 가능하지만 네트워크 이후 flash의 실제 운영 측정은 미수행.
|
||||
- Scoped popup이 필요하면 기존 HM Portal 경계의 API 설계를 별도 판단한다. 이번에 Component API/Portal을 바꾸지 않았다.
|
||||
- Slider halo 및 GNB/LNB inverse focus outline을 일반 Focus 정책으로 통합하려면 Default 디자인 변경을 포함해 별도 결정한다.
|
||||
- Light/Dark와 Theme selector UI, browser별 렌더 차이, 새 accessibility 대비 승인 절차는 이번 구조 작업 범위 밖이다.
|
||||
|
||||
## 검증 화면
|
||||
|
||||
Global External에서 실제 Select Portal을 연 화면: [runtime-theme-global-select.jpg](../../_local/runtime-theme-global-select.jpg). Primary/Active/Focus/Accent/Destructive는 각각 다른 값이다.
|
||||
|
||||
|
||||
## 작업 시간 (Asia/Seoul)
|
||||
|
||||
- Started At: 2026-10-06 14:40:37 KST
|
||||
- Completed At: 2026-10-06 15:01:50 KST
|
||||
- Actual Duration: 21분 13초
|
||||
- Estimated Duration: 최초 45–60분
|
||||
- Variance: 예상보다 23분 47초–38분 47초 단축
|
||||
|
||||
# 후속: 디자인팀 실시간 Theme 운영 — 2026-10-06
|
||||
|
||||
이 문서 앞부분은 이전 Runtime 전환 기록이다. Git baseline `d1015309ab43fba69c8959a3dda660358e0e8fa1` 이후 JSON을 컬러 원본으로 연결하고 독립 Theme artifact/URL Loader/Storybook Preview를 추가한 결과는 [Designer Theme Report](designer-theme-report.md)에 기록했다. 사용 방법은 [디자인팀 Theme 가이드](designer-theme-guide.md), 운영/배포는 [Theme delivery](theme-delivery.md)다. 과거 수치를 새 검증 수치로 대체하지 않았다.
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# Runtime Theme Contract v1
|
||||
|
||||
2026-10-06 · 분류: HM_THEME / Theme infrastructure. Component props, DOM, Radix 상태, Portal, 키보드, ARIA, breakpoint는 변경하지 않았다.
|
||||
|
||||
## 현재 파일과 적용 흐름
|
||||
|
||||
`themes/default.json` → 생성 `packages/ui/src/theme/primitives.css` / `default.css` HM fallback / Semantic → `<component>.css` Component Mapping → `[data-slot]` CSS → UI. 운영 External JSON은 같은 Contract로 Runtime 적용한다.
|
||||
|
||||
`packages/ui/src/theme/contract.ts`는 외부 JSON key, CSS property, 타입과 생성 Default 데이터를 연결하는 공개 계약이다. `runtime.ts`가 검증 후 DOM scope에 inline CSS 변수를 넣는다. 서비스는 `@hm-design/ui/theme`를 사용한다. `src/theme/preview.ts`는 같은 함수의 호환 adapter다. Production URL Loader는 `loader.ts`의 fetch/resolve/load API이며 회사 CDN 주소는 없다. `.storybook/preview.tsx`는 JSON Default + partial을 Global로 적용하고 Designer Playground가 JSON URL/파일 Preview를 제공한다. React Production Provider/Publish API는 없다.
|
||||
|
||||
운영/분리 배포: [Theme delivery](theme-delivery.md). 디자인팀 Preview/편집: [사용법](designer-theme-guide.md). 전체 입력/API 계약: [Runtime 계약표](runtime-theme-contract.md).
|
||||
|
||||
React 외 실행 형식으로 변환할 때도 이 문서의 Token·CSS Custom Properties·Runtime 적용 계약을 유지한다. Target Runtime, standalone asset, `file://` 검증 및 AI/도구 독립성 기준은 [Runtime Portability Policy](runtime-portability.md)를 따른다.
|
||||
|
||||
소비 CSS는 `tailwindcss` → `@hm-design/ui/tailwind.css` → `@hm-design/ui/theme.css` 순서로 앱 entry에서 로드한다. Dashboard 상태 표시가 있으면 그 뒤에 `@hm-design/ui/dashboard-status.css`와 소비 Badge mapping을 연결한다([상태 예제](../patterns/dashboard-layout.md#dashboard-업무-상태-공통-계약)). Status CSS는 변수만 정의하며 73개 Runtime Theme 계약에 새 상태 Token을 추가하지 않는다. [Tailwind 검색 범위](runtime-portability.md#21-tailwind-v4-소비자-검색-범위)와 [Noto Sans KR·Asset 경로](runtime-portability.md#41-noto-sans-kr-경로-실행-환경별-확인)는 실행 환경별 소비 가이드를 따른다.
|
||||
|
||||
Default는 특별한 컴포넌트 내부 상수가 아니라 이 계약을 만족하는 기본 제공 Theme Set이다. 주입이 없으면 패키지 CSS의 Default preset을 사용한다. `defaultRuntimeTheme`는 동일한 값을 가진 JSON 형태 preset이다. Story의 CSS fallback / Default preset 주입 비교와 계약 값 검사로 두 표현의 일치를 검증한다.
|
||||
|
||||
## 공개 경계
|
||||
|
||||
- **공개 Runtime 계약**: Semantic 색상 역할, 기존 그라데이션 stop 세트, 선택적 Theme 팔레트, 기존 radius 2개. 모든 Token은 선택적이다.
|
||||
- **내부 Derived**: `--hm-sem-surface-highlight`(Accent 45%), gradient 조합, shadow/overlay 조합, shadcn adapter(`--primary`, `--ring` 등). 외부 JSON은 이 변수를 직접 쓰지 않는다.
|
||||
- **내부 Component Mapping**: `--hm-checkbox-*`, `--hm-calendar-*`, `--hm-pagination-*` 등. 기존 이름을 유지한다. Runtime JSON으로 받지 않는다.
|
||||
- Typography, height, spacing, gradient geometry, shadow geometry, responsive layout은 코드 계약이다. 이번 컬러 구조 변경으로 Runtime 범위를 확대하지 않았다.
|
||||
- Typography 현황(2026-10-08): 기존 크기와 ui_viz 승인 변경값을 유지한다. `packages/ui/src/theme/*.css`의 font-size는 px이고 shadcn 유래 Tailwind class(`text-sm` 등)는 rem 기반이라 단위가 혼재한다. 소비자가 Tailwind class나 `className`으로 크기를 덮어쓰면 Theme 값보다 우선할 수 있다. Typography Role(heading/body/label 등)은 아직 공통 Token으로 도입하지 않았고 Runtime JSON 73개 계약에도 포함하지 않는다.
|
||||
|
||||
기존 25개 allowlist의 JSON 이름과 validation을 유지하고 Active / on Active / Inverse Text / Subtle Surface / Accent / on Accent를 추가했다. 같은 CSS property로 향하는 중복 공개 key는 없다. Component 수백 개의 Token을 요구하는 대신 Theme 팔레트는 선택적 확장이다. 핵심 역할만 지정해도 사용할 수 있다. 팔레트 이름은 색상 계열을 유지하며, 역할 Token은 각각 독립적으로 override할 수 있다.
|
||||
|
||||
전체 입력 목록과 영향은 [Runtime 계약표](runtime-theme-contract.md), 구조와 적용 절차는 이 문서와 [Theme delivery](theme-delivery.md)를 따른다. 과거 감사 CSV·작업 보고서는 테스트 ZIP에 포함하지 않는다. 새 공개 Token 추가는 패키지/계약 변경이다. `schemaVersion: 1` 입력 구조를 유지하며 별도 발행/versioning 시스템은 만들지 않았다.
|
||||
|
||||
## 기본 적용
|
||||
|
||||
앱 초기 CSS에서 순서를 유지한다.
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "@hm-design/ui/tailwind.css";
|
||||
@import "@hm-design/ui/theme.css";
|
||||
```
|
||||
|
||||
Theme 주입을 하지 않으면 HM Default가 자동 적용된다. `tailwind.css`의 HM과 중복되던 upstream root/dark 색상 preset은 제거했고 chart/sidebar fallback만 base layer에 남겼다. `theme.css`는 Default를 먼저 import하고 기존 Component/Pattern import 순서를 유지한다. Filter Bar state padding 순서도 바꾸지 않았다. Runtime inline 값은 이후 기본 CSS import보다 우선한다. `!important`로 별도 override한 소비 CSS는 이 우선순위 계약 밖이다.
|
||||
|
||||
## External Theme 적용
|
||||
|
||||
```ts
|
||||
import { applyRuntimeTheme, resetRuntimeTheme } from "@hm-design/ui/theme"
|
||||
|
||||
const accepted = applyRuntimeTheme(document.documentElement, {
|
||||
schemaVersion: 1,
|
||||
tokens: {
|
||||
"color.action.primary": "#6837a8",
|
||||
"color.state.active": "#007f73",
|
||||
"color.stroke.input.focus": "#c75b00",
|
||||
"color.accent": "#ffd36d",
|
||||
"color.action.destructive": "#d41466",
|
||||
},
|
||||
})
|
||||
// 선택적 교체 및 해제. 실제 Theme 값은 서비스의 검증/승인 과정을 거친다.
|
||||
resetRuntimeTheme(document.documentElement)
|
||||
```
|
||||
|
||||
기존 Preview 함수는 입력이 `unknown`이고 반환은 `boolean`이다. 새 공개 함수도 같은 형태다. 외부 Primary는 Badge default, Pagination current, Button outline, GNB/LNB current ink를 바꾼다. Brand Button/GNB 면은 기존 stop 계약, Solid Button 면은 팔레트 계약을 사용하므로 Primary 한 개로 gradient나 Solid 면을 임의 재설계하지 않는다. 기존 stop 세트 전체를 제공하면 해당 gradient도 바뀐다.
|
||||
|
||||
## Partial / Empty / Invalid / Switching
|
||||
|
||||
- `{ schemaVersion: 1, tokens: {} }`: 유효한 빈 Theme이며 Default를 사용한다.
|
||||
- 부분 Token만 제공: 같은 scope의 미지정 값은 HM Default를 사용한다. Theme A에서 B로 교체할 때 A의 키가 남지 않는다.
|
||||
- `null`, 단순 `{}`, 잘못된 schemaVersion, 알 수 없는 key, Component Token key, 잘못된 값: false를 반환하고 이 helper의 이전 override를 해제한다. 정상적인 host에서는 Default로 돌아간다. host가 원래 소유한 inline override는 복원한다.
|
||||
- 불투명 6자리 HEX만 허용한다. radius는 2–12px 정수만 허용한다. `var()`, `url()`, CSS 함수, `!important`, 임의 CSS rule은 입력할 수 없다.
|
||||
- 기존 3개의 gradient stop 그룹은 각각 전체 또는 미지정만 허용한다. 한 stop만 보내면 문서 전체를 거부한다.
|
||||
- 입력 전체를 검증한 뒤 DOM에 쓴다. 하나의 잘못된 값 때문에 CSS가 오염되는 대신 마지막 override를 해제한다. 이 atomic fallback은 기존 정책을 유지했다.
|
||||
- cleanup은 helper가 덮기 전 inline 값과 priority 및 `data-hm-theme` 속성을 복원한다. 같은 scope는 한 owner가 적용/해제를 관리한다. 서로 다른 helper 호출자가 같은 scope를 동시에 소유하는 다중 owner 시스템은 제공하지 않는다.
|
||||
|
||||
## Global / Scoped Theme
|
||||
|
||||
`document.documentElement` 주입은 Global이다. container를 전달하면 helper가 `data-hm-theme="runtime"`을 설정한다. CSS의 `:root, [data-hm-theme]` 규칙이 그 scope에서 Semantic/Derived/Component Mapping을 다시 계산한다. 단순히 후손 변수만 바꾸고 root에서 이미 계산된 Component 값을 상속하는 문제를 피한다.
|
||||
|
||||
```ts
|
||||
const panel = document.querySelector<HTMLElement>("#service-panel")!
|
||||
applyRuntimeTheme(panel, {
|
||||
schemaVersion: 1,
|
||||
tokens: { "color.state.active": "#345678" },
|
||||
})
|
||||
```
|
||||
|
||||
명시적 scope는 **독립 HM preset + 자신의 partial override**다. Global A 아래 Scoped B의 미지정 값은 A가 아니라 HM Default다. 이 정책은 밝은/어두운 모드나 Brand selector를 새로 만들지 않고 서로 독립된 scope를 검증하기 위한 것이다. 소비 CSS만으로 적용하려면 `[data-hm-theme="service-a"] { --hm-sem-state-active: ...; }`를 기본 CSS 뒤에 선언한다. 기존 host inline 값이 있는 경우에는 그 값이 우선한다.
|
||||
|
||||
**Portal 경계**: CSS inheritance는 DOM을 따른다. Select, Date Picker, Sheet 등 body Portal은 wrapper scope의 자손이 아니다. Global Theme에서는 그대로 같은 값을 받는다. 현재 HM SelectContent / PopoverContent / DatePicker는 Portal container API를 공개하지 않는다. 따라서 wrapper만으로 popup을 scoped Theme로 바꿀 수 없으며, popup은 Global Theme를 따른다. raw Radix Portal을 직접 조합하는 사용처에서 이미 container를 설정할 수 있다면 해당 Theme scope를 쓰거나 포털 호스트에 같은 계약을 적용한다. 기존 HM popup에 scope 전달을 추가하는 일은 별도 API/구조 작업이다.
|
||||
|
||||
## 의미별 독립성 및 기존 표현
|
||||
|
||||
- Active: Checkbox checked, Radio selected, Switch on, Slider range/thumb border, Progress fill, Calendar single / range edge, Date Picker의 Calendar selected.
|
||||
- Focus: `color.stroke.input.focus` → `--hm-sem-stroke-input-focus` → `--ring`. Checkbox/Calendar/Input/Select 등의 Focus와 Active는 독립적이다. Default에서는 둘 다 Light 2지만 Verification에서는 다른 값이다.
|
||||
- Primary: Badge default, Pagination current, Button outline, GNB/LNB current/strong ink. Active와 alias하지 않는다.
|
||||
- Accent 45%: Select/Menu Item/Calendar hover·range middle/Pagination hover/Table row hover·selected 등의 Soft Highlight. 내부 `--hm-sem-surface-highlight`를 사용한다. Accent 100% + On Accent: Toggle/Toggle Group의 Persistent Selected Surface, Menubar 상위 Highlighted/Open, Navigation Menu Trigger/Open. Navigation Current는 Primary + contextual surface로 Control Active와 구분한다.
|
||||
- On Accent: Primary와 sibling Semantic이다. `color.on.accent`와 CSS-only package fallback은 독립 기본값을 가지며 Primary를 override해도 On Accent는 함께 바뀌지 않는다.
|
||||
- Destructive: Input 오류, Badge/Button/Menu 오류 표현. Primary/Active/Focus와 독립적이다.
|
||||
- Slider Hover/Drag halo는 Primary 20%, Keyboard Focus halo는 Focus Semantic 20%다. Focus와 Hover/Drag가 겹치면 Focus 색이 우선한다. GNB/LNB는 contextual navigation focus 예외(초록 면의 inverse outline, Current/Open 면의 contextual outline)를 유지한다. 팔레트 기반 Table header/Tab/navigation 장식은 Control Active 의미가 아니다. Data Table Sort Indicator는 별도 Green Light 3 palette 기반이며 [Pattern 문서](../patterns/data-table.md)를 따른다.
|
||||
|
||||
## 초기 렌더
|
||||
|
||||
Default CSS는 첫 paint 전에 로드한다. Story scope는 `useLayoutEffect`로 적용해 React paint 전에 값을 준비한다. 서비스 Global 적용은 앱 bootstrap에서 React render 전에 호출할 수 있다. 네트워크 Theme를 나중에 받으면 Default → External 변화가 생길 수 있으며 bootstrap/캐시 정책의 책임이다. 네트워크 flash 측정이나 hydration 시스템은 이번에 만들지 않았다.
|
||||
|
||||
## 검증 Story
|
||||
|
||||
`src/theme/runtime-theme.stories.tsx`는 숨김 `verification` Story다. 기존 컴포넌트 API로 Button/Input/Checkbox/Radio/Switch/Select/Slider/Progress/Calendar/DatePicker/Pagination/Badge/Table과 Command를 렌더링하며 Navigation Preview는 GNB / LNB 중 하나만 선택한다. Dashboard Theme 예제의 콘텐츠에는 외부 GNB/LNB를 넣지 않는다.
|
||||
|
||||
- `/iframe.html?id=theme-runtime-contract--comparison&viewMode=story`: HM Default / External 비교.
|
||||
- `/iframe.html?id=theme-runtime-contract--global-external&viewMode=story`: Global External 및 실제 Select/DatePicker Portal 검증.
|
||||
- `/iframe.html?id=theme-runtime-contract--verification&viewMode=story`: 자동 DOM/계약/독립성/cleanup 검사 결과.
|
||||
- `/iframe.html?id=theme-runtime-contract--dashboard&viewMode=story`: 기존 Dashboard composition에 Scoped Theme 적용.
|
||||
|
||||
`verification-baseline.json`은 Runtime 전환 전 CSS root 변수 그래프의 **Story 전용 고정 snapshot**이다. Production import가 없다. 새 Default와 같은 컴포넌트를 렌더링해 색상·geometry를 비교한다. 기준값을 새 CSS에서 재생성하지 않는다.
|
||||
|
||||
|
||||
자동 play와 같은 검사를 Run verification 버튼으로 다시 실행할 수 있다. Verification 화면의 transition-duration만 0으로 고정해 중간 애니메이션 프레임이 최종값 검사에 섞이지 않도록 했다. Production transition/동작은 바꾸지 않았다.
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# shadcn 기반 구현 원칙
|
||||
|
||||
> 적용 범위: HMDesignSystem의 새로운 Component / Pattern / Story 구현과 기존 테스트 디자인 재현
|
||||
>
|
||||
> 이 문서는 `docs/guidelines/프로젝트-지침.md`의 컴포넌트 구현 규칙을 보완하는 공통 지침이다. 두 문서가 충돌하면 더 구체적인 컴포넌트 문서(`docs/components/`, `docs/patterns/`)의 사용자 확정 결정과 이 문서의 재사용·구조 원칙을 함께 적용한다.
|
||||
|
||||
> **2026-10-07 사용자 확정 — AI 기본 구현 정책의 상세 Source of Truth.** 사용자가 별도로 기존 컴포넌트/디자인 시스템 사용, Theme 적용, Semantic Token 사용, Navigation 정책 준수를 요청하지 않아도 AI는 모든 신규·수정 UI에 자동 적용한다. 기본 전제는 **Existing Component First / Theme Applied By Default / Single Navigation Mode / Minimum Custom**이다. 과거 문서·예제와 충돌하면 이 정책과 [Navigation Single Mode Policy](navigation-single-mode.md)를 우선한다.
|
||||
|
||||
## 1. 구현 전 필수 판단
|
||||
|
||||
작업을 시작하기 전에 반드시 다음 질문에 답한다.
|
||||
|
||||
먼저 변경 범위를 `STORYBOOK_ONLY | PRODUCTION_UI | BOTH` 중 하나로 정하고, 아래 기존 구현과 사용례를 검색한다. 디렉터리 존재 여부와 관련 후보를 확인한 뒤 필요한 파일만 읽는다. CENTER 규격은 CENTER 작업에만 적용하며 일반 UI의 근거로 섞지 않는다.
|
||||
|
||||
| 탐색 대상 | 확인 내용 |
|
||||
|---|---|
|
||||
| `packages/ui` | 공개 exports, `src/components/ui`, `src/patterns`, Theme / Component Mapping |
|
||||
| `src/components` | 대표 Component Story, 기존 Variant / Prop, `story-board.tsx` 등 shared helpers |
|
||||
| `src/patterns` | Layout / Pattern Story, `dashboard-work-status.ts` 등 공통 상태 mapping |
|
||||
| `src/center` | CENTER 관련 Story 및 `center-stories.css`의 Common Card 구현 |
|
||||
| 기존 Storybook stories / shared helpers | 유사 대표/PRIMARY Story, composition, 상태·선택·Card mapping과 공통 helper |
|
||||
| `docs/components/`, `docs/patterns/` | 후보의 현재 API, 적용 Token, 사용자 확정 결정 |
|
||||
|
||||
Storybook은 QA 도구이자 **구현 Reference Catalog**다. Button은 Button 대표 Story, Dashboard는 Dashboard 대표 Story, CENTER 카드는 CENTER Common Card Story부터 확인한다. 현재 Story의 시각 언어를 무시해 새 패턴을 만들지 않는다. `verification` Story는 계약/상태 검증용이며 운영 화면의 기본 Reference가 아니다. 특히 혼합 Navigation의 **VERIFICATION ONLY** Story는 신규 구현 근거로 사용하지 않는다. 일반 업무 화면은 Storybook Story·사이드바에 등록하지 않고 별도 React 화면과 독립 미리보기로 제공한다.
|
||||
|
||||
Component와 Theme은 함께 설계한다. Component 탐색과 동시에 기존 Theme / Token을 확인하고, 조합과 Semantic 연결을 포함해 화면을 구현한다. 구현 후 Theme을 별도 후처리로 추가하는 흐름은 사용하지 않는다. Theme 우선순위와 Source 수정 조건은 [테마 토큰 가이드](테마-토큰-가이드.md)의 AI 기본 적용 정책을 따른다.
|
||||
|
||||
> 이 화면을 현재 `@hm-design/ui` + shadcn 조합으로 만들 수 있는가?
|
||||
|
||||
답이 YES이면 그 구조와 동작을 유지하고 HM 디자인 값만 CSS와 Token으로 적용한다. 기본 방향은 **shadcn을 HM 디자인으로 스타일링**하는 것이며, HM 디자인을 이유로 shadcn을 대체하는 별도 시스템을 만들지 않는다.
|
||||
|
||||
## 2. 구현 우선순위
|
||||
|
||||
다음 순서를 건너뛰지 않는다.
|
||||
|
||||
1. 기존 `@hm-design/ui` Component / Pattern 재사용
|
||||
2. 기존 Variant / Prop으로 요구사항 표현
|
||||
3. 기존 Composition으로 해결
|
||||
4. shadcn 기본 Component / Composition 활용
|
||||
5. Radix 등 shadcn이 사용하는 Primitive 활용
|
||||
6. 위 방법으로 해결할 수 없을 때만 최소 Custom 구현
|
||||
|
||||
기반 기술의 운영 우선순위는 `@hm-design/ui existing → shadcn → Radix Primitive → Composition → CSS / Token Visual → Minimum Custom`이다. 이는 새 기반 기능이 필요한 경우의 선택 기준이며, 이미 있는 HM Variant / Composition을 먼저 확인하는 위 재사용 순서를 건너뛰는 이유가 아니다. 모든 단계에서 Visual은 HM Theme / Token으로 연결하며 직접 primitive 동작을 만드는 것은 마지막 수단이다.
|
||||
|
||||
판단 예시는 다음과 같다.
|
||||
|
||||
| 필요 | 우선 선택 |
|
||||
|---|---|
|
||||
| Button | 기존 HM Button |
|
||||
| Popup | Popover / Dropdown Menu |
|
||||
| Disclosure | Collapsible / Accordion |
|
||||
| Mobile navigation | Sheet |
|
||||
| 검색 조합 | Input / Select / Command / Combobox |
|
||||
| Empty / Result 화면 | 기존 Pattern / Component composition |
|
||||
| Search Form | Input + Select + Button |
|
||||
| Dashboard Empty | Dashboard Layout + 기존 Empty / Data Table composition |
|
||||
| Filter 화면 | Filter Bar |
|
||||
| GNB User Menu | Avatar + Dropdown Menu |
|
||||
|
||||
## 3. 구조·동작과 시각의 소유권
|
||||
|
||||
shadcn 기반으로 구현 가능한 형태라면 shadcn/Radix의 구조, API, 상태 모델, 접근성, keyboard interaction을 최대한 유지한다.
|
||||
|
||||
### shadcn/Radix 구조를 유지하는 영역
|
||||
|
||||
- DOM / Component 구조
|
||||
- Public API
|
||||
- controlled / uncontrolled 방식
|
||||
- `data-state`
|
||||
- ARIA
|
||||
- keyboard navigation
|
||||
- focus management
|
||||
- Portal
|
||||
- Dismiss / Escape 동작
|
||||
- `asChild`
|
||||
- composition 방식
|
||||
|
||||
### HM이 소유하고 조정할 수 있는 영역
|
||||
|
||||
- CSS와 CSS Custom Properties
|
||||
- Component Token과 Semantic Token mapping
|
||||
- height, padding, gap
|
||||
- border, radius, color, shadow
|
||||
- typography
|
||||
- responsive layout
|
||||
|
||||
즉, **Behavior / Structure는 shadcn/Radix를 유지하고 Visual은 HM Theme로 조정**한다.
|
||||
|
||||
시각 차이는 HM CSS / Token으로, spacing·radius·color 차이는 Component Token으로 해결한다. CSS로 해결 가능한 차이를 위해 새 prop이나 별도 Component API를 추가하지 않는다.
|
||||
|
||||
## 4. Component와 Pattern
|
||||
|
||||
이미 `@hm-design/ui`에 구현된 Component가 있으면 shadcn Component를 다시 복사하거나 비슷한 별도 버전을 만들지 않는다.
|
||||
|
||||
Pattern도 새로운 primitive를 만들기 전에 기존 Component composition을 우선한다. 반복 사용성과 고유한 계약이 실제 제품 사용에서 확인될 때만 별도 Pattern으로 승격한다. 시각 결과가 기존 Component 조합으로 표현 가능하다면 Pattern은 조합과 상태 연결만 소유하고 primitive의 동작이나 스타일을 복제하지 않는다.
|
||||
|
||||
shadcn/Radix에 이미 존재하는 interaction은 새로 구현하지 않는다. 기존 Component 여러 개로 표현할 수 있는 화면은 composition으로 해결한다.
|
||||
|
||||
Status / Badge / Card / Navigation에 공통 mapping이 있으면 함께 재사용한다. 상태 Badge는 기존 Badge → status mapping → 기존 variant → 필요한 최소 확장 순으로 판단한다. Dashboard 상태는 `src/patterns/dashboard-work-status.ts`, CENTER 카드는 `src/center/center-stories.css`를 확인한다. 이 파일들은 현재 Story 범위의 구현이므로 재사용 시 Production export 여부와 파일 책임을 확인하고 Story helper를 Production에 직접 import하지 않는다. 색상 값·Card CSS를 복사하거나 Dashboard 전용 Badge를 중복 생성하지 않는다.
|
||||
|
||||
package exports, public props, public types, package entry, Story ID, `meta.title`을 불필요하게 변경하지 않는다. 요구사항상 변경이 필요하면 소비처, 호환성 및 Story 링크 영향을 먼저 확인하고 사유를 기록한다.
|
||||
|
||||
## 5. Storybook과 Production 구현 분리
|
||||
|
||||
### 5.1 Storybook Reference와 신규 화면 생성의 분리
|
||||
|
||||
일반적인 "화면 만들어줘", 대시보드 또는 업무 페이지 제작 요청에서 Storybook은 기존 디자인 시스템을 확인하는 Reference로만 사용한다. 신규 업무 화면을 Story로 등록하거나 Storybook 사이드바에 추가하지 않으며, 기존 대표 Story를 생성 화면으로 교체하지 않는다. 화면 확인을 위해 Storybook 실행을 필수로 요구하지 않고, 별도의 React 화면을 빌드한 뒤 독립적인 미리보기를 제공한다. H-2 또는 유사 자동화가 사용 가능한 경우 `preview.html`·desktop/mobile 이미지 등 독립 미리보기 산출을 지원할 수 있지만, 이는 Storybook Story 등록과 별개의 역할이다. Storybook 전용 helper/CSS는 운영 화면에 사용하지 않는다.
|
||||
|
||||
다음처럼 사용자가 Storybook 수정을 명시한 경우에는 예외로 처리한다: 새로운 Component 또는 Pattern 추가/수정, 대표 또는 Verification Story 작성, Storybook Reference 자체 개선. 이 예외에서도 화면 생성 결과와 Storybook Reference 변경을 서로 다른 범위로 기록한다.
|
||||
|
||||
Storybook에서 디자인 상태를 비교하기 위한 표현 때문에 Production API를 추가하지 않는다.
|
||||
|
||||
`STORYBOOK_ONLY`의 배치·specimen 표현 문제는 Story 범위에서 해결한다. 이를 이유로 Production Component를 불필요하게 수정하지 않는다. 실제 Production 동작 문제가 있으면 필요한 Production 변경과 관련 Story를 함께 다루며 Story CSS override로만 숨기지 않는다.
|
||||
|
||||
다음은 Story 전용 표현으로 처리할 수 있다.
|
||||
|
||||
- Hover 고정 specimen
|
||||
- Selected + Hover 비교
|
||||
- Spacing guide
|
||||
- Token reference
|
||||
|
||||
Story 안의 실제 화면과 상호작용에는 가능한 한 production `@hm-design/ui` Component를 그대로 사용한다. 특정 상태를 보여줄 때도 production Component의 실제 API와 shadcn/Radix 상태 모델을 우선한다. Story 전용 helper나 selector는 검수 표현에만 한정하며 production bundle이나 Public API에 포함하지 않는다.
|
||||
|
||||
## 6. Custom 구현 허용 조건
|
||||
|
||||
별도 Custom 구현은 다음을 모두 확인한 뒤에만 허용한다.
|
||||
|
||||
- 기존 HM Component로 해결할 수 없다.
|
||||
- 기존 Component composition으로 해결할 수 없다.
|
||||
- 기존 Variant / Prop 확장이 요구사항·API 의미에 부적절하다.
|
||||
- shadcn 기본 Component 및 Radix Primitive 조합으로 해결하기 어렵다.
|
||||
- CSS / Token 조정만으로 해결할 수 없다.
|
||||
- 실제 제품에서 반복적으로 필요한 독립적인 동작 또는 API가 있다.
|
||||
|
||||
Custom 구현은 필요한 동작과 API에 한해 최소 범위로 제한한다. 결과 보고와 관련 컴포넌트 또는 패턴 문서에는 다음을 기록한다.
|
||||
|
||||
- 왜 shadcn으로 해결할 수 없었는지
|
||||
- 왜 기존 HM 조합으로 해결할 수 없었는지
|
||||
- 왜 기존 Variant 확장이 부적절하고 Radix 조합도 어려운지
|
||||
- 추가한 Custom 범위
|
||||
- Public API 영향
|
||||
|
||||
## 7. 금지 사항
|
||||
|
||||
- 디자인을 맞추기 위해 shadcn을 통째로 다시 작성한다.
|
||||
- Radix state logic을 재구현한다.
|
||||
- 기존 HM Component와 유사한 Custom Component를 추가한다.
|
||||
- Storybook 검증만을 위한 Production prop을 추가한다.
|
||||
- CSS로 해결 가능한 차이에 새 API를 추가한다.
|
||||
- 이전 ui-viz JSX, DOM, class 또는 상태 로직을 그대로 복사한다.
|
||||
- 기존 Component와 Story를 확인하지 않고 Button / Input / Badge / Card / Dialog / Select / Table / Navigation 등을 생성한다.
|
||||
- 기존 Badge가 있는데 Dashboard 전용 Badge를 새로 만들거나 기존 Common Card CSS를 복사한다.
|
||||
- 기존 Theme Token이 있는데 색상·spacing·값을 하드코딩하거나 같은 Token을 여러 파일에 중복 정의한다.
|
||||
- UI 구현 후 Theme을 선택적 후처리로 적용한다.
|
||||
- Storybook 표현 문제만을 위해 Production 코드를 바꾸거나 Production 동작 문제를 Story CSS override로 숨긴다.
|
||||
- GNB / LNB / Dashboard 혼합은 운영 페이지에 사용하지 않는다. 혼합 Navigation은 현재 단일 모드 정책의 조건을 충족하는 기술 검증 Story에만 둔다.
|
||||
|
||||
## 8. 구현·검토 체크리스트
|
||||
|
||||
- [ ] 현재 `@hm-design/ui`에 재사용할 Component 또는 Pattern이 있는가?
|
||||
- [ ] 기존 Variant / Prop / Composition과 유사 대표 Story, 공통 mapping을 먼저 확인했는가?
|
||||
- [ ] shadcn 기본 Component / Composition으로 해결할 수 있는가?
|
||||
- [ ] Radix 또는 기존 기반 라이브러리가 동작과 접근성을 이미 제공하는가?
|
||||
- [ ] Component composition으로 별도 primitive 없이 표현할 수 있는가?
|
||||
- [ ] 남은 차이는 HM CSS / Token만으로 맞출 수 있는가?
|
||||
- [ ] Story 전용 요구가 Production API로 유입되지 않았는가?
|
||||
- [ ] Custom이 필요하다면 모든 허용 조건과 기록 요건을 충족했는가?
|
||||
- [ ] Theme / Semantic 연결, 독립 fallback, 변경 범위와 public API 보호를 지켰는가?
|
||||
- [ ] 단일 Navigation Mode와 같은 모드의 반응형 표현을 지켰는가?
|
||||
|
||||
## 9. AI 신규 화면 구현 순서
|
||||
|
||||
AI 화면 구현의 단일 실행 순서는 저장소 루트의 [AGENTS.md Quick Start](../../AGENTS.md#ai-화면-구현-quick-start)를 따른다. 후보 Component·Pattern의 실제 export, 사용 문서, 대표 Story는 [통합 Reference Index](component-pattern-reference.md)에서 찾는다. 일반 업무 화면은 Storybook에 등록하지 않고 별도 React 화면으로 생성하며, 화면별 빌드·필요한 검증·독립 미리보기를 결과로 제공한다. 상세 재사용·Theme·Navigation 조건은 이 문서와 연결된 Source of Truth를 유지한다.
|
||||
|
||||
구현 중에는 다음 단계에 필요한 최소 확인만 하고, 완료 후 diff / 요구사항 / 관련 Story / 필요한 프로젝트 검증을 한 번에 수행한다. 같은 조건으로 성공한 검증을 형식적으로 반복하지 않는다.
|
||||
|
||||
## 10. 명시적 예외
|
||||
|
||||
사용자가 새로운 Component 실험, Theme 미적용 prototype, 외부 디자인 완전 재현, 기존 디자인 시스템과 독립된 POC를 명시적으로 요청한 경우에만 기본 정책의 예외를 검토한다. 예외의 목적·범위·사유와 Theme 미적용 여부를 기록하고 기존 시스템에 영향을 주지 않는 별도 Story / 실험 범위로 격리한다. Production exports / Theme Source / 공통 mapping을 임의로 변경하지 않는다.
|
||||
|
||||
이 예외는 혼합 Navigation 운영을 자동 허용하지 않는다. Navigation 조합을 명시적으로 요청받아도 정책 변경인지 기술 검증인지 구분하며 [Navigation Single Mode Policy](navigation-single-mode.md)의 `verification` 태그와 **VERIFICATION ONLY** 조건을 유지한다.
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# Theme JSON 운영과 독립 배포
|
||||
|
||||
## Repository와 배포 단위
|
||||
|
||||
하나의 Git Repository에서 소스·Theme·Storybook·문서를 관리한다. UI, CENTER Graphic, Theme JSON은 각각 별도 배포 단위다. 현재 두 패키지는 `private: false`이며 compiled dist(JS·타입·CSS)를 export한다. Library build는 `npm run build:packages`로 구현되어 있다. Registry publish 이력은 테스트 ZIP 검증 범위 밖이며 테스트 ZIP 실행에는 Registry 접근·publish가 필요하지 않다. Theme JSON의 독립 산출 경로는 구현했다. 실제 CDN URL·인프라·인증·CI publish는 미정이다.
|
||||
|
||||
운영 배포 경계는 `packages/ui` → UI package, `packages/center-graphic` → Graphic package, `themes/*.json` → Theme CDN이다. **Git push ≠ Theme CDN publish**다. 운영 CI/CD는 JSON 검증·독립 artifact 산출 후 CDN에 배포하는 별도 단계로 구성하며, 현재 저장소에는 실제 CDN publish pipeline이 구현되어 있지 않다.
|
||||
|
||||
```text
|
||||
HMDesignSystem/
|
||||
├─ packages/
|
||||
│ ├─ ui/ # @hm-design/ui: logic, contract, mapping, runtime
|
||||
│ └─ center-graphic/ # 독립 Graphic package
|
||||
├─ themes/
|
||||
│ ├─ default.json # 71개 public color 값의 공식 Default 원본
|
||||
│ ├─ service-example.json # 5개 역할만 바꾸는 명시적 예제
|
||||
│ └─ drafts/ # 과거 초안; Theme 산출 대상 아님
|
||||
├─ scripts/
|
||||
│ ├─ theme-templates/ # 컬러 literal 없이 alias/geometry만 소유
|
||||
│ ├─ theme-defaults.mjs # JSON -> package CSS/default 데이터 생성
|
||||
│ └─ theme-artifacts.mjs # JSON validation -> 독립 CDN artifact
|
||||
├─ src/theme/ # Storybook Preview/Editor/verification 소비자
|
||||
└─ docs/guidelines/
|
||||
```
|
||||
|
||||
`themes/default.json`은 기존 v1 `{schemaVersion:1,tokens:{...}}` 계약을 사용한다. Component Token은 없다. 73개 Runtime 입력 중 71개 컬러를 포함한다. 나머지 두 radius와 layout은 패키지 template에서 관리한다. 서비스 Theme는 같은 계약의 부분 컬러 override다.
|
||||
|
||||
## 단일 Default 원본과 package fallback
|
||||
|
||||
사람이 관리하는 컬러 값은 `themes/*.json`이다. Default JSON과 `scripts/theme-templates`에서 `packages/ui/src/theme/primitives.css`, `default.css`, `default-values.generated.ts`를 생성한다. Template는 기존 alias/geometry 정책을 유지하고 HEX literal을 따로 관리하지 않는다. JSON의 역할값이 팔레트 alias의 값과 같으면 기존 alias를 유지하고, 달라지면 JSON의 역할값을 우선한다.
|
||||
|
||||
패키지는 생성된 CSS fallback snapshot을 포함하므로 네트워크 Theme가 없거나 실패해도 기본 화면을 렌더링한다. 이미 배포한 UI package의 fallback은 **그 패키지 버전에서 생성한 HM Default snapshot**이다. 최신 운영 Default JSON을 CDN에서 로드하면 UI 재배포 없이 새 값이 적용된다. 네트워크 장애에서 최신 CDN 값까지 보장하는 시스템은 아니며, 그 경우 패키지 snapshot으로 복원한다.
|
||||
|
||||
Theme-only CDN 배포는 `npm run build:themes`만 실행한다. UI 재빌드·version up·registry publish가 필요 없다. 이후 UI 패키지를 새로 유지보수/검증할 때는 `npm run generate:theme-defaults`로 repo의 생성 snapshot을 갱신한다. `typecheck/build/build-storybook`의 pre hook은 JSON/template/생성 snapshot의 불일치를 검사한다. 이 검사는 Theme artifact 산출의 선행 조건이 아니다.
|
||||
|
||||
## 개발팀 Runtime 적용
|
||||
|
||||
설치/초기 서비스 연결 시 UI Component와 CSS를 가져온다. 실제 `themeUrl`은 서비스 설정에서 결정하며 package 내부에는 회사 CDN 주소가 없다.
|
||||
|
||||
```ts
|
||||
import "@hm-design/ui/theme.css"
|
||||
import {
|
||||
fetchRuntimeTheme, resolveRuntimeTheme, loadRuntimeTheme,
|
||||
applyRuntimeTheme, resetRuntimeTheme,
|
||||
} from "@hm-design/ui/theme"
|
||||
|
||||
// serviceConfig.themeUrl은 CDN/API 설정값이다.
|
||||
const root = document.documentElement
|
||||
const success = await loadRuntimeTheme(root, serviceConfig.themeUrl)
|
||||
// success=false: 이 root의 override를 해제하고 host/package HM fallback 유지.
|
||||
|
||||
// 별도의 Default JSON + 서비스 partial을 사용할 때:
|
||||
try {
|
||||
const defaults = await fetchRuntimeTheme(serviceConfig.defaultThemeUrl)
|
||||
const serviceTheme = await fetchRuntimeTheme(serviceConfig.themeUrl)
|
||||
applyRuntimeTheme(root, resolveRuntimeTheme(serviceTheme, defaults))
|
||||
} catch {
|
||||
resetRuntimeTheme(root)
|
||||
}
|
||||
|
||||
// 이미 받은 설정/API/로컬 JSON에도 같은 Apply 계약을 사용한다.
|
||||
// Apply에는 unknown 전체 validation이 있다.
|
||||
applyRuntimeTheme(root, configurationTheme)
|
||||
|
||||
// 설정 URL의 버전/hash가 바뀌거나 운영 refresh 이벤트가 오면 다시 호출한다.
|
||||
// 라이브러리/Component 재빌드와 브라우저 reload는 필요 없다.
|
||||
await loadRuntimeTheme(root, serviceConfig.updatedThemeUrl)
|
||||
```
|
||||
|
||||
- `fetchRuntimeTheme(url, RequestInit?)`: HTTP status/JSON/기존 allowlist 검증 후 partial 문서 반환. DOM을 건드리지 않고 실패 시 throw한다. URL/캐시/credentials/signal은 소비 서비스에서 지정한다.
|
||||
- `resolveRuntimeTheme(theme, defaults?)`: 패키지 Default + 선택적 supplied Default + partial override를 검증 후 합친다. Component Token을 만들지 않는다. 그룹의 일부 stop만 제공하는 입력은 거부한다.
|
||||
- `loadRuntimeTheme(root,url,{request?,defaults?}?)`: fetch/검증/선택적 merge/Apply의 편의 함수. 오류/invalid/HTTP 실패 시 false와 package fallback. 더 늦게 도착한 이전 요청은 새 Theme를 덮거나 reset하지 않는다. 직접 Apply/Reset도 pending load를 무효화한다.
|
||||
- `isRuntimeTheme`: DOM 변경 없는 사전 검증. `applyRuntimeTheme`/`resetRuntimeTheme`: 기존 public Apply/Reset 경로와 host inline 값/priority 복원 정책 유지.
|
||||
|
||||
앱 시작 전/후 어디서든 호출할 수 있다. CSS는 최초 paint 전에 로드한다. 네트워크 응답 전에는 package fallback, 응답 후에는 External Theme다. 로딩 timeout이 필요하면 서비스에서 `AbortSignal.timeout(...)` 또는 AbortController를 RequestInit에 제공한다. 로더는 polling/push subscription을 자동 시작하지 않는다. 이미 열린 서비스가 최신 JSON을 쓰려면 소비 서비스의 refresh 이벤트/설정 갱신/호출 정책이 필요하다.
|
||||
|
||||
## 디자인팀 workflow
|
||||
|
||||
1. `themes/service-example.json`을 서비스 파일로 복사하거나 `themes/default.json`의 상위 컬러 역할을 수정한다. 공통 팔레트/gradient보다 Primary/Active/Focus/Accent 같은 역할을 우선 편집한다.
|
||||
2. Storybook의 `Theme/Designer Playground/Editor`에서 URL/파일 Import로 JSON을 불러온다. 기본값은 `themes/default.json`을 Runtime merge하여 표시한다. 개발 서버에서는 Default JSON import의 HMR Preview도 가능하다. 다른 서비스 파일을 변경한 뒤 같은 URL을 Load URL로 다시 불러오면 CSS/Component 수정 없이 즉시 반영한다.
|
||||
3. 실제 Input/Control/Calendar/Date Picker/메뉴/표/내비게이션과 1280/768/390 레이아웃을 확인한다. Editor의 값 변경도 같은 공식 Contract를 사용한다.
|
||||
4. Editor를 이용했다면 Export한 JSON을 해당 `themes/*.json`에 저장해 Git에 포함한다. Editor는 repository 파일을 자동으로 덮어쓰지 않는다.
|
||||
5. Git diff/review 후 디자인팀의 승인된 운영 절차로 commit/push한다.
|
||||
6. 아래 Theme-only 산출물을 CDN에 배포한다. Git은 이력/검토, CDN은 Runtime delivery를 담당한다. 서비스가 Git 파일을 직접 읽지 않는다.
|
||||
|
||||
## Theme-only 배포
|
||||
|
||||
```powershell
|
||||
npm run build:themes
|
||||
# 결과: theme-dist/themes/default.json, service-example.json 및 추가한 top-level JSON
|
||||
```
|
||||
|
||||
전체 문서 validation을 통과한 뒤에만 산출한다. `themes/drafts`는 제외한다. 삭제된 source의 옛 top-level JSON artifact도 출력 디렉터리에서 제거한다. 산출물은 `theme-dist/themes/`이며 UI/Graphic bundle과 별개다. 이 디렉터리의 파일만 선택한 CDN origin에 업로드하고 서비스 설정의 Theme URL을 갱신한다. CDN infrastructure/publish credentials/자동 pipeline은 구현하지 않았다. CDN은 cross-origin 사용을 위한 CORS 응답과 JSON Content-Type을 설정해야 한다.
|
||||
|
||||
## Cache/버전/롤백 권장안
|
||||
|
||||
인프라 미정 상태의 **추천안**은 `/themes/service-a/<content-hash>.json` 또는 `/themes/service-a/v12.json`처럼 변경마다 별도 파일을 발행하고 서비스 설정을 새 URL로 바꾸는 방식이다. 이전 파일을 유지하므로 rollback은 이전 URL로 돌린 후 Loader를 다시 호출한다. 버전 파일에는 장기 `Cache-Control: public, max-age=31536000, immutable`을 사용할 수 있다. AWS도 versioned 파일/디렉터리 이름으로 갱신하는 방식을 안내한다. [CloudFront 파일 versioning](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/UpdatingExistingObjects.html), [MDN Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control).
|
||||
|
||||
대안은 `/themes/service-a.json?v=<content-hash>`다. CDN cache key가 query를 포함하도록 설정해야 효과가 있다. 안정 URL을 덮어쓰려면 짧은 TTL/`no-cache`와 ETag 재검증 또는 CDN purge 정책을 함께 정한다. 브라우저의 `fetch(...,{cache:"no-store"})`만으로 CDN edge 캐시를 항상 갱신한다고 보장하지 않는다. Preview는 no-store로 요청하지만 Production 캐시 정책은 소비 서비스/CDN 설정의 책임이다. [MDN HTTP caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching), [MDN Request.cache](https://developer.mozilla.org/en-US/docs/Web/API/Request/cache).
|
||||
|
||||
실제 운영 URL/versioning 방식은 서비스 설정에서 확정한다. 자동 버전 관리/manifest/publish/rollback 서비스는 미구현이다.
|
||||
|
||||
## 보안/제한
|
||||
|
||||
공개 allowlist의 HEX6/기존 radius 값만 Production Runtime이 허용한다. Theme artifact와 Designer UI는 컬러 전용이며 Component Token/임의 CSS/HTML/JS/URL 값은 거부한다. invalid Import는 마지막 유효 Preview를 유지하고 URL Loader 실패는 package HM Default로 돌아간다. Partial은 교체이며 이전 Theme key가 남지 않는다.
|
||||
|
||||
Global root Theme는 body Portal에도 적용된다. wrapper 밖 Portal은 Global을 사용하며 Scoped 전달은 기존 FOLLOW_UP이다. 동일 root는 한 owner가 관리한다. 새 상위 역할/Mapping/behavior가 필요한 변경은 UI package 작업이며 단순 Theme 컬러 변경과 구분한다.
|
||||
@@ -0,0 +1,119 @@
|
||||
# 개발 적용 가이드 (검토용 개정안)
|
||||
|
||||
페이지 구현 전에 [Navigation Single Mode Policy](navigation-single-mode.md)를 적용한다. AI 코드 생성·제안, 신규/기존 화면, 프로토타입, Production 모두 GNB / LNB / DASHBOARD 중 정확히 하나만 선택한다. 모바일도 같은 체계의 Responsive Variant를 사용하며 별도 요청 없이 Navigation 조합을 생성하거나 제안하지 않는다.
|
||||
|
||||
> 대상: 디자인팀과 서비스 개발팀. 현재 UI/CENTER compiled dist build/export, JSON Default/서비스 partial, 공개 Runtime 검증·Global/Scoped Apply와 URL Loader, Storybook JSON Preview, Theme-only artifact 산출이 구현되었다. Registry publish 이력은 테스트 ZIP 검증 범위 밖이며 실제 CDN infrastructure/publish pipeline은 다음 단계다. 테스트 ZIP은 로컬 workspace와 포함 dist를 사용하므로 Registry 접근·publish가 필요하지 않다. 아래 장기 운영 예시의 `@company/ui`는 목표명이며 현재 import는 `@hm-design/ui`다. 현재 실제 API/독립 배포 절차는 [Theme delivery](theme-delivery.md), 디자인팀 workflow는 [Theme guide](designer-theme-guide.md)를 따른다.
|
||||
|
||||
## 1. 착수 순서
|
||||
|
||||
AI 신규·수정 UI 작업은 사용자의 별도 요청 없이 기존 Component / Storybook Reference 우선, Theme / Semantic 기본 적용, 단일 Navigation, 최소 Custom을 적용한다. 변경 범위(`STORYBOOK_ONLY | PRODUCTION_UI | BOTH`)를 정한 뒤 [AI 신규 화면 구현 순서](shadcn-기반-구현-원칙.md#9-ai-신규-화면-구현-순서)를 따른다. Theme 사용과 Theme Source 수정은 [테마 토큰 가이드](테마-토큰-가이드.md)의 별도 조건을 적용한다.
|
||||
|
||||
### 1.1 패키지·운영 기반 도입 순서
|
||||
|
||||
아래는 서비스 도입 계획이며 이미 구현된 HM Component를 새로 생성하라는 순서가 아니다.
|
||||
|
||||
1. 서비스별 사용 현황을 모아 공통화할 컴포넌트 3~5개와 필요한 Semantic Token을 선정한다. 예: Button, Input, Card, Dialog, Select.
|
||||
2. `@company/ui`의 공개 API, 기본 Theme CSS, Semantic Token 계약을 버전 관리한다. 이름·타입·기본값·허용 범위를 한곳에서 정의한다.
|
||||
3. shadcn 컴포넌트는 Design System 저장소에서만 도입한다. upstream 버전/도입일과 변경 이유를 기록하고 회사 API·토큰·접근성 기준에 맞게 수정한다. 이후 upstream 변경도 diff를 검토해 선택적으로 반영한다.
|
||||
4. Storybook이 패키지의 **공개 진입점**을 사용하게 하고 대표 상태 및 Theme Preview를 추가한다.
|
||||
5. 한 앱에서 패키지와 기본 CSS를 설치한다. 앱 번들에 import하지 않은 별도 로컬 서버의 Theme JSON을 Loader로 읽어 값만 바꾸고, 앱 재빌드 없이 다음 진입·새로고침에서 반영되는지 확인한다. 운영 단계에서는 이를 API/CDN Publish 경로로 바꾼다.
|
||||
|
||||
서비스 앱에서 `shadcn add`로 컴포넌트를 별도 생성하거나 공통 컴포넌트 코드를 복사해 고치지 않는다. 서비스 전용 조합은 공개 컴포넌트를 감싸서 만든다. 공통 API 변경이 필요하면 Design System 변경으로 제안한다.
|
||||
|
||||
## 2. 단일 저장소의 파일 책임과 패키지 경계
|
||||
|
||||
현재 파일 배치와 목표 배치를 구분한다. **한 Git 저장소**에서 코드, Storybook, Theme 초안을 함께 버전 관리할 수 있으며, 별도 Git 저장소나 monorepo 구성은 필수가 아니다.
|
||||
|
||||
| 위치 | 현재/목표 | 책임과 배포 경계 |
|
||||
|---|---|---|
|
||||
| `packages/ui/src/components/ui/`, `src/hm-theme.css` | 현재 구현 | Production Component와 Demo 스타일 분리. `@hm-design/ui` compiled dist export (JS·타입·CSS) |
|
||||
| `.storybook/`, `src/components/*.stories.tsx` | 현재 시제품 → 계속 사용 | 실제 컴포넌트 검수. 패키지 산출물에는 Story를 넣지 않음 |
|
||||
| `packages/ui/src/patterns/`, `packages/ui/src/theme.css` | 현재 구현 | 업무/Layout composition과 기본 Theme CSS. Library 산출물은 `npm run build:packages`로 생성 |
|
||||
| `packages/center-graphic/` | 현재 구현 | `@hm-design/center-graphic` 독립 package 배포 경계. UI package와 필수 의존 없음 |
|
||||
| `packages/ui/src/theme/contract.ts`, `runtime.ts` | 현재 구현 | 공개 73개 입력, 형식/범위 검증, atomic fallback·cleanup, Global/Scoped 적용. `@hm-design/ui/theme` export |
|
||||
| `themes/drafts/` | Runtime MVP 제안 | 디자인팀의 Theme 값 초안·검수 이력. Git 커밋만으로 운영 Theme이 바뀌지 않으며 패키지에도 넣지 않음 |
|
||||
| `themes/*.json` | 현재 구현 | `themes/default.json`은 공식 Default Color Source of Truth. 검증된 top-level JSON은 Theme CDN 배포 단위이며 Git push와 CDN publish는 별도 단계 |
|
||||
| 서비스 앱의 bootstrap | 개발팀 앱 | 패키지 버전 설치, themeId/mode 결정, API/CDN fetch·캐시·실패 처리, 검증된 값 적용 |
|
||||
|
||||
패키지 공개 파일은 `package.json`의 export·포함 목록과 빌드 결과로 확인한다. 기본 CSS와 컴포넌트 CSS 규칙은 패키지 버전에 묶인다. 외부 발행 대상은 계약에 등록된 **기존 Semantic Token 값**이며, 변경된 JSON/CSS 초안을 자동으로 운영 앱에 노출하지 않는다. 필요해지면 `packages/ui`와 `apps/storybook`으로 재배치해도 Git 저장소는 하나로 유지한다.
|
||||
|
||||
검증 계약은 Storybook·서비스 앱·Theme Publish가 공유해야 한다. 현재 검증·적용 도우미는 `@hm-design/ui/theme`에서 공개한다. 네트워크 URL, tenant 식별, 인증, 갱신 시점은 개발팀이 서비스 앱에서 관리한다.
|
||||
|
||||
## 3. Token 연결과 기본값
|
||||
|
||||
현재 연결은 Primitive → Semantic → Component Mapping이다. 다음은 실제 HM selector와 Token 이름을 사용하는 예다. 컬러 원본은 `themes/default.json`이며 생성 CSS를 별도 수동 원본으로 관리하지 않는다. 전체 입력은 [Runtime 계약표](runtime-theme-contract.md)를 따른다.
|
||||
|
||||
```css
|
||||
/* 앱 초기 렌더 전에 로드 */
|
||||
@import "tailwindcss";
|
||||
@import "@hm-design/ui/tailwind.css";
|
||||
@import "@hm-design/ui/theme.css";
|
||||
|
||||
/* package의 Input Component Mapping이 Semantic을 소비하는 예 */
|
||||
:root, [data-hm-theme] {
|
||||
--hm-input-border-color: var(--hm-sem-stroke-input-default);
|
||||
}
|
||||
[data-slot="input"] {
|
||||
border-color: var(--hm-input-border-color);
|
||||
}
|
||||
```
|
||||
|
||||
Tailwind `bg-primary`를 사용한다면 package의 `tailwind.css`에서 `--color-primary` → `--primary` → `--hm-sem-action-primary` 연결을 확인한다. 클래스 이름이 같다는 이유만으로 Theme 연동이 성립하지 않는다. 컴포넌트 코드의 임의 hex/px 값, Primitive 직접 참조, 서비스별 `!important` override는 허용하지 않는다. 구조적 고정값은 Runtime 계약 밖의 코드 값으로 별도 관리할 수 있다.
|
||||
|
||||
## 4. Theme 문서와 버전 호환성
|
||||
|
||||
MVP의 Theme 문서 예시는 다음과 같다. `tokens`는 **선택적 override**이므로 없는 키는 기본 CSS를 사용한다. 현재 Light/Dark 구분이 없어 모드별 구조는 두지 않는다.
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"themeId": "default",
|
||||
"themeVersion": "2026-10-02.1",
|
||||
"tokens": {
|
||||
"color.action.primary": "#7c3aed",
|
||||
"radius.input.default": "6px"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `schemaVersion`은 필드 구조와 Token 계약의 호환성을 판단한다. 앱이 지원하지 않는 버전이면 Theme 전체를 적용하지 않는다.
|
||||
- `themeVersion`은 Publish할 때마다 새로 발행하는 불변 값이다. `@company/ui` 패키지 버전과 독립적이다.
|
||||
- 구버전 앱이 남아 있는 동안에는 해당 앱의 계약에서 지원하지 않는 Token을 운영 Theme에 발행하지 않는다. 계약 확장은 패키지 릴리스와 앱 업데이트가 먼저다.
|
||||
|
||||
현재 HM Storybook 시제품은 브랜드 배경과 주요 버튼의 면·테두리 그라데이션을 역할별 stop 색상으로 조합한다. 초안 JSON에는 allowlist에 등록한 stop 색만 6자리 HEX로 적고, 배경·버튼 면·버튼 테두리 각각의 stop 세트는 부분 적용하지 않는다. 각도·stop 위치·그림자는 기본 CSS 규칙으로 유지한다. `radius.input.default`와 `radius.container.default`는 별도 Semantic 역할로 연결하고 2–12px 정수를 공통 Runtime API에서 허용한다. Placeholder는 확인된 `color.fg.placeholder` 기본값을 사용한다. `ui_viz`의 Button D-087 색을 HM Runtime Semantic stop에 연결한 것은 현재 저장소의 **구현된 Runtime 계약**이다. 운영 allowlist로 승격하기 전에 디자인팀이 역할 이름·범위·호환성을 승인해야 한다.
|
||||
|
||||
초기 검증 정책은 색상은 승인된 형식(예: 불투명 hex), radius는 허용 범위의 `px` 값으로 제한한다. shadow, font, spacing을 추가할 때에는 **각각 별도 타입·범위·호환성 검사**를 설계한다. 임의 CSS 함수, `url()`, `var()`, `!important`, selector, JavaScript는 Theme 값으로 허용하지 않는다. 접근성 검토는 개별 값의 형식 검사와 별도로 수행한다.
|
||||
|
||||
## 5. 앱 Theme Loader의 책임과 실패 처리
|
||||
|
||||
1. 앱이 기본 Theme CSS를 먼저 로드한다.
|
||||
2. API/CDN에서 활성 버전 포인터와 불변 Theme 문서를 가져온다. timeout과 캐시 정책을 명시한다.
|
||||
3. 문서 전체의 스키마 버전, ID, allowlist, 값 타입·범위를 검증한다. **하나라도 실패하면 일부 키만 적용하지 않는다.**
|
||||
4. 검증이 끝난 뒤 이전 Runtime override 키를 제거하고 허용된 키만 적용한다. Theme을 교체할 때 남은 오래된 키가 기본값을 가리지 않게 한다.
|
||||
5. fetch·검증 실패 시 Runtime override를 제거하고 기본 CSS를 사용한다. 실패 원인과 적용한 버전을 기록한다.
|
||||
6. 운영 초기에는 새 페이지 진입·새로고침 때 최신 활성 버전을 읽는다. 열린 화면의 실시간 교체는 별도 요구가 생길 때 도입한다.
|
||||
|
||||
현재 적용 함수는 unknown 입력을 자체 검증하며 문서 전체 실패 시 atomic fallback한다. Global은 document root, Scoped는 명시적 wrapper에 적용하고 cleanup으로 host 기존 inline 값/priority를 복원한다. Theme ID 확인·URL/캐시 정책은 서비스 책임이며 공개 URL Loader는 HTTP/JSON/Runtime 계약을 검증한다. 렌더 중 깜빡임이 문제라면 초기 Theme 로딩 순서와 CSS 적용 시점을 앱 bootstrap에서 조정한다. 캐시는 성능용이며 검증과 기본값 fallback을 대체하지 않는다.
|
||||
|
||||
**서비스 전역 Theme는 Global Runtime Theme를 권장한다.** `applyRuntimeTheme(document.documentElement, theme)` 또는 `loadRuntimeTheme(document.documentElement, themeUrl)`로 적용하면 body Portal도 같은 Theme를 받는다.
|
||||
|
||||
**Scoped wrapper Theme는 wrapper 밖 body Portal에 자동 전달되지 않는다.** SelectContent, PopoverContent(Date Picker·Combobox 포함), DropdownMenuContent/ContextMenuContent 등 내부 body Portal popup은 Global Theme를 따른다. 이 Content wrapper에는 Theme scope를 전달하는 `container` API가 없다. 별도 export된 Radix Portal wrapper의 props를 Content의 container 지원으로 간주하지 않는다. Scoped는 DOM 자손에만 적용되며 세부 apply/merge/fallback/scope 계약은 [Runtime Theme](runtime-theme.md)를 따른다.
|
||||
|
||||
## 6. Storybook 작성·검수 기준
|
||||
|
||||
- Story import는 `@company/ui`의 공개 진입점과 기본 CSS를 사용한다. 로컬 내부 파일을 직접 가리켜 패키지 배포 결과와 달라지는 구성을 피한다.
|
||||
- 컴포넌트별 공개 props, variant/state, 오류·비활성·로딩, 키보드/포커스 동작을 지원 범위에 맞게 기록한다.
|
||||
- 현재 Theme verification Story에서 Default/External과 Runtime 계약을 비교한다. Theme selector toolbar는 없다. Preview는 운영 Publish를 수행하지 않고 패키지 Runtime API를 사용한다.
|
||||
- 값 검수는 실제 컴포넌트의 기본/hover/focus/disabled/error 상태, 대비·가독성, 잘림·넘침을 포함한다. Theme 변경이 닿는 Story에 집중한다.
|
||||
- 검수 기록에는 `@company/ui` 버전, Theme 버전 또는 Draft ID, 결과를 남긴다. Storybook 검수 후 실제 앱에서도 같은 계약이 적용되는지 통합 확인한다.
|
||||
|
||||
## 7. 개발 완료 기준
|
||||
|
||||
- 단일 저장소에서 만든 `@company/ui`의 공개 컴포넌트·기본 CSS를 서비스 앱이 버전으로 설치하고, Theme 장애에도 기본값으로 동작한다.
|
||||
- 별도 제공되는 승인된 Semantic Token 값만 변경해 Button 등 대표 컴포넌트가 앱 재빌드 없이 새 진입·새로고침에서 바뀐다. 패키지 CSS 규칙 변경에는 새 패키지 버전이 필요하다.
|
||||
- 패키지 Runtime API는 잘못된 키·값·스키마 버전을 거부한다. 서비스별 themeId 일치 검사는 향후 Loader가 담당한다(현재 API가 ID를 검증한다고 간주하지 않는다).
|
||||
- Theme 교체·해제 시 이전 override가 남지 않는다.
|
||||
- Storybook Preview와 앱에서 동일한 Theme 문서를 같은 계약으로 검증한다.
|
||||
- 컴포넌트 API 변경은 Theme Publish만으로 처리하지 않고 패키지 릴리스와 앱 업데이트로 전달한다.
|
||||
|
||||
운영 절차는 [운영/배포 가이드](운영-배포-가이드.md)를 따른다.
|
||||
@@ -0,0 +1,104 @@
|
||||
# 운영·배포 가이드 (검토용 개정안)
|
||||
|
||||
> 이 문서는 장기 운영 목표도 포함한다. 현재 UI/CENTER compiled dist build/export, JSON Default/partial과 공개 URL Loader/Apply, Designer JSON Preview, `npm run build:themes` 독립 artifact 산출이 구현되어 있다. Registry publish 이력은 테스트 ZIP 검증 범위 밖이며 Theme Admin, 실제 CDN/pipeline은 미구현이다. 테스트 ZIP은 포함 dist와 로컬 workspace를 사용하며 Registry 접근·publish는 필수가 아니다. 아래 `@company/ui`는 목표명이며 현재 패키지는 `@hm-design/ui`다. 이번 단계의 실제 배포/캐시/롤백은 [Theme delivery](theme-delivery.md)를 따른다. MVP 수동 운영과 향후 자동화를 구분한다.
|
||||
|
||||
단일 `HMDesignSystem` Git 저장소에서 컴포넌트 소스·기본 CSS, Storybook, Theme 초안을 함께 관리한다. **Git 커밋, 패키지 릴리스, Theme Publish는 서로 다른 단계**다. 소스에 CSS/JSON을 수정하거나 Storybook을 빌드하는 것만으로 사용 중인 앱에 변경이 전달되지 않는다.
|
||||
|
||||
| 같은 저장소의 변경 | 실제 배포 산출물 | 앱 반영 |
|
||||
|---|---|---|
|
||||
| 컴포넌트 구현·selector·기본 CSS·Token 계약 | 새 `@company/ui` 버전 | 개발팀이 의존성 업데이트·통합 검증·앱 배포 |
|
||||
| 이미 지원하는 Semantic Token 값 | 검증·승인된 불변 Theme 버전과 API/CDN 활성 포인터 | 연동된 Loader가 다음 진입·새로고침에서 가져와 적용; 앱 재배포 없음 |
|
||||
| Storybook Story·내부 문서·Manager 스타일 | 내부 Storybook 빌드 | 검수 환경 갱신; 서비스 앱 반영 경로가 아님 |
|
||||
|
||||
패키지와 Theme은 같은 Git 변경 이력을 추적하되 각자 버전·검증·승인·롤백을 기록한다. CI를 도입하면 변경 파일별로 관련 검증을 선택하고, Publish는 별도 승인 단계에서 실행한다. Theme 초안을 저장소에 두더라도 승인되지 않은 값을 운영 URL에 자동 공개하지 않는다.
|
||||
|
||||
## 1. 두 배포 경로
|
||||
|
||||
### 컴포넌트·계약 변경
|
||||
|
||||
```text
|
||||
요구사항·영향 분석 → Design System 코드/Story 변경 → 검수·PR
|
||||
→ @company/ui 버전 릴리스 → 서비스 앱 의존성 업데이트
|
||||
→ 앱 통합 확인 → DEV → STG → PROD
|
||||
```
|
||||
|
||||
DOM, 공개 props/variant/state, interaction, 접근성, 기본 CSS의 selector·상태·레이아웃 규칙, 새 Semantic Token·스키마는 이 경로를 따른다. shadcn upstream 업데이트도 Design System 저장소에서 검토한 뒤 이 경로로 반영한다. 패키지 버전과 앱별 적용 버전을 기록한다. 서비스는 패키지 새 버전을 자동 수용하지 않고 앱 테스트·배포를 거쳐 사용한다.
|
||||
|
||||
### 기존 Theme 값 변경
|
||||
|
||||
```text
|
||||
Draft 값 수정 → 실제 @company/ui Storybook Preview → 자동 검증
|
||||
→ 디자인/운영 승인 → 불변 Theme 버전 생성 → STG 확인
|
||||
→ 운영 활성 포인터 전환 → API/CDN 제공 → 앱 새 진입·새로고침 시 로드
|
||||
```
|
||||
|
||||
이 경로는 **이미 배포된 앱이 지원하는 Semantic Token의 값**에 한정한다. Theme Loader가 API/CDN을 읽도록 연동된 앱에서는 앱/패키지 재배포 없이 다음 진입·새로고침에 반영한다. Theme을 발행하기 위해 Storybook이나 앱을 다시 빌드하지 않는다. Storybook은 Preview 환경이며 Publish 권한과 분리한다.
|
||||
|
||||
## 2. Publish 전 검증 게이트
|
||||
|
||||
| 게이트 | 확인 사항 | 실패 시 |
|
||||
|---|---|---|
|
||||
| 계약 검증 | `schemaVersion`, themeId, Token allowlist, 필드/값 타입, 단위·범위 | Publish 중단 |
|
||||
| 호환성 | 운영 중인 앱의 `@company/ui`/계약 버전이 모든 발행 키를 지원 | 앱 업데이트 선행 또는 값 범위 축소 |
|
||||
| UI Preview | 실제 컴포넌트의 대표 상태와 대비·가독성, 잘림·넘침 | Draft 수정 후 재검수 |
|
||||
| 환경 검증 | STG에서 앱이 문서를 받아 적용하고 장애 시 기본값 사용 | PROD 활성화 보류 |
|
||||
| 승인·기록 | 변경 요청, 변경 전후 값, 검수 결과, 승인자, 예정 시각·영향 범위 | 기록 완료 전 보류 |
|
||||
|
||||
값 검증은 클라이언트와 Publish 서버 양쪽에서 수행한다. 클라이언트 검증은 오래된 앱과 잘못 배포된 문서로부터 UI를 보호하고, 서버 검증은 잘못된 Theme 발행을 막는다. 자동 검증을 통과해도 색 대비나 UI 안전성이 자동으로 보장되는 것은 아니다.
|
||||
|
||||
## 3. 버전, 활성화, 캐시
|
||||
|
||||
- Theme 문서는 `themeId`와 `themeVersion`으로 식별하는 **불변 산출물**로 저장한다. 발행 후 내용을 덮어쓰지 않는다.
|
||||
- 환경별 활성 포인터(예: STG/PROD)가 버전을 가리킨다. Publish와 rollback은 이 포인터 변경으로 수행한다.
|
||||
- API/CDN은 활성 포인터와 버전 문서를 제공한다. 버전 URL은 오래 캐시할 수 있고, 활성 포인터는 짧은 TTL 또는 재검증 가능한 ETag 정책을 사용한다. 구체 TTL은 운영 요구와 CDN 특성에 맞춰 확정한다.
|
||||
- 앱은 새 페이지 진입·새로고침 시 활성 버전을 확인한다. 포인터 전환과 CDN 전파 사이의 지연을 운영 반영 시간에 포함해 기록한다.
|
||||
- `@company/ui` 버전과 Theme 버전은 별개다. Publish 기록에는 검수한 패키지 버전 및 운영 앱의 최소 지원 계약 버전을 함께 남긴다.
|
||||
|
||||
## 4. Rollback 절차
|
||||
|
||||
1. 장애 범위, 현재 활성 Theme 버전, 직전 정상 버전, 영향 앱을 확인한다.
|
||||
2. 직전 정상 버전의 계약 호환성을 확인하고 운영 활성 포인터를 해당 **기존 불변 버전**으로 되돌린다.
|
||||
3. API/CDN에서 활성 포인터와 문서가 일치하는지 확인한다. 필요한 경우 포인터 캐시를 갱신하고 전파 시간을 관찰한다.
|
||||
4. 새 페이지 진입·새로고침에서 실제 앱의 시각 결과와 적용 버전을 확인한다. fetch/검증 오류에는 기본 Theme fallback이 동작하는지 확인한다.
|
||||
5. 사고 기록에 원인, 시작·복구 시각, 영향 범위, 후속 조치를 남긴다. 문제가 있는 버전을 덮어써서 복구하지 않는다.
|
||||
|
||||
이미 열려 있는 탭은 초기 운영 정책상 즉시 교체되지 않는다. 즉시 전환이 필요한 서비스가 확인되면 갱신 신호와 탭 동기화를 별도 설계한다. 활성 포인터나 CDN이 일시적으로 실패하면 앱은 검증되지 않은 값을 적용하지 않고 기본 Theme을 유지한다.
|
||||
|
||||
## 5. 단계별 운영 도입
|
||||
|
||||
| 단계 | 운영 방식 | 필수 증거 |
|
||||
|---|---|---|
|
||||
| 현재 구현 | compiled dist build/export 패키지·JSON Theme/URL Loader/Apply·Designer Preview·Theme-only artifact. Registry publish 이력은 ZIP 검증 범위 밖; 운영 CDN Publish 미구현 | JSON/Runtime/Default parity/레이아웃 및 URL 실패 fallback 검증 |
|
||||
| MVP 서비스 도입 | 구현된 패키지 빌드·로컬 Theme JSON·Storybook Preview를 서비스 앱에 연결하고 수동 검증. Production Publish 없음 | 동일 패키지 컴포넌트의 override/fallback, 새로고침 확인 |
|
||||
| 운영 기반 | Registry, 불변 버전, 검증 API, 승인 기록, 환경별 활성 포인터, API/CDN, 롤백 절차 | STG→PROD 발행과 이전 버전 복귀의 실제 앱 검증 |
|
||||
| 운영 확장 | Theme Admin, 역할별 권한, 감사 로그, 자동 시각·접근성 검사, 관측 | 승인·배포·복구 이력과 서비스별 적용 버전 추적 |
|
||||
|
||||
Admin 없이 시작하더라도 Publish를 단순 파일 덮어쓰기나 디자인 툴 자동 직결로 만들지 않는다. 운영 기반 단계에서는 최소한 검증, 불변 버전, 승인, 활성화, 롤백을 갖춘다.
|
||||
|
||||
## 6. 단계별 책임과 산출물
|
||||
|
||||
| 단계 | 담당과 산출물 |
|
||||
|---|---|
|
||||
| Design System 구현 | **디자인팀**이 Token 의도·값, 컴포넌트와 계약 변경, 기본 Theme, Storybook·Preview 검수, `@company/ui` 패키지 버전을 담당 |
|
||||
| Theme 운영·발행 | **디자인팀**이 서버 검증, 불변 버전 저장, 승인 기록, 환경별 활성화, API/CDN·롤백을 담당 |
|
||||
| 서비스 적용 | **개발팀**이 패키지 업데이트, Theme ID 선택, Loader와 fallback, 앱 통합 검증을 담당 |
|
||||
|
||||
디자인팀은 Design System 구현과 Theme 운영을 함께 수행한다. 표는 조직 분리가 아니라 작업 단계를 구분한다. 같은 사람이 여러 단계를 수행할 수 있으며, 단계마다 변경 내용·검수 결과·승인·반영 버전을 남긴다. 운영 시작 전 각 단계의 실제 담당자와 승인자를 정하고, 디자인 승인만으로 운영 활성화가 자동 실행되는지는 디자인팀의 권한 정책에서 결정한다.
|
||||
|
||||
## 7. 운영 체크리스트
|
||||
|
||||
### 컴포넌트 릴리스
|
||||
|
||||
- [ ] 변경이 컴포넌트/API/기본 CSS 규칙/계약 경로인지 분류했다.
|
||||
- [ ] 관련 Story와 패키지 공개 API를 갱신하고 접근성·주요 상태를 확인했다.
|
||||
- [ ] 패키지 버전을 발행하고 앱별 업데이트·테스트·배포를 기록했다.
|
||||
- [ ] 영향 Theme 문서의 스키마/Token 호환성을 확인했다.
|
||||
|
||||
### Theme Publish
|
||||
|
||||
- [ ] Git의 Theme 초안 변경과 운영 Publish를 구분하고, 기존 Semantic allowlist 값만 발행 대상으로 검증했다.
|
||||
- [ ] 실제 컴포넌트 Storybook Preview와 STG 앱에서 검수했다.
|
||||
- [ ] 승인자·변경 내용·호환 패키지 버전·이전 활성 버전을 기록했다.
|
||||
- [ ] 새 불변 버전을 저장하고 운영 활성 포인터를 전환했다.
|
||||
- [ ] API/CDN 응답과 새로고침한 운영 앱의 적용 버전·fallback을 확인했다.
|
||||
- [ ] 필요 시 이전 버전으로 되돌릴 수 있는 포인터와 절차를 확인했다.
|
||||
@@ -0,0 +1,310 @@
|
||||
# Theme & Token Architecture
|
||||
|
||||
## AI Theme 기본 적용 정책 (2026-10-07 사용자 확정)
|
||||
|
||||
사용자가 별도로 "테마를 적용해줘", "Semantic Token을 사용해줘"라고 요청하지 않아도 모든 AI 신규·수정 UI는 Theme을 기본 적용한다. Theme은 Component 탐색·설계 단계부터 포함되는 구현 조건이다. 전체 구현 순서는 [AI/Component 구현 원칙](shadcn-기반-구현-원칙.md)을 따른다.
|
||||
|
||||
- 설계 계층은 **Primitive → Semantic → Component Mapping**을 유지한다.
|
||||
- 값 선택 순서는 **Existing Semantic Token → 해당 Existing Component Token → Existing CSS Variable → 필요한 경우 최소 Semantic 확장**이다. 이는 탐색 순서이며 Component Mapping 계층을 제거하거나 다른 Component의 내부 Token을 직접 공유하는 규칙이 아니다.
|
||||
- 기존 Token이 있는 색상·spacing·radius·typography·값을 먼저 고정값으로 작성하지 않는다. 같은 Token을 여러 파일에 중복 정의하지 않는다. 정의되지 않은 속성은 기존 shadcn 기본 구현과 구조적 값의 계약을 유지하고 임의 Token을 양산하지 않는다.
|
||||
- 특정 Reference 색상을 그대로 재현하라는 요청에도 먼저 의미가 맞는 기존 Theme / Semantic Token에 연결한다. 정확한 값이 기존 Theme과 다르면 Theme 값 변경의 필요성과 영향을 확인한다. 독립 Reference 검증 표현은 Story 범위와 Production Theme 대상 여부를 먼저 확인하고 예외 사유를 기록한다.
|
||||
|
||||
```css
|
||||
/* 금지: UI에서 Theme 색상을 고정 */
|
||||
.status { color: #00C896; }
|
||||
/* 기존 의미에 맞는 Token 연결 예: 실제 진행 control 색 역할일 때 */
|
||||
.status { color: var(--hm-sem-state-active); }
|
||||
```
|
||||
|
||||
위 예는 상태 역할이 실제로 일치할 때만 적용한다. Dashboard 업무 Status는 기존 mapping을 먼저 확인하며 모든 업무 상태를 Control Active에 합치지 않는다. 문서의 Token·색상 예시는 현재 코드/계약과 대조하고 존재하지 않는 Token을 실제 값처럼 사용하지 않는다.
|
||||
|
||||
### Theme 사용과 Theme Source 수정
|
||||
|
||||
| 작업 | 기본 처리 |
|
||||
|---|---|
|
||||
| 기존 Token / CSS Variable을 UI에서 사용 | 별도 승인 없이 기본 적용 |
|
||||
| `themes/default.json`, Primitive / Semantic 값 자체 변경 | 요청 충족에 필요한 명시적 사유와 영향을 확인한 뒤 진행 |
|
||||
| 새 Semantic / 공개 Runtime Token 추가 | 기존 계약으로 해결 불가능한 경우 최소 확장; 소비처·fallback·호환성 및 package 변경 범위 확인 |
|
||||
|
||||
Theme 적용 요청을 Theme Source 수정 허가로 해석하지 않는다. UI를 연결하기 위해 Default/Primitive 값을 임의 변경하지 않는다. Color Source of Truth는 `themes/*.json`이며 생성된 fallback CSS/데이터를 별도 원본처럼 수동 수정하지 않는다.
|
||||
|
||||
### Runtime과 독립 fallback
|
||||
|
||||
```text
|
||||
themes/*.json
|
||||
→ Runtime Theme Loader
|
||||
→ CSS Custom Properties
|
||||
→ Semantic
|
||||
→ Component Mapping
|
||||
→ UI
|
||||
```
|
||||
|
||||
앱 초기 CSS에서 기존 `@hm-design/ui/theme.css` 적용을 확인하고, Theme 주입이 없으면 HM Default를 사용한다. 이미 적용된 Theme 환경과 공개 Runtime API를 재사용하며 별도 Theme 엔진을 만들지 않는다. Global / Scoped / Portal 경계와 partial / invalid / cleanup 계약은 [Runtime Theme](runtime-theme.md)를 따른다.
|
||||
|
||||
기존 공개 계약이 지원하는 color-only 변경은 Theme JSON만 별도 배포해 반영할 수 있어야 하며 UI package 재배포를 요구하지 않도록 Component에 값을 고정하지 않는다. 새 Token·CSS 규칙·Runtime 계약 변경은 package 변경이며 Theme JSON만으로 처리할 수 없다. 실제 배포 경로는 [Theme delivery](theme-delivery.md)를 따른다.
|
||||
|
||||
Primary와 On Accent는 독립 fallback을 가진 sibling Semantic이다. Primary override가 On Accent를 변경하게 만들거나 둘을 alias하지 않는다. Active / Focus / Destructive 및 Global / Scoped의 기존 독립성도 유지한다. 같은 Default 값이라는 이유로 fallback을 합치거나 새 상호 의존성을 만들지 않는다.
|
||||
|
||||
Theme 미적용 prototype / 독립 POC 등의 예외는 사용자의 명시적 요청과 기존 시스템에 영향 없는 별도 범위가 있을 때만 [AI 구현 예외 기준](shadcn-기반-구현-원칙.md#10-명시적-예외)에 따라 적용한다. 이 확정 정책은 아래 과거 예시의 상충 문구보다 우선한다.
|
||||
|
||||
## 목적
|
||||
|
||||
HMDesignSystem은 shadcn/ui를 기반으로 하며 하나의 HM 기본 테마를 운영한다.
|
||||
|
||||
HM Default는 기본 제공 Theme preset이다. 외부 서비스 Theme는 같은 공개 Runtime 계약을 사용해 값만 교체한다. Theme selector UI, Light/Dark 엔진은 만들지 않는다. 실제 계약/API와 scope 정책은 [Runtime Theme](runtime-theme.md)를 따른다.
|
||||
|
||||
디자인 변경 시 가능하면 React 컴포넌트 코드를 수정하지 않고 Theme / Token CSS 수정만으로 반영할 수 있는 구조를 유지한다.
|
||||
|
||||
---
|
||||
|
||||
## 기본 구조
|
||||
|
||||
HMDesignSystem의 스타일 값은 기본적으로 다음 구조를 따른다.
|
||||
|
||||
```text
|
||||
Primitive
|
||||
→ Semantic (공통 Theme 역할)
|
||||
→ Component Mapping Token
|
||||
→ Component
|
||||
```
|
||||
|
||||
컬러 Token은 Primitive → Semantic → Component Mapping의 3계층으로 구분한다. 공통 Theme에는 typography/radius/geometry도 포함하며, 선택적 공개 palette 사용과 컴포넌트 고유 geometry는 현재 구현 계약을 따른다.
|
||||
|
||||
---
|
||||
|
||||
## Global Theme Token
|
||||
|
||||
여러 컴포넌트에서 공통으로 사용하는 디자인 정책을 관리한다.
|
||||
|
||||
주요 관리 대상:
|
||||
|
||||
- Color
|
||||
- Typography
|
||||
- Radius
|
||||
- Shadow
|
||||
- Control Height
|
||||
- 공통 Spacing
|
||||
- 공통 상태 표현
|
||||
|
||||
현재 HM 공통 Token 예(컬러 원본은 `themes/default.json`, 아래 CSS는 생성 결과의 일부):
|
||||
|
||||
```css
|
||||
:root, [data-hm-theme] {
|
||||
--hm-sem-action-primary: var(--hm-color-primary);
|
||||
--hm-sem-bg-surface: var(--hm-color-surface-default);
|
||||
--hm-sem-stroke-input-default: var(--hm-color-border-control);
|
||||
--hm-radius-input: var(--hm-radius-control-md);
|
||||
}
|
||||
```
|
||||
|
||||
공통 디자인 정책이 변경되는 경우 Global Theme Token을 수정하여 관련된 여러 컴포넌트에 함께 반영할 수 있어야 한다.
|
||||
|
||||
---
|
||||
|
||||
## Component Mapping Token
|
||||
|
||||
각 컴포넌트는 공통 Theme 역할을 Component Mapping Token을 통해 사용한다.
|
||||
|
||||
예:
|
||||
|
||||
```css
|
||||
--hm-button-radius: var(--hm-radius-button);
|
||||
--hm-input-radius: var(--hm-radius-input);
|
||||
--hm-select-radius: var(--hm-radius-input);
|
||||
|
||||
--hm-input-border-color: var(--hm-sem-stroke-input-default);
|
||||
--hm-select-border-color: var(--hm-sem-stroke-input-default);
|
||||
--hm-card-border-color: var(--hm-sem-stroke-input-default);
|
||||
```
|
||||
|
||||
이 구조를 통해 공통 변경과 개별 변경을 모두 지원한다.
|
||||
|
||||
예를 들어:
|
||||
|
||||
```css
|
||||
--hm-radius-input: 6px;
|
||||
```
|
||||
|
||||
로 변경하면 이를 사용하는 Input, Select 등이 함께 변경된다. Button radius는 별도 Mapping을 따른다.
|
||||
|
||||
반대로 특정 컴포넌트만 변경해야 할 경우:
|
||||
|
||||
```css
|
||||
--hm-input-radius: 8px;
|
||||
```
|
||||
|
||||
처럼 Component Mapping Token만 변경할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
## Component 적용 원칙
|
||||
|
||||
컴포넌트 내부에는 실제 색상값이나 공통 디자인 값을 가능한 직접 작성하지 않는다.
|
||||
|
||||
피해야 하는 예:
|
||||
|
||||
```tsx
|
||||
className="bg-[#2563eb] border-[#d1d5db] rounded-[8px]"
|
||||
```
|
||||
|
||||
대신 Theme / Component Mapping Token을 CSS selector에서 사용한다.
|
||||
|
||||
예:
|
||||
|
||||
```css
|
||||
[data-slot="input"] {
|
||||
border-color: var(--hm-input-border-color);
|
||||
border-radius: var(--hm-input-radius);
|
||||
}
|
||||
```
|
||||
|
||||
shadcn의 기존 구조와 CVA variant 구조는 가능한 유지한다.
|
||||
|
||||
HMDesignSystem을 위해 shadcn 컴포넌트를 완전히 별도 구조로 재작성하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 공통화 기준
|
||||
|
||||
현재 값이 같다는 이유만으로 무조건 하나의 Component Token으로 합치지 않는다.
|
||||
|
||||
반대로 모든 값을 컴포넌트별 Token으로 만들지도 않는다.
|
||||
|
||||
판단 기준은 다음과 같다.
|
||||
|
||||
### Global Theme Token
|
||||
|
||||
여러 컴포넌트에서 동일한 디자인 정책으로 함께 변경되어야 하는 값
|
||||
|
||||
예:
|
||||
|
||||
- 기본 Border Color
|
||||
- 기본 Radius
|
||||
- Primary Color
|
||||
- Control Height
|
||||
- 기본 Typography
|
||||
|
||||
### Component Mapping Token
|
||||
|
||||
특정 컴포넌트에서 향후 독립적으로 변경될 가능성이 있는 값
|
||||
|
||||
예:
|
||||
|
||||
- Button Primary Background
|
||||
- Input Border
|
||||
- Dialog Background
|
||||
- Checkbox Checked Color
|
||||
|
||||
---
|
||||
|
||||
## State 관리
|
||||
|
||||
실제로 존재하는 상태에 대해서만 Token을 구성한다.
|
||||
|
||||
Control Active, Persistent Selected Surface, Navigation Current/Open은 서로 구분한다. Focus는 Active와 sibling Semantic이고 GNB/LNB는 contextual navigation focus 예외를 유지한다. Invalid는 Destructive border, Invalid + Focus는 Destructive border + Focus indication이다. 컴포넌트별 적용 범위는 해당 Component/Pattern 문서와 아래 HM Token 연결 절을 따른다.
|
||||
|
||||
예:
|
||||
|
||||
- default
|
||||
- hover
|
||||
- active / pressed
|
||||
- focus
|
||||
- selected / checked
|
||||
- disabled
|
||||
- error / invalid
|
||||
|
||||
size × variant × state × property 형태로 무조건 세분화하여 Token 수를 늘리지 않는다.
|
||||
|
||||
실제 디자인 차이가 있는 경우에만 분리한다.
|
||||
|
||||
---
|
||||
|
||||
## 단일 Theme 원칙
|
||||
|
||||
HM Default preset은 하나이며 Light/Dark preset이나 사용자 Theme selector는 없다. 공개 Runtime API는 동일 계약의 외부 값과 독립 Scoped override를 지원한다. 아래 제한은 전환 UI/제품별 Theme catalog를 새로 설계하지 않는다는 의미이며 구현된 Global/Scoped 적용을 금지하지 않는다.
|
||||
|
||||
현재 필요하지 않은 구조:
|
||||
|
||||
- ThemeProvider 기반 테마 전환
|
||||
- Light / Dark Theme Switching
|
||||
- 별도 Multi Theme 전환 UI/catalog
|
||||
- 별도 Multi Brand catalog
|
||||
- 사용자 Theme Selector
|
||||
- Runtime Theme Selector
|
||||
|
||||
향후 확장은 가능하게 유지하되, 미래 가능성을 위해 현재 구조를 복잡하게 만들지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 작업 원칙
|
||||
|
||||
새로운 컴포넌트를 추가할 때 다음 순서로 판단한다.
|
||||
|
||||
1. 기존 Global Theme Token으로 표현 가능한지 확인
|
||||
2. 공통 디자인 정책이라면 기존 Global Theme Token 재사용
|
||||
3. 독립 변경 가능성이 있다면 Component Mapping Token 추가
|
||||
4. 컴포넌트에는 실제 디자인 값을 직접 작성하지 않음
|
||||
5. Storybook에서도 실제 소비 프로젝트와 동일한 Component API 사용
|
||||
|
||||
HMDesignSystem의 핵심 원칙은 다음과 같다.
|
||||
|
||||
**공통 디자인 정책은 Theme Token 하나를 수정하여 관련 컴포넌트에 함께 반영할 수 있어야 한다.**
|
||||
|
||||
동시에:
|
||||
|
||||
**특정 컴포넌트만 변경해야 할 경우 Component Mapping Token을 통해 독립적으로 override할 수 있어야 한다.**
|
||||
|
||||
---
|
||||
|
||||
## 현재 구현 기준
|
||||
|
||||
### Token 이름과 위치
|
||||
|
||||
위 예시의 Token 이름은 개념 설명용이다. 외부 ui_viz Reference (ZIP 미포함, 읽기 전용)는 초기 디자인 참고 자료다. 현재 Production 코드와 실제 Storybook을 1차 기준으로 Component/Pattern 문서의 HM 결정과 대조한다. 실제 저장소 기준은 다음과 같다.
|
||||
|
||||
| 계층 | 위치 | 이름 |
|
||||
|---|---|---|
|
||||
| Primitive | `packages/ui/src/theme/primitives.css` | `--hm-<family>-<step>` — ui_viz Primitive 이름 그대로 |
|
||||
| 공통 Theme (Global/Semantic) | `packages/ui/src/theme/default.css` | `--hm-color-*`, `--hm-radius-*`, `--hm-control-height-*`, `--hm-font-*`, `--hm-sem-*` |
|
||||
| shadcn/Tailwind adapter | `default.css` 하단 | `--primary`, `--border`, `--radius` 등을 HM Token에 연결 |
|
||||
| Component Mapping | `packages/ui/src/theme/<component>.css` | `--hm-<component>-<속성>` |
|
||||
|
||||
- **Primitive**: ui_viz와 다른 이름을 만들지 않고 변환 대응표도 두지 않는다. ui_viz에 Primitive로 등록되지 않은 값(예: Button 범위 Component 값)은 Primitive로 올리지 않는다.
|
||||
- **Component Mapping**: ui_viz에 등록된 Component Token(예: `button.radius`, `button.height.md`)은 그 속성 이름을 그대로 쓴다(`--hm-button-radius`, `--hm-button-height-md`). ui_viz의 CSS 변수는 `--button-radius`처럼 접두어가 없지만, HM은 모든 Token에 `--hm-`을 붙인다. Tailwind v4의 `--color-*`·`--radius-*` 같은 theme namespace와 소비 앱 변수와의 충돌을 피하기 위해서다. ui_viz에 없는 값은 같은 형식으로 정의한다.
|
||||
- **파일**: 컴포넌트 전용 Mapping Token과 `[data-slot]` selector 스타일은 `packages/ui/src/theme/<component>.css`에 두고 `packages/ui/src/theme.css`가 import한다. 공통 값은 `default.css`에 둔다. Demo/App 페이지 스타일은 넣지 않는다. 컴포넌트 값은 Tailwind arbitrary value(`bg-[var(...)]`)가 아니라 이 CSS의 selector가 Mapping Token을 참조해 적용한다.
|
||||
- Mapping Token의 기본값은 공통 Theme Token을 참조하고(`--hm-button-radius: var(--hm-radius-button)`), 해당 컴포넌트만 따로 바꿀 수 있다.
|
||||
- **Focus Color**(`default.css`): `--hm-color-border-focus` → `--hm-sem-stroke-input-focus` → `--ring` 한 줄 체인이 일반 Focus 색(Input·Textarea·Select·Button·Checkbox·Tabs·Slider·Menubar 등)의 공통 소스다. 현재 **시험 적용값**은 Light 2(`--hm-green-light-2`, `#266651`)이며 이전 값은 `--hm-color-primary`(`#123328`)였다. 사용자 최종 확정 전이다. Semantic 값·이름, Runtime 계약과 Theme JSON 구조는 유지한다.
|
||||
- 2026-10-06 Focus 정합: Slider Hover/Drag halo는 기존 Primary 20% / 4·6px를 유지하고, 키보드 Focus halo만 Focus Semantic 20%로 분리한다. Focus + Hover는 4px Focus, Focus + Drag는 6px Focus가 우선하며 Disabled는 후광이 없다. Range·Thumb border는 Active Semantic을 유지한다.
|
||||
- Menubar 상위 Trigger Highlighted/Open 면은 Accent 100%를 유지하고 키보드 Focus에 Navigation Menu와 같은 3px `--ring` 50% 독립 ring을 더한다. Open + Focus는 Accent 면과 Focus ring을 함께 표시한다. Item Highlighted는 Accent 45%, Disabled는 기존 규칙을 유지한다. Navigation Menu는 변경하지 않는다. 일반 Story의 computed style로 검증하고 Designer 전용 button outline을 근거로 사용하지 않는다.
|
||||
- **Active State Semantic**(`default.css`, 2026-10-06): `--hm-sem-state-active`(→ `--hm-sem-palette-green-light-2` → `--hm-green-light-2`, `#266651`)와 `--hm-sem-on-state-active`(→ `--hm-color-text-inverse`, 흰색, 대비 약 6.8:1). Checked·Selected control·ON·Active fill을 포괄하며 Navigation/Pagination Current는 포함하지 않는다. Checkbox checked, Radio selected(border·dot), Switch on, Slider range·thumb border, Progress indicator, Calendar single selected·range start/end의 `--hm-<component>-*`가 이를 참조한다. Badge default와 Pagination current는 대상이 아니며 Primary Green(`--hm-sem-action-primary`)을 쓴다. 이 한 줄을 바꾸면 위 컴포넌트가 함께 바뀌고 Focus는 영향을 받지 않는다.
|
||||
- Focus chain(`--hm-color-border-focus`)과 **sibling**이다. 현재 값은 둘 다 Light 2지만 의미가 다르므로 어느 쪽도 다른 쪽을 참조하지 않는다. 같은 색일 때 fill(Active)과 바깥 ring(Focus)의 형태로 구분한다.
|
||||
- Progress는 현재 Active와 색 정책이 같아 같은 Semantic을 쓴다. 의미가 달라지면 별도 Semantic으로 분리한다.
|
||||
- 대상 아님: Toggle / Toggle Group(on = `--accent` 면, 별도 정책), GNB·LNB·Navigation Menu current(기존 Strong Brand 언어), Error/Destructive, Button.
|
||||
- **Persistent Selected Surface**: Toggle / Toggle Group의 Selected 및 Selected + Hover = `--accent` 100% / `--accent-foreground`(Git baseline `d101530` 매핑, 2026-10-06 원복). 아이콘은 전경을 따르고 border는 기존 default 투명 / outline input stroke를 유지한다. Active override에 영향받지 않고 Accent override를 따른다. Focus override는 별도 ring만 바꾼다.
|
||||
- Color 역할 정리: Hover / Soft surface = `--accent` 45% (Select·Command·Combobox·Menu Item highlighted, Calendar hover·range middle, Pagination hover, Table row hover·selected) / Persistent Selected Surface = `--accent` 100%(Toggle·Toggle Group) / Menubar Top Trigger Highlighted·Open 및 Navigation Menu Trigger·Open = `--accent` 100% + On Accent / Active Control fill = `--hm-sem-state-active`(위 적용 대상) / Focus = Focus chain / Strong Brand·Navigation·Badge default·Pagination current = Primary Green(`--hm-sem-action-primary`, `#123328`) / Error = Destructive.
|
||||
- Runtime 공개 계약(`packages/ui/src/theme/contract.ts`)에 Active/on Active를 포함했다. `src/theme/preview.ts`는 패키지 Runtime API의 호환 어댑터다.
|
||||
- **Floating Surface Semantic**(`default.css`): `--hm-sem-floating-background|border-width|border-color|radius|shadow`. Dropdown Menu·Context Menu·Popover·Hover Card·Menubar content·Navigation Menu panel의 `--hm-<component>-*`가 이를 참조한다. Toast(Sonner)는 notification surface라 이를 쓰지 않고 `--hm-toast-*`를 따로 둔다(Component Token은 유지하며 컴포넌트별 override 가능). Date Picker·Combobox는 Popover를 조합하므로 별도 mapping이 없다. Tooltip(어두운 면)·Dialog/Alert Dialog/Sheet/Drawer(modal)·Select·Card·Command는 대상이 아니다.
|
||||
|
||||
> **[임시 — 정리되면 삭제] 현재 코드와 위 기준의 차이**
|
||||
> - Primitive 이름: `--hm-warm-neutral-{light,mid,dark}`는 ui_viz `hm-neutral-warm-*`, `--hm-beige-light`는 `hm-neutral-beige-light`, `--hm-green-tint`(#BDDFD4)는 `hm-fg-on-green-secondary`이다. ui_viz Primitive에 없는 이름: `--hm-neutral-gray-500`(#999999, ui_viz는 `color.placeholder`), `--hm-warm-neutral-pale`(#DEDBD2, ui_viz는 Primitive 없이 `color.stroke.input.default`), `--hm-green-tone-1~4`(Button 범위 gradient 값), `--hm-warm-neutral-secondary`(#EDE8DB), `--hm-warm-neutral-muted`(#F4F1EA, 두 값은 ui_viz 문서에서 찾지 못함). Button 작업으로 ui_viz 이름 그대로 추가한 Primitive: `--hm-brown-light-2/-1`, `--hm-brown-dark-1`, `--hm-orange-light-4`, `--hm-orange-default`.
|
||||
> - Button Mapping Token은 ui_viz 이름(`--hm-button-radius`, `--hm-button-height-sm/md/lg`, `--hm-button-brand-{green,brown,orange}-{background,border}`, `--hm-button-solid-*` 등)을 따른다. Gray(`#767676`/`#5A5A5A`), Brown·Orange gradient의 일부 stop은 ui_viz가 Button 범위 Component 값으로 정의한 값이라 초기에는 Primitive 없이 `button.css`에 두었으나 현재는 `default.css`의 공개 Theme palette로 이동했다(아래 Runtime 절).
|
||||
> - `--hm-radius-button`과 `--hm-radius-input`(Input의 `--hm-input-radius`, Select의 `--hm-select-radius`가 참조)은 공통 `--hm-radius-control-md`(4px)를 참조하며 컨테이너도 같은 값을 쓴다. `--hm-radius-select`는 Select 작업 후 참조하는 곳이 없다. ui_viz는 `radius.input.default` 4px와 `radius.container.default` 4px를 값이 같아도 alias하지 않는다. ui_viz는 Button radius를 Button 범위 Component 값(`button.radius` 4px)으로 두고 다른 컴포넌트로 확대 해석하지 않는다고 적는다.
|
||||
|
||||
### HM 값과 shadcn 기본값
|
||||
|
||||
- ui_viz에 `Confirmed` 규격이 있는 속성 → Theme Token 적용
|
||||
- 정의가 없거나 `TBD`·`HOLD` 등 미확정인 속성 → 새 Token을 만들지 않고 shadcn 기본 구현 유지
|
||||
|
||||
### Light / Dark
|
||||
|
||||
현재 HM Theme Token에는 Light/Dark 구분이 없다. 하나의 HM Theme 값만 정의하며, Dark 전용 값이나 Light/Dark 분기를 추가하지 않는다. 실제 디자인 요구로 Light/Dark가 정의되면 그 시점에 Token 구조를 검토한다.
|
||||
|
||||
|
||||
## 2026-10-06 Runtime 구조 적용
|
||||
|
||||
이 절과 [Runtime Theme](runtime-theme.md)가 위 초기 시제품 상태 설명보다 우선한다. 모든 Component Mapping은 기존 이름을 유지하고 Semantic / 선택적 Theme palette를 소비한다. Button의 이전 로컬 HEX는 Default preset의 공개 팔레트로 옮겼다. 외부 주입은 공개 Semantic/palette만 허용하고 Component별 Token은 입력 계약에 포함하지 않는다. Default 값과 외부 값은 같은 계약을 사용한다. Accent 45%는 내부 surface-highlight Semantic으로 공통화했다. 전체 계약과 Default 값은 [계약표](runtime-theme-contract.md), 실제 적용·제한은 [Runtime Theme](runtime-theme.md)를 참고한다.
|
||||
|
||||
# 후속: 디자인팀 컬러 Runtime 편집 — 2026-10-06
|
||||
|
||||
현재 Storybook의 `Theme/Designer Playground/Editor`에서 JSON URL/파일을 Preview하고 상위 컬러 17개 역할을 편집·Reset·Export/Import할 수 있다. Component Token은 직접 수정하지 않는다. 운영 컬러 원본은 `themes/default.json`/서비스 JSON이며 package fallback CSS/데이터는 생성한다. Production은 `@hm-design/ui/theme`의 URL Loader/Apply를 사용한다. Theme artifact는 UI package와 별도 배포한다. 상세 절차/제한은 [디자인팀 Theme 가이드](designer-theme-guide.md), [Theme delivery](theme-delivery.md), Runtime 적용·제한은 [Runtime Theme](runtime-theme.md)를 참고한다. 기존 디자인 정책/과거 기록은 유지한다.
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
# Runtime Theme 기반 Design System 프로젝트 지침 (개정안)
|
||||
|
||||
> 상태: 현재 하나의 Git Repository에서 UI/CENTER npm workspaces의 compiled dist build/export, JSON 컬러 원본, Global/Scoped Runtime Apply·URL Loader, 통합 Storybook/Designer JSON Preview, Theme-only artifact를 관리한다. Registry publish 이력은 테스트 ZIP 검증 범위 밖이며 실제 CDN/API/Publish pipeline은 미구현이다. 현재 구현의 운영·배포 절차는 [Theme delivery](theme-delivery.md)를 따른다.
|
||||
|
||||
> **2026-10-07 사용자 확정 정책은 개정안 상태와 관계없이 필수 적용한다.** AI의 모든 신규·수정 UI는 사용자의 별도 지시 없이 기존 Component / Storybook Reference를 먼저 확인하고 Theme / Semantic Token과 단일 Navigation Mode를 기본 적용하며 Custom은 최소화한다. 필수 요약은 [AGENTS.md](../../AGENTS.md), 상세 구현 정책은 [shadcn 기반 구현 원칙](shadcn-기반-구현-원칙.md), Theme 정책은 [테마 토큰 가이드](테마-토큰-가이드.md), Navigation 정책은 [Navigation Single Mode Policy](navigation-single-mode.md)가 각각 담당한다.
|
||||
|
||||
## 1. 목적과 결정
|
||||
|
||||
사내 공통 UI는 shadcn을 **구현 기반이자 upstream 참고원**으로 사용한다. Design System 저장소에서 가져온 코드를 회사 규칙에 맞게 관리하고 `@company/ui`로 배포한다. 서비스는 shadcn 코드를 직접 가져오거나 수정하지 않고 공통 패키지를 사용한다.
|
||||
|
||||
컴포넌트의 구조·동작·기본 스타일 규칙은 패키지 버전으로, 운영 중 바꿀 수 있는 디자인 값은 CSS Custom Properties 기반 Runtime Theme으로 관리한다. **소스와 검수 자료는 하나의 Git 저장소에서 관리하되, 배포 산출물은 `@company/ui` 패키지와 Theme 버전으로 나눈다.** Storybook은 같은 저장소의 실제 컴포넌트로 내부 검수, Theme Preview, 문서화를 수행한다. 서비스가 소비하는 배포물은 패키지와 Theme 데이터이며, Storybook 자체가 배포 API는 아니다.
|
||||
|
||||
| 변경 경계 | 소유 대상 | 반영 경로 |
|
||||
|---|---|---|
|
||||
| 컴포넌트 코드/API·기본 CSS 규칙 | DOM, variant, state, interaction, 접근성, 구조적 layout, selector와 기본 스타일 규칙 | Design System 변경 → `@company/ui` 릴리스 → 앱 의존성 업데이트 및 배포 |
|
||||
| Runtime Theme 값 | 사전에 허용한 Semantic Token의 값 | Preview → 검증/승인 → 불변 Theme 버전 Publish → API/CDN → 앱 재방문·새로고침 시 적용 |
|
||||
| Theme 계약 | Token 이름·타입·의미, 스키마, 허용 범위 | Design System 코드 변경·릴리스, 호환성 확인 후 Theme 발행 |
|
||||
|
||||
**이미 지원하는 Semantic Token 값만 변경할 때** Theme Publish 후 앱 재배포 없이 반영할 수 있다. Git에 CSS/JSON을 수정·커밋하는 행위만으로 운영 앱이 자동 갱신되지는 않는다. Theme Loader와 API/CDN 연결, 검증·승인·Publish가 먼저 구축되어야 한다. 새로운 Token, selector, 컴포넌트 API가 필요한 변경은 값 Publish로 처리할 수 없다.
|
||||
|
||||
## 2. 아키텍처와 소유권
|
||||
|
||||
```text
|
||||
shadcn upstream → HM Design System 단일 Git 저장소
|
||||
├─ 컴포넌트·기본 CSS ── 패키지 릴리스(@company/ui) ──→ 개발팀 앱 업데이트·배포
|
||||
├─ Semantic Theme 초안 ── 검증·승인·Publish ──────→ Theme API/CDN
|
||||
└─ Storybook ── 실제 컴포넌트·Theme Preview │
|
||||
↓
|
||||
앱 Theme Loader → CSS 변수 적용
|
||||
```
|
||||
|
||||
- **디자인팀 — Design System**: Token 의미와 값, 컴포넌트 디자인, shadcn 도입·업스트림 변경 검토, 컴포넌트 구현/API와 접근성, Semantic Token 계약, 기본 Theme, `@company/ui` 패키지 릴리스, Storybook 및 Preview 검수를 담당한다.
|
||||
- **디자인팀 — Theme 운영**: Theme 값 검증, 불변 버전 저장, 승인·Publish·롤백, API/CDN 제공, 캐시 및 관측을 담당한다. MVP에서는 수동 절차로 시작할 수 있다.
|
||||
- **개발팀**: `@company/ui`와 기본 CSS 사용, 서비스 앱의 Theme 식별·로딩·적용·실패 대응, 패키지 업데이트 및 앱 릴리스를 담당한다.
|
||||
|
||||
위 항목은 조직 분리가 아니라 작업 단계와 책임의 구분이다. 같은 담당자가 여러 단계를 수행해도 각 산출물과 승인·반영 기록은 남긴다.
|
||||
|
||||
## 3. Token 계약
|
||||
|
||||
```text
|
||||
Primitive (설계·빌드 시 관리) → Semantic (컴포넌트 참조) → Component
|
||||
```
|
||||
|
||||
컴포넌트는 `blue-600` 같은 Primitive나 `#2563eb`, `8px` 같은 실제 값을 직접 참조하지 않는다. Tailwind를 쓰는 경우에도 `bg-primary`의 `--color-primary` → `--primary` → `--hm-sem-action-primary` adapter 연결을 확인한다. **Runtime override는 공개 allowlist의 Semantic/Theme palette 값만** 허용한다. Primitive, 컴포넌트 전용 내부 변수, 임의 CSS selector/rule/JavaScript는 Publish 대상이 아니다.
|
||||
|
||||
현재 allowlist는 `packages/ui/src/theme/contract.ts`의 73개 입력이다(색상 Semantic/Theme palette와 input/container radius). 정확한 이름·타입·기본값·허용 범위는 이 계약과 `runtime-theme-contract.md`를 따른다. Shadow geometry는 현재 공개 입력이 아니다. Typography와 spacing은 실제 사용처와 레이아웃 영향 검토 후 개별 등록한다. 문서에 예시가 있다는 이유로 곧바로 Runtime 허용 Token이 되지 않는다.
|
||||
|
||||
기본 Theme은 `@company/ui`가 배포하는 CSS에 포함하고 앱이 초기 렌더 전에 로드한다. Runtime 값이 없거나 가져오기·검증에 실패해도 기본값으로 UI가 렌더링되어야 한다. 현재 Light/Dark 구분은 없으며 단일 HM Theme 값을 기준으로 한다([테마 토큰 가이드](테마-토큰-가이드.md) 참조).
|
||||
|
||||
## 4. 변경 분류
|
||||
|
||||
| 요청 예 | 처리 | 판단 기준 |
|
||||
|---|---|---|
|
||||
| 기존 `color.action.primary` 값 변경 | Theme Publish | 계약에 등록된 Semantic Token의 값만 변경 |
|
||||
| 기존 공개 radius 값 변경 | Theme Publish(운영 도입 후) | 허용 범위·레이아웃·접근성 검증 통과; shadow geometry는 현재 계약 밖 |
|
||||
| 새 색상 Token 이름 또는 의미 추가 | 패키지/계약 릴리스 후 Theme Publish | 구버전 앱의 호환성 확인 필요 |
|
||||
| Button DOM, icon slot, variant, loading state 변경 | 패키지 릴리스 + 앱 업데이트 | API·구조·동작 변경 |
|
||||
| Dialog 상호작용, 포커스, 접근성 변경 | 패키지 릴리스 + 앱 업데이트 | 실행 코드 변경 |
|
||||
| grid/flex 배치, display/position, animation logic 변경 | 패키지 또는 앱 코드 배포 | 디자인 값만으로 안전하게 표현할 수 없음 |
|
||||
| 앱 화면의 콘텐츠·업무 로직 변경 | 앱 배포 | Design System 바깥의 책임 |
|
||||
|
||||
애매한 요청은 디자인팀과 개발팀이 영향 범위를 먼저 분류한다. Semantic Token이라는 이름만 붙여 레이아웃 구조나 동작을 Runtime으로 우회하지 않는다.
|
||||
|
||||
## 5. 단일 Git Repository와 산출물 구조
|
||||
|
||||
`HMDesignSystem`을 **하나의 Git 저장소**로 관리한다. 저장소 수와 배포물 수는 다르다. 동일 저장소의 변경이라도 컴포넌트·기본 CSS는 패키지 릴리스, 기존 Semantic Token 값은 Theme Publish, Storybook은 내부 검수용 빌드로 각각 처리한다. 개발팀의 서비스 앱은 이 저장소의 소스 파일을 직접 복사하지 않고 배포된 패키지와 Theme API/CDN을 소비한다.
|
||||
|
||||
현재 파일 배치는 다음과 같다. Library dist 산출물과 Runtime URL Loader는 구현되어 있다. Registry publish 이력은 테스트 ZIP 검증 범위 밖이며 운영 CDN/API/Publish pipeline은 별도 도입 대상이다.
|
||||
|
||||
```text
|
||||
HMDesignSystem/
|
||||
├─ packages/ui/src/components/ui/ # Production UI
|
||||
├─ packages/ui/src/patterns/ # 업무/Layout composition
|
||||
├─ packages/ui/src/theme/ # CSS Mapping, contract.ts, runtime.ts
|
||||
├─ packages/center-graphic/src/ # 독립 CENTER 구현
|
||||
├─ src/ # Demo, Component/Pattern/Theme/CENTER Story
|
||||
├─ packages/*/dist/ # compiled ESM JS, 타입 선언, CSS
|
||||
├─ themes/*.json # 공식 Default / 서비스 partial
|
||||
├─ themes/drafts/ # 검토용 Theme 자료
|
||||
├─ .storybook/ # 통합 검수 환경
|
||||
├─ docs/ # Component/Pattern/Theme 문서
|
||||
└─ package.json # npm workspaces 및 Demo/Storybook build
|
||||
```
|
||||
|
||||
패키지 산출물에는 공개 컴포넌트·기본 CSS·필요한 계약만 포함하고 Story, Draft Theme, 내부 문서는 제외한다. Theme Publish는 승인된 초안을 검증해 불변 버전으로 저장하고 활성 버전을 API/CDN에서 제공하는 별도 절차다. 패키지 릴리스와 Theme Publish는 같은 저장소의 변경 이력을 참조하지만 버전·승인·롤백을 각각 관리한다. 폴더를 `packages/ui`·`apps/storybook`으로 나눌 필요가 확인되면 **동일 Git 저장소 안에서** 재배치할 수 있다.
|
||||
|
||||
### 5.1 패키지 구성과 Storybook 통합 (구현됨 — 2026-10-02 패키지 분리)
|
||||
|
||||
**하나의 Git 저장소 + 두 개의 독립 배포 패키지 + 하나의 통합 Storybook**을 기본으로 한다. 2026-10-02에 npm workspaces(`packages/*`)로 분리했다. `packages/ui`(`@hm-design/ui`)에 Button·Input·Select·Card·Dialog와 Theme(`src/theme/*.css`, `theme.css`, `tailwind.css`)이 있고, `packages/center-graphic`(`@hm-design/center-graphic`)은 ui-viz CENTER Web Reference(제목·정보·노선/공간·시설물)를 이식해 구현했다(기준 문서 `docs/center-graphic/README.md`). 둘은 서로 의존하지 않는다. 루트는 Storybook(`.storybook/`, `src/**/*.stories.tsx`; UI는 `Components`, CENTER는 `Center Graphic` 최상위 그룹)과 Demo(`src/App.tsx`, `src/hm-theme.css`), 컴포넌트 문서(`docs/components/`)를 유지하며 Story와 Demo는 `@hm-design/ui`를 workspace 패키지로 import한다. 두 패키지는 `private: false`이며 ESM JS·타입 선언·CSS를 compiled `dist`에서 export한다. `npm run build:packages`가 library 산출물을 생성하며 Registry publish 이력은 테스트 ZIP 검증 범위 밖이다. 소비 앱은 `@import "tailwindcss"; @import "@hm-design/ui/tailwind.css"; @import "@hm-design/ui/theme.css"`를 쓴다. UI dist에는 `fonts/`의 글꼴과 OFL이 포함되며 compiled `theme.css`는 상대 `./fonts/` 경로를 사용한다. Source `packages/ui/src/theme.css`는 `/fonts/NotoSansKR-variable.ttf`를 사용하므로 App/Storybook 개발 서버는 `public/fonts/`를 서빙한다.
|
||||
|
||||
아래는 목표 구조다.
|
||||
|
||||
| 패키지 | 내용 |
|
||||
|---|---|
|
||||
| `@hm-design/ui` | shadcn 기반 공통 UI 컴포넌트와 공통 Theme(디자인 Token, CSS Custom Properties, 런타임 Theme 주입) |
|
||||
| `@hm-design/center-graphic` | 서비스 공통 그래픽 리소스와 그래픽 컴포넌트(Icon, Illustration, Empty State·Loading·Status Graphic 등) |
|
||||
|
||||
```text
|
||||
HMDesignSystem/
|
||||
├─ packages/
|
||||
│ ├─ ui/ components/ styles/ tokens/ theme/
|
||||
│ └─ center-graphic/ icons/ illustrations/ graphics/
|
||||
├─ storybook/
|
||||
└─ package.json workspace 설정
|
||||
```
|
||||
|
||||
- 두 패키지는 따로 설치하고 따로 버전을 올릴 수 있어야 한다. 그래픽만 필요한 프로젝트가 UI 패키지 전체를 설치하지 않도록 한다.
|
||||
- 의존성 방향: UI → Design Token / Theme, center-graphic → 자체 그래픽 리소스. `center-graphic → ui` 필수 의존은 만들지 않고, 공유가 필요하면 최소 수준의 Token만 공유한다.
|
||||
- `tokens` 또는 `theme` 패키지의 별도 분리는 규모가 커지거나 여러 제품이 테마를 독립적으로 공유해야 할 때 검토한다. 그 전에는 두 패키지로 운영한다.
|
||||
- Storybook은 하나로 통합해 UI와 Center Graphic을 함께 검수한다. 메뉴 구조는 패키지 폴더 구조와 같을 필요가 없고 검토 편의를 기준으로 한다. 컴포넌트는 §6.3의 규칙대로 대표 Story가 바로 열리는 1뎁스 항목으로 두고, Playground·Compatibility·verification Story는 기본 메뉴에서 숨긴다. Center Graphic도 같은 원칙을 따른다.
|
||||
- 다른 문서의 `@company/ui`는 `@hm-design/ui`의 자리표시자로 읽고, 이름 치환은 패키징 작업 때 한꺼번에 한다. Storybook 최상위 그룹은 CENTER 구현 시점에 `Center Graphic`만 추가했고 기존 `Components`는 이름을 바꾸지 않았다(`UI Components`로의 개명은 필요할 때 정한다).
|
||||
|
||||
#### CENTER 규격의 적용 범위
|
||||
|
||||
`center-graphic`은 독립 배포 패키지의 경계다. ui_viz의 CENTER 도메인 문서가 정의하는 노선, 시설물, 지도 및 관련 시각화 규격은 `center-graphic`의 구현·검증 작업에서 참조할 수 있다. 반대로 `ui`의 Button, Input, Select 같은 일반 UI 컴포넌트는 CENTER 문서를 디자인 근거로 쓰지 않는다. 따라서 CENTER 문서 참조 제한은 저장소 전체의 금지가 아니라 패키지와 작업 도메인에 따른 적용 범위 규칙이다.
|
||||
|
||||
- `ui` 작업 → 현재 HM UI 문서·구현·대표 Story를 먼저 참조한다. 외부 ui_viz UI 자료는 제공된 경우에만 참고한다.
|
||||
- `center-graphic` 작업 → CENTER 관련 규격을 참조할 수 있다.
|
||||
- 두 패키지의 규격을 임의로 섞지 않고, 한쪽의 값을 다른 패키지의 근거로 쓰지 않는다.
|
||||
- **CENTER 작업 규칙**: 현재 Source of Truth는 `docs/center-graphic/README.md`, `packages/center-graphic/src/`의 구현·승인 asset과 `src/center/`의 대표 Story다. 외부 ui_viz는 ZIP 미포함 선택적 Reference이며 제공된 경우에만 owner 문서의 명시 상태(`CONFIRMED_CANONICAL`/`CONFIRMED_BY_DESIGN` → `PLATFORM_MAPPING` → `Candidate`/`TBD`/`HOLD`) → 승인 asset → Web Reference 구현 순서로 참고한다. 승인되지 않은 후보 SVG와 미확정값은 구현 근거로 쓰지 않는다. 개별 그래픽 수치는 `docs/center-graphic/`에 기록한다.
|
||||
|
||||
## 6. 컴포넌트·Storybook·CSS 작업 규칙
|
||||
|
||||
Theme Token의 계층, 매핑 방식, Runtime Theme 적용 방식은 [테마 토큰 가이드](테마-토큰-가이드.md)를 따른다. 이 절에서는 중복해서 정의하지 않는다.
|
||||
|
||||
React 외 Runtime으로 구현·변환하는 작업은 [Runtime Portability Policy](runtime-portability.md)의 Target Runtime, 동일 Design/Theme Contract, 상대 asset 경로와 실제 실행 검증 기준을 추가로 따른다. 기존 컴포넌트·Token·Runtime Theme을 복제해 별도 Source of Truth를 만들지 않는다.
|
||||
|
||||
Component / Pattern / Story를 구현하거나 기존 테스트 디자인을 재현할 때는 [shadcn 기반 구현 원칙](shadcn-기반-구현-원칙.md)을 먼저 적용한다. 핵심 우선순위는 **기존 `@hm-design/ui` 재사용 → 기존 Variant / Prop → 기존 Composition → shadcn → Radix Primitive → 최소 Custom 구현**이다. Behavior / Structure는 shadcn/Radix를 유지하고 Visual은 구현 시작부터 HM Theme / Token에 연결한다. Custom 구현은 해당 문서의 허용 조건을 모두 충족하고 사유·범위·Public API 영향을 기록한 경우에만 허용한다.
|
||||
|
||||
> 패키지 초기화용 base 규칙은 `packages/ui/src/tailwind.css`, Demo 레이아웃 규칙은 `src/hm-theme.css`가 소유한다. Component CSS에는 전역 규칙을 넣지 않는다. 공개 API와 상태·규격은 각 `docs/components/` 문서와 현재 대표 Story를 따른다. Brand label의 Text Border는 두께가 `EXPERIMENTAL`이라 적용하지 않는다.
|
||||
|
||||
### 6.1 스타일 값의 출처
|
||||
|
||||
1. 현재 HM Component/Pattern 문서와 대표 Story에서 해당 속성의 확정 규격 및 기존 Semantic/Component Token을 확인한다.
|
||||
2. 기존 Token으로 연결한다. 외부 ui_viz는 제공된 경우에만 HM 정책과 충돌하지 않는 참고 자료로 사용한다.
|
||||
3. HM 지정값이 없으면 shadcn 기본 구현을 유지한다. 미확정 Reference 값을 확정값으로 쓰지 않는다.
|
||||
|
||||
HM 지정값이 있는 색상, radius, control height, typography 등은 Theme Token을 사용한다. HM에서 별도 값을 지정하지 않은 hover 동작, focus 구조, 애니메이션, 아이콘 크기, 접근성 속성 등은 shadcn 기본 구현을 유지하며, 임의로 새로 정의하거나 취향에 따라 바꾸지 않는다.
|
||||
|
||||
### 6.2 컴포넌트 구현
|
||||
|
||||
- 구현 전에 현재 `@hm-design/ui` + shadcn 조합으로 해결 가능한지 확인하고, 가능하면 새 primitive나 유사 Component를 만들지 않는다.
|
||||
- shadcn의 기본 컴포넌트 구조를 최대한 유지한다. CVA `variant`/`size`, `data-slot`, `cn(..., className)`, Radix 구조를 특별한 이유 없이 바꾸지 않는다.
|
||||
- HM 디자인은 기존 구조 안에서 Token과 스타일 값을 연결해 구현한다. HM 스타일만을 위한 wrapper 컴포넌트를 추가하지 않는다.
|
||||
- 기존 export, props 이름, variant 구조 등 공개 API를 불필요하게 바꾸지 않는다. API를 깨는 변경은 별도 합의 후 진행한다.
|
||||
- `<Button />`, `<Input />`, `<Select />`처럼 기본 API만 사용해도 HM 기본 디자인이 적용되는 구조를 지향한다. 소비자가 `hm-button`, `hm-input` 같은 HM 전용 class를 붙여야 하는 구조는 만들지 않는다.
|
||||
- `className`은 소비 프로젝트의 스타일 확장용 API로 유지한다. 기본 디자인을 주입하는 필수 통로로 쓰지 않는다.
|
||||
- shadcn/Radix가 제공하는 controlled/uncontrolled 상태, `data-state`, ARIA, keyboard navigation, focus management, Portal, Dismiss/Escape, `asChild`와 composition 방식을 새 로직으로 대체하지 않는다.
|
||||
|
||||
### 6.3 Storybook
|
||||
|
||||
- Story는 실제 소비 프로젝트와 같은 Component import와 props API를 사용한다. Story 전용 class, wrapper, CSS로 컴포넌트 외형을 보정하지 않으며, Storybook에서만 정상으로 보이는 구조를 만들지 않는다.
|
||||
- hover 고정 specimen, Selected + Hover 비교, spacing guide, Token reference 같은 검수 표현을 위해 Production prop이나 Public API를 추가하지 않는다. 필요한 표현은 Story 범위에만 두고 실제 화면에는 production `@hm-design/ui` Component를 사용한다.
|
||||
- Story 배치를 위한 gap, flex, grid 스타일은 사용할 수 있지만 컴포넌트 자체의 외형을 바꿔서는 안 된다.
|
||||
- 컴포넌트가 실제 지원하는 default, variant, size, disabled 및 주요 상태를 확인한다. invalid, open 등 해당 컴포넌트에 실제로 있는 상태만 다루고, 없는 상태를 문서화를 위해 만들지 않는다.
|
||||
- Storybook은 구현 Reference Catalog이자 컴포넌트 단위의 검수와 사용법 문서화 도구다. 신규 UI 구현 전 유사 대표/PRIMARY Story를 먼저 확인한다. `Demo/App` 화면은 여러 컴포넌트를 조합한 적용 결과 확인용이며, Demo의 페이지·타일 UI를 Story의 기준으로 쓰지 않는다.
|
||||
- 컴포넌트 Story는 현재 HM 대표 Story의 공통 구조를 따른다. 컴포넌트 이름의 대표 Story 하나에 종류·크기·상태를 한 화면으로 모으고, 같은 구역을 `verification` 태그 Story(예: Roles, Sizes, Brand States)로도 둔다. `verification` Story는 메뉴와 검색에서 기본으로 숨기고(`.storybook/main.ts`의 `tags`) 직접 URL과 사이드바 태그 필터로 연다. HM 전용 Story(Playground, Compatibility)도 같은 태그로 숨긴다. 기본 사이드바에는 컴포넌트가 하위 메뉴 없는 1뎁스 항목 하나로만 보이고(대표 Story 하나만 노출되면 Storybook이 컴포넌트 항목에 합친다), 클릭하면 한 페이지에서 전체 검수 내용이 열린다. Story 이름과 순서, 행·열 구성은 현재 HM 유사 대표 Story의 공통 구성을 먼저 따른다. 외부 ui_viz Story는 제공된 경우에만 충돌하지 않는 참고로 사용한다.
|
||||
- 대표 Story의 Focus 구역은 pseudo-state 복제 대신 `play` 함수로 실제 요소에 focus를 준다(ui_viz도 Focus는 정적 specimen을 만들지 않는다). hover 같은 상태가 ui_viz에 없으면 `.storybook/pseudo-states.ts`를 쓰지 않는다. 구역 배치 helper는 `src/components/story-board.tsx`의 `StateBoard`를 쓴다.
|
||||
- hover, pressed 같은 pseudo state는 Storybook이 `:hover`/`:active` 규칙을 `data-sb-hover`/`data-sb-active` 규칙으로 복제해 고정한다(`.storybook/pseudo-states.ts`). 복제만 하고 값을 새로 정의하지 않으므로 실제 디자인을 보정하는 용도로 쓰지 않는다. Story에서는 `data-sb-*` 속성만 붙인다.
|
||||
- ui_viz에서 값이 `TBD`인 상태(예: Button Focus)는 별도 specimen을 만들지 않고 키보드로 확인하는 안내만 둔다. ui_viz에 없는 상태(예: Button `aria-invalid`)와 `REFERENCE_ONLY` 항목은 검수 Story에 넣지 않는다. shadcn 호환 API는 `Compatibility` Story로 분리한다.
|
||||
|
||||
### 6.4 CSS 범위
|
||||
|
||||
- 컴포넌트 스타일(배포 대상)과 Demo/App 전용 스타일을 파일 단위로 분리한다.
|
||||
- 배포 대상 CSS에는 소비 프로젝트 전체에 영향을 줄 수 있는 전역 스타일을 가능한 한 넣지 않는다. `html`, `body`, `*`, 일반 태그 selector를 이용한 margin, font-family, reset, scroll behavior 등은 Demo/App 영역에서 관리한다.
|
||||
- 컴포넌트 스타일은 해당 컴포넌트 범위(`data-slot` 등) 안에서 동작하게 한다.
|
||||
- 컴포넌트 CSS에 container, hero, page grid, 화면 바깥 margin 같은 페이지 레이아웃 스타일을 넣지 않는다. 배치는 소비 프로젝트 또는 Demo/App이 맡는다.
|
||||
|
||||
### 6.5 Light / Dark 기준
|
||||
|
||||
현재 HM Theme에는 Light/Dark별 디자인 값이 정의되어 있지 않다. HM Token이나 HM 전용 스타일을 추가할 때 새로운 `dark:` 분기나 Dark 전용 Token을 만들지 않는다.
|
||||
|
||||
기존 shadcn 코드의 컴포넌트 `dark:` 클래스와 `tailwind.css`의 dark variant는 HM 요구사항이 없는 한 정리 목적으로 제거하지 않는다. 중복 root/dark 색상 preset은 Runtime 정합성 작업에서 이미 제거했으며 HM Dark preset을 새로 추가하지 않는다.
|
||||
|
||||
### 6.6 작업 방식
|
||||
|
||||
- 요청하지 않은 전체 리팩토링을 하지 않는다. 변경 범위는 요청된 컴포넌트와 직접 필요한 범위로 제한한다.
|
||||
- 새 패턴은 전체에 적용하기 전에 기준 컴포넌트에서 먼저 검증하고, 검증된 방식만 다른 컴포넌트로 확장한다.
|
||||
- 기존 구조가 정상 동작하면 취향이나 코드 스타일 선호만으로 바꾸지 않는다.
|
||||
- 새 abstraction, wrapper, helper, 스타일 구조를 만들기 전에 기존 프로젝트 패턴을 재사용할 수 있는지 먼저 확인한다.
|
||||
- Multi Theme, Multi Brand, Dark Mode 등 요구되지 않은 미래 요구사항을 가정해 현재 구조를 과도하게 설계하지 않는다.
|
||||
|
||||
### 6.7 ui_viz 규격의 구현 (Button에서 검증된 방식)
|
||||
|
||||
- 이 절의 ui_viz 설명은 초기 Button/Input/Select 도입 근거다. 현재 정합성 감사는 Production 코드와 실제 Storybook을 1차 기준으로 문서와 대조한다. ui_viz `Confirmed`와 과거 보고서는 참고 근거이며 이후 HM 결정은 §6.9 및 Component/Pattern 문서를 따른다.
|
||||
- 구현 근거는 `Confirmed`뿐이다. `Candidate`, `Temporary`, `TBD`, `HOLD`, `REVIEW VALUE`, `EXPERIMENTAL`, `Known Deviation`은 확정값으로 쓰지 않는다. 컴포넌트마다 `docs/components/<name>.md`의 최신 Decision 절을 먼저 확인하고(Button은 §1.1 D-093), 본문에서 "대체"로 표시된 이전 값과 `governance/current-rules.md` 같은 요약은 근거로 쓰지 않는다. `_local/`, `generated/`, `assets/`(대부분 CENTER)는 규격 근거로 읽지 않는다. `ui` 작업에서는 CENTER 문서의 값을 쓰지 않는다(`center-graphic` 작업의 범위는 §5.1 참고).
|
||||
- ui_viz의 Web 구현 요소(`.button-*` 같은 class, DOM 구조, `is-hover` 같은 강제 상태 class, Source Class Mapping)는 복사하지 않는다. 규격의 의미(역할, treatment, 값, 상태)만 가져와 `ui_viz 규격 → shadcn Component API → HMDesignSystem 구현` 순서로 대응시킨다.
|
||||
- 이름이 같다는 이유로 대응시키지 않는다. 소비자가 고르는 디자인 축인지, 내부 state인지, Token으로 풀 값인지, selector 수준의 상태인지로 판단한다. 예: ui_viz Button의 `secondary`는 Brown 브랜드 역할이며 shadcn `secondary`와 의미가 다르다. ui_viz가 역할과 treatment 두 축을 두는 컴포넌트는 두 축을 각각 prop으로 나눈다. Button은 `variant`가 역할(`primary`/`secondary`/`modify`/`destructive`/`cancel`), `appearance`가 treatment(`brand`/`solid`/`outline`), `size`가 `sm`/`default`/`lg`(= ui_viz Small/Medium/Large; ui_viz가 `default`를 Medium 별칭으로 정의)이며 기본값 `<Button />`은 primary + brand다. `data-variant`는 역할, `data-appearance`는 treatment이다.
|
||||
- ui_viz에 정의된 역할·treatment가 shadcn에 없으면 variant를 추가할 수 있다. 이는 기존 API를 깨는 것이 아니다. 기존 shadcn 이름은 별칭으로 유지한다(Button: `default` = primary, `outline` = primary + outline; ui_viz도 `outline`을 별칭으로 유지한다). ui_viz에 없는 shadcn variant(`ghost`, `link`)와 size(`xs`, `icon*`)는 삭제하지 않고 shadcn 기본 스타일 그대로 둔다. 이 호환 variant에는 `data-appearance`를 붙이지 않아 HM 스타일이 적용되지 않는다.
|
||||
- 스타일 적용 구조는 다음과 같다.
|
||||
- **CVA(`ui/<component>.tsx`)**: shadcn의 구조·레이아웃·접근성 class(focus, invalid ring 포함)를 유지한다. HM 규격이 정의한 동작(예: Button의 transition 없음, 눌림 이동 없음, disabled 커서)의 shadcn class는 HM variant에서 빼고 shadcn 호환 variant에만 남긴다. size처럼 호환 variant와 공유하는 축은 CVA 값을 그대로 두고 theme CSS가 HM 요소에만 덮어쓴다.
|
||||
- **`packages/ui/src/theme/<component>.css`**: 해당 컴포넌트의 Mapping Token(`--hm-<component>-*`)과 HM 규격 스타일(색, radius, height, padding, typography, 상태 표면)을 둔다. `[data-slot="<component>"]`와 그 컴포넌트에 실제 있는 `data-variant`/`data-size` 등 data attribute, pseudo state로 선택한다. 모든 컴포넌트가 `data-variant`를 가져야 하는 것은 아니다. Demo/App 페이지 스타일은 넣지 않는다. 이 파일은 `packages/ui/src/theme.css`가 import하고, Demo 스타일은 `src/hm-theme.css`에 둔다.
|
||||
- theme CSS가 `box-shadow`를 선언할 때(bevel, 상태 표면) 끝에 `var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000)`를 포함한다. shadcn의 focus·invalid ring이 `box-shadow`라서, 포함하지 않으면 theme CSS가 ring을 가린다. 이 변수는 `:root` Token이 아니라 요소의 선언에서 직접 참조해야 한다.
|
||||
- 소비자는 `variant`/`size` 같은 API만 고르고 HM 전용 class를 붙이지 않는다. `className`은 확장용이다.
|
||||
- ui_viz가 `Confirmed`로 정의한 상태 표현은 그 범위에서 shadcn 기본 시각 스타일을 대체할 수 있다. ui_viz에 정의가 없거나 미확정인 동작과 상태(예: Button의 Focus는 전역 `TBD`, `aria-invalid`는 정의 없음)는 shadcn 기본 구현을 유지하며 HM이 임의로 만들지 않는다.
|
||||
- ui_viz가 컴포넌트 안에 Field(Label, Required 별표, Helper/Error 문구)를 포함해도 shadcn 컴포넌트에 wrapper를 추가하지 않는다. Input은 입력 컨트롤만 담당하고 Label·문구는 소비 조합이다. Story와 Demo는 일반 요소에 shadcn Token 유틸(`text-muted-foreground`, `text-destructive`)과 ui_viz 수치(Label 14/20/500, 문구 12/18 또는 Error 11/18, field gap 8)를 맞춰 조합한다. Field/Label 컴포넌트 도입은 별도 결정이다.
|
||||
- ui_viz prop 이름이 native HTML 속성과 겹치면(Input `size`) ui_viz 이름을 쓰고 native 속성을 대체한다. 단 native 속성이 CSS 폭과 무관해 쓰임이 없을 때만 그렇게 한다. Input은 `size="small" | "medium" | "large"`이고 생략하면 ui_viz 기본값 Large(42px)다. 기본값은 컴포넌트마다 ui_viz 문서를 따르므로 Button의 `default`(= Medium)와 같다고 가정하지 않는다.
|
||||
- 상태는 컴포넌트에 이미 있는 native selector(Input: `:focus-visible`, `[aria-invalid="true"]`, `:disabled`, `[readonly]`)로 적용하고 새 `data-state`를 만들지 않는다. ui_viz가 "shadow·glow 없음"을 Confirmed로 정의한 상태(Input Focus·Error)는 TSX에서 shadcn ring class를 빼고 CSS에서 `box-shadow: none`을 선언한다. ui_viz에 정의가 없어 shadcn ring을 유지하는 Button과 구분한다.
|
||||
- ui_viz에서 `Temporary`인 값은 구현하지 않고 shadcn 기본값을 유지한다. 같은 상태에서 Confirmed인 값(Input Disabled 면 White)만 덮어쓴다. 단 Input Disabled opacity `.6`은 ui_viz에서는 `Temporary`이지만, **2026-10-02 사용자 결정**(Select 작업에서 확정된 Disabled 시각 intent와의 정합)으로 Input과 Select에 한해 `0.6`으로 확정되었다. 이전의 "`.6` Temporary → 미적용" 판단은 이 결정으로 superseded 되었다. 전역 Disabled opacity Token으로 승격하지 않으며, Button 등 다른 컴포넌트는 각자의 Confirmed 규격 또는 별도 사용자 결정을 따른다.
|
||||
- Radix 기반 컴포넌트(Select)는 Radix가 제공하는 상태 속성(`data-state`, `data-placeholder`, `data-highlighted`, `data-side`)과 `:disabled`로 상태를 적용하고, `data-size` 외에 HM 전용 상태 속성이나 상태 prop을 만들지 않는다. size는 DOM 요소가 있는 Trigger에 두며, ui_viz 값(Select: `medium` | `large`, 기본 `large`)이 shadcn 값(`sm` | `default`)을 대체한다.
|
||||
- Overlay(Content)는 Radix positioning을 유지하고 ui_viz가 정한 시각 관계만 prop 기본값과 CSS로 맞춘다(Select: `sideOffset` -1, 폭은 `--radix-select-trigger-width`, 연결된 윤곽). ui_viz 윤곽과 충돌하는 shadcn 장식(ring, 4px 간격 translate)은 제거하고, ui_viz에 값이 없는 그림자는 shadcn 기본(`shadow-md`)을 유지한다.
|
||||
- 상태 색이 `Temporary`여도 상태를 어떻게 표현하는지(Select: Selected는 Hover와 같은 배경 면, check icon·border 없음)가 Confirmed이면 표현은 구현하고 색은 shadcn Token(`accent`)을 그대로 쓴다. ui_viz에 없는 shadcn 장식(Select의 check indicator)은 제거한다. ui_viz에 정의가 없는 상태(Select Error)는 shadcn `aria-invalid` 스타일을 유지하고 theme CSS가 그 색을 덮지 않게 한다.
|
||||
- ui_viz가 glyph를 Source로 제공하면(Select indicator 18×18) shadcn의 lucide 아이콘을 그 glyph로 바꾸고 색은 Mapping Token으로 둔다. ui_viz가 사용자 결정으로 확정한 값은 그 컴포넌트에 한해 구현한다(Select·Input Disabled opacity .6, 위 항목 참조). 같은 값이 다른 컴포넌트에서 `Temporary`여도 그쪽으로 확대하지 않는다.
|
||||
- ui_viz에서 가져온 값에는 출처 주석을 한 줄 남긴다. 문서 경로, 절 또는 Decision, 상태만 적고 원문을 복사하지 않는다.
|
||||
|
||||
```css
|
||||
/* ui_viz: docs/components/button.md §1.1 D-087 — Confirmed */
|
||||
```
|
||||
|
||||
### 6.8 컴포넌트 문서 (`docs/components/`)
|
||||
|
||||
각 UI 컴포넌트의 현재 최종 규격, API, Token, 상태 및 사용자 결정은 이 저장소의 `docs/components/<component>.md`를 기준으로 한다(현재 Component 문서와 Field 소비 가이드 포함). 이 지침은 프로젝트 공통 원칙만 두고, 컴포넌트별 수치와 API 목록은 누적하지 않는다. §6.7의 Button·Input·Select 예시는 원칙을 설명하기 위한 것이며 값의 기준은 컴포넌트 문서다.
|
||||
|
||||
| 문서 | 담는 내용 |
|
||||
|---|---|
|
||||
| `프로젝트-지침.md` | 프로젝트 공통 원칙, 작업 순서 |
|
||||
| `테마-토큰-가이드.md` | Token 구조 공통 원칙(Primitive → Semantic → Component Mapping) |
|
||||
| `docs/components/<component>.md` | 컴포넌트 고유 규격, API, 상태, 실제 Token 목록, 사용자 확정 결정, 보류 항목 |
|
||||
| 외부 ui_viz Reference (ZIP 미포함) | 제공된 경우에만 참고하는 원본 규격과 근거. 현재 HM 구현·정책을 대체하지 않는다 |
|
||||
|
||||
- HM이 따로 확정한 값 때문에 구현이 ui_viz `Temporary`와 달라지면 그 결정을 컴포넌트 문서의 "사용자 확정 결정"에 날짜와 함께 기록한다. 컴포넌트 문서의 사용자 결정은 ui_viz의 Temporary·TBD보다 우선한다.
|
||||
- overlay 컴포넌트(Dialog)는 StateBoard를 쓰지 않고 트리거로 실제 Radix를 여는 대표 Story와 `defaultOpen`으로 열린 verification Story를 둔다. Radix의 Portal·focus·ARIA·Escape 동작은 시각 규격을 위해 바꾸지 않는다. `asChild` 래퍼(`DialogClose` 등)가 `data-slot`을 자식 컴포넌트(Button) 위에 덮어쓰면 `[data-slot]` 기반 HM 스타일이 사라지므로, 래퍼는 asChild일 때 `data-slot` prop을 아예 넘기지 않는다(`undefined`로 넘겨도 덮어쓴다).
|
||||
- Floating/Overlay 계열(Dropdown Menu, Context Menu, Popover, Tooltip, Hover Card)은 Dialog와 같은 방식으로 다룬다: 트리거로 실제 Radix를 여는 대표 Story + `defaultOpen` verification Story(`Open`), Radix의 Portal·positioning·focus 이동/복귀·Escape·ARIA는 시각 규격을 위해 바꾸지 않고, 상태는 Radix 속성(`data-highlighted`, `data-disabled`, `data-state`)만 쓴다. surface·윤곽·그림자가 같아도 컴포넌트마다 `--hm-<component>-*`를 두고 서로 참조하지 않는다(공통 Semantic 승격은 반복 근거가 확정된 뒤 검토; 후보는 `docs/components/popover.md` §14).
|
||||
- Modal/Panel 계열(Sheet, Drawer, Alert Dialog)도 Dialog와 같은 방식으로 다룬다: 대표 Story는 트리거로 실제 Radix(Drawer는 vaul)를 열고 `defaultOpen` verification Story를 둔다. Radix의 Portal·Overlay·focus trap·initial focus·Escape·focus 복귀·ARIA는 바꾸지 않는다. Dialog 수치는 복사하지 않고 `--hm-<component>-*`를 따로 둔다(공통 Semantic 후보: modal surface·border·shadow·radius). Action/Cancel처럼 안에 Button이 들어가는 Radix 부품은 `<Button asChild><Primitive/></Button>`로 합성하고 Primitive에는 `data-slot`을 붙이지 않는다. vaul 같은 래퍼가 Radix 기본 동작을 끄면(Drawer의 `autoFocus` 기본 false) 모달에서는 되살린다. 모달 확인은 좁은 viewport(390px) 한 번을 포함한다. Floating(Dropdown Menu·Popover·Tooltip·Hover Card)과 Modal은 역할이 달라 값을 서로 참조하지 않는다.
|
||||
- Navigation 계열은 둘로 나눈다. 페이지 안 primitive(Breadcrumb, Pagination)는 링크를 `a`로 유지하고 router에 의존하지 않으며(`asChild`), 현재 위치는 `aria-current="page"`로 표시한다. anchor에는 native disabled가 없으므로 `aria-disabled="true"`이면 `tabIndex=-1`과 클릭 취소를 컴포넌트가 보장하고, Button의 `buttonVariants`/token을 가져다 쓰지 않는다. Menu Navigation(Navigation Menu, Menubar)은 Radix 구조와 키보드 모델을 유지하고 Dropdown Menu의 token을 참조하지 않는다. 역할은 Dropdown Menu = 명령, Navigation Menu = 목적지 이동(link), Menubar = 메뉴 바다. 모바일 navigation 패턴은 이 컴포넌트들이 아니라 GNB/Layout 단계에서 정한다.
|
||||
- Story 파일의 Tailwind 임의값 클래스(`w-[360px]` 등)가 새 파일에서 개발 서버에 생성되지 않는 일이 있었다. Story 배치용 크기·grid는 필요하면 `style`로 지정한다(컴포넌트 외형 보정이 아니라 배치).
|
||||
- Data / Feedback 계열(Table, Progress, Skeleton, Avatar, Scroll Area)의 원칙: Table은 정적 표시 primitive로 정렬·필터·선택 state·페이지 로직을 넣지 않고(이후 Data Table Pattern), 좁은 화면은 카드로 바꾸지 않고 container 가로 scroll만 쓴다. Story 안에서 Table을 감싸는 grid item에는 `minWidth: 0`을 준다(Table 최소 폭이 부모를 넓힘). Radix가 소유하는 동작(Progress의 ARIA, Avatar 이미지/fallback, Scroll Area의 scroll)은 바꾸지 않는다. 상태색(success/warning/error)이 확정되기 전에는 상태 variant를 만들지 않는다.
|
||||
- theme CSS는 `@layer` 밖이라 Tailwind utility(`className`)보다 항상 우선한다. 소비자가 `className`으로 모양을 정하는 것이 역할인 primitive(Skeleton)는 기본 모양을 `@layer components`에 둔다. 그 외 컴포넌트에서 크기·색을 바꾸려면 prop(`size`) 또는 `--hm-<component>-*` Token을 쓴다.
|
||||
- Selection Controls(Slider, Toggle, Toggle Group)의 원칙: Slider는 값 입력, Toggle은 pressed 상태, Toggle Group은 선택 그룹이며 역할 비교는 `docs/components/toggle.md` §14에 둔다. Toggle/Toggle Group은 Button과 모양이 가까워도 Button의 role/appearance 체계와 `--hm-button-*`를 상속하지 않고 독립 Token을 쓴다. `className`으로 크기·radius·레이아웃을 확장하는 것이 자연스러운 컨트롤(Toggle, Toggle Group)은 기본 모양을 `@layer components`에 둔다(Skeleton과 같은 규칙). Radix가 `role="slider"`를 Root가 아닌 Thumb에 두는 것처럼 접근 가능한 이름이 내부 요소에 필요한 경우 컴포넌트가 `aria-label`을 전달한다. Radix가 조건부로만 내는 속성(ToggleGroup의 `data-orientation`)에 기대지 말고 `:not()`으로 선택한다.
|
||||
- Search / Date 계열(Calendar, Command, Date Picker, Combobox)의 원칙: Calendar(react-day-picker)와 Command(cmdk)는 기반 라이브러리가 날짜 계산·필터링·키보드·ARIA를 소유하는 primitive이고, 라이브러리 기본 class(`rdp-*`)와 속성(`data-selected`, `data-selected="true"` 등)에 시각만 연결한다. Date Picker는 Popover + Calendar, Combobox는 Popover + Command를 public API로 조합한 Component이며 Popover를 복제하지 않는다. 조합 컴포넌트가 `PopoverContent`에 `data-slot`을 넘기면 Popover의 `data-slot`을 덮어써 Popover 스타일이 사라지므로 넘기지 않는다. Popover의 padding·폭은 Popover 코드를 바꾸지 않고 `style`의 `--hm-popover-padding`/`--hm-popover-width`로 인스턴스마다 덮는다. 새 dependency는 shadcn 구현이 쓰는 최소(`react-day-picker`, `cmdk`)만 `packages/ui`에 추가하고, 날짜 표시는 `Intl.DateTimeFormat`과 native `Date`로 해결한다. field형 trigger는 grid/flex 부모를 넓히지 않도록 `min-width: 0`을 둔다. Select = 짧은 목록(Radix Select), Combobox = 검색 가능한 선택(Popover + Command)으로 구분한다.
|
||||
- Layout / Media 계열(Carousel, Aspect Ratio, Resizable)의 원칙: 소비자 레이아웃 확장성이 우선이라 기본 모양은 `@layer components`에 두고 크기·폭·gap·control 위치는 소비자 utility가 이기게 한다. 시각 규격이 없는 Aspect Ratio는 CSS·Token 파일을 만들지 않는다. Carousel(Embla)과 Resizable(react-resizable-panels v4: `Group`/`Panel`/`Separator`, 숫자=픽셀, 단위 없는 문자열="%")은 라이브러리가 interaction을 소유하며 persistence·Dashboard state·autoplay는 넣지 않는다. Embla/패널 Root는 grid·flex 부모의 `min-width:auto`로 페이지를 밀 수 있으므로 Root에 `min-w-0`을 두고, 한 단계 감싼 wrapper에는 소비자가 `min-w-0`을 준다. 컨트롤(Carousel 이전/다음)은 좁은 부모에서 잘리지 않도록 가장자리 안쪽에 둔다. Resizable은 모든 panel에 `minSize`를 준다.
|
||||
- Floating surface Semantic 후보(2026-10-02 C묶음 후 비교): Dropdown Menu, Popover, Hover Card, Menubar, Navigation Menu(viewport)는 흰 면(`--hm-sem-bg-surface`), 1px `--hm-warm-neutral-pale`, radius `--hm-radius-container`(4px), 그림자 `0 0 12px rgb(0 0 0 / .2)`이 같다. Date Picker와 Combobox의 popup은 Popover를 그대로 써서 같은 값이다. Command는 면과 radius만 같고 border·그림자가 없다(컨테이너 안에서 쓰는 전제). Tooltip은 어두운 면이라 제외, Dialog/Sheet/Drawer/Alert Dialog는 modal이라 별도(radius 8px 등). 승격 후보 이름은 `--hm-sem-floating-background|border|radius|shadow`이며, 2026-10-03에 `default.css`로 승격했고 5개 컴포넌트 token이 이를 참조한다(Component Token 유지, 화면 변화 없음).
|
||||
- ui_viz에 `docs/components/<name>.md`가 없는 컴포넌트(Card)는 해당 역할을 정의한 pattern 문서(Card: `docs/patterns/content-container.md` Type B)를 근거로 삼는다. 문서가 `Confirmed`가 아닌 owner 채택(`SOURCE_ADOPTED`, `LOCAL_EXCEPTION`) 값을 쓰면 컴포넌트 문서에 그 상태를 구분해 적고, 정의되지 않은 요소(Card의 Footer 등)는 shadcn 기본값으로 둔다.
|
||||
- 새 컴포넌트를 완료하면 같은 템플릿(상태, 적용 근거, 기본 규격, 상태 규격, API, Token, 유지 항목, 변경 항목, Storybook, 사용자 결정, 미확정, 관련 파일, 검증 상태)으로 문서를 추가한다. 없는 상태나 API는 채우지 않는다.
|
||||
- CENTER 문서는 UI 컴포넌트 문서의 근거로 쓰지 않는다.
|
||||
|
||||
### 6.9 ui-viz의 위치와 새 컴포넌트 기준 (2026-10-02 결정)
|
||||
|
||||
외부 ui_viz Reference (ZIP 미포함)는 구현 방향이 바뀌기 전에 쓰던 테스트/검증 프로젝트이며 HMDesignSystem의 최종 기준 구현체가 아니다. §6.1, §6.7의 "Confirmed만 구현 근거" 방식은 Button·Input·Select·Card·Dialog·Textarea를 만든 방식으로 남기되, 앞으로의 기준은 다음과 같다.
|
||||
|
||||
- ui-viz의 값을 그대로 복제하는 것을 목표로 하지 않는다. 기존에 검토·확정된 디자인 의도와 수치는 참고 자료로 쓰고, 현재 HM 방향에 맞는지 확인해 가져온다.
|
||||
- HOLD / TBD / Temporary / 미정의 값은 그대로 적용하지 않고 현재 HM Token 체계와 공통 디자인 원칙(기존 Primitive/Semantic, 구조가 가까운 기존 컴포넌트 패턴)으로 새로 정한다. ui-viz에 규격이 없는 컴포넌트(Checkbox, Radio Group, Switch, Badge, Alert, Separator)도 같은 방식으로 정식 컴포넌트로 구현한다.
|
||||
- shadcn/Radix의 접근성, 상태 관리, 키보드 동작, API 구조를 우선 유지한다. wrapper는 필요한 경우에 한해 추가한다(예: Tabs의 frame과 이동 버튼).
|
||||
- 다른 컴포넌트의 component token을 값이 같다는 이유로 직접 재사용하지 않는다. 컴포넌트마다 `--hm-<component>-*`를 두고 공통 Semantic을 참조한다.
|
||||
- 최종 기준은 실제 구현과 `docs/components/<component>.md`다. 문서에는 값마다 ui-viz 상태명을 분류하지 않고 참고 근거, 현재 HM 규격, 구현 방식, 보류 항목만 기록한다. 기존 문서의 Confirmed/Temporary 표기는 당시 이력이다.
|
||||
- 개별 수치는 이 지침에 넣지 않는다.
|
||||
|
||||
### 6.10 Pattern 계층 (2026-10-03 도입)
|
||||
|
||||
- `packages/ui/src/components/ui`는 독립 primitive, `packages/ui/src/patterns`는 여러 primitive를 조합한 반복 업무 UI(Data Table, Filter Bar)다. Pattern 문서는 `docs/patterns/`에 둔다(Story는 `Patterns/Data/<이름>` 또는 `Patterns/Layout/<이름>`).
|
||||
- Pattern은 primitive의 시각 스타일을 복제하거나 다시 정의하지 않고 composition과 상태 연결만 맡는다. Pattern 전용 스타일은 toolbar·footer·loading/empty wrapper·레이아웃 정도로 `--hm-<pattern>-*` token에 둔다. primitive(`Table` 등)에는 업무 로직을 넣지 않는다.
|
||||
- Pattern은 서로 종속시키지 않고(Filter Bar는 Data Table 없이도 쓴다), 외부 라이브러리 state(TanStack)는 그대로 노출한다. Story는 로컬 sample data로 실제 조합을 검증한다.
|
||||
|
||||
### 6.11 Layout 단계의 기준 (2026-10-03 결정)
|
||||
|
||||
개별 UI 컴포넌트와 Layout/Pattern(GNB, LNB, Dashboard, Page)의 기준을 구분한다.
|
||||
|
||||
```text
|
||||
UI primitive / component → 현재 @hm-design/ui 구현, docs/components, shadcn·Radix 구조. ui-viz는 참고 (§6.9)
|
||||
Layout / Pattern → 현재 HM 대표 Story / Pattern 문서와 단일 Navigation 정책 우선
|
||||
외부 ui_viz Reference (ZIP 미포함)는 충돌하지 않는 범위의 참고 자료
|
||||
구현은 현재 HM primitive / pattern으로 재구성
|
||||
```
|
||||
|
||||
- Layout은 현재 HM 대표 Story와 사용자 확정 Pattern 규격을 먼저 재사용한다. ui-viz의 구조·시각 결과(높이, 배경, 선택·현재 표현, 간격, 영역 비율, 폭, 콘텐츠와의 연결)는 현재 기준 및 단일 Navigation 정책과 충돌하지 않는 범위에서 참고한다. 구형 Navigation 조합이나 색상 하드코딩을 복원하지 않는다.
|
||||
- 구현 기술은 현재 HM을 따른다: Radix 접근성·keyboard·focus·Portal, shadcn API, 기존 primitive 조합(Navigation Menu, Dropdown Menu, Sheet, Collapsible 등). ui-viz의 class·DOM·state 코드는 복사하지 않고, ui-viz가 정의하지 않은 반응형은 현재 HM 접근성·사용성을 우선한다.
|
||||
- ui-viz 값을 primitive에 반영하지 않는다. Layout 전용 재색칠은 `[data-slot="<pattern>"]` 범위 안에서만 하고, 하위 primitive의 기본 Story는 바뀌지 않아야 한다.
|
||||
- Pattern 문서에는 ui-viz 비교표를 두고 차이를 MATCHED / IMPLEMENTATION_CHANGE / INTENTIONAL_CHANGE / MISSING / FOLLOW_UP으로 분류한다. 개별 px 값은 지침이 아니라 Pattern 문서에 둔다.
|
||||
|
||||
### 6.12 Navigation Single Mode (2026-10-07 사용자 확정)
|
||||
|
||||
**ONE PAGE = ONE NAVIGATION MODE.** GNB ONLY / LNB ONLY / DASHBOARD ONLY 중 정확히 하나를 선택한다. 독립 모드를 결합하는 GNB + LNB, GNB + Dashboard, LNB + Dashboard, 세 가지 중첩은 금지한다. GNB ONLY 내부 Sidebar에서 기존 LNB 메뉴 부품을 재사용하는 것은 독립 LNB Mode 추가가 아니며 허용한다. Sidebar 외곽 배경만 Green Light 2로 적용하고 메뉴 디자인·구조·동작은 유지한다.
|
||||
|
||||
- AI 기반 코드 생성·제안, 신규 페이지 구현, 기존 화면 수정, 프로토타입, Storybook, 실제 Production 구현 전체에 동일하게 적용한다. AI는 별도 요청이 없는 한 Navigation 조합을 생성하거나 제안하지 않는다.
|
||||
- 모바일에서도 선택한 체계를 유지한다. LNB Desktop(≥1024px) → LNB Mobile Header(≤1023px)는 같은 LNB Mode다.
|
||||
- Dashboard 운영 화면은 전용 Header(48px)·업무 상태 필터 + Content로 구성하며 모든 Sidebar 사용을 금지한다. 외부 GNB나 일반 LNB를 추가하지 않는다. Sidebar 없는 최소 폭 960px과 Dashboard 내부 가로 스크롤을 유지한다. 기존 `DashboardSidebar`와 `sidebar` slot 및 1232px 구성은 호환성·검증용이며 신규 운영 Reference가 아니다.
|
||||
- Navigation 간 CSS 변수·height/width 계산·breakpoint·spacing 교차 의존성을 만들지 않는다.
|
||||
- 대표/PRIMARY Story는 단일 모드의 운영 구성만 사용한다. 기존 혼합 Story와 Dashboard Sidebar 기술 검증은 verification 태그와 **VERIFICATION ONLY — 운영 사용 금지** 표시로 유지할 수 있으며 신규 화면 구현 기준으로 사용하지 않는다. 상세는 [Navigation Single Mode Policy](navigation-single-mode.md)를 따른다.
|
||||
|
||||
상세 기준은 [Navigation Single Mode Policy](navigation-single-mode.md)를 따른다. 이전 조합 기준과 검증 이력은 운영·AI 생성의 근거가 아니다.
|
||||
|
||||
## 7. Storybook 운영 규칙
|
||||
|
||||
- Story는 패키지의 공개 컴포넌트를 import한다. 내부용 복제 컴포넌트나 가짜 Preview UI로 검수를 대체하지 않는다.
|
||||
- 각 주요 컴포넌트에 기본/variant/state, disabled·error·loading, 키보드 및 포커스 상태를 해당 컴포넌트의 실제 지원 범위에 맞게 문서화한다.
|
||||
- 현재 Default/External/URL Loader 및 계약 검증은 Theme verification Story로 비교한다. `Theme/Designer Playground/Editor`는 JSON URL/파일 Preview와 최소 컬러 편집을 제공한다. Preview는 패키지 Runtime 계약을 사용하며 운영 Publish는 수행하지 않는다.
|
||||
- 컴포넌트 PR에서는 관련 Story와 기본 Theme·대표 Runtime Theme을 확인한다. Theme Publish에서는 변경한 Token이 닿는 Story, 대비·가독성, 화면 넘침을 확인한다.
|
||||
- Storybook 빌드/배포 버전과 `@company/ui` 버전을 표시해 검수한 결과와 서비스 패키지 버전을 연결한다. Storybook에서 보인다고 해서 앱의 실제 반영을 확인한 것으로 간주하지 않는다.
|
||||
|
||||
## 8. 도입 순서와 완료 조건
|
||||
|
||||
| 단계 | 구축 범위 | 다음 단계 진입 조건 |
|
||||
|---|---|---|
|
||||
| 현재 구현 | UI/CENTER compiled dist build/export, Component·Pattern, 공개 Runtime 계약/API·URL Loader, Default/External verification Story, Theme-only artifact. Registry publish 이력은 ZIP 검증 범위 밖; 운영 CDN/Publish pipeline은 미구현 | 코드/문서/실제 Storybook 정합성 검증 |
|
||||
| MVP | 단일 저장소에서 `@company/ui` 패키징, 기본 CSS, Semantic 계약, Storybook, 로컬 JSON Preview/Loader | 같은 패키지 컴포넌트에서 값 override와 fallback을 실제로 확인 |
|
||||
| 운영 기반 | 불변 Theme Registry, 스키마 검증, 버전·호환성, 승인 Publish, API/CDN, 롤백·로그 | 앱 재배포 없이 Theme 값 반영, 실패 시 기본값, 이전 버전 복귀를 확인 |
|
||||
| 운영 확장 | 디자인팀 Admin, 권한·감사, 자동 검수·모니터링 | 운영 절차와 담당자가 정해지고 실제 서비스 적용 기록이 남음 |
|
||||
|
||||
### 8.1 초기 도입 순서 (구현 이력)
|
||||
|
||||
```text
|
||||
Button / Input / Select 완료 + 컴포넌트 문서 정리 (docs/components)
|
||||
↓
|
||||
Card 완료
|
||||
↓
|
||||
Dialog 완료
|
||||
↓
|
||||
패키지 구조 분리 (packages/ui, packages/center-graphic)
|
||||
↓
|
||||
packages/ui 구조에서 나머지 UI 컴포넌트 추가
|
||||
```
|
||||
|
||||
- **Card와 Dialog 완료를 패키지 분리 작업의 시작 기준점으로 한다.** 그 기준을 충족해 2026-10-02에 분리했다(§5.1).
|
||||
- 현재 Storybook은 `Components`, `Patterns`, `Center Graphic`으로 통합 운영한다. `Theme`의 verification Story는 기본 sidebar에서 숨긴다.
|
||||
- 패키지 구성은 §5.1을 따른다(하나의 Git 저장소, `@hm-design/ui`와 `@hm-design/center-graphic`, 독립 설치·버전 관리).
|
||||
- `center-graphic` 실제 작업을 시작할 때 CENTER 문서와 assets의 읽기 범위·우선순위를 별도로 확정한다. 그 전까지 §5.1의 CENTER 적용 범위를 따른다.
|
||||
|
||||
상세 적용 절차는 [개발 적용 가이드](개발-적용-가이드.md), 발행과 복구 절차는 [운영/배포 가이드](운영-배포-가이드.md)를 따른다.
|
||||
@@ -0,0 +1,356 @@
|
||||
# Dashboard (Pattern)
|
||||
|
||||
> **SINGLE NAVIGATION MODE / 2026-10-08 Sidebar 운영 정책.** DASHBOARD ONLY 운영 화면은 Sidebar를 사용하지 않는다. 전용 Header(48px)·업무 상태 필터와 Content를 사용하며 Header는 GNB가 아니다. 독립 GNB / LNB / Dashboard 모드는 함께 사용하지 않는다. [공통 정책](../guidelines/navigation-single-mode.md)이 이전 조합·Sidebar 허용 기록보다 우선한다.
|
||||
|
||||
## 1. 현재 계약
|
||||
|
||||
Dashboard 운영 화면은 전용 Header/Menu·업무 상태 필터와 Content로 구성하며 모든 Sidebar 사용을 금지한다. 외부 GNB와 일반 LNB를 결합하지 않는다. 운영 Reference는 Sidebar 없는 대표 `Patterns/Layout/Dashboard → Dashboard` Story다.
|
||||
|
||||
| 공개 API | 역할 |
|
||||
|---|---|
|
||||
| `DashboardLayout` | 바깥 scroll root + 내부 frame. 운영 화면은 `header`, `children`을 사용하고 `sidebar`를 전달하지 않음 |
|
||||
| `DashboardHeader` | Dashboard 내부 Header. 기본 높이 48px |
|
||||
| `DashboardSidebar` | 기존 호환성·검증 전용 공개 API. 신규 Dashboard 운영 화면 사용 금지. 기존 폭 272px·메뉴 세로 스크롤 기능은 유지 |
|
||||
| `DashboardMain` | `<main>`, 콘텐츠 및 세로 스크롤 |
|
||||
| `DashboardContent` | 콘텐츠 stack, gap 24px |
|
||||
|
||||
`DashboardSidebar` 공개 Export와 선택적 `sidebar` slot은 기존 소비자 호환성·검증을 위해 유지한다. API 존재가 운영 허용을 뜻하지 않으며 신규 운영 화면에는 사용하지 않는다. GNB/LNB나 Sidebar를 전달하던 운영 화면은 적합한 단일 Navigation Mode의 운영 구성으로 마이그레이션한다. slot은 임의 ReactNode를 받으며 런타임 자동 차단은 없다. 운영 사용 금지와 기능 제거는 별도 작업이며, API 제거·런타임 차단은 소비처 영향 조사와 사용자 결정 후 진행한다.
|
||||
|
||||
```tsx
|
||||
<DashboardLayout
|
||||
header={<DashboardHeader>Dashboard Header / Menu</DashboardHeader>}
|
||||
>
|
||||
<DashboardMain>
|
||||
<PageHeader>…</PageHeader>
|
||||
<DashboardContent>…</DashboardContent>
|
||||
</DashboardMain>
|
||||
</DashboardLayout>
|
||||
```
|
||||
|
||||
## 2. 최소 폭·스크롤·스타일
|
||||
|
||||
- 운영 최소 폭 = Sidebar 없는 960px(`--hm-dashboard-min-width`).
|
||||
- Sidebar 포함 1232px(960 + 기존 Sidebar 폭 272px)은 호환성·검증 구성의 값이며 현재 운영 기준이 아니다. 해당 구현·CSS는 호환성을 위해 유지한다.
|
||||
- 최소 폭 이하에서는 바깥 `dashboard-scroll` 안에서 전용 Header/Content 전체를 가로 스크롤한다. 모바일에서도 Sidebar를 추가하지 않는다.
|
||||
- Main이 세로 스크롤을 담당하고 바깥 frame 높이는 `--hm-dashboard-height`(기본 `100dvh`)다. `className`, `style`, DOM props는 바깥 root에 전달한다.
|
||||
- Root 기본 접근성은 `role="region"`, `aria-label="Dashboard"`, `tabIndex={0}`. `data-navigation-mode="DASHBOARD"`는 해당 체계를 식별한다.
|
||||
- Main padding 34px, content gap 24px. Header 콘텐츠 radius 12px와 관련 shadow 유지. 기존 Sidebar 콘텐츠 radius 18px·shadow는 호환성·검증용 구현에만 남는다.
|
||||
- Dashboard는 `--hm-dashboard-*`와 공통 Semantic만 사용한다. GNB 높이·join, LNB 폭·gradient, 1024px 모바일 숨김 override에 의존하지 않는다.
|
||||
- Filter Bar는 Dashboard 최소 폭 frame 안에서 한 줄과 기존 control 폭을 유지한다. 넓은 개별 콘텐츠는 Main 또는 카드 내부에서 스크롤한다.
|
||||
|
||||
## 3. Storybook
|
||||
|
||||
대표 `Patterns/Layout/Dashboard → Dashboard`는 Compact Header 48px + 상태 필터 + 콘텐츠다. Compact 상태 바 geometry(높이 40px, 중심 50% + 18px, 28px control, 36px padding, radius 16px 및 오목한 날개), 전체/진행/보류/완료 count 색상은 기존 기준을 유지한다. count 진행 `#00c896` / 보류 `#ff9500`는 2026-10-07 사용자 결정에 따른 Story 전용 값이다.
|
||||
|
||||
### Dashboard 업무 상태 공통 계약
|
||||
|
||||
업무 상태 계약은 `packages/ui/src/patterns/dashboard-status/contract.ts`가 기준이다. `active | hold | done`은 실제 상태이고, `all | active | hold | done | issue`는 필터 ID다. `issue`는 상태를 대체하지 않는 독립 Boolean flag이며 `hold + issue`처럼 동시에 표현할 수 있다. `done`의 표시명은 `완료`를 유지한다.
|
||||
|
||||
계약은 label, tone, 접근성 label, count 계산(`countDashboardFilter`), 필터 매칭(`matchesDashboardFilter`)을 제공한다. React adapter는 기존 `Badge`, `Button`, `FilterBar`, `Progress`를 사용하고, 다른 Runtime은 동일한 `data-status`, `data-issue="true"`, `data-filter`, `aria-pressed` vocabulary를 사용한다. 선택 상태는 `aria-pressed="true"`가 우선하며 hover/focus/disabled는 대상 Runtime의 interaction state로 동일하게 매핑한다. 상태 색상 원본은 `packages/ui/src/patterns/dashboard-status/dashboard-status.css`이며 `@hm-design/ui/dashboard-status.css`로 공개한다. 기존 `src/patterns/dashboard-status-contract.css`는 원본을 import하는 호환 경로로 유지한다. 이 변수는 기존 Story 색상을 보존하는 compatibility mapping이며 HM Theme token은 변경하지 않는다. Static HTML 기준 샘플은 `docs/patterns/dashboard-status-static.html`이다.
|
||||
|
||||
패키지 소비자는 앱 CSS entry에서 Theme 다음에 상태 CSS를 명시적으로 로드한다. React API import만으로 CSS가 자동 적용되지는 않는다.
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "@hm-design/ui/tailwind.css";
|
||||
@import "@hm-design/ui/theme.css";
|
||||
@import "@hm-design/ui/dashboard-status.css";
|
||||
```
|
||||
|
||||
상태 CSS는 변수 정의만 제공한다. 기존 `Badge variant="outline"`에는 화면의 wrapper에서 아래 mapping을 적용한다. 완료의 흰색 ink가 유지되므로 기존 어두운 Badge 배경도 함께 유지한다. `issue` Badge는 실제 상태 Badge와 별도로 렌더링한다.
|
||||
|
||||
```css
|
||||
.dashboard-status [data-status="active"] { --dashboard-status-color: var(--hm-dashboard-status-active); }
|
||||
.dashboard-status [data-status="hold"] { --dashboard-status-color: var(--hm-dashboard-status-hold); }
|
||||
.dashboard-status [data-status="done"] { --dashboard-status-color: var(--hm-dashboard-status-done); }
|
||||
.dashboard-status [data-status="issue"][data-issue="true"] { --dashboard-status-color: var(--hm-dashboard-status-issue); }
|
||||
.dashboard-status [data-slot="badge"][data-status] {
|
||||
--hm-badge-outline-text: var(--dashboard-status-color);
|
||||
--hm-badge-outline-background: color-mix(in srgb, var(--hm-sem-palette-green-dark-2) 90%, transparent);
|
||||
--hm-badge-outline-border-color: color-mix(in srgb, var(--hm-sem-palette-black) 50%, transparent);
|
||||
}
|
||||
```
|
||||
|
||||
HTML adapter는 같은 상태 변수를 자신의 `color` 등에 연결한다. package dist를 정적 자산으로 배치하면 `theme.css` 다음에 `dashboard-status.css`를 `<link>`로 로드한다. 저장소의 기존 Static HTML 예제는 호환 CSS 경로를 그대로 사용한다. React와 HTML 모두 `done`은 `--hm-dashboard-status-done`을 참조하며, 이 alias는 `:root`와 `[data-hm-theme]`에서 각 scope의 white token으로 재계산한다. active/hold/issue 색상과 Runtime Theme의 73개 계약은 유지한다.
|
||||
|
||||
#### React 소비 예제: 데이터 → 상태 변수 → Badge
|
||||
|
||||
`active`(진행) → `--hm-dashboard-status-active`, `hold`(보류) → `--hm-dashboard-status-hold`, `done`(완료) → `--hm-dashboard-status-done`이다. 독립 `issue === true`는 `--hm-dashboard-status-issue`로 별도 Badge를 표시한다. `tone`은 의미 분류이며 Badge variant나 CSS를 자동 선택하지 않는다. 위 CSS mapping과 import 순서를 앱 CSS entry에 적용한 다음 아래 공개 API를 사용한다.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Badge, dashboardFilterDefinitions, matchesDashboardFilter,
|
||||
type DashboardFilterId, type DashboardStatusRecord,
|
||||
} from "@hm-design/ui"
|
||||
|
||||
type Work = DashboardStatusRecord & { id: string }
|
||||
const works: Work[] = [
|
||||
{ id: "T-1", status: "active" },
|
||||
{ id: "T-2", status: "hold", issue: true },
|
||||
{ id: "T-3", status: "done" },
|
||||
]
|
||||
|
||||
export function WorkBadges({ filter = "all" }: { filter?: DashboardFilterId }) {
|
||||
const visible = works.filter(work => matchesDashboardFilter(work, filter))
|
||||
return <div className="dashboard-status">
|
||||
{visible.map(work => <div key={work.id}>
|
||||
<Badge variant="outline" data-status={work.status}>
|
||||
{dashboardFilterDefinitions[work.status].label}
|
||||
</Badge>
|
||||
{work.issue === true && <Badge variant="outline" data-status="issue" data-issue="true">
|
||||
{dashboardFilterDefinitions.issue.label}
|
||||
</Badge>}
|
||||
</div>)}
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
`filter="issue"`에서도 T-2의 상태는 `hold`로 유지되고 보류 Badge와 이슈 Badge가 함께 표시된다. `issue`를 `DashboardStatusId`에 추가하거나 `destructive` 등 다른 Badge variant에 임의로 연결하지 않는다.
|
||||
|
||||
Reference: [React 代表 Story](../../src/patterns/dashboard-layout.stories.tsx)の `statusBadge` と [Story CSS](../../src/patterns/dashboard-layout.stories.css)の Badge mapping、[公開状態 CSS 原本](../../packages/ui/src/patterns/dashboard-status/dashboard-status.css)、[Static HTML](dashboard-status-static.html)。上の例はこれらの状態・色 mappingを公開 APIで消費する例であり、Storyモジュールは importしない。コード・Propsはソースと照合済みで、この文書改善作業では例の実行・画面検証はしていない。
|
||||
|
||||
Sidebar 검증 Story `WithSidebar`, `SidebarOnly`, `ThirdLevel`, `LongContent`, `Framed`는 기존 기능·상태 데이터를 보존하는 **VERIFICATION ONLY — 운영 사용 금지** 화면이다. `verification` 태그로 기본 Story 메뉴에서 제외하며 이름과 화면에 운영 금지를 표시한다. 신규 운영 화면 Reference로 사용하지 않는다. `Loading`, `Empty`도 verification 전용이며 Sidebar 없는 Compact 구성을 검사한다. 대표 Story ID `patterns-layout-dashboard-layout--all`과 기존 검증 Story ID는 유지한다. 이전 GNB + LNB / GNB Only / LNB Only Story 구성은 §5의 역사 기록이다.
|
||||
|
||||
## 4. 단일 모드 통합 검증 (2026-10-07, headless Edge)
|
||||
|
||||
> **HISTORICAL RECORD:** 아래 결과는 2026-10-07 당시 코드의 검증 기록이다. Sidebar 포함 1232px 사례는 현재 운영 허용이나 이번 정책 수정의 검증 결과를 뜻하지 않는다. 현재 운영 계약은 §1–3의 Sidebar 없는 960px 구성이다.
|
||||
|
||||
- Navigation/Theme Story × viewport 총 48개 케이스 통과. 1280, 1232, 1024, 1023, 960, 959, 390, 320px에서 주요 모드를 확인했다.
|
||||
- Dashboard에 외부 GNB/LNB 없음, Compact 960px / Sidebar 포함 1232px 최소 폭과 내부 가로 스크롤 유지, 문서 가로 넘침 0px.
|
||||
- LNB 1024/1023 전환, Sheet Escape·포커스 복귀·preventDefault 링크 선택·Desktop/Mobile 선택 상태 공유 확인. Dashboard 그룹 Enter/Space, 현재 링크 및 hover 유지 확인.
|
||||
- Theme Preview는 GNB/LNB 중 하나만 렌더링하고 Dashboard Theme 콘텐츠는 외부 Navigation을 제외한다. Runtime Theme 계약 검증 136개 통과, 브라우저 runtime error 0.
|
||||
- 타입 검사, UI package build, Storybook static build, Production 앱 build, `git diff --check` 통과. 기존 문서 예시 `bg-[var(...)]` CSS 경고 및 Storybook chunk 크기 경고는 남아 있다.
|
||||
|
||||
|
||||
## 5. 이전 구현·검증 이력 (HISTORICAL RECORD)
|
||||
|
||||
아래 내용은 정책 전환 이전 기록을 보존한 것이다. **현재 구현·운영·AI 생성의 근거로 사용하지 않는다.** GNB/LNB 혼합, 모바일 체계 전환, 교차 CSS 변수 및 이전 Story 구성은 위 §1–3과 공통 SINGLE NAVIGATION MODE 정책으로 대체되었다. 아래 테스트 수치는 이전 코드의 결과이며 이번 변경의 검증 결과가 아니다.
|
||||
|
||||
<details>
|
||||
<summary>2026-10-07 단일 모드 전환 이전 기록</summary>
|
||||
|
||||
### Dashboard Layout (Pattern)
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6.10, §6.11)를 따른다.
|
||||
|
||||
```text
|
||||
Layout visual reference: 외부 ui_viz Reference (ZIP 미포함) (dashboard-composition.md §7·§8·§19, Variant A Sidebar / Variant B Top-header)
|
||||
Implementation: current @hm-design/ui patterns / primitives
|
||||
```
|
||||
|
||||
#### 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (2026-10-03) |
|
||||
| 기반 | GNB, LNB, Page Header, Card, Badge, Progress, Filter Bar, Data Table 조합 (새 dependency 없음) |
|
||||
| 대표 Story | `Patterns/Layout/Dashboard Layout` → `Dashboard Layout` (verification: Third Level, GNB Only, LNB Only, Long Content, Loading, Empty, Framed) |
|
||||
| 함께 만든 Pattern | `Page Header` (`page-header.md`) |
|
||||
| 보류 | §12 |
|
||||
|
||||
#### 2. 목적 / 범위
|
||||
|
||||
GNB / LNB / Page Header / Main을 실제 화면으로 조립해 Layout contract를 확정한다. 새 UI를 만들지 않는다. 메뉴·페이지 내용은 소비자가 slot과 children으로 넣고, Dashboard는 메뉴 schema를 모른다. 소유하는 것: 화면 프레임(높이), main 스크롤, 콘텐츠 면(배경·padding·둥근 모서리·그림자), 콘텐츠 세로 리듬(gap).
|
||||
|
||||
#### 3. ui-viz visual reference
|
||||
|
||||
ui-viz에서 확인한 구조(실제 파일: `compositions/dashboard-variant-specimens.{jsx,css}`, `top-header-specimen.{jsx,css}`, `storybook/{LNB,GNB,Layout}.stories.jsx`, `layout-composition.jsx`, `dashboard-composition.md`):
|
||||
|
||||
| 영역 | ui-viz 구조 | 현재 HM 대응 | 이번 구현 |
|
||||
|---|---|---|---|
|
||||
| 조립 | Variant A(LNB 272 + 콘텐츠), Variant B(GNB 70 + 콘텐츠), Variant C(Compact 48). **GNB+LNB 동시 사용 예 없음** | `DashboardLayout` | slot 방식. 둘 다 / 하나만 |
|
||||
| GNB | 70px green, 콘텐츠와 12px 접점 | `GNB` | slot `header` |
|
||||
| LNB | 272px green, 콘텐츠와 18px 접점 | `LNB` | slot `sidebar` |
|
||||
| Content surface | `#F5F0E5`, padding 34, 접점 쪽 radius(LNB 18 / GNB 12), 그림자 `-8px 0 18px`(LNB) · `0 -8px 18px`(GNB) rgba(7,30,24,.18), 별도 border 없음 | 신규 | `DashboardMain` |
|
||||
| 스크롤 | "메뉴/헤더는 고정, 콘텐츠 영역에서만 스크롤"(Confirmed). `overflow-y:auto`, `overscroll-behavior: contain` | 신규 | `DashboardMain`이 스크롤 소유 |
|
||||
| Page heading | breadcrumb 12px(정적 `p`, 컴포넌트 아님) → 제목 28 → 설명, space-between·align end·gap 24, **하단 20px** | Breadcrumb + 신규 Pattern | `PageHeader` |
|
||||
| 간격 | 외곽 34, 카드 사이 24, 카드 padding 24, 카드 제목→본문 24, 조회→결과 16 | Card, `DashboardContent` | 24 gap 토큰, 나머지는 Card/Filter Bar 값 |
|
||||
| 조회 | 결과 카드 안 검색(라벨 좌측, 최대 360) + 표 | Filter Bar + Data Table | Card 안에서 조합 (gap 16) |
|
||||
| KPI | Dashboard에 Panel/Card 4종(KPI 전용 아님) | Card + Badge + Progress | Story 조합만 |
|
||||
|
||||
#### 4. GNB / LNB 조립 정책
|
||||
|
||||
| 구성 | 사용 | 근거 |
|
||||
|---|---|---|
|
||||
| GNB only (Variant B) | `header` | ui-viz 기준 있음 |
|
||||
| LNB only (Variant A) | `sidebar` | 모든 viewport에서 Sidebar 유지. 최소 폭 이하에서는 Dashboard 전체 가로 스크롤 |
|
||||
| GNB + LNB | `header` + `sidebar` | ui-viz에 기준 없음. 사용자 요청으로 조립한 INTENTIONAL 구성(대표 Story) |
|
||||
|
||||
##### Brand / Profile ownership (2026-10-06, `lnb.md` §3c)
|
||||
|
||||
| 구성 | Desktop Brand | Desktop Profile | Mobile Brand | Mobile Profile |
|
||||
|---|---|---|---|---|
|
||||
| GNB + LNB | GNB(`GNBBrand`) | GNB(`GNBUser`), LNBProfile 없음 | GNB Mobile Header | GNB Sheet의 `GNBUser`, 별도 LNB Profile 없음 |
|
||||
| LNB Only | `LNBBrand` | `LNBProfile` | 동일한 `LNBBrand` | 동일한 `LNBProfile` |
|
||||
| GNB only | GNB(`GNBBrand`) | GNB(`GNBUser`) | GNB Mobile Header | GNB Profile |
|
||||
|
||||
Brand/서비스명과 Profile의 소유권은 소비자가 composition에서 `LNBBrand`와 `LNBProfile`을 넣고 빼서 정한다. LNB Only의 `LNBProfile`은 `DropdownMenuTrigger asChild`로 합성하며 작은 viewport에서도 Sidebar와 함께 유지한다. GNB + LNB에서는 LNBProfile을 렌더링하지 않는다.
|
||||
|
||||
GNB+LNB일 때: GNB가 전체 폭 위에 평평하게 놓이고 LNB와 콘텐츠가 그 아래 한 줄. GNB의 12px 접점 띠는 숨기고(`::after`) 콘텐츠는 **왼쪽 모서리만 18px**, 위쪽 오른쪽 모서리는 직각이다. 작은 viewport에서도 이 접점을 유지한다. GNB 자체의 모바일 헤더·Sheet 정책은 그대로 유지한다.
|
||||
|
||||
#### 5. 구조 / API
|
||||
|
||||
```tsx
|
||||
<DashboardLayout header={<GNB>…</GNB>} sidebar={<LNB>…</LNB>}>
|
||||
<DashboardMain>
|
||||
<PageHeader>…</PageHeader>
|
||||
<DashboardContent>…Card, Filter Bar, Data Table…</DashboardContent>
|
||||
</DashboardMain>
|
||||
</DashboardLayout>
|
||||
```
|
||||
|
||||
- `DashboardLayout`: 바깥 `data-slot="dashboard-scroll"`이 가로 스크롤을 맡고, 안쪽 `data-slot="dashboard-layout"`(flex column)이 최소 폭과 `header`/`sidebar` slot을 유지한다. 기존 `className`, `style`, DOM props는 바깥 scroll root에 전달된다. `data-header`/`data-sidebar`는 안쪽 frame에 있다. DOM 순서는 header → (sidebar → main)이다.
|
||||
- Scroll root는 기본 `role="region"`, `aria-label="Dashboard"`, `tabIndex={0}`이며 DOM props로 변경할 수 있다. CSS 변수는 root에서 내부 frame으로 상속된다.
|
||||
- `DashboardMain`: `<main>`, 콘텐츠 면이자 세로 스크롤 컨테이너. 고유 `id`/`aria-label`이 필요하면 소비자가 준다.
|
||||
- `DashboardContent`: 섹션 세로 stack(gap 24). 섹션 스타일은 입히지 않는다.
|
||||
- GNB / LNB를 wrapper로 다시 감싸지 않고 그대로 slot에 넣는다.
|
||||
|
||||
#### 6. Content Surface (접점 소유권)
|
||||
|
||||
GNB / LNB는 콘텐츠 radius를 만들지 않는다. 자기 쪽 "연장 띠"만 둔다(GNB `::after` 12px = `--hm-gnb-join`, LNB `::after` 18px = `--hm-lnb-join`). 콘텐츠 면이 그 위에 positioned로 얹혀 둥근 모서리에서 띠가 보이게 한다(`DashboardMain`이 `position: relative`이고 DOM상 뒤).
|
||||
|
||||
| 상태 | radius | shadow |
|
||||
|---|---|---|
|
||||
| header | `gnb-join gnb-join 0 0` | `0 -8px 18px rgba(7,30,24,.18)` |
|
||||
| sidebar (모든 viewport) | `lnb-join 0 0 lnb-join` | `-8px 0 18px rgba(7,30,24,.18)` |
|
||||
| header + sidebar (모든 viewport) | sidebar와 같음, GNB 띠 숨김 | 같음 |
|
||||
| (둘 다 없음) | 0 | 없음 |
|
||||
|
||||
값은 `--hm-gnb-join`, `--hm-lnb-join`을 읽는다(하드코딩 없음). 콘텐츠 면 뒤 색(`--hm-dashboard-background`)은 LNB 끝 색 `--hm-lnb-background-end`.
|
||||
|
||||
#### 7. Main Layout
|
||||
|
||||
- padding 34px(`--hm-dashboard-padding`, 모든 viewport), 세로 gap 24px(`--hm-dashboard-gap`), 배경 `--hm-sem-bg-content`.
|
||||
- `min-width: 0`: Main, `DashboardContent`와 그 직접 자식. 표·Carousel·Combobox가 페이지를 밀지 않는다(검증: 1600px 넓은 내용도 Main `scrollWidth`를 늘리지 않음).
|
||||
- 스크롤: 바깥 Dashboard frame 높이는 `--hm-dashboard-height`(기본 `100dvh`)이며 가로 scrollbar도 이 높이에 포함된다. Main이 세로 스크롤, 바깥 frame이 Dashboard 전체 가로 스크롤을 맡는다. Main은 `overflow-x:auto`, `overscroll-behavior-y:contain`으로 가로 제스처가 바깥 frame으로 전달된다. LNB는 메뉴 영역만 자체 세로 스크롤한다.
|
||||
- 임베드 시 높이는 `--hm-dashboard-height`로 바꾼다(Story `Framed`).
|
||||
|
||||
#### 8. Page Header
|
||||
|
||||
별도 Pattern(`page-header.md`)으로 분리했다. 모든 화면이 반복해서 쓰고(Breadcrumb + h1 + actions), 간격(하단 20)이 ui-viz Layout contract라서다. Breadcrumb는 HM Breadcrumb를 그대로 쓰며 Page Header는 간격만 맡는다.
|
||||
|
||||
#### 9. Desktop / Mobile
|
||||
|
||||
```yaml
|
||||
Dashboard Minimum Width Policy: HORIZONTAL_SCROLL
|
||||
Content minimum width: 960px
|
||||
With Sidebar: 960px + 272px = 1232px
|
||||
Below minimum width: KEEP_LAYOUT_AND_SCROLL
|
||||
Compact / Sidebar mobile hide: NONE
|
||||
```
|
||||
|
||||
- 충분한 폭에서는 기존 배치를 사용한다. 작은 viewport에서는 내부 최소 폭을 유지하고 Dashboard 전용 root에서만 가로 스크롤한다. Sidebar slot은 항상 렌더링된다.
|
||||
- 960px 근거: KPI 4 × 200px + 간격 3 × 24px + padding 2 × 34px + 세로 scrollbar 여유 20px. Compact 상태 바 실측 약 549px, 브랜드 약 125px, 날개 35px도 이 폭에서 겹치지 않는다.
|
||||
- Sidebar 구성은 콘텐츠 최소 폭에 LNB 폭을 더한다. Desktop은 기존 `--hm-lnb-width`를 읽고, ≤1023px에서는 Dashboard 내부에만 `--hm-lnb-width: var(--hm-dashboard-sidebar-width, 272px)`와 LNB `display:flex`를 적용한다. 공통 `lnb.css`의 모바일 숨김은 유지된다.
|
||||
- 기본 콘텐츠 최소 폭은 `--hm-dashboard-min-width`로 조정할 수 있다. 기본값 960px은 현재 Desktop 구성 측정 기준이며 별도 콘텐츠를 넣으면 그 구성을 기준으로 조정한다.
|
||||
- Compact 메뉴는 숨기거나 wrap하지 않는다. Dashboard 안 Filter Bar도 한 줄과 기존 control 폭을 유지하며 공통 Filter Bar 모바일 정책은 변경하지 않는다. 본문 텍스트는 자연스럽게 줄바꿈한다.
|
||||
- GNB의 전역 메뉴·모바일 Sheet와 일반 LNB의 공통 responsive 정책, Theme/Color Token, Story ID/hierarchy는 변경하지 않는다.
|
||||
|
||||
#### 10. Accessibility / Keyboard
|
||||
|
||||
- landmark: `header`(GNB), `nav[aria-label=주 메뉴]`, `aside`(LNB) 안 `nav[aria-label=업무 메뉴]`, `main`, `nav[aria-label=breadcrumb]`. 화면의 `h1`은 Page Header 제목 하나.
|
||||
- Tab 순서(측정): GNB(브랜드, 메뉴 4, 사용자) → LNB(항목, 그룹, 하위) → Breadcrumb → 페이지 액션 → 검색 → 상태 → 초기화 → 표. focus trap은 Sheet/Dialog에서만.
|
||||
- skip-to-content 링크는 만들지 않았다(§12). `aside`에는 이름이 없고 안의 `nav`가 이름을 갖는다.
|
||||
|
||||
#### 11. 비교 (ui-viz ↔ 현재 HM)
|
||||
|
||||
| 항목 | ui-viz | 현재 HM | 분류 |
|
||||
|---|---|---|---|
|
||||
| GNB 70 / LNB 272 | 같음 | `--hm-gnb-height` / `--hm-lnb-width` | MATCHED |
|
||||
| 콘텐츠 면 색·padding 34 | `#F5F0E5`, 34 | 같음 | MATCHED |
|
||||
| 접점 radius (LNB 18 / GNB 12), shadow | 같음 | 같음. 소유를 Dashboard로 이동 | MATCHED |
|
||||
| 헤더·메뉴 고정, 콘텐츠만 스크롤 | 같음 | `DashboardMain` 스크롤 | MATCHED |
|
||||
| 접점 띠가 콘텐츠 뒤에서 이어짐 | 형제 absolute | GNB/LNB `::after` + positioned Main | IMPLEMENTATION_CHANGE |
|
||||
| Page heading 간격(하단 20, h 28, gap 24) | 같음 | `PageHeader` | MATCHED |
|
||||
| Breadcrumb | 12px 정적 `p`, 컴포넌트 아님 | HM Breadcrumb(14px, 링크) | INTENTIONAL_CHANGE (사용자 지시) |
|
||||
| 제목 태그 | `h2` | `h1` | INTENTIONAL_CHANGE (페이지당 h1) |
|
||||
| 카드 | Content Card(흰색), padding 24 | HM Card | MATCHED |
|
||||
| 조회 + 표 | 결과 카드 안, 검색 → 표 16 | Card 안 Filter Bar + Data Table, gap 16 | MATCHED |
|
||||
| 표 density | ui-viz Table | HM Data Table(Table primitive) | MATCHED (primitive 기준) |
|
||||
| GNB + LNB 동시 | 없음 | 조립(GNB 평평, LNB 아래) | INTENTIONAL_CHANGE / FOLLOW_UP |
|
||||
| 반응형 | ≤1100px/48px OUT_OF_SCOPE | Dashboard 최소 폭 유지 + native 가로 스크롤. GNB 전역 Sheet 정책 유지 | INTENTIONAL_CHANGE |
|
||||
| 스크롤바 | 기본 | 기본 | — |
|
||||
| ui-viz 화면과 픽셀 비교 | — | 미실시(ui-viz Storybook 미실행) | — |
|
||||
|
||||
#### 12. 남은 차이 / 누락
|
||||
|
||||
| 항목 | 분류 |
|
||||
|---|---|
|
||||
| GNB+LNB brand 중복 | 해결 (2026-10-06, §4 Brand ownership: GNB만, LNBBrand 없음) |
|
||||
| GNB+LNB 조립 시각 확정(두 gradient 이음새) | FOLLOW_UP — 사용자 결정 필요 |
|
||||
| LNB only의 모바일 탐색 | 동일 Sidebar를 가로 스크롤로 탐색 (2026-10-07 최소 폭 정책, §9) |
|
||||
| skip-to-content, `main` 이름 | FOLLOW_UP |
|
||||
| ui-viz Variant C(Compact 48 + Status Filter Bar) | 대표 Story에 구현됨. 최소 폭 960px으로 유지 |
|
||||
| 실기기 touch/trackpad, ui-viz 픽셀 비교 | 미확인 |
|
||||
| 문서 스크롤 방식(frame 높이 없이 body 스크롤) 변형 | 필요 시 `--hm-dashboard-height: auto` + GNB `sticky`로 가능하나 검증하지 않음 |
|
||||
|
||||
##### 누락 컴포넌트 점검
|
||||
|
||||
| 항목 | 필요도 | 이유 | 다음 단계 |
|
||||
|---|---|---|---|
|
||||
| KPI Card | 권장 | Card+Badge+Progress를 4곳에서 직접 조합. 반복되면 Pattern | 실제 제품 요구 때 |
|
||||
| Page-level Empty State | 권장 | 표 Empty만 있고 페이지 전체 빈 상태(첫 사용·권한 없음) 없음 | 요구 확인 후 |
|
||||
| Page Loading | 후속 | Skeleton + Data Table `loading` 조합으로 충분(Story `Loading`) | 보류 |
|
||||
| Bulk Action Bar | 권장 | 표 선택 후 일괄 작업 영역 없음 | Data Table 확장 때 |
|
||||
| Filter Chip / 적용 조건 표시 | 후속 | Filter Bar 문서 §12에 기록됨 | 보류 |
|
||||
| Mobile Filter Drawer | 후속 | Dashboard에서는 최소 폭 안에서 기존 Filter Bar를 유지 | 보류 |
|
||||
| Notification Center, Global Search Result | 후속 | GNB 문서 | 보류 |
|
||||
| Skip link | 권장 | 키보드 사용자: GNB/LNB 건너뛰기 | 접근성 보완 때 |
|
||||
|
||||
#### 13. 검증 (2026-10-07, 최소 폭 정책)
|
||||
|
||||
현재 검증 결과는 §9의 HORIZONTAL_SCROLL 정책을 따른다. 아래 Desktop 전용 정책 검증은 이전 작업 이력이다.
|
||||
|
||||
당시 Storybook 포트 6007, Chromium, 높이 900px에서 실측했다.
|
||||
|
||||
| viewport | Compact 내부 폭 | Compact 가로 스크롤 | GNB + LNB 내부 폭 | GNB + LNB 가로 스크롤 | 메뉴·콘텐츠 | 레이아웃 붕괴 |
|
||||
|---|---:|---|---:|---|---|---|
|
||||
| 1280 | 1265 | 없음 | 1280 | 없음 | 유지 | 없음 |
|
||||
| 1024 | 1009 | 없음 | 1232 | 있음 | 유지 | 없음 |
|
||||
| 1023 | 1008 | 없음 | 1232 | 있음 | 유지 | 없음 |
|
||||
| 768 | 960 | 있음 | 1232 | 있음 | 유지 | 없음 |
|
||||
| 390 | 960 | 있음 | 1232 | 있음 | 유지 | 없음 |
|
||||
| 320 | 960 | 있음 | 1232 | 있음 | 유지 | 없음 |
|
||||
|
||||
- 대표 Compact Story는 바깥 Story 설명 때문에 문서에 세로 scrollbar 15px이 있다. 위 내부 폭은 이를 제외한 가용 폭이며 `100vw`를 사용하지 않는다. 모든 조합에서 문서 가로 넘침은 0px이다.
|
||||
- Sidebar 폭 272px, Main padding 34px, Filter Bar 검색 폭 280px·높이 34px이 모든 폭에서 유지된다. KPI 4개는 작은 viewport에서도 한 줄, 각 약 201px이다. Main 자체 가로 넘침은 0px이다.
|
||||
- Third Level, GNB Only, LNB Only, Long Content, Loading, Empty, Framed를 1280·320px에서 추가 확인했다. 문서 가로 넘침 0px, 넓은 1600px 내용은 카드 내부에서만 스크롤, Framed 높이는 scrollbar를 포함해 640px으로 유지된다.
|
||||
- 320px에서 native 가로 wheel 입력으로 Compact scrollLeft 655px / Sidebar 구성 912px(각 최대값)에 도달하고 0px으로 복귀했다. 문서 scrollLeft는 0px으로 유지된다. 스크롤 root의 ArrowRight 입력도 동작한다.
|
||||
- Compact Tab 순서 전체 → 활성 → 대기 → 비활성 → 검색을 확인했다. 비활성 버튼에 focus하면 scrollLeft 435px으로 자동 이동하며 버튼 전체와 focus indicator가 viewport 안에 보인다. 스크롤 JS, touch-action 제한, 입력 이벤트 차단은 추가하지 않았다. 실제 touch·trackpad·Shift+wheel 장치는 별도 실기기 확인이 필요하다.
|
||||
- 일반 GNB Story의 모바일 높이 56px·desktop nav 숨김은 유지된다. 공통 GNB/LNB CSS와 JS는 수정하지 않았다.
|
||||
- 실제 터치 장치 테스트는 수행하지 않았다.
|
||||
- `npm run typecheck`, 최종 UI package build, Storybook static build, `git diff --check` 통과. 기존 문서 예시 `bg-[var(...)]` CSS 경고와 500kB chunk 경고는 유지된다.
|
||||
|
||||
|
||||
##### 이전 Desktop 전용 정책 검증 (2026-10-07, Storybook / Chrome)
|
||||
|
||||
- Dashboard Story 9개 × 1280·1024·1023·390 = 36개 조합 통과. 대표 Compact 메뉴는 Desktop 표시, Mobile 높이 0. Sidebar는 Desktop 폭 272px, Mobile DOM에서 제거된다.
|
||||
- 1280: GNB + LNB Main 폭 1008px. 1024: Main 폭 752px. 1023·390: Main이 각각 1023·390px 전체 폭을 사용하며 빈 sidebar 공간 없음. 모든 조합에서 문서 및 Main 가로 overflow 0.
|
||||
- Mobile: Dashboard 로컬 메뉴·Compact 상태 필터가 접근성 tree에 없고 Tab으로 접근되지 않는다. GNB Sheet에는 기존 전역 메뉴만 표시하고 Dashboard 업무 메뉴는 없다.
|
||||
- Desktop LNB hover·active 변경·그룹 열림/닫힘 통과. 열린 LNB Profile portal은 1024 → 1023 전환 시 함께 제거되고 1024로 복귀하면 sidebar가 다시 표시된다.
|
||||
- 패키지 build·typecheck 통과. Story 수정 후 루트 `tsc -b` 통과. 브라우저 runtime error 없음.
|
||||
|
||||
##### 이전 검증 이력
|
||||
|
||||
아래 모바일 메뉴 관련 기록은 이전 정책의 이력이다. 현재 정책은 §9이며 모든 viewport에서 Dashboard 메뉴·콘텐츠를 유지하고 최소 폭 이하에서 가로 스크롤한다.
|
||||
|
||||
- 1280: 문서 스크롤 없음(`scrollHeight == clientHeight`), Main만 스크롤(scrollTop 400에서 GNB y 0·LNB y 70 유지). h1 1개, landmark 위 목록.
|
||||
- 1024 desktop / 1023 mobile 전환, 768, 390: 문서·Main 가로 overflow 0. GNB-only, LNB-only, Long Content(긴 제목 + 1600px 내용) 모두 overflow 0.
|
||||
- 390: 모바일 Sheet에 전역/로컬 그룹 표시, current 표시.
|
||||
- 2026-10-06 `Third Level`: 1280·1024 GNB 70 + LNB 272(3뎁스 active "전체 업무", 상위 "업무 관리"·"업무 현황" current), 1023·768·390 GNB 56 + 햄버거, LNB `display:none`, Sheet에서 업무 메뉴 → 업무 관리(+12) → 업무 현황(+24) → 3뎁스 링크, active 동일. Sheet에서 다른 3뎁스를 고르면 LNB active도 같이 바뀐다. 5개 폭 모두 문서 가로 overflow 0.
|
||||
- 2026-10-06 `LNB Only` 모바일: 1280·1024 GNB 없음 + LNB 272, radius `18 0 0 18`; 1023·768·390 상단 바 56(햄버거 1개, header 1개) + LNB 숨김, radius `12 12 0 0`, 가로 overflow 0. Sheet는 "업무 메뉴" 하나(전역 메뉴 없음), active 동일. `Dashboard Layout`·`Third Level`(GNB + LNB)은 5개 폭 모두 GNB 하나(1023 이하 56 + 햄버거 1개), Sheet 전역 + 업무 메뉴 그대로.
|
||||
- 2026-10-06 Brand ownership: `Dashboard Layout`·`Third Level` 1280·1024 brand = GNB 1개(LNBBrand 없음, LNB 메뉴 top 98 = GNB 70 + 28), 1023·390 GNB 모바일 바 1개. `LNB Only` 1280·1024 LNBBrand 1개(상단 바 없음), 1023·390 Local 모바일 바 1개. 서비스명은 모든 폭에서 1개. 수치는 `lnb.md` §8.
|
||||
- 기존 Pattern(GNB 70/12px 띠, LNB 272, Data Table, Filter Bar) Story 오류·overflow 없음.
|
||||
- `npm run typecheck`, `npm run build` 통과.
|
||||
|
||||
#### 14. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/patterns/dashboard-layout/{dashboard-layout.tsx,index.ts}
|
||||
packages/ui/src/patterns/page-header/{page-header.tsx,index.ts}
|
||||
packages/ui/src/theme/{dashboard-layout.css,page-header.css}
|
||||
src/patterns/dashboard-layout.stories.tsx
|
||||
```
|
||||
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,94 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>HM Dashboard Status Contract - Static HTML</title>
|
||||
<link rel="stylesheet" href="../../packages/ui/dist/theme.css" />
|
||||
<link rel="stylesheet" href="../../src/patterns/dashboard-status-contract.css" />
|
||||
<style>
|
||||
@font-face { font-family: "Noto Sans KR"; src: url("../../public/fonts/NotoSansKR-variable.ttf") format("truetype"); font-weight: 100 900; }
|
||||
:root {
|
||||
--dashboard-work-progress: var(--hm-dashboard-status-active);
|
||||
--dashboard-work-hold: var(--hm-dashboard-status-hold);
|
||||
--dashboard-work-issue: var(--hm-dashboard-status-issue);
|
||||
--dashboard-work-surface: var(--hm-sem-palette-green-dark-2);
|
||||
}
|
||||
body { margin: 0; min-width: 960px; background: var(--hm-sem-bg-content); color: var(--hm-sem-fg-primary); font-family: "Noto Sans KR", sans-serif; }
|
||||
main { max-width: 1120px; margin: 0 auto; padding: 32px; }
|
||||
.panel { padding: 24px; border-radius: var(--hm-radius-container); background: var(--hm-sem-bg-surface); box-shadow: var(--hm-card-shadow); }
|
||||
.toolbar, .filters, .badges { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.toolbar { justify-content: space-between; margin-bottom: 20px; }
|
||||
.filters { padding: 10px; border-radius: var(--hm-radius-container); background: var(--dashboard-work-surface); }
|
||||
button { min-height: 32px; padding: 0 12px; border: 1px solid color-mix(in srgb, var(--hm-sem-palette-black) 50%, transparent); border-radius: var(--hm-radius-input); background: color-mix(in srgb, var(--hm-sem-palette-black) 35%, transparent); color: var(--hm-sem-palette-white); font: inherit; cursor: pointer; }
|
||||
button:hover { background: color-mix(in srgb, var(--hm-sem-palette-black) 55%, transparent); }
|
||||
button:focus-visible { outline: var(--hm-dashboard-nav-focus-outline); }
|
||||
button[aria-pressed="true"] { background: color-mix(in srgb, var(--hm-sem-palette-green-dark-2) 90%, transparent); border-color: var(--hm-sem-palette-green-light-4); }
|
||||
button[data-filter="issue"] { background: color-mix(in srgb, var(--hm-sem-action-destructive) 25%, transparent); }
|
||||
[data-slot="badge"] { background: color-mix(in srgb, var(--hm-sem-palette-green-dark-2) 90%, transparent); }
|
||||
[data-slot="badge"][data-status] { color: var(--dashboard-status-color); border-color: color-mix(in srgb, var(--hm-sem-palette-black) 50%, transparent); }
|
||||
[data-status="active"] { --dashboard-status-color: var(--dashboard-work-progress); }
|
||||
[data-status="hold"] { --dashboard-status-color: var(--dashboard-work-hold); }
|
||||
[data-status="done"] { --dashboard-status-color: var(--hm-dashboard-status-done); }
|
||||
[data-status="issue"][data-issue="true"] { --dashboard-status-color: var(--dashboard-work-issue); }
|
||||
.count { margin-left: auto; color: var(--hm-sem-fg-secondary); }
|
||||
.result { margin-top: 16px; }
|
||||
[hidden] { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<section class="panel" aria-labelledby="title">
|
||||
<div class="toolbar">
|
||||
<div><h1 id="title">Dashboard 업무 상태</h1><p id="summary" role="status">전체 업무 0건</p></div>
|
||||
<div class="filters" role="group" aria-label="업무 상태 필터" data-status-filter-group></div>
|
||||
</div>
|
||||
<div class="result" aria-live="polite"><div class="badges" id="results" aria-label="필터 결과"></div><p id="empty" hidden>조건에 맞는 업무가 없습니다.</p></div>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
const contract = {
|
||||
filters: ["all", "active", "hold", "done", "issue"],
|
||||
labels: { all: "전체", active: "진행", hold: "보류", done: "완료", issue: "이슈" },
|
||||
records: [
|
||||
{ name: "대시보드 화면 검토", status: "active" },
|
||||
{ name: "업무 자료 정리", status: "hold", issue: true },
|
||||
{ name: "현장 점검 보고", status: "done" },
|
||||
],
|
||||
};
|
||||
const state = { selected: "all" };
|
||||
const matches = (record, filter) => filter === "all" || (filter === "issue" ? record.issue === true : record.status === filter);
|
||||
const count = (filter) => contract.records.filter((record) => matches(record, filter)).length;
|
||||
const group = document.querySelector("[data-status-filter-group]");
|
||||
const results = document.querySelector("#results");
|
||||
const summary = document.querySelector("#summary");
|
||||
const empty = document.querySelector("#empty");
|
||||
function render() {
|
||||
group.replaceChildren(...contract.filters.map((filter) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.dataset.filter = filter;
|
||||
button.dataset.status = filter;
|
||||
if (filter === "issue") button.dataset.issue = "true";
|
||||
button.setAttribute("aria-pressed", String(state.selected === filter));
|
||||
button.setAttribute("aria-controls", "results");
|
||||
button.textContent = `${contract.labels[filter]} ${count(filter)}건`;
|
||||
button.addEventListener("click", () => { state.selected = filter; render(); });
|
||||
return button;
|
||||
}));
|
||||
const filtered = contract.records.filter((record) => matches(record, state.selected));
|
||||
results.replaceChildren(...filtered.map((record) => {
|
||||
const item = document.createElement("span");
|
||||
item.dataset.slot = "badge";
|
||||
item.dataset.status = record.status;
|
||||
if (record.issue) item.dataset.issue = "true";
|
||||
item.textContent = `${record.name} · ${contract.labels[record.status]}${record.issue ? " · 이슈" : ""}`;
|
||||
return item;
|
||||
}));
|
||||
summary.textContent = `${contract.labels[state.selected]} 업무 ${filtered.length}건`;
|
||||
empty.hidden = filtered.length !== 0;
|
||||
}
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,103 @@
|
||||
# Data Table (Pattern)
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7, Pattern 절)를 따른다. 이 문서는 Data Table Pattern 고유의 API·상태·결정만 기록한다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-03 |
|
||||
| 기반 | `@tanstack/react-table` 9.2.4 + HM Table primitive |
|
||||
| 대표 Story | `Patterns/Data/Data Table` → `Data Table` (`With Filter Bar`는 숨김 verification) |
|
||||
| 보류 | §12 |
|
||||
|
||||
## 2. 목적 / 사용 범위
|
||||
|
||||
업무 목록 화면의 표. 기존 `Table`(presentation primitive)에 정렬·행 선택·페이지·열 표시·로딩/빈 상태·행 액션을 조합한다. `Table`에는 어떤 로직도 넣지 않았다. 서버 데이터 호출·서버 사이드 페이지/정렬·가상 스크롤은 범위 밖이다.
|
||||
|
||||
## 3. 구성 요소
|
||||
|
||||
`DataTable`, `useDataTable`, `DataTableColumnHeader`, `DataTableViewOptions`, `DataTablePagination`, `dataTableSelectColumn`. (`DataTable`이 `DataTablePagination`을 기본으로 포함한다.)
|
||||
|
||||
## 4. 구조
|
||||
|
||||
```
|
||||
Table primitive (정적 표시, 변경 없음)
|
||||
↑ 조합
|
||||
DataTable pattern ── useDataTable (TanStack useTable + 기능 등록)
|
||||
├ DataTableColumnHeader 정렬 버튼
|
||||
├ dataTableSelectColumn HM Checkbox 선택 열
|
||||
├ DataTableViewOptions HM Dropdown Menu + Button
|
||||
└ DataTablePagination HM Pagination + Select(페이지 크기) + 선택 수 요약
|
||||
```
|
||||
|
||||
## 5. Public API
|
||||
|
||||
```tsx
|
||||
const table = useDataTable({ columns, data, getRowId, initialState }) // TanStack TableOptions에서 features만 뺀 것
|
||||
<DataTableViewOptions table={table} labels={{ name: "이름" }} />
|
||||
<DataTable table={table} loading emptyMessage="..." pagination pageSizeOptions={[10, 20, 50]} />
|
||||
```
|
||||
|
||||
- `columns`: `DataTableColumnDef<TData>[]`(TanStack `ColumnDef`, 등록된 기능 포함). 정렬 header는 `({ column }) => <DataTableColumnHeader column={column} title="이름" />`.
|
||||
- `DataTable`: `table`, `loading`, `emptyMessage`, `pagination`, `pageSizeOptions`, `className`. 그 외 TanStack 옵션은 `useDataTable`에 그대로 전달한다(복제 prop 없음).
|
||||
- `data`와 `columns`는 참조가 안정적이어야 한다(모듈 상수, `useMemo`, state).
|
||||
- 타입: `DataTableColumnDef`, `DataTableInstance`, `DataTableOptions`, `DataTableProps`.
|
||||
|
||||
## 6. 상태 관리
|
||||
|
||||
TanStack Table v9의 state model 그대로. v9는 기능을 명시 등록하는 방식(`tableFeatures`)이라 v8 예제(`useReactTable`, `getCoreRowModel`)와 다르다. 등록한 기능: column filtering, global filtering, row sorting, row selection, row pagination, column visibility와 각 row model(`filteredRowModel`, `sortedRowModel`, `paginatedRowModel`), `includesString`·`equalsString` filter, `alphanumeric`·`basic`·`datetime` sort.
|
||||
|
||||
- 상태는 TanStack 내부가 소유한다(필요하면 `useDataTable`에 `state` / `atoms`로 제어). 검색은 `table.setGlobalFilter`, 열 필터는 `table.getColumn(id)?.setFilterValue`, 초기화는 `resetGlobalFilter(true)` / `resetColumnFilters(true)`로 한다. 필터 변경 시 TanStack이 page index를 0으로 되돌린다.
|
||||
- error는 Data Table이 소유하지 않는다(소비자가 Alert / Toast로 처리).
|
||||
|
||||
조회 버튼을 사용하는 목록은 [FilterBar의 draft / applied 소비 예제](filter-bar.md#61-조회-버튼으로-적용하는-소비-예제)를 따른다. 입력 중에는 draft만 바꾸고 조회 시 table 필터 또는 `useMemo`로 만든 `data`를 갱신한다. 초기화는 입력값과 적용값, 사용 중인 table 필터와 페이지를 소비자가 함께 처리한다. 대표 Data Table Story는 표 기능의 운영 Reference이며, `WithFilterBarStory`(`patterns-data-data-table--with-filter-bar-story`)는 **입력 즉시 적용**을 확인하는 숨김 verification이다. 조회 버튼으로 조건을 확정하는 공개 API나 서버 호출 기능은 제공하지 않는다.
|
||||
|
||||
## 7. 조합 컴포넌트
|
||||
|
||||
Table, Checkbox(header mixed), Pagination, Select(페이지 크기), Dropdown Menu(열 표시), Button(열 표시 trigger), Skeleton(loading). 행 액션은 소비자가 열 정의에서 Dropdown Menu(또는 Context Menu)로 구성한다. Story는 행 액션 결과를 Sonner `toast`로 보이지만 Pattern은 Sonner에 의존하지 않는다.
|
||||
|
||||
## 8. Styling / Token
|
||||
|
||||
Header·cell·row·selected 면은 `packages/ui/src/theme/table.css`(`--hm-table-*`)가 소유하고 Pattern은 다시 정의하지 않는다. 컬러 역할(2026-10-06): 선택 행 면 = `--accent` 45%(hover·selected+hover와 같은 Soft Surface, Select Highlight / Selected와 동일한 Accent 45% 정책), 선택 Checkbox = `--hm-sem-state-active`(Light 2). 면은 hover와 같으므로 선택 여부는 Checkbox와 선택 수 요약으로 구분한다. Pattern 전용: `packages/ui/src/theme/data-table.css`의 `--hm-data-table-*`(footer gap·padding, summary 색·크기, 정렬 버튼 간격·아이콘 크기·활성 색, empty padding·색, skeleton 높이). 신규 Primitive·Semantic 없음.
|
||||
|
||||
- Loading: header 유지, body는 Skeleton 행(최대 5행), `aria-busy`.
|
||||
- Sort Direction Accent: `--hm-data-table-sort-active-color`는 현재 `--hm-sem-palette-green-light-3`(Green Light 3 palette)를 참조한다. 별도 State Semantic으로 승격하지 않았으며 Primary/Active/Accent 역할에 자동 연결되지 않는다. Token 이름의 `active`는 Control Active Semantic 계약을 뜻하지 않는다.
|
||||
- Empty: 한 행을 열 수만큼 `colSpan`, 문구는 `emptyMessage`(원본 0건과 필터 결과 0건 동일 UI).
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
- `Patterns/Data/Data Table` → `Data Table`(대표: 선택·정렬·열 표시·행 액션·페이지 크기·페이지), `With Filter Bar`(숨김 Filter Bar 연동 검증). verification: `WithFilterBar`, `Loading`, `Empty`.
|
||||
- 샘플 데이터는 `src/patterns/sample-users.ts`(47행, 네트워크 없음).
|
||||
|
||||
## 10. Accessibility
|
||||
|
||||
semantic `<table>` 유지. 정렬 열 `<th aria-sort>`(ascending / descending), 정렬 버튼 `aria-label`("이름 정렬, 오름차순"), 버튼이라 Enter / Space 동작. 선택 Checkbox는 `aria-label`("현재 페이지 전체 선택", "행 선택"), header는 `aria-checked="mixed"`. 선택 수 요약은 `role="status"`. Pagination은 기존 `nav` + `aria-current` / `aria-disabled`.
|
||||
|
||||
## 11. Responsive
|
||||
|
||||
Table primitive의 container가 가로 스크롤한다. Pattern은 `[data-slot="data-table"] { min-width: 0 }`로 grid / flex 부모 안에서 페이지를 밀지 않게 한다. 390px에서 페이지 가로 overflow 없음(표만 520px 안에서 스크롤), footer는 요약 / 페이지 크기 / 페이지 이동이 줄바꿈된다. 카드형 변환은 없다.
|
||||
|
||||
## 12. 보류 / 비범위
|
||||
|
||||
- column resizing·pinning, grouping, tree, virtualization, infinite scroll, drag reorder, 서버 사이드 페이지/정렬, 편집 가능한 표.
|
||||
- 선택 action toolbar(선택 수 요약만 제공), 행 클릭 이동, 다중 정렬 UI.
|
||||
- 정렬 header와 열 표시 메뉴의 열 이름: header가 컴포넌트일 수 있어 메뉴 문구는 `labels`로 받는다.
|
||||
- 확인하지 못한 것: 스크린 리더 낭독, 키보드 Tab 순서 전체, 페이지 크기 Select 조작, 행 선택 + 페이지 이동 후 선택 유지.
|
||||
- `With Filter Bar` verification Story의 DatePicker / Combobox는 `controlled={true}`를 사용한다. 초기화는 부모의 검색·상태·날짜·부서 state와 `table.resetGlobalFilter(true)` / `table.resetColumnFilters(true)`를 함께 비운다. 날짜·부서의 undefined 초기화는 선택값을 비우며, 해당 목적의 `key` remount를 제거했다. 외부 값 변경은 자식 콜백을 호출하지 않으므로 테이블 필터는 소비자가 명시적으로 초기화해야 한다. 팝업 닫기·검색어·달력 월 초기화는 별도 계약이며, 이 Story는 일반 운영 Reference로 승격하지 않는다.
|
||||
- **Legacy 호환 범위**: `controlled`를 생략한 DatePicker / Combobox는 기존 `value !== undefined` 판정을 유지한다. 외부 undefined 초기화 시 내부 값이 다시 표시될 수 있으므로, 과거 `key` remount 우회 설명은 이 Legacy 호출에 한정한다. 신규 연동은 명시적 Controlled 모드를 사용한다.
|
||||
|
||||
## 13. 관련 파일 / 검증
|
||||
|
||||
- 2026-10-08: `With Filter Bar` verification Story 소스를 격리 Headless Edge 환경에서 실행했다. 부서(검색/global filter) + 상태/등록일(column filter)로 1행 조회 후 초기화하면 전체 47개와 기존 표 표시를 복원하고 날짜·부서 Trigger DOM을 유지했다. 영향 범위 타입 검사 통과. 공용 dist / Storybook 빌드 및 전체 시각 회귀는 수행하지 않았다.
|
||||
|
||||
```
|
||||
packages/ui/src/patterns/data-table/{data-table.tsx,index.ts}
|
||||
packages/ui/src/theme/data-table.css
|
||||
packages/ui/src/index.ts, theme.css, tailwind.css(@source patterns)
|
||||
src/patterns/data-table.stories.tsx, sample-users.ts
|
||||
```
|
||||
|
||||
- `npm run typecheck`, `npm run build` 통과(2026-10-03).
|
||||
- Browser 확인: 정렬 none → asc → desc → none과 `aria-sort`, header mixed / 전체 / 해제와 선택 수 요약·`data-state="selected"`, 페이지 이동, 열 숨김(메뉴 유지), 행 액션이 행 데이터를 받음(toast), Loading(skeleton 30개, `aria-busy`), Empty(`colSpan`), 390px overflow 없음. 연동은 `filter-bar.md` §13.
|
||||
@@ -0,0 +1,149 @@
|
||||
# Filter Bar (Pattern)
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7, Pattern 절)를 따른다.
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (현재 HM 규격) |
|
||||
| 기준일 | 2026-10-03 |
|
||||
| 기반 | HM primitives 조합 (의존성 추가 없음) |
|
||||
| 대표 Story | `Patterns/Data/Filter Bar` → `Filter Bar`, 연동 검증은 `Patterns/Data/Data Table` → `With Filter Bar` (숨김 verification) |
|
||||
| 보류 | §12 |
|
||||
|
||||
## 2. 목적 / 사용 범위
|
||||
|
||||
목록·업무 화면의 검색 조건 행. Data Table 없이도 쓸 수 있고, 값은 소비자가 소유한다. ui-viz의 Search Field(선택값 칩 + 후보)는 별개의 복합 컨트롤이라 이 Pattern에 넣지 않았다.
|
||||
|
||||
## 3. 구성 요소
|
||||
|
||||
`FilterBar`(flex wrap 레이아웃), `FilterBarSearch`(검색 아이콘 + HM Input + 지우기), `FilterBarActions`(행 끝 정렬: 초기화·조회 등). 나머지는 HM Select / Combobox / Date Picker / Button을 children으로 직접 둔다. schema 기반 필터 생성은 없다.
|
||||
|
||||
## 4. 구조
|
||||
|
||||
```tsx
|
||||
<FilterBar>
|
||||
<FilterBarSearch value={q} onValueChange={setQ} placeholder="이름·부서 검색" />
|
||||
<Select>…</Select> <Combobox … /> <DatePicker … />
|
||||
<FilterBarActions><Button variant="cancel" appearance="outline">초기화</Button></FilterBarActions>
|
||||
</FilterBar>
|
||||
```
|
||||
|
||||
## 5. Public API
|
||||
|
||||
- `FilterBar`, `FilterBarActions`: `div` props. wrapper에 role을 두지 않는다.
|
||||
- `FilterBarSearch`: `value`, `onValueChange`(필수), `size`(Input size, 기본 `medium`), `placeholder`(기본 "검색"), 나머지 `input` props. `aria-label` 기본값은 placeholder. 값이 있으면 지우기 버튼(`aria-label="검색어 지우기"`).
|
||||
- Reset은 소비자가 Button과 callback으로 구성한다(Pattern에 `onReset` prop 없음).
|
||||
- Dashboard의 빠른 상태 전환(`all / active / hold / done / issue`)은 `FilterBar`가 소유하지 않는다. `FilterBar`는 검색·Select·Combobox·DatePicker 등의 조회 조건 layout으로 유지하고, 상태 필터는 Dashboard status contract를 소비하는 별도 조합으로 배치한다.
|
||||
|
||||
## 6. 상태 관리
|
||||
|
||||
Pattern은 상태가 없다. Data Table과 연결할 때는 검색 → `table.setGlobalFilter`, 열 선택 → `column.setFilterValue`로 소비자가 잇는다(`data-table.md` §6).
|
||||
|
||||
FilterBar에는 `onSearch / onSubmit / onReset` 공개 API나 조회 시점 강제가 없다. 검색어 지우기도 `onValueChange("")`만 호출한다. 즉시 적용할지 조회 버튼으로 적용할지, 초기화 후 바로 전체 목록을 복원할지는 소비자가 결정한다. [대표 Story](../../src/patterns/filter-bar.stories.tsx)의 조회 버튼에는 실행 callback이 없고 조건 수는 입력값으로 계산한다. [DataTable 연동 verification](../../src/patterns/data-table.stories.tsx)의 `WithFilterBarStory`는 입력 즉시 table 필터를 변경하며 조회 버튼은 없다. 이 둘을 조회 버튼 적용 계약으로 해석하지 않는다.
|
||||
|
||||
DatePicker / Combobox는 대표 Story에서 `controlled={true}`로 사용한다. 부모가 각 `value`를 소유하고 초기화 시 `setDate(undefined)` / `setCategory(undefined)`로 비우므로, 선택값 초기화 목적의 `key` remount는 필요 없다. 검색·상태 등 나머지 조회 조건도 부모가 함께 초기화한다. FilterBar 공개 API와 초기화 / 조회 Outline 버튼 세트는 유지한다. 외부 값 초기화는 자식 `onValueChange`를 호출하지 않으며, 팝업 닫기·검색어·달력 월 초기화를 보장하지 않는다.
|
||||
|
||||
### 6.1 조회 버튼으로 적용하는 소비 예제
|
||||
|
||||
`draft`는 입력·선택 중인 조건, `applied`는 실제 목록에 적용한 조건이다. 아래 예는 조회 클릭에만 `draft`를 복사하고, 초기화 클릭에는 두 상태와 페이지를 함께 초기화해 전체 목록을 즉시 복원한다. Pattern의 강제 동작이 아닌 소비 정책이다. Theme/Tailwind CSS는 [React 연결 순서](../guidelines/runtime-portability.md#2-실제-hm-계약과-파일-경로)에 따라 앱 entry에서 로드한다.
|
||||
|
||||
```tsx
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
Button, DataTable, FilterBar, FilterBarActions, FilterBarSearch,
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue, useDataTable,
|
||||
type DataTableColumnDef,
|
||||
} from "@hm-design/ui"
|
||||
|
||||
type User = { id: string; name: string; status: "active" | "inactive" }
|
||||
type Conditions = { query: string; status: string }
|
||||
const empty: Conditions = { query: "", status: "all" }
|
||||
const rows: User[] = [
|
||||
{ id: "U-1", name: "김한맥", status: "active" },
|
||||
{ id: "U-2", name: "이한맥", status: "inactive" },
|
||||
]
|
||||
const columns: DataTableColumnDef<User>[] = [
|
||||
{ accessorKey: "name", header: "이름" },
|
||||
{ accessorKey: "status", header: "상태", cell: ({ row }) => row.original.status === "active" ? "활성" : "비활성" },
|
||||
]
|
||||
|
||||
export function QueryList() {
|
||||
const [draft, setDraft] = useState<Conditions>({ ...empty })
|
||||
const [applied, setApplied] = useState<Conditions>({ ...empty })
|
||||
const data = useMemo(() => rows.filter(row =>
|
||||
row.name.toLocaleLowerCase().includes(applied.query.trim().toLocaleLowerCase()) &&
|
||||
(applied.status === "all" || row.status === applied.status)
|
||||
), [applied])
|
||||
const table = useDataTable({ columns, data, getRowId: row => row.id })
|
||||
const apply = () => { setApplied({ ...draft }); table.setPageIndex(0) }
|
||||
const reset = () => {
|
||||
setDraft({ ...empty })
|
||||
setApplied({ ...empty })
|
||||
table.setPageIndex(0)
|
||||
}
|
||||
return <>
|
||||
<FilterBar>
|
||||
<FilterBarSearch value={draft.query} onValueChange={query => setDraft(s => ({ ...s, query }))} placeholder="이름 검색" />
|
||||
<Select value={draft.status} onValueChange={status => setDraft(s => ({ ...s, status }))}>
|
||||
<SelectTrigger size="medium" aria-label="사용자 상태"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체 상태</SelectItem>
|
||||
<SelectItem value="active">활성</SelectItem>
|
||||
<SelectItem value="inactive">비활성</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FilterBarActions>
|
||||
<Button type="button" variant="cancel" appearance="outline" size="default" onClick={reset}>초기화</Button>
|
||||
<Button type="button" variant="primary" appearance="outline" size="default" onClick={apply}>조회</Button>
|
||||
</FilterBarActions>
|
||||
</FilterBar>
|
||||
<p role="status">조회 결과 {data.length}건</p>
|
||||
<DataTable table={table} emptyMessage="조건에 맞는 사용자가 없습니다." />
|
||||
</>
|
||||
}
|
||||
```
|
||||
|
||||
이 예의 사용자 상태는 Dashboard 업무 상태와 다른 도메인이다. `data`/`columns` 참조를 안정적으로 유지하고 적용 조건으로 만든 결과만 DataTable에 전달한다. table 내부 global/column 필터를 사용하는 방식이면 조회 callback에서 해당 필터를 갱신하고 초기화 callback에서 `resetGlobalFilter(true) / resetColumnFilters(true)`도 호출한다. 서버 조회라면 `applied`를 요청 조건으로 사용하고 결과·loading·error는 소비자가 관리한다. 코드·Props는 공개 소스와 대조했으며 이 문서 작업에서 실행 검증은 하지 않았다.
|
||||
|
||||
## 7. 조합 컴포넌트
|
||||
|
||||
Input, Select, Combobox, Date Picker, Button(그 외 Popover 등 자유). 각 컨트롤의 모양·상태·접근성은 해당 컴포넌트가 소유한다.
|
||||
|
||||
## 8. Styling / Token
|
||||
|
||||
레이아웃만: `packages/ui/src/theme/filter-bar.css`의 `--hm-filter-bar-*`(gap 8px, control 폭 180px, search 폭 280px, 아이콘 크기·위치·색). 검색 텍스트 시작점은 아이콘 inset 12px + 크기 16px + gap 8px이며, Input의 Focus / Error / Readonly 상태에서도 같은 위치를 유지한다(Input 자체 CSS는 변경하지 않음). 직접 자식인 Select / Combobox / Date Picker trigger와 Input에 컨트롤 폭을 준다. 640px 이하: search는 한 줄 전체, 컨트롤은 한 줄에 2개씩, actions는 한 줄에 우측 정렬. spacing 전역 scale이 없어 값은 Pattern 내부 값이다.
|
||||
|
||||
## 9. Storybook
|
||||
|
||||
`Patterns/Data/Filter Bar` → `Filter Bar`: 검색, 상태 Select, 분류 Combobox, 날짜 Date Picker, 초기화 / 조회, 적용된 조건 수.
|
||||
|
||||
대표 Story의 초기화 / 조회는 목록의 반복 조회·보조 작업 세트다. [Button 사용 위치와 버튼 세트 규칙](../components/button.md#5-사용-위치와-버튼-세트)에 따라 둘 다 `appearance="outline"`, `size="default"`를 사용하고 역할만 `cancel` / `primary`로 구분한다. FilterBar는 Button appearance를 강제하지 않으며 소비자가 같은 세트의 appearance와 size를 맞춘다. 대표 Story의 조회는 실행 callback이 없는 조합 예시이고, 적용된 조건 수는 입력 상태에 즉시 반영된다.
|
||||
|
||||
## 10. Accessibility
|
||||
|
||||
검색은 `type="search"` + `aria-label`, 아이콘은 `aria-hidden`. 컨트롤마다 `aria-label`을 준다(Story 기준). Reset·Clear는 `button`. wrapper는 시맨틱 변경 없음. 브라우저 기본 검색 지우기 버튼은 숨기고 자체 버튼을 쓴다.
|
||||
|
||||
## 11. Responsive
|
||||
|
||||
390px: search 전체 폭, 상태 / 분류 한 줄, 날짜 한 줄, actions 우측 정렬. 페이지 가로 overflow 없음. 모바일 Filter Drawer는 만들지 않았다.
|
||||
|
||||
## 12. 보류 / 비범위
|
||||
|
||||
active filter chip·필터 수 배지 시스템, schema-driven 필터, URL / localStorage 연동, 고급 필터 Popover, 날짜 범위.
|
||||
|
||||
**Legacy 호환 범위**: DatePicker / Combobox에서 `controlled`를 생략하면 기존처럼 `value={undefined}`를 Uncontrolled로 취급하므로, 외부 undefined 초기화 시 내부 값이 다시 표시될 수 있다. 기존 `key` remount 우회는 이 Legacy 방식에서 사용할 수 있으나, 대표 Story는 명시적 Controlled 모드로 전환하고 해당 우회를 제거했다. Combobox의 `""`는 유효한 옵션 값이므로 일반적인 선택 없음 계약으로 대체하지 않는다.
|
||||
|
||||
## 13. 관련 파일 / 검증
|
||||
|
||||
- 2026-10-08: 대표 Story 소스를 격리 Headless Edge 환경에서 실행해 검색·분류·날짜 초기화, 적용 조건 수 0, 두 Trigger DOM 동일성 유지를 확인했다. 선택값 초기화 목적의 remount가 없다. 영향 범위 타입 검사 통과. 공용 dist / Storybook 빌드 및 전체 시각 회귀는 수행하지 않았다.
|
||||
|
||||
```
|
||||
packages/ui/src/patterns/filter-bar/{filter-bar.tsx,index.ts}
|
||||
packages/ui/src/theme/filter-bar.css
|
||||
src/patterns/filter-bar.stories.tsx
|
||||
```
|
||||
|
||||
- `npm run typecheck`, `npm run build` 통과(2026-10-03).
|
||||
- Browser 확인(`With Filter Bar`, 47행): 검색 "설계" → 10행, + 상태 "대기" → 2행, + 날짜 10/11 → 1행, 검색 "zzzz" → 빈 결과(문구, `colSpan`), 초기화 → 47행 복원. 390px에서 위 배치, overflow 없음.
|
||||
@@ -0,0 +1,283 @@
|
||||
# GNB (Pattern)
|
||||
|
||||
> **SINGLE NAVIGATION MODE.** 독립 GNB / LNB / Dashboard Mode는 함께 사용하지 않는다. 2026-10-08 확정: GNB ONLY는 기존 LNB 부품을 재사용하는 내부 Sidebar를 포함할 수 있다(§15). [공통 정책](../guidelines/navigation-single-mode.md)이 이전 조합 기록보다 우선한다.
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6.10 Pattern 계층, §6.11 Layout 단계 기준)를 따른다.
|
||||
|
||||
```text
|
||||
Layout visual reference: 외부 ui_viz Reference (ZIP 미포함) (Patterns/GNB, Variant B Top-header)
|
||||
Implementation: current @hm-design/ui primitives / patterns
|
||||
```
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (2026-10-03 ui-viz 시각 재정합) |
|
||||
| 기반 | HM primitives 조합 (새 dependency 없음) |
|
||||
| 대표 Story | `Patterns/Layout/GNB` → `GNB` (verification: Default, Navigation Menu Open, User Menu Open, Long Labels, With Logo, Utilities, Mobile, Mobile Menu Open) |
|
||||
| 이력 | 첫 구현은 밝은 surface 64px였다. Layout은 ui-viz 시각 우선이라는 결정(지침 §6.11)으로 green 70px로 재정합했다 |
|
||||
| 보류 | §13 |
|
||||
|
||||
## 2. 목적 / 사용 범위
|
||||
|
||||
서비스 전역 상단 바(브랜드, 주 메뉴, 사용자). 새 Navigation/Menu primitive가 아니라 기존 primitive의 조합이다. 메뉴 데이터는 소비자가 children으로 그린다.
|
||||
|
||||
| | 역할 |
|
||||
|---|---|
|
||||
| GNB | 전역 영역·서비스 이동 |
|
||||
| LNB | 현재 영역 내부의 세부 navigation (`lnb.md`) |
|
||||
| Dashboard | 별도의 Dashboard Navigation Mode. GNB와 결합하지 않는다 |
|
||||
|
||||
ui-viz에서 GNB(Variant B, top-header)와 LNB(Variant A, sidebar)는 서로 대체하는 별개 Layout이다. 독립 Navigation Mode의 혼합은 금지한다. GNB ONLY의 내부 Sidebar에서 LNB 구현 부품을 재사용하는 것은 별도 LNB Mode 추가가 아니다(§15).
|
||||
|
||||
## 3. 구성 요소
|
||||
|
||||
| export | 역할 |
|
||||
|---|---|
|
||||
| `GNB` | `<header>` 바. `sticky` prop |
|
||||
| `GNBBrand` | 브랜드 링크(`<a>`, `asChild` 가능). 로고(`img`/`svg`, 있을 때만) + `<span>`(`<small>` eyebrow, `<strong>` 이름) |
|
||||
| `GNBNav` | 주 메뉴 영역(desktop 전용). 안에 `NavigationMenu viewport={false}` |
|
||||
| `GNBUtilities` | 오른쪽 영역 |
|
||||
| `GNBUser` | 사용자 trigger(Avatar + 이름/역할, 글자는 mobile에서 숨김). `DropdownMenuTrigger asChild`로 사용 |
|
||||
| `GNBMobileMenu` / `GNBMobileLink` / `GNBMobileGroup` | mobile 햄버거 + Sheet + 링크/그룹 |
|
||||
|
||||
검색·알림은 컴포넌트가 아니다. ghost Button + Tooltip + Badge + Popover(+Command) 조합을 `GNBUtilities`에 둔다(Story `Utilities`).
|
||||
|
||||
## 4. 구조
|
||||
|
||||
```tsx
|
||||
<GNB sticky>
|
||||
<GNBMobileMenu title="메뉴">…<GNBMobileLink href active>…</GNBMobileLink></GNBMobileMenu>
|
||||
<GNBBrand href="/"><span><small>한맥 업무 시스템</small><strong>프로젝트 대시보드</strong></span></GNBBrand>
|
||||
<GNBNav>
|
||||
<NavigationMenu viewport={false} aria-label="주 메뉴">
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem><NavigationMenuLink href active>대시보드</NavigationMenuLink></NavigationMenuItem>
|
||||
<NavigationMenuItem value="projects">
|
||||
<NavigationMenuTrigger aria-current="page">프로젝트</NavigationMenuTrigger>
|
||||
<NavigationMenuContent><ul><li><NavigationMenuLink href>…</NavigationMenuLink></li></ul></NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</GNBNav>
|
||||
<GNBUtilities>
|
||||
<DropdownMenu><DropdownMenuTrigger asChild><GNBUser name description><Avatar/></GNBUser></DropdownMenuTrigger>…</DropdownMenu>
|
||||
</GNBUtilities>
|
||||
</GNB>
|
||||
```
|
||||
|
||||
`viewport={false}`가 필수다: 2Depth 패널은 각 trigger 아래에서 content-width(`max-content`)로 열리며 짧은 패널도 trigger 폭 이상을 유지한다. trigger 중심에 정렬하고 viewport 가장자리에서는 clamp한다. 긴 라벨은 한 줄을 유지하며 최대 가용 폭을 넘으면 패널 안에서 가로 스크롤한다(공유 viewport는 메뉴 전체 아래에 중앙 정렬된다).
|
||||
|
||||
## 5. ui-viz 비교 (2026-10-03, 원본: `dashboard-composition.md` §19, `top-header-specimen.css/jsx`)
|
||||
|
||||
> HISTORICAL_RECORD: 아래 표는 2026-10-03 비교 기록이다. 현재 패널 폭·정렬은 §4, Current/Open 우선순위는 §7을 따른다.
|
||||
|
||||
| 항목 | ui-viz | 기존 HM(첫 구현) | 최종 HM | 분류 |
|
||||
|---|---|---|---|---|
|
||||
| 높이 | Header 70 (위 4 + 메뉴 면 66) | 64 | 70 (`--hm-gnb-height`) | MATCHED |
|
||||
| 배경 | 세로 4-stop `#1F8265/#165C47/#114838/#08241C` 0/30/65/100%, 높이 82px(70+12), 반복 없음 | 흰 surface | 같은 stop(`--hm-green-tone-1~4`), 82px 단일 gradient | MATCHED |
|
||||
| 콘텐츠 접점 | 12px, 콘텐츠 상단 radius 12, 뒤로 gradient 연속 | 없음 | `::after` 12px strip(`--hm-gnb-join`). 콘텐츠가 positioned·뒤에 오고 radius를 가져야 한다 | IMPLEMENTATION_CHANGE |
|
||||
| 로고 | 있을 때만, 가로(로고 왼쪽·이름 오른쪽), 없으면 영역·간격 없음 | 이름만 | 같음. eyebrow(11/700, `#BDDFD4`) + 이름(16/700), 1px 글자 테두리 | MATCHED |
|
||||
| 메뉴 위치 | grid 좌우 같은 폭 → header 전체 가운데 | brand 옆 왼쪽 | `minmax(0,1fr) auto minmax(0,1fr)` grid, 중앙 오차 0 | MATCHED |
|
||||
| 메뉴 항목 | 위 4·높이 66, 최소 112, padding 0 16, radius 4 4 0 0, White 700 16, 아이콘 없음, 글자 테두리 없음 | 34px text button | 같음 (Navigation Menu를 GNB 범위 안에서만 재색칠) | MATCHED |
|
||||
| hover | Black 10% + White | `--accent` | Black 10% + White | MATCHED |
|
||||
| current / selected shape | Beige(`bg.content`) + `#123328`, 위 radius 4, 아래 좌우 12px 오목 곡선, hover가 덮지 않음 | 연한 초록 면 | 같음 (`data-active`/`aria-current` + `::before/::after` radial) | MATCHED |
|
||||
| 열림(open) | White 면 + `#123328`, 곡선 숨김 | `--accent` | 같음 (`data-state=open`). current와 구분 | MATCHED |
|
||||
| 2뎁스 패널 | White, 부모 폭, 항목 56/16/16·24·700 가운데, hover·현재 Beige, 하단 radius 8, shadow `0 6px 16px .1` | 320px floating | 같음 | MATCHED |
|
||||
| utility | 원형 버튼·avatar 제거(2026-10-01 결정). 이름·역할만 | 검색·알림·Avatar | 기본은 이름·역할만. 검색·알림은 선택 조합(흰 ghost Button) | MATCHED / 선택 조합 |
|
||||
| 사용자 영역 | 이름 16/700 + 역할 12 `#BDDFD4`, 오른쪽 끝 (ui-viz는 avatar 없음) | Avatar + 글자 | HM Avatar(default 32px) + 글자 + Dropdown Menu. mobile은 Avatar만 | INTENTIONAL_CHANGE (2026-10-03 사용자 지시로 Avatar 추가) |
|
||||
| desktop 구조 | Brand / Menu / Profile | 같음 | 같음 | MATCHED |
|
||||
| mobile 구조 | 없음(≤1100px/48px은 OUT_OF_SCOPE) | 햄버거 + Sheet | 유지. breakpoint 1024px | INTENTIONAL_CHANGE (ui-viz에 기준 없음) |
|
||||
| 2뎁스 열기 | hover/click/ArrowDown, Escape + focus 복귀 | Radix | Radix Navigation Menu(hover delay, 키보드 model) | IMPLEMENTATION_CHANGE |
|
||||
|
||||
## 6. Public API
|
||||
|
||||
- `GNB`: `header` props + `sticky`(기본 false). `data-slot="gnb"`, sticky면 `data-sticky`.
|
||||
- `GNBUser`: `button` props + `name`(필수), `description`, children = Avatar(default size, 이미지 실패 시 `AvatarFallback`). `aria-label` 기본 `"<name> 사용자 메뉴"`. `data-slot`은 spread 뒤에 둬서 `DropdownMenuTrigger asChild`가 덮지 않는다.
|
||||
- `GNBMobileMenu`: 햄버거(`lg:hidden`)와 Sheet. `title`, `side`, `open`/`defaultOpen`/`onOpenChange`. `GNBMobileLink`는 router `preventDefault`가 있어도 닫히도록 context로 직접 닫는다(Radix Close는 prevented click을 건너뛴다).
|
||||
|
||||
## 7. Navigation 원칙
|
||||
|
||||
- 주 메뉴는 HM Navigation Menu다. `<nav>` landmark는 Navigation Menu가 렌더하므로 `GNBNav`는 role을 만들지 않는다. Menubar는 쓰지 않는다.
|
||||
- 현재 위치: Link는 `active`(`data-active` + `aria-current="page"`), 하위가 있는 Trigger는 소비자가 `aria-current="page"`를 준다. 하위 링크의 `active`는 패널 안 Beige.
|
||||
- 상태 구분: Hover = Black 10%, Current = Beige + Primary, Open = White + Primary(패널과 이어짐). Current + Hover는 Current, Open + Hover는 Open을 유지하며 Current + Open에서는 Open visual이 우선한다. Focus는 GNB의 contextual navigation outline을 유지한다. Radix의 `data-state`, `data-active`, `aria-current`로 시각만 맞춘다.
|
||||
- Navigation Menu primitive는 수정하지 않았다. 모든 재색칠은 `[data-slot="gnb"] …` 안에서만 일어나며 Navigation Menu Story는 변하지 않는다.
|
||||
|
||||
## 8. Desktop / Mobile
|
||||
|
||||
**AI 구현 필수**: `GNBNav`는 모바일에서 숨겨질 뿐 모바일 메뉴를 자동 생성하지 않는다. 최초 구현부터 Desktop `GNBNav`와 Mobile `GNBMobileMenu`를 함께 넣고 같은 메뉴 데이터·현재 경로를 사용한다. `GNBMobileMenu`가 햄버거와 Sheet를 모두 제공하므로 별도 메뉴 버튼이나 Sheet를 추가하지 않는다. 기본 GNB는 `GNBMobileLink / GNBMobileGroup`을 사용한다. 링크 선택은 router의 `preventDefault()` 여부와 관계없이 Sheet를 닫으며, 닫기 버튼·Escape·focus trap·메뉴 버튼으로의 포커스 복귀는 기존 Sheet/Radix 동작을 유지한다.
|
||||
|
||||
Reference는 [대표 Story 소스](../../src/patterns/gnb.stories.tsx)의 `All`(`Patterns/Layout/GNB → GNB`, `patterns-layout-gnb--all`) 안 Overview·Usage · GNB Sidebar를 좁은 폭에서 확인한다. `Mobile / Mobile Menu Open`은 숨김 verification이며 동작 검사에만 사용한다. Sidebar를 포함하면 [§15의 공개 API 소비 예제](#15-gnb-only-sidebar-composition-2026-10-08)를 따른다. LNB 모바일 링크 재사용 시 자동 닫기 Context가 다르므로 controlled Sheet를 route handler에서 닫고 Desktop 복귀 시에도 닫는다. 이는 GNB ONLY 내부 Composition이며 별도 LNB ONLY 모드가 아니다.
|
||||
|
||||
- Desktop(1024px 이상): 위 표. 높이 70, 좌우 28, column gap 24.
|
||||
- Mobile(1023px 이하): 높이 56, padding 16. 주 메뉴·eyebrow·사용자 글자를 숨기고 햄버거(흰 ghost Button, `aria-label`)를 맨 앞에, 사용자는 같은 Avatar 하나만 보인다(중복 없음). 햄버거 → HM Sheet(left): 제목, 링크(44px), 그룹 제목. Radix가 focus trap·Escape·focus 복귀를 맡는다. breakpoint를 768에서 1024로 올린 이유: 메뉴 4개(항목 최소 112) 가운데 정렬은 그 이하에서 brand/profile이 너무 좁아진다. ui-viz에는 반응형 기준이 없다.
|
||||
- 중첩 그룹(2026-10-06): `GNBMobileGroup` 안에 `GNBMobileGroup`을 두면 한 단계 들여쓴다(`--hm-gnb-mobile-nested-indent` 12 = 링크 padding, 위 `--hm-gnb-mobile-nested-gap` 8). GNB 자체의 깊은 메뉴를 Sheet에 같은 트리로 보이기 위한 CSS만 추가했고 API·동작은 그대로다. 접기/펼치기와 상위 그룹 current 표시는 없다(모든 단계가 펼쳐진 목록, 현재 위치는 링크의 `aria-current`). 글자는 GNB Sheet 계약 14/20/500(current 700) 그대로다. LNB 모바일 표현은 LNB 전용 API를 따른다.
|
||||
|
||||
## 9. Styling / Token
|
||||
|
||||
`packages/ui/src/theme/gnb.css`의 `--hm-gnb-*`.
|
||||
|
||||
| Token | 값 |
|
||||
|---|---|
|
||||
| `--hm-gnb-height` | 70px (mobile 56px). Layout이 top offset으로 읽는다 |
|
||||
| `--hm-gnb-join` | 12px. 콘텐츠 접점(바 아래로 gradient가 이어지는 길이) |
|
||||
| `--hm-gnb-background` | 세로 4-stop gradient(`--hm-green-tone-1~4`) |
|
||||
| `--hm-gnb-padding-x` / `--hm-gnb-column-gap` | 28 / 24 (mobile 16) |
|
||||
| `--hm-gnb-z-index` | 40 (sticky일 때만; overlay는 portal z-50) |
|
||||
| menu | inset-top 4, join 12, min-width 112, padding-x 16, 16/700 |
|
||||
| current / open / panel | Beige(`--hm-sem-bg-content`)+`--hm-sem-action-primary` / White / White, 하단 radius 8, shadow |
|
||||
|
||||
새 Primitive/Semantic은 GNB에서 추가하지 않았다. 4-stop은 공개 `--hm-sem-action-surface-stop-*`, 글자 보조색은 `--hm-sem-palette-green-tint`를 참조한다. 하위 패널 radius 8·shadow, 글자 테두리는 ui-viz가 검토값으로 둔 값을 그대로 가져왔다.
|
||||
|
||||
## 10. Position / z-index / Layout 연결
|
||||
|
||||
- 기본 relative(일반 흐름), `sticky` 옵션, fixed 없음. GNB Mode 페이지가 sticky를 쓸지 결정한다.
|
||||
- 접점은 일반/sticky 모두 같은 12px gradient를 콘텐츠의 둥근 윗모서리 바깥에만 mask로 그린다. sticky stacking context에서도 콘텐츠 중앙을 초록색 띠로 덮지 않는다. 높이·gradient·메뉴·프로필·색상은 공통이고 sticky selector는 position/top/z-index만 바꾼다.
|
||||
- `Sticky` verification Story와 대표 Story의 Sticky 예시는 같은 스크롤 영역을 사용한다. 테두리와 스크롤바 폭을 제거해 일반 예시와 폭을 맞추며, 휠/터치와 포커스 후 PageDown/End/Home으로 스크롤할 수 있다.
|
||||
- z-index: GNB 40 < portal overlay 50. 하위 패널은 GNB 안(z 2~3)에서 열려 콘텐츠 위에 그려진다. 체계는 바꾸지 않았다.
|
||||
- GNB Mode 콘텐츠 계약: 바 다음 콘텐츠 면은 `position: relative`, DOM상 뒤, 윗모서리 `border-radius: var(--hm-gnb-join) var(--hm-gnb-join) 0 0`, 시작 y = `var(--hm-gnb-height)`(접점 strip과 겹침, 밀어내지 않음). 높이는 `--hm-gnb-height`로 읽는다.
|
||||
|
||||
## 11. Accessibility / 검증
|
||||
|
||||
- `<header>` + Navigation Menu `<nav aria-label>`, mobile `<nav aria-label>`, 그룹 `role="group"`. 아이콘 버튼 `aria-label`, Badge `aria-hidden`.
|
||||
- Focus: 흰 outline(초록 위), current/open은 초록 outline.
|
||||
- 확인(2026-10-03, Storybook): 1280 데스크톱 높이 70, 메뉴 항목 위 4·높이 66·최소 112, 메뉴 중앙 오차 0, current Beige `#F5F0E5`/`#123328`/700, 열림 White 패널·긴 하위 이름 줄바꿈. 1024/1023 전환, 390 햄버거→Sheet 열림, Escape·링크 클릭 모두 닫힘 + 햄버거로 focus 복귀(녹색 재정합 후 재확인), 가로 overflow 없음(1024, 1023, 390, long labels). `npm run typecheck`, `npm run build` 통과. Navigation Menu Story computed style 변화 없음.
|
||||
|
||||
## 12. 변경하지 않은 것
|
||||
|
||||
Navigation Menu, Dropdown Menu, Sheet, Avatar, Button, Tooltip, Badge, Popover, Command 코드·theme CSS는 수정하지 않았다.
|
||||
|
||||
## 13. 남은 차이 / 보류
|
||||
|
||||
| 항목 | 분류 |
|
||||
|---|---|
|
||||
| ui-viz 후보 1뎁스 18/28·폭 160·padding 24 (ui-viz도 FOLLOW_UP) | FOLLOW_UP. 현재 2Depth 패널은 고정 160px이 아니라 §4의 content-width/center/clamp |
|
||||
| 하위 패널 radius 8·shadow, 글자 테두리 (ui-viz REVIEW VALUE) | 값 그대로 적용 |
|
||||
| 독립 GNB + LNB 또는 GNB + Dashboard | 사용 금지. GNB ONLY 내부 Sidebar 부품 재사용은 §15 범위에서 허용 |
|
||||
| sticky일 때 접점 strip 처리, 콘텐츠 radius 소유 | 접점 mask 정합 완료(2026-10-06). 콘텐츠 radius는 소비 Layout 소유 |
|
||||
| 긴 메뉴명이 공간을 넘으면 메뉴 영역에서 잘림 | 한계 |
|
||||
| 로고 asset 없음(테스트 이미지만), 로고 크기·간격은 ui-viz D-070 미확정 | MISSING |
|
||||
| 실기기 터치, 1100~1280 사이 시각 | 미확인 |
|
||||
| GNB Sheet 그룹 접기·상위 current 표시 | 미지원. 현재 링크 목록 방식 유지 |
|
||||
| Notification Center, 전역 검색 결과, Organization/App Switcher | 후속 |
|
||||
|
||||
## 14. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/patterns/gnb/{gnb.tsx,index.ts}
|
||||
packages/ui/src/theme/gnb.css
|
||||
src/patterns/gnb.stories.tsx
|
||||
```
|
||||
|
||||
## 15. GNB ONLY Sidebar Composition (2026-10-08)
|
||||
|
||||
대표 `Patterns/Layout/GNB → GNB`의 **Usage · GNB Sidebar**에서 확인한다. 직접 동작 검증은 숨김 `GNB Sidebar` Story(`patterns-layout-gnb--sidebar`)를 사용한다. 기존 `LNB → VERIFICATION ONLY · GNB + LNB`는 운영 기준으로 승격하지 않는다.
|
||||
|
||||
구조는 **GNB Header → Sidebar + Main Content**다. Sidebar는 공개 `LNB / LNBNav / LNBGroup / LNBItem`을 그대로 사용한다. 기존 DOM/data-slot을 유지하고 브랜드·사용자 영역은 GNB만 담당한다. `LNBBrand / LNBProfile`은 기본 조합에서 생략한다. 새로운 GNBSidebar 공개 컴포넌트·메뉴 로직은 없다.
|
||||
|
||||
공용 `@hm-design/ui/theme.css`의 opt-in HTML attribute 계약:
|
||||
|
||||
- 프레임: `data-hm-gnb-layout="sidebar"`. 소비자가 높이를 지정하며 Grid가 GNB의 자연 높이와 남은 본문 높이를 배분한다.
|
||||
- 본문: 프레임의 직계 `data-hm-gnb-body`. Desktop은 기존 `--hm-lnb-width` 열 + 가용 콘텐츠 열, Mobile은 콘텐츠 한 열이다.
|
||||
- Sidebar: 본문의 직계 `<LNB data-hm-gnb-sidebar>`. 기존 `data-slot="lnb"`는 유지한다. 이 범위의 `--hm-lnb-background`만 `var(--hm-sem-palette-green-light-2)`로 덮어쓴다. 기본 Green Light 2는 `#266651`이며 Runtime Theme override도 그대로 따른다. 측면 접점 배경은 LNB의 기존 `--hm-lnb-background-end`를 유지한다.
|
||||
- Main: 본문의 직계 `<main data-hm-gnb-main>`. 공용 CSS가 면·접점·스크롤을 제공하며 콘텐츠 padding은 소비자가 정한다. GNB의 상단 접점과 LNB의 측면 접점은 기존 값을 재사용한다.
|
||||
|
||||
메뉴 폭·폰트·높이·padding·gap·아이콘·Depth·Hover·선택·연결선·스크롤은 LNB 그대로다. 반투명 Hover/Open 면의 합성 결과만 배경 변경에 따라 달라질 수 있으며 별도 보정하지 않는다. 그룹은 기존 Collapsible의 `open / onOpenChange`를 사용하고 GNB NavigationMenu 열림과 독립적으로 관리한다. 선택 시 새 자동 펼침을 추가하지 않는다.
|
||||
|
||||
Desktop은 ≥1024px, Mobile은 ≤1023px이다. Mobile에서 Sidebar는 기존 LNB CSS로 숨기고 **GNBMobileMenu 하나**에 `LNBMobileGroup / LNBMobileLink` 목록을 넣는다. `LNBMobileHeader`나 `LNBMobileMenu`를 함께 렌더하지 않는다. 모바일 Sheet 기본 면, 44px 링크, 14/20/500(current 700), 중첩 들여쓰기, Escape·포커스 복귀를 유지한다.
|
||||
|
||||
`LNBMobileLink`의 자동 닫기 Context는 LNBMobileMenu 전용이다. GNBMobileMenu 안에서 재사용할 때는 아래처럼 Sheet를 controlled로 연결하고 동일 route handler에서 닫는다. router가 `preventDefault()`를 호출해도 닫혀야 한다. Desktop 복귀 시 Sheet 상태를 닫는 처리도 소비자 Composition에 둔다. 모바일 그룹은 기존 펼쳐진 링크 목록이며 데스크톱 Collapsible을 복제하지 않는다.
|
||||
|
||||
### 소비자 사용 예제
|
||||
|
||||
아래는 실제 공개 API와 공용 Theme만 사용하는 React 예제다. 메뉴 데이터·현재 경로·그룹 상태는 소비자 소유이며 Story 파일을 import할 필요가 없다. 실제 앱에서는 `go`의 상태 갱신을 router 이동과 현재 경로 읽기로 연결한다.
|
||||
|
||||
```tsx
|
||||
import { useEffect, useState, type MouseEvent, type ReactNode } from "react"
|
||||
import {
|
||||
GNB, GNBBrand, GNBNav, GNBUtilities, GNBUser, GNBMobileMenu,
|
||||
LNB, LNBNav, LNBGroup, LNBItem, LNBMobileGroup, LNBMobileLink,
|
||||
NavigationMenu, NavigationMenuList, NavigationMenuItem,
|
||||
NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink,
|
||||
Avatar, AvatarFallback, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem,
|
||||
} from "@hm-design/ui"
|
||||
import "@hm-design/ui/theme.css"
|
||||
|
||||
type Node = { id: string; label: string; children?: Node[] }
|
||||
const tree: Node[] = [
|
||||
{ id: "overview", label: "대시보드" },
|
||||
{ id: "projects", label: "프로젝트 관리", children: [
|
||||
{ id: "projects/design", label: "디자인 관리", children: [
|
||||
{ id: "projects/design/tokens", label: "토큰 관리" },
|
||||
{ id: "projects/design/components", label: "컴포넌트 관리" },
|
||||
] },
|
||||
{ id: "projects/tasks", label: "업무 목록" },
|
||||
] },
|
||||
]
|
||||
|
||||
export function Example() {
|
||||
const [path, setPath] = useState("projects/design/tokens")
|
||||
const [groups, setGroups] = useState<Record<string, boolean>>({ projects: true, "projects/design": true })
|
||||
const [sheetOpen, setSheetOpen] = useState(false)
|
||||
useEffect(() => {
|
||||
const desktop = window.matchMedia("(min-width: 1024px)")
|
||||
const sync = () => { if (desktop.matches) setSheetOpen(false) }
|
||||
desktop.addEventListener("change", sync)
|
||||
return () => desktop.removeEventListener("change", sync)
|
||||
}, [])
|
||||
const onPath = (id: string) => path === id || path.startsWith(`${id}/`)
|
||||
const go = (id: string) => (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
setPath(id)
|
||||
setSheetOpen(false)
|
||||
}
|
||||
const desktopMenu = (nodes: Node[]): ReactNode => nodes.map(node => node.children ? (
|
||||
<LNBGroup key={node.id} label={node.label} current={onPath(node.id)}
|
||||
open={groups[node.id] ?? false} onOpenChange={open => setGroups(s => ({ ...s, [node.id]: open }))}>
|
||||
{desktopMenu(node.children)}
|
||||
</LNBGroup>
|
||||
) : <LNBItem key={node.id} href={`#${node.id}`} active={path === node.id} onClick={go(node.id)}>{node.label}</LNBItem>)
|
||||
const mobileMenu = (nodes: Node[]): ReactNode => nodes.map(node => node.children ? (
|
||||
<LNBMobileGroup key={node.id} label={node.label}>{mobileMenu(node.children)}</LNBMobileGroup>
|
||||
) : <LNBMobileLink key={node.id} href={`#${node.id}`} active={path === node.id} onClick={go(node.id)}>{node.label}</LNBMobileLink>)
|
||||
|
||||
return <div data-hm-gnb-layout="sidebar" style={{ height: "100dvh" }}>
|
||||
<GNB>
|
||||
<GNBMobileMenu title="메뉴" open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||
<div data-slot="lnb-mobile-nav">{mobileMenu(tree)}</div>
|
||||
</GNBMobileMenu>
|
||||
<GNBBrand href="#overview" onClick={go("overview")}><span><strong>프로젝트 관리</strong></span></GNBBrand>
|
||||
<GNBNav>
|
||||
<NavigationMenu viewport={false} aria-label="주 메뉴">
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem><NavigationMenuLink href="#overview" active={path === "overview"} onClick={go("overview")}>대시보드</NavigationMenuLink></NavigationMenuItem>
|
||||
<NavigationMenuItem value="projects">
|
||||
<NavigationMenuTrigger aria-current={onPath("projects") ? "page" : undefined}>프로젝트</NavigationMenuTrigger>
|
||||
<NavigationMenuContent><ul><li>
|
||||
<NavigationMenuLink href="#projects/tasks" active={path === "projects/tasks"} onClick={go("projects/tasks")}>업무 목록</NavigationMenuLink>
|
||||
</li></ul></NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</GNBNav>
|
||||
<GNBUtilities>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<GNBUser name="김한맥" description="프로젝트 관리자"><Avatar><AvatarFallback>김</AvatarFallback></Avatar></GNBUser>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end"><DropdownMenuItem>프로필</DropdownMenuItem></DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</GNBUtilities>
|
||||
</GNB>
|
||||
<div data-hm-gnb-body>
|
||||
<LNB data-hm-gnb-sidebar><LNBNav aria-label="서비스 메뉴">{desktopMenu(tree)}</LNBNav></LNB>
|
||||
<main data-hm-gnb-main><h1>프로젝트 업무</h1><p>현재 경로: {path}</p></main>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
사용자 메뉴와 콘텐츠 padding은 기존 GNB 사용 예제에 따라 소비자가 추가한다. Theme Source, LNB 원본, Runtime 계약, 공개 API는 변경하지 않는다. 공통 요약은 [AGENTS.md](../../AGENTS.md), 탐색 경로는 [Reference Index](../guidelines/component-pattern-reference.md)를 따른다.
|
||||
|
||||
검증(2026-10-08, 기존 Storybook 6024): 1440px Sidebar 272px / Green Light 2, 2·3Depth 선택·그룹 Enter/Space·Hover·선택 경로·메뉴 스크롤 확인. 768px·375px에서 GNB 56px / Sidebar 숨김 / Main 전체 폭 / 흰 Sheet 1개 / 링크 44px·14px / 선택 시 닫기·Escape·포커스 복귀 확인. 열린 Sheet에서 Desktop 복귀 시 닫힘과 선택 경로 보존 확인. 기존 GNB Overview·LNB 대표 메뉴·Dashboard Header computed style 비교 동일, 검사한 화면의 Console error 없음. 앱 TypeScript 검사 통과. 전체 프로젝트 회귀 테스트와 실기기 터치 검증은 수행하지 않았다.
|
||||
@@ -0,0 +1,238 @@
|
||||
# LNB (Pattern)
|
||||
|
||||
> **2026-10-07 확정: SINGLE NAVIGATION MODE.** GNB / LNB / Dashboard는 함께 사용하지 않는다. AI 코드 생성·제안, 신규/기존 페이지, 프로토타입, Storybook, Production 전체에 적용한다. [공통 정책](../guidelines/navigation-single-mode.md)이 이전 조합 기록보다 우선한다.
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6.10, §6.11)를 따른다.
|
||||
|
||||
```text
|
||||
Layout visual reference: 외부 ui_viz Reference (ZIP 미포함) (Patterns/LNB, Variant A Sidebar)
|
||||
Implementation: current @hm-design/ui primitives / patterns (HM Collapsible + plain elements)
|
||||
```
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (2026-10-03), 3뎁스 추가 (2026-10-06) |
|
||||
| 기반 | HM Collapsible + 일반 요소 (새 dependency 없음) |
|
||||
| 대표 Story | `Patterns/Layout/LNB` → `LNB` (verification: Third Level, Third Level Long Labels, Default, Selected, Expanded, Selected Child, Collapsed With Selection, With Logo, Long Labels, Scroll, Third Level Scroll, VERIFICATION ONLY · GNB + LNB, LNB Standalone Desktop, LNB Standalone Mobile, LNB Standalone Mobile Menu Open) |
|
||||
| 보류 | §11 |
|
||||
|
||||
## 2. 목적 / 역할
|
||||
|
||||
LNB Mode 페이지의 독립 Navigation 체계. Desktop 열과 LNB Mobile Header는 같은 메뉴·선택 상태의 Responsive 표현이다. GNB 또는 Dashboard와 조합하지 않는다.
|
||||
|
||||
## 3. 구성 요소와 API
|
||||
|
||||
| export | 역할 |
|
||||
|---|---|
|
||||
| `LNB` | `<aside>` 열. 폭 `--hm-lnb-width` |
|
||||
| `LNBBrand` | 선택적. LNB Mode의 서비스 브랜드를 담당한다(§3c). 세로형(로고 위·서비스명 아래, 같은 시작선). 로고는 `img`/`svg`가 있을 때만. `<span>`에 `<small>` eyebrow + `<strong>` 이름 |
|
||||
| `LNBNav` | 메뉴 영역 `<nav>`(`aria-label` 지정). 길면 이 영역만 스크롤 |
|
||||
| `LNBItem` | 링크(`<a>`, `asChild`). `active` → `aria-current="page"`. 앞의 `svg` 자식 = 아이콘(lucide) |
|
||||
| `LNBGroup` | 2뎁스 그룹: HM `Collapsible`(`open`/`defaultOpen`/`onOpenChange`), `label`, `current`(하위 현재 → 이름·화살표 노랑). 그룹 안에 `LNBGroup`을 중첩하면 3뎁스(§3a) |
|
||||
| `LNBProfile` | 하단 사용자 메뉴 trigger `<button>`: Avatar(children, default size) + 이름·역할 글자. `DropdownMenuTrigger asChild`로 사용 |
|
||||
|
||||
```tsx
|
||||
<LNB>
|
||||
{/* LNB Mode의 서비스 브랜드 */}
|
||||
<LNBBrand><span><small>한맥 업무 시스템</small><strong>프로젝트 대시보드</strong></span></LNBBrand>
|
||||
<LNBNav aria-label="서비스 메뉴">
|
||||
<LNBItem href="#a" active><LayoutGridIcon />메뉴 1</LNBItem>
|
||||
<LNBGroup label="메뉴 2" current><LNBItem href="#b" active>메뉴 2-1</LNBItem></LNBGroup>
|
||||
</LNBNav>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<LNBProfile name="김한맥" description="프로젝트 관리자"><Avatar>…</Avatar></LNBProfile>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="min-w-(--radix-dropdown-menu-trigger-width)">
|
||||
<DropdownMenuLabel>김한맥</DropdownMenuLabel>
|
||||
<DropdownMenuItem>프로필 설정</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">로그아웃</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</LNB>
|
||||
```
|
||||
|
||||
Collapsible의 trigger/content는 `asChild`로 자체 `button`/`div`를 써서 Collapsible의 기본 text-trigger 스타일을 쓰지 않는다(Radix `aria-expanded`/`aria-controls`/Enter/Space는 그대로).
|
||||
|
||||
## 3a. 3뎁스 (2026-10-06)
|
||||
|
||||
현재 interaction은 2Depth에서 들여쓴 링크 면, 3Depth에서 connector 끝부터 시작하는 링크 면에 한정한다. 3Depth connector는 상태와 무관하게 Green Tint 40%이며 장식 행은 pointer interaction을 소유하지 않는다. 아래 비교표는 도입 시점 기록이며 최신 Mapping은 §5를 따른다.
|
||||
|
||||
기준: 3Depth 도입 당시의 디자이너 공유 자료(외부 Reference, ZIP 미포함). 현재 구현 기준과 사용법은 이 문서, `packages/ui/src/patterns/lnb/`와 `src/patterns/lnb.stories.tsx`의 대표 Story를 따른다. ui-viz에는 3뎁스가 없다.
|
||||
|
||||
사용: 기존 `LNBGroup`을 그룹 안에 중첩한다. 새 컴포넌트·`depth` prop·state 없음. 실제 선택 링크 = `active`, 그 경로의 모든 그룹 = `current`(소비자 경로 상태에서 계산). 닫힌 그룹은 내용을 unmount하므로, 바깥 그룹을 접었다 펼 때 안쪽 그룹 열림을 유지하려면 각 그룹 `open`/`onOpenChange`를 소비 화면이 가진다.
|
||||
|
||||
```tsx
|
||||
<LNBGroup label="프로젝트 관리" current open={open.projects} onOpenChange={…}>
|
||||
<LNBGroup label="디자인 관리" current open={open.design} onOpenChange={…}>
|
||||
<LNBItem href="#tokens" active>컬러 토큰</LNBItem>
|
||||
<LNBItem href="#components">컴포넌트 스타일</LNBItem>
|
||||
</LNBGroup>
|
||||
<LNBItem href="#tasks">업무 목록</LNBItem>
|
||||
</LNBGroup>
|
||||
```
|
||||
|
||||
| 항목 | 명세 | 구현 | 분류 |
|
||||
|---|---|---|---|
|
||||
| 글자 | 1·2뎁스 18/28/700, 3뎁스 18/24/500, 선택 700 | 같음 | MATCHED |
|
||||
| 3뎁스 행 | 최소 48, 상하 12, 링크 padding-left 88(2뎁스 68 + 20) | 같음 (1·2뎁스 56 유지) | MATCHED |
|
||||
| 3뎁스 글자 장식 | 외곽선·그림자 없음 | 같음 | MATCHED |
|
||||
| 긴 이름 | 줄바꿈, 행 높이 증가 | 48 / 72 / 96 (1·2·3줄), ellipsis 없음, 가로 overflow 0 | MATCHED |
|
||||
| 선택 | Beige + `--hm-sem-action-primary`, 오른쪽 곡선 | 기존 current 규칙 그대로(폭 +1, 위·아래 오목 곡선) | MATCHED |
|
||||
| 상위 경로 | 1·2뎁스 노랑, 접혀도 유지 | `current` → `--hm-yellow-accent` | MATCHED |
|
||||
| 연결선 | 3뎁스만, 1px, 세로선 x 68, 가로 12, 글자까지 8, 행 세로 중앙, 마지막 행 중앙에서 끝 | 행마다 background 2겹(세로 `1px × 100%`, 마지막 행 `1px × 50%`, 가로 `12 × 1` at `calc(50% - 0.5px)`). DOM·tab stop·aria 없음 | MATCHED / IMPLEMENTATION_CHANGE(명세의 "배경 레이어" 방식) |
|
||||
| 연결선 색 | 기본·hover `--hm-green-tint` 40%, 선택 `--hm-green-light-2` 40% | 같음. 선택 색은 선택된 행의 선에만 적용(나머지 행은 기본색) | MATCHED |
|
||||
| 접힘 | 하위 메뉴·연결선 함께 숨김 | Radix가 내용을 unmount → 선도 같이 사라짐 | MATCHED |
|
||||
| 중첩 그룹 | 배경·테두리·padding 중복, 폭 감소, 오른쪽 틈 금지 | 안쪽 그룹은 면·선·padding-bottom 없음, 폭 100%(바깥 그룹과 같은 252), 선택 항목 오른쪽 273(1px 겹침) | MATCHED |
|
||||
| 안쪽 그룹 이름 위치 | 명세 수치 없음, 이미지상 2뎁스 글자와 같은 시작선 | trigger padding-left = 68 − 화살표 18 − gap 12 = 38 → 글자 x 68 | INTENTIONAL_CHANGE(이미지 기준) |
|
||||
|
||||
모바일(<1024): LNB ONLY는 Desktop 열을 숨기고 같은 메뉴를 독립 `LNBMobileHeader / LNBMobileMenu`로 표시한다(§3b). GNB ONLY 내부 Sidebar는 [GNB §15](gnb.md#15-gnb-only-sidebar-composition-2026-10-08)의 GNB 단일 Sheet를 사용하며 독립 LNB Mobile Header/Menu를 추가하지 않는다. DASHBOARD ONLY 운영 화면에는 Sidebar가 없다([Dashboard 현재 계약](dashboard-layout.md#1-현재-계약)).
|
||||
|
||||
<details>
|
||||
<summary>HISTORICAL RECORD — 이전 GNB Sheet 혼합 안내</summary>
|
||||
|
||||
아래 안내는 이전 구성의 기록이며 현재 운영·신규 화면 Reference가 아니다.
|
||||
|
||||
모바일(<1024): LNB는 그대로 숨긴다(icon-only collapsed 없음, 새 breakpoint 없음). 같은 지역 메뉴 트리를 GNB 모바일 Sheet에 중첩 `GNBMobileGroup`으로 조합한다(`Patterns/Layout/Dashboard Layout` → `Third Level`, `gnb.md` §8). Sheet는 GNB의 시각 언어(14px 링크, Beige current)를 쓰고 LNB의 gradient·곡선·연결선은 가져가지 않는다. 화면에 GNB가 없을 때의 모바일 탐색은 §3b.
|
||||
|
||||
</details>
|
||||
|
||||
## 3b. LNB Mobile Header (2026-10-07)
|
||||
|
||||
| viewport | Desktop LNB | LNB Mobile Header |
|
||||
|---|---|---|
|
||||
| ≥1024px | VISIBLE | HIDDEN |
|
||||
| ≤1023px | HIDDEN | VISIBLE |
|
||||
|
||||
모바일 Header와 Sheet는 LNB의 Responsive 표현이며 GNB가 아니다. 기존 GNB 모바일 부품 조합을 LNB 전용 공개 API로 대체했다. HM Sheet가 focus trap, Escape, focus 복귀를 맡고 링크 선택 시 router가 preventDefault를 사용해도 Sheet를 닫는다.
|
||||
|
||||
| export | 역할 |
|
||||
|---|---|
|
||||
| `LNBMobileHeader` | 모바일 전용 Header, 높이 56px / padding 16px. ≥1024px에서 자체 숨김 |
|
||||
| `LNBMobileBrand` | 서비스명 및 선택적 로고, 링크 또는 `asChild` |
|
||||
| `LNBMobileMenu` | 햄버거 + Sheet. `title`, `side`, `open/defaultOpen/onOpenChange` |
|
||||
| `LNBMobileLink` | Sheet 링크, `active`, `asChild`, 선택 시 닫기 |
|
||||
| `LNBMobileGroup` | 중첩 지역 메뉴 그룹 |
|
||||
|
||||
```tsx
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[var(--hm-lnb-width)_minmax(0,1fr)]">
|
||||
<LNB>
|
||||
<LNBBrand>…</LNBBrand>
|
||||
<LNBNav aria-label="서비스 메뉴">{desktopMenu(tree)}</LNBNav>
|
||||
<LNBProfile name={profile.name}>…</LNBProfile>
|
||||
</LNB>
|
||||
<div>
|
||||
<LNBMobileHeader>
|
||||
<LNBMobileMenu title="서비스 메뉴">
|
||||
{mobileMenu(tree)}
|
||||
<LNBProfile name={profile.name}>…</LNBProfile>
|
||||
</LNBMobileMenu>
|
||||
<LNBMobileBrand href="/">서비스명</LNBMobileBrand>
|
||||
</LNBMobileHeader>
|
||||
<main>…</main>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Desktop/Mobile은 메뉴 트리, 현재 위치, 그룹 상태, Profile 데이터 원본을 공유한다. 모바일 Sheet 링크는 44px 높이, 14/20/500(current 700)을 유지한다. 중첩 그룹은 12px 들여쓴다. Header/Sheet 스타일은 `--hm-lnb-mobile-*`와 공통 Semantic을 사용하며 GNB/Dashboard 변수에 의존하지 않는다.
|
||||
|
||||
## 3c. 브랜드·프로필 소유
|
||||
|
||||
LNB Mode가 Desktop Brand/Profile과 Mobile Header/Sheet Profile을 담당한다. 다른 Navigation에 소유권을 넘기지 않는다. `LNBBrand`는 선택적이며 생략하면 빈 자리 없이 메뉴가 올라간다. 공통 LNB 메뉴 스크롤·geometry는 그대로 유지한다.
|
||||
|
||||
기존 `WithGNB` Story는 `verification` 태그 및 화면과 이름에 **VERIFICATION ONLY**를 표시한 기술 검증 전용이다. 운영·신규 페이지·AI 생성 예제로 사용하지 않는다.
|
||||
|
||||
## 4. ui-viz 비교 (원본: `dashboard-composition.md` §6, `dashboard-variant-specimens.css/jsx`)
|
||||
|
||||
| 항목 | ui-viz | 최종 HM | 분류 |
|
||||
|---|---|---|---|
|
||||
| 폭 | track 272, visual 290(오른쪽 18 bleed) | 열 272(`--hm-lnb-width`), 뒤로 18(`--hm-lnb-join`) 같은 색 연장 | MATCHED (연장은 `::after`) |
|
||||
| 배경 | 90° 5-stop `#123328/#205241/#266651/#249473/#266651` 0/30/50/90/100%, 트랙 폭, 트랙 밖 `#266651` | 같음 (`--hm-green-*`) | MATCHED |
|
||||
| 상단 | 위 28, 브랜드 padding 0 10 22, 메뉴 위 20 | 위 28, 브랜드 inset 30(=20+10), 아래 22, 메뉴 위 20 | MATCHED |
|
||||
| 브랜드 | 로고 조건부, 위 로고·아래 서비스명, eyebrow 11/700, 이름 17 | 같음 | MATCHED |
|
||||
| 항목 | 18/28/700, 최소 56, padding 14 12 14 18, White, 아이콘 20 opacity .5, 글자 테두리 1.5px + 그림자 `1px 2px 1px` | 같음 | MATCHED |
|
||||
| hover | Black 10%, 콘텐츠 경계까지, 시작 쪽 radius 4 | 같음 | MATCHED |
|
||||
| current | Beige + `#123328`, 왼쪽 radius 8, 오른쪽 20+1 연장, 위·아래 18 오목 곡선, 안쪽 깊이 그림자(마지막 25% fade), 글자 효과 없음 | 같음. 깊이는 span 대신 background gradient 3겹으로 근사 | MATCHED / IMPLEMENTATION_CHANGE |
|
||||
| nested(2뎁스) | 상위 = disclosure 버튼, 화살표 접힘 → 펼침 90°, 펼친 그룹 Black 20% + 1px `#266651` 선 + 왼쪽 radius 8, 하위 padding-left 68, 하단 8, 하위 선택 시 상위 노랑 `#FFC600` | 같음. HM Collapsible(Radix) 사용 | MATCHED / IMPLEMENTATION_CHANGE |
|
||||
| divider | Brand/Profile 구분선 없음, 그룹 구분선은 그룹이 둘 이상일 때만(현재 없음) | 없음 | MATCHED |
|
||||
| profile | 이름 16 + 역할 12, avatar 없음, 아래 22 | 위치·글자 같음 + Avatar 32px(gap 12, 세로 중앙) | INTENTIONAL_CHANGE |
|
||||
| 외곽 brand 테두리(`.sidebar::after`) | D-093 예시에 없음, FOLLOW_UP | 그리지 않음 | FOLLOW_UP |
|
||||
| collapsed(열 자체 접힘) | ui-viz에 없음 | 만들지 않음 (`--hm-lnb-collapsed-width` 없음). ui-viz의 "Collapsed"는 그룹 접힘이다 | — |
|
||||
| mobile | ui-viz에 없음 | 열 숨김, `--hm-lnb-width: 0px`. LNB Mobile Header + LNB Sheet로 같은 목적지 표시(§3b) | INTENTIONAL_CHANGE |
|
||||
| 스크롤 | ui-viz는 `overflow:hidden`(넘치면 잘림) | 메뉴 영역만 세로 스크롤(brand/profile 고정) | INTENTIONAL_CHANGE |
|
||||
| 아이콘 | 4종 자체 SVG | lucide 아이콘을 소비자가 자식으로 둠(새 icon system 없음) | IMPLEMENTATION_CHANGE |
|
||||
|
||||
## 5. Token / CSS Variables
|
||||
|
||||
`packages/ui/src/theme/lnb.css`의 `--hm-lnb-*`.
|
||||
|
||||
| Token | 값 |
|
||||
|---|---|
|
||||
| `--hm-lnb-width` | 272px (1023px 이하에서 0px) |
|
||||
| `--hm-lnb-join` | 18px. 콘텐츠 둥근 모서리·bleed |
|
||||
| `--hm-lnb-background` / `-background-end` | 5-stop gradient / `#266651` |
|
||||
| item | min-height 56, 18/28/700, gap 12, 아이콘 20, 글자 테두리 1.5px |
|
||||
| current | `--hm-sem-bg-content` + `--hm-sem-action-primary`, radius 8 0 0 8 |
|
||||
| group | open face Black 20%, 선 1px `--hm-sem-palette-green-light-2`, 하위 padding-start 68, current 글자 `--hm-sem-palette-yellow-accent` |
|
||||
| `--hm-lnb-third-*` | min-height 48, padding-block 12, text padding-start 88(메뉴 행 기준), line-height 24, weight 500 / current 700, connector x `= --hm-lnb-subitem-padding-start`(68)·length 12·width 1, 모든 상태에서 `--hm-sem-palette-green-tint` 40%. 실제 link는 connector 끝부터 시작 |
|
||||
|
||||
새 Primitive: `--hm-yellow-accent: #ffc600`(`primitives.css`). ui-viz `color.action.accent-on-green`이며 HM 팔레트에 없어 추가했다. 그 외 새 Semantic은 없다.
|
||||
|
||||
## 6. LNB Mode 콘텐츠 연결
|
||||
|
||||
- 열 옆 콘텐츠 면: `position: relative`, DOM상 뒤, 왼쪽 모서리 `border-radius: var(--hm-lnb-join) 0 0 var(--hm-lnb-join)`, grid 첫 열 = `var(--hm-lnb-width)`. 열의 초록은 콘텐츠 뒤로 `--hm-lnb-join`만큼 이어진다.
|
||||
- current 항목은 열 오른쪽 가장자리를 넘어 콘텐츠와 1px 겹쳐 이음새 없이 이어진다(열은 overflow를 자르지 않는다).
|
||||
- 높이는 소비자가 정한다(열은 `min-height: 100%`).
|
||||
- GNB 또는 Dashboard와 조합하지 않는다. 콘텐츠 계산은 LNB 자체 변수만 사용한다.
|
||||
|
||||
## 7. Accessibility / 상호작용
|
||||
|
||||
`<aside>` + `<nav aria-label>`, 현재 `aria-current="page"`, 그룹 trigger는 Radix `aria-expanded`/`aria-controls`. `LNBProfile`은 native button + 기본 accessible name `"<name> 사용자 메뉴"`이며 Avatar·이름·역할의 inner content 폭에 interaction을 한정한다(`fit-content` + 안쪽 hit padding). 이 버튼에 pointer cursor와 LNB focus-visible outline을 쓴다. `DropdownMenuTrigger asChild`로 합성하면 Enter/Space 열기, Arrow 이동, Escape 닫기와 trigger focus 복귀는 Radix가 담당한다. 접힌 그룹의 하위는 DOM에서 빠져 focus되지 않는다. 아이콘은 장식(`aria-hidden`, 소비자가 지정).
|
||||
|
||||
## 8. 검증 (2026-10-03, Storybook)
|
||||
|
||||
> HISTORICAL_RECORD: 아래 날짜별 geometry·상태 측정은 당시 기록이다. 현재 링크 interaction 범위·connector 색은 §3a/§5, Profile interaction은 §7을 따른다.
|
||||
|
||||
- 1280: current 항목 x 20→273, 높이 56, 18/28/700, Beige `#F5F0E5`/`#123328`, radius `8 0 0 8`, 그룹·하위 x 20→272. 하위 선택 시 상위 `rgb(255,198,0)`, 접힌 채여도 유지. 긴 이름은 줄바꿈(112px), 스크롤 영역은 메뉴만 스크롤하고 profile은 아래 22px 고정. 그룹 trigger Enter로 펼침.
|
||||
- 1000px: 열 `display:none`, 가로 overflow 없음. 390px은 GNB 모바일 메뉴로 대체.
|
||||
- `npm run typecheck`, `npm run build` 통과. 기존 primitive Story 변화 없음.
|
||||
|
||||
- 3뎁스(2026-10-06, Storybook): 위 §3a 수치를 computed style로 확인(1280 열 272, 3뎁스 x 20 → 272/선택 273, 그룹 2개 모두 x 20·폭 252, 안쪽 그룹 배경 투명·`::before` 없음). 키보드: Enter로 바깥 그룹 접힘(노랑 유지, 3뎁스·선 사라짐), Space로 다시 펼침(안쪽 그룹 열림 유지), Tab → 안쪽 그룹 Enter로 접힘. trigger는 `button` + `aria-expanded`/`aria-controls`, 선택 링크 `aria-current="page"`, focus-visible 흰 outline. 기존 Selected Child 수치 변화 없음(2뎁스 68/56/700, 그룹 하단 8, 선 1px).
|
||||
|
||||
- 스크롤 소유(2026-10-06, Storybook): 열 = flex column, brand·profile `flex: none`, `LNBNav`만 `flex: 1 1 auto; min-height: 0; overflow-y: auto`(열 overflow 없음, 높이는 Story shell / Dashboard Layout이 결정). 메뉴 위 20을 nav margin 2 + padding 18에서 nav padding 20(`--hm-lnb-nav-padding-top`)으로 옮겨 스크롤 영역이 brand 바로 아래에서 시작(첫 항목 y 그대로). 측정: Scroll(460) nav 93.7→370 = brand bottom → profile top, client 276 / scroll 830, 최하단 마지막 항목 bottom 351.7(= nav bottom − 18); Third Level Scroll(460) client 276 / scroll 990, 3뎁스 선택 행·곡선·연결선 함께 스크롤; Dashboard Third Level 1280·1024×600 nav 163.7→510, client 346 / scroll 526, 최하단 491.7. 1023px 열 `display:none`, 가로 overflow 없음.
|
||||
|
||||
- 왼쪽 scrollbar(2026-10-06, Storybook): 오른쪽 native scrollbar(263→273)가 content box를 10px 줄여 스크롤 시 메뉴 면 262·선택 263·곡선 262로 콘텐츠와 떨어졌다. `LNBNav`에 `direction: rtl`(native scrollbar만 왼쪽), 직계 자식 `direction: ltr` + `margin-left: calc(100% + inset-start − width − 1px)`(= −scrollbar 폭, 없으면 0) + `width: auto`(stretch)로 scrollbar가 왼쪽 inset 20 안(0→10)에 들어가고 행은 x 20→272(선택 273) 유지. 스크롤 없음/있음 모두 메뉴 면 272·선택 273·곡선 272·join 272, 아이콘 38·글자 70·chevron 38·2뎁스 글자 88·3뎁스 글자 108·connector 88. LNB Scroll·Third Level Scroll, Dashboard Third Level 1280·1024×600 같은 값. Tab/Enter/Space, `aria-expanded`, `aria-current`, focus-visible 유지.
|
||||
|
||||
- Brand ownership(2026-10-06, Storybook, 높이 700, 1280·1024·1023·390): GNB + LNB(Dashboard `Dashboard Layout`·`Third Level`, LNB `With GNB`) 데스크톱 보이는 brand = `gnb-brand` 1개, LNBBrand 없음, 1023·390 GNB 모바일 바 1개. LNB Only(Dashboard `LNB Only`, `LNB Standalone Desktop`) 데스크톱 `lnb-brand` 1개·상단 바 0, 1023·390 Local 모바일 바 1개(`gnb-brand` 1개). 서비스명 "프로젝트 대시보드"는 모든 경우 화면에 1개, 가로 overflow 0. 메뉴 geometry(GNB + LNB, 1280): Brand 제거 전 nav top 163.7(GNB 70 + 열 padding 28 + Brand 65.7) → 후 98, 첫 항목 118(= 98 + 20), nav bottom = profile top 610(높이 700) 그대로. Dashboard Third Level 1280×600: nav 98→510, client 346 → 412 / scroll 526, 최하단 마지막 항목 492(= nav bottom − 18), profile top 510 고정. 3뎁스 행 48·padding 88·500(current 700, Beige, x 20→273), 상위 그룹 노랑, 행 x 20→272, 곡선 18 — 이전 값과 같음.
|
||||
|
||||
## 9. 변경하지 않은 것
|
||||
|
||||
2026-10-07 단일 모드 검증: 1280·1232·1024·1023·960·959·390·320px에서 LNB Mode에 GNB/Dashboard가 없고 Desktop 열과 Mobile Header 중 하나만 보임을 확인했다. 390px Sheet Escape·포커스 복귀·preventDefault 링크 닫기, 1024px에서 Sheet 숨김, Desktop/Mobile 선택 상태 공유 통과.
|
||||
|
||||
Collapsible, Navigation Menu 등 기존 primitive 코드·theme CSS는 수정하지 않았다. `primitives.css`에 `--hm-yellow-accent` 한 줄만 추가했다.
|
||||
|
||||
## 10. 남은 차이
|
||||
|
||||
| 항목 | 분류 |
|
||||
|---|---|
|
||||
| 외곽 brand 테두리, 그룹 구분선(그룹이 둘 이상일 때) | FOLLOW_UP / MISSING (ui-viz도 미확정·현재 사용 안 함) |
|
||||
| 로고 asset(테스트 이미지만), 크기·간격 D-070 | MISSING |
|
||||
| 선택 시 하위 그룹 자동 펼침 | 서비스별 TBD(ui-viz도 TBD) |
|
||||
| 하위 hover/selected 시작 위치 | 현재 2Depth는 들여쓴 링크 면, 3Depth는 connector 끝부터 시작하는 링크 면 사용(§3a) |
|
||||
| 실기기, 1024~1280 사이 시각 | 미확인 |
|
||||
| 4뎁스 이상 | 명세 없음. 3뎁스 규칙만 정의(더 깊이 중첩하면 3뎁스 모양이 반복된다) |
|
||||
| 선택 경로 전체 연결선을 선택색으로 칠할지 | 현재 모든 connector는 Green Tint 40%를 유지한다(§3a/§5). 선택은 Beige 면 + Primary 글자 + 700 weight로 표현하며, 경로 전체 선의 선택색은 별도 결정 전까지 적용하지 않는다 |
|
||||
| LNB 모바일 Sheet 그룹 접기·상위 current 표시 | 현재 링크 목록 방식. Desktop 18px / Mobile 14px 유지 |
|
||||
| LNB Mobile Header 공개 API | 구현 완료. GNB/Dashboard 의존성 없음 |
|
||||
|
||||
## 11. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/patterns/lnb/{lnb.tsx,lnb-mobile.tsx,index.ts}
|
||||
packages/ui/src/theme/lnb.css
|
||||
packages/ui/src/theme/primitives.css (--hm-yellow-accent)
|
||||
src/patterns/lnb.stories.tsx
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
# Loading Screen (Pattern)
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6.10, §6.11)를 따른다.
|
||||
|
||||
```text
|
||||
Layout visual reference: 외부 ui_viz Reference (ZIP 미포함) (dashboard-composition.md §18, platform-mapping/dashboard-loading-web.md)
|
||||
Implementation: 의존하는 primitive 없음. 24개 막대 스피너 + 텍스트, CSS animation만 사용
|
||||
```
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (2026-10-06) |
|
||||
| 대표 Story | `Patterns/Layout/Loading Screen` → `All` (Inline, Custom label, Narrow parent, Fullscreen) |
|
||||
| 경위 | 요청은 "Loading Bar"였으나 ui_viz에 Loading Bar는 없었다. 실제 있는 Dashboard LoadingScreen(스피너)을 사용자가 지정해 구현했다 |
|
||||
|
||||
## 2. 역할 구분
|
||||
|
||||
| 컴포넌트 | 상황 |
|
||||
|---|---|
|
||||
| Progress | 진행률을 안다 (0~100%) |
|
||||
| Skeleton | 콘텐츠 모양을 안다 |
|
||||
| Sonner `loading` | 작업 중임을 알리는 toast |
|
||||
| **Loading Screen** | 진행률도 모양도 모르는 채 영역/화면 전체가 준비 중 |
|
||||
|
||||
Progress는 Indeterminate 표현을 여전히 갖지 않는다(`progress.md` §11 유지). 버튼 안 inline spinner는 만들지 않았다(ui_viz §18.1과 동일).
|
||||
|
||||
## 3. 구성 / API
|
||||
|
||||
| export | 역할 |
|
||||
|---|---|
|
||||
| `LoadingScreen` | 초록 면 + 스피너 + 텍스트. 기본 inline, `fullscreen`이면 viewport 전체 |
|
||||
|
||||
```tsx
|
||||
<LoadingScreen /> // inline, 부모 폭
|
||||
<LoadingScreen>데이터를 불러오는 중</LoadingScreen> // 라벨 교체("..."는 자동 추가)
|
||||
<LoadingScreen fullscreen /> // position: fixed, inset 0
|
||||
```
|
||||
|
||||
- 추가 prop은 `fullscreen`뿐이다. `speed`, `size` 같은 prop은 만들지 않았다. 값은 `--hm-loading-*` Token으로 바꾼다.
|
||||
- 닫기 버튼, 취소, async lifecycle, 표시/숨김 fade(0.3s)는 두지 않았다. 소비자가 로딩 동안 mount하고 끝나면 제거한다.
|
||||
- Fullscreen은 Portal을 쓰지 않는다. transform이 있는 조상 안에 두면 `fixed`가 어긋난다.
|
||||
|
||||
## 4. 규격 (`packages/ui/src/theme/loading-screen.css`)
|
||||
|
||||
| 항목 | ui_viz | HM | 판정 |
|
||||
|---|---|---|---|
|
||||
| 스피너 | 44×44, 24 bars, 막대 2.5×10, 15° 간격, 첫 막대 12시 | 동일 (실측 44×44, 막대 2.5×10, origin 1.25px 22px) | MATCHED |
|
||||
| 모션 | 막대 opacity 1→0.08, 1.2s linear infinite, 지연 i×0.05s, 회전 없음 | 동일 | MATCHED |
|
||||
| 텍스트 | 14/500/-0.02em, white 60%, 끝에 정적 "..." | 동일, 폰트는 `--hm-font-family-base` | MATCHED |
|
||||
| Inline | padding 36/24, radius 6, inset shadow 24px .45, gap 12, 가운데 정렬 | 동일 (실측 높이 149, ui_viz smoke 148은 폰트 line box 차이) | MATCHED |
|
||||
| Fullscreen | fixed inset 0, assembly는 block·gap 0, 스피너는 묶음 leading edge(텍스트가 44px보다 넓으면 두 중심축이 어긋남), z-index 9999 | fixed inset 0, inline과 같은 column·gap 12로 묶음 전체를 viewport 중앙(스피너·텍스트 중심축 일치) | INTENTIONAL_CHANGE (2026-10-06 사용자 지시: 스피너가 왼쪽으로 치우쳐 보임) |
|
||||
| 면 색 | `#0a3322` (Dashboard-local) | `--hm-sem-action-primary` (Default `#123328`) | INTENTIONAL_CHANGE (HM Primitive 재사용, 새 색 만들지 않음) |
|
||||
| 폰트 | Inter → Noto Sans KR | HM 기본 폰트 | INTENTIONAL_CHANGE |
|
||||
| 막대 끝 | radius 1.5 → 렌더에서 pill | `9999px` | MATCHED |
|
||||
| 숨김/표시 fade 0.3s | 있음 | 없음 | FOLLOW_UP (소비자 lifecycle) |
|
||||
| 닫기 버튼 + ESC | demo 전용(REFERENCE_ONLY) | 없음 (Story에 로컬 닫기 Button만) | OLD_TEST_ONLY |
|
||||
| focus trap, background 차단, task lifecycle | TBD | 만들지 않음 | FOLLOW_UP |
|
||||
|
||||
## 5. Token
|
||||
|
||||
`--hm-loading-screen-{background, foreground, inline-padding-block, inline-padding-inline, inline-radius, inline-shadow, gap, z-index}`, `--hm-loading-spinner-{size, bar-width, bar-height, bar-count, bar-color, bar-min-opacity, duration, stagger}`, `--hm-loading-text-{font-size, font-weight, letter-spacing, color}`. 면/전경은 `--hm-sem-action-primary` / `--hm-sem-on-action-primary`, 그림자는 `--hm-sem-palette-black`을 참조한다. 신규 Primitive 없음.
|
||||
|
||||
## 6. Accessibility / Reduced Motion (INTENTIONAL_CHANGE)
|
||||
|
||||
ui_viz는 스피너 semantics와 reduced motion을 TBD로 남겼다. HM은 최소한만 정했다.
|
||||
|
||||
- 루트 `role="status"`(polite live region)가 텍스트를 알린다. 막대 컨테이너는 `aria-hidden`.
|
||||
- `prefers-reduced-motion: reduce`: 애니메이션을 끄고 막대 opacity를 시계 방향으로 줄여 정적 형태로 보여준다.
|
||||
- 진행률이 없으므로 `aria-valuenow` 등은 쓰지 않는다. focus 이동, 배경 입력 차단, `aria-busy`는 소비자 책임.
|
||||
|
||||
## 7. 검증 (2026-10-06)
|
||||
|
||||
- `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. 새 warning 없음(기존 chunk size 안내는 이전부터).
|
||||
- 실측: inline 면 `rgb(18,51,40)`, padding 36/24, radius 6, shadow inset 24px .45, 스피너 44×44, 막대 24개, animation `hm-loading-fade` 1.2s, 지연 0.05s, spinner→text gap 12.
|
||||
- Fullscreen(2026-10-06 수정 후): spinner centerX = label centerX = viewport centerX, 묶음 centerY = viewport centerY, gap 12. 1920/768/390 모두 오차 0, 가로 overflow 없음. reduced-motion에서도 위치 동일(animation만 none). inline은 높이 149, gap 12로 변화 없음.
|
||||
- 1920 / 768 / 390, 일반 / `prefers-reduced-motion: reduce` 모두 Playwright(headless Chromium)로 실측했다.
|
||||
- 기존 Progress / Skeleton / Sonner / Data Table / Dashboard Layout 코드는 수정하지 않았다.
|
||||
|
||||
## 8. 관련 파일
|
||||
|
||||
```
|
||||
packages/ui/src/patterns/loading-screen/{loading-screen.tsx,index.ts}
|
||||
packages/ui/src/theme/loading-screen.css
|
||||
packages/ui/src/theme.css (import)
|
||||
packages/ui/src/index.ts (export)
|
||||
src/patterns/loading-screen.stories.tsx
|
||||
.storybook/preview.tsx (Patterns/Layout 정렬)
|
||||
```
|
||||
@@ -0,0 +1,110 @@
|
||||
# Page Header (Pattern)
|
||||
|
||||
현재 HM 구현의 기준 문서. 공통 원칙은 [프로젝트 지침](../guidelines/프로젝트-지침.md), [Button 규칙](../components/button.md), [단일 Navigation 정책](../guidelines/navigation-single-mode.md)을 따른다.
|
||||
|
||||
```text
|
||||
Layout visual reference: 외부 ui_viz Reference (ZIP 미포함) (dashboard-composition.md §8 Page Heading)
|
||||
Implementation: HM Breadcrumb / Button / Dropdown Menu composed as children
|
||||
```
|
||||
|
||||
## 1. 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 구현 상태 | 구현 완료 (2026-10-03) |
|
||||
| 대표 Story | [Patterns/Layout/Page Header → Page Header](../../src/patterns/page-header.stories.tsx) (`patterns-layout-page-header--all`) |
|
||||
| Dashboard 사용 예 | [Patterns/Layout/Dashboard → Dashboard](../../src/patterns/dashboard-layout.stories.tsx). 기존 대표 Story와 Dashboard 내부 조합 유지 |
|
||||
| 분리 이유 | 모든 화면이 Breadcrumb + h1 + 액션을 반복하고, 하단 20px이 Layout contract |
|
||||
|
||||
## 2. 구성 / API
|
||||
|
||||
| export | 역할 |
|
||||
|---|---|
|
||||
| `PageHeader` | 행(flex, space-between, 하단 정렬, 줄바꿈) |
|
||||
| `PageHeaderContent` | 시작 쪽 세로 묶음 |
|
||||
| `PageHeaderTitle` | `h1`(화면의 유일한 h1) |
|
||||
| `PageHeaderDescription` | 설명/메타 `p` |
|
||||
| `PageHeaderActions` | 끝 쪽 액션 행(Button, Dropdown Menu) |
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList,
|
||||
BreadcrumbPage, BreadcrumbSeparator, Button,
|
||||
PageHeader, PageHeaderActions, PageHeaderContent,
|
||||
PageHeaderDescription, PageHeaderTitle,
|
||||
} from "@hm-design/ui"
|
||||
|
||||
<PageHeader>
|
||||
<PageHeaderContent>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem><BreadcrumbLink href="#home">홈</BreadcrumbLink></BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem><BreadcrumbLink href="#projects">프로젝트</BreadcrumbLink></BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem><BreadcrumbPage>업무 현황</BreadcrumbPage></BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<PageHeaderTitle>업무 현황</PageHeaderTitle>
|
||||
<PageHeaderDescription>프로젝트별 업무 진행 상황을 확인하고 조회합니다.</PageHeaderDescription>
|
||||
</PageHeaderContent>
|
||||
<PageHeaderActions>
|
||||
<Button type="button" variant="primary" appearance="brand" size="default">신규 등록</Button>
|
||||
</PageHeaderActions>
|
||||
</PageHeader>
|
||||
```
|
||||
|
||||
Breadcrumb와 액션 API는 정의하지 않는다(composition). 구분선은 없다(ui-viz에 없음).
|
||||
|
||||
대표 예제는 지원되는 `primary × brand`, `size="default"`(Medium 34px) Action 1개를 사용한다. 화면당 Brand는 최대 1개이며 `appearance`를 명시한다. 복수 Action의 PageHeader 전용 세트 규칙은 아직 확정하지 않았으므로 기존 Dashboard fixture를 공통 세트 규칙으로 일반화하지 않는다. 보조 Action이 필요하면 [Button의 사용 위치·세트 규칙과 허용 조합](../components/button.md)을 확인하고, 불명확한 조합은 사용자 결정 후 적용한다.
|
||||
|
||||
Story의 Breadcrumb hash 링크와 등록 버튼은 조합 예제다. 실제 화면은 해당 Runtime의 라우팅과 등록 처리 callback을 연결한다. React 소비 화면은 entry에서 `@hm-design/ui/theme.css`를 로드한다. Storybook은 기존 전역 Theme 연결을 재사용한다.
|
||||
|
||||
## 3. Token (`packages/ui/src/theme/page-header.css`)
|
||||
|
||||
| Token | 값 | ui-viz |
|
||||
|---|---|---|
|
||||
| `--hm-page-header-gap` | 24px | MATCHED (gap 24) |
|
||||
| `--hm-page-header-margin-bottom` | 20px | MATCHED (D-043, 첫 카드까지 20) |
|
||||
| `--hm-page-header-breadcrumb-gap` | 10px | MATCHED |
|
||||
| title | 28/36/700, 아래→설명 8px | MATCHED (`h2` → `h1`은 INTENTIONAL_CHANGE) |
|
||||
| description | 14/20, `--hm-sem-fg-secondary` | MATCHED (색은 ui-viz 임시값 대신 Semantic) |
|
||||
| actions gap | 8px | 정의 없음(Button 간격) |
|
||||
|
||||
Breadcrumb는 HM Breadcrumb 그대로(14px, 링크). ui-viz는 12px 정적 텍스트였다(INTENTIONAL_CHANGE).
|
||||
|
||||
## 4. Responsive / Accessibility
|
||||
|
||||
`PageHeaderContent`는 `flex: 1 1 320px; min-width: 0`, 액션은 줄바꿈 후 오른쪽 정렬(390px에서 제목 아래 줄). 긴 제목은 `overflow-wrap: anywhere`. 화면당 `h1`은 하나만 둔다. wrapper에 role을 두지 않는다.
|
||||
|
||||
PageHeader는 콘텐츠의 제목 영역이며 Navigation Mode를 정의하지 않는다. 독립 대표 Story는 Navigation을 추가하지 않은 조합 예제다. 실제 페이지는 `GNB | LNB | DASHBOARD` 중 하나만 선택하고 반응형에서도 유지한다. Dashboard에서는 Dashboard 내부 Header/Menu/Sidebar와 합성하며 외부 GNB/LNB를 함께 넣지 않는다.
|
||||
|
||||
Dashboard Compact 최소 폭 960px(Sidebar 포함 1232px)과 내부 가로 스크롤은 Dashboard Layout 계약이다. PageHeader 단독 사용처에 이 최소 폭을 자동 적용하지 않는다. PageHeader 자체에는 고정 최소 폭이나 별도 breakpoint가 없으며 기존 flex wrapping과 Theme 규격을 유지한다. 외부 콘텐츠 padding은 소비 Layout이 소유한다.
|
||||
|
||||
## 5. 검증 / 관련 파일
|
||||
|
||||
### 현재 대표 Reference
|
||||
|
||||
- [독립 대표 Story 소스](../../src/patterns/page-header.stories.tsx): Breadcrumb + Title + Description + Actions. 운영 조합에 별도 Story CSS나 Dashboard 최소 폭을 추가하지 않는다.
|
||||
- Storybook 경로: `Patterns/Layout/Page Header → Page Header`, ID: `patterns-layout-page-header--all`.
|
||||
- 기존 Dashboard 대표 Story는 Dashboard 전체 Layout의 Reference로 유지한다. Verification Story는 운영 Reference로 사용하지 않는다.
|
||||
|
||||
### 2026-10-08 독립 대표 Story 검증
|
||||
|
||||
- 기존 6024 Storybook에서 독립 메뉴와 대표 Story 렌더링 확인. 1440×900 / 390×844에서 네 구성 요소가 표시되고 문서·body의 가로 넘침 없음. 모바일 Action은 아래 줄 오른쪽으로 배치된다.
|
||||
- Computed style: Title 28/36/700, Breadcrumb 14/20, Description 14/20와 Secondary 전경, PageHeader gap 24px·하단 margin 20px·Actions gap 8px 유지. 기존 Noto Sans KR와 Theme Semantic 연결 확인.
|
||||
- `primary × brand`, `size="default"` 버튼 1개·높이 34px 확인. Breadcrumb 링크 → Action의 Tab 순서와 focus-visible 링 유지. 콘텐츠의 보이는 `h1`은 1개다.
|
||||
- 기존 Dashboard 대표 Story 정상 렌더. Header 48px·Variant C Typography·상태 필터 유지, 이슈 5건 조회 및 전체 47건 복원 확인. 390px에서 Dashboard 최소 폭 960px과 내부 가로 스크롤 유지.
|
||||
- PageHeader / Dashboard Console 오류·경고 없음. Storybook 관리자에는 기존 `PopoverProvider ariaLabel`의 Storybook 11 예정 변경 경고 1건이 있다.
|
||||
- 새 Story만 대상으로 한 최소 TypeScript 검사와 변경 파일 diff check 통과. 앱 범위 검사는 다른 작업의 Controlled Props와 패키지 선언 불일치 오류 6건으로 실패했으며 해당 파일은 수정하지 않았다. 전체 Component 회귀·build는 이번 범위에서 수행하지 않았다.
|
||||
|
||||
### 과거 검증 기록 (기존 문서)
|
||||
|
||||
당시 Dashboard Layout Story(1280, 390, 긴 제목)에서 page-header 상단이 Main padding 34 아래, 하단 20px 뒤에 첫 콘텐츠인 것을 확인했다. 이는 당시 Dashboard 조합의 검증 기록이며, 현재 Dashboard 최소 폭·내부 가로 스크롤 정책이나 모든 PageHeader 소비 화면의 padding 규격을 대신하지 않는다.
|
||||
|
||||
```
|
||||
packages/ui/src/patterns/page-header/{page-header.tsx,index.ts}
|
||||
packages/ui/src/theme/page-header.css
|
||||
src/patterns/page-header.stories.tsx
|
||||
src/patterns/dashboard-layout.stories.tsx
|
||||
```
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>HM Design System · UI Preview</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+10475
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "hm-design-system-poc",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"generate:theme-defaults": "node scripts/theme-defaults.mjs",
|
||||
"build:themes": "node scripts/theme-artifacts.mjs",
|
||||
"check:theme-defaults": "node scripts/theme-defaults.mjs --check",
|
||||
"pretypecheck": "npm run check:theme-defaults && npm run build:packages",
|
||||
"prebuild": "npm run check:theme-defaults && npm run build:packages",
|
||||
"prebuild-storybook": "npm run check:theme-defaults && npm run build:packages",
|
||||
"dev": "vite --host 127.0.0.1",
|
||||
"build": "tsc -b && vite build",
|
||||
"build:dashboard-export": "npm run check:theme-defaults && npm run build:packages && vite build --config vite.dashboard-export.config.ts && node scripts/finalize-dashboard-export.mjs",
|
||||
"h2:preview": "node scripts/h2-preview.mjs",
|
||||
"preview": "vite preview --host 127.0.0.1",
|
||||
"storybook": "storybook dev -p 6024 --host 0.0.0.0 --no-open --disable-telemetry",
|
||||
"build-storybook": "storybook build -o storybook-static --disable-telemetry",
|
||||
"typecheck": "tsc -b && npm run typecheck --workspaces --if-present",
|
||||
"build:ui": "npm run build -w @hm-design/ui",
|
||||
"build:center-graphic": "npm run build -w @hm-design/center-graphic",
|
||||
"build:packages": "npm run build --workspaces --if-present"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hm-design/center-graphic": "*",
|
||||
"@hm-design/ui": "*",
|
||||
"lucide-react": "^1.49.0",
|
||||
"react": "19.3.0",
|
||||
"react-dom": "19.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/react-vite": "10.6.1",
|
||||
"@tailwindcss/vite": "4.3.3",
|
||||
"@types/node": "24.7.0",
|
||||
"@types/react": "19.3.0",
|
||||
"@types/react-dom": "19.3.0",
|
||||
"@vitejs/plugin-react": "6.1.1",
|
||||
"storybook": "10.6.1",
|
||||
"tailwindcss": "4.3.3",
|
||||
"typescript": "7.0.2",
|
||||
"vite": "8.3.0"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# @hm-design/center-graphic
|
||||
|
||||
React 19 CENTER graphics. Published entrypoints contain ESM JavaScript, TypeScript declarations, component CSS and embedded SVG assets.
|
||||
|
||||
```tsx
|
||||
import { CenterTitle, TunnelFacility } from "@hm-design/center-graphic"
|
||||
import { CenterTitleReferenceDemo } from "@hm-design/center-graphic/reference"
|
||||
```
|
||||
|
||||
Both JavaScript entrypoints automatically import the component stylesheet. Use a bundler that handles CSS imports. The stylesheet is also available explicitly as `@hm-design/center-graphic/style.css`.
|
||||
|
||||
Vite processes SVG imports and `import.meta.glob` during the repository build. Published JavaScript and declarations do not require Vite or `vite/client`. React and React DOM are peers and are not bundled. This package does not depend on `@hm-design/ui`.
|
||||
|
||||
From the HMDesignSystem repository, run `npm run build -w @hm-design/center-graphic`. `npm pack` runs this build automatically. Only `dist/`, this README and the package manifest are packed; raw source assets and Storybook files are excluded.
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@hm-design/center-graphic",
|
||||
"version": "0.1.0",
|
||||
"private": false,
|
||||
"description": "HM Design System center graphics (title, information, route / spatial, facility). Independent of @hm-design/ui.",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./reference": {
|
||||
"types": "./dist/reference.d.ts",
|
||||
"import": "./dist/reference.js",
|
||||
"default": "./dist/reference.js"
|
||||
},
|
||||
"./style.css": "./dist/style.css"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"typecheck": "tsc -p tsconfig.json",
|
||||
"build": "node ../../scripts/build-package.mjs center-graphic",
|
||||
"prepack": "npm run build"
|
||||
},
|
||||
"sideEffects": [
|
||||
"**/*.css",
|
||||
"./dist/index.js",
|
||||
"./dist/reference.js"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<svg width="50" height="95" viewBox="0 0 50 95" fill="none" shape-rendering="geometricPrecision" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_450_11759)">
|
||||
<g clip-path="url(#clip1_450_11759)">
|
||||
<g filter="url(#filter0_i_450_11759)">
|
||||
<path d="M-132.027 29.9823C-132.027 22.9122 -126.296 17.1808 -119.226 17.1808H4.74567C11.8157 17.1808 17.5471 22.9122 17.5471 29.9823V65.6915H-132.027V29.9823Z" fill="url(#paint0_linear_450_11759)"/>
|
||||
<path d="M-132.027 29.9823C-132.027 22.9122 -126.296 17.1808 -119.226 17.1808H4.74567C11.8157 17.1808 17.5471 22.9122 17.5471 29.9823V65.6915H-132.027V29.9823Z" fill="url(#paint1_linear_450_11759)"/>
|
||||
</g>
|
||||
<path d="M-119.226 17.7141H4.74609C11.5214 17.7143 17.0137 23.2073 17.0137 29.9826V65.1584H-131.494V29.9826C-131.494 23.2072 -126.001 17.7141 -119.226 17.7141Z" stroke="black" stroke-width="1.06678"/>
|
||||
<path d="M36.6543 66.2539C42.546 66.2539 47.3222 71.0302 47.3223 76.9219V93.9707H-161.892V76.9219C-161.892 71.0303 -157.115 66.254 -151.224 66.2539H36.6543Z" fill="url(#paint2_linear_450_11759)" stroke="#400500" stroke-width="4.26714"/>
|
||||
<g filter="url(#filter1_ii_450_11759)">
|
||||
<path d="M-159.758 76.922C-159.758 72.2086 -155.937 68.3877 -151.224 68.3877H36.6544C41.3677 68.3877 45.1887 72.2086 45.1887 76.922V90.7699C45.1887 91.359 44.711 91.8366 44.1219 91.8366H-158.691C-159.28 91.8366 -159.758 91.359 -159.758 90.7699V76.922Z" fill="url(#paint3_linear_450_11759)"/>
|
||||
</g>
|
||||
<path d="M36.6543 67.8545C41.6622 67.8545 45.7216 71.914 45.7217 76.9219V90.7695C45.7217 91.6532 45.0057 92.37 44.1221 92.3701H-158.691C-159.575 92.3699 -160.291 91.6532 -160.291 90.7695V76.9219C-160.291 71.914 -156.231 67.8545 -151.224 67.8545H36.6543Z" stroke="black" stroke-opacity="0.76" stroke-width="1.06678"/>
|
||||
<g filter="url(#filter2_f_450_11759)" style="mix-blend-mode:plus-lighter">
|
||||
<path d="M37.3662 69.8745C41.1957 69.8745 44.2996 72.9787 44.2998 76.8081V92.2769H-158.389V76.8081C-158.388 72.9788 -155.284 69.8747 -151.455 69.8745H37.3662Z" stroke="url(#paint4_linear_450_11759)" stroke-width="1.06678"/>
|
||||
</g>
|
||||
<mask id="path-7-outside-1_450_11759" maskUnits="userSpaceOnUse" x="-167.123" y="-0.639648" width="219" height="69" fill="black">
|
||||
<rect fill="white" x="-167.123" y="-0.639648" width="219" height="69"/>
|
||||
<path d="M47.5605 12.9385C37.7631 12.9385 29.8205 20.8804 29.8203 30.6777V63.4736H21.3857V30.6777C21.3856 20.8804 13.4429 12.9385 3.64551 12.9385V12.9336H-118.183V12.9385C-127.98 12.9387 -135.922 20.8805 -135.922 30.6777V63.4736H-144.356V30.6777C-144.357 20.8804 -152.299 12.9386 -162.097 12.9385V6.79492H47.5605V12.9385ZM47.5625 5.86328H-162.123V4.36035H47.5625V5.86328Z"/>
|
||||
</mask>
|
||||
<path d="M47.5605 12.9385C37.7631 12.9385 29.8205 20.8804 29.8203 30.6777V63.4736H21.3857V30.6777C21.3856 20.8804 13.4429 12.9385 3.64551 12.9385V12.9336H-118.183V12.9385C-127.98 12.9387 -135.922 20.8805 -135.922 30.6777V63.4736H-144.356V30.6777C-144.357 20.8804 -152.299 12.9386 -162.097 12.9385V6.79492H47.5605V12.9385ZM47.5625 5.86328H-162.123V4.36035H47.5625V5.86328Z" fill="#FF7700"/>
|
||||
<path d="M47.5605 12.9385V17.2056C49.9172 17.2056 51.8277 15.2952 51.8277 12.9385H47.5605ZM29.8203 30.6777L25.5532 30.6776V30.6777H29.8203ZM29.8203 63.4736V67.7408C32.177 67.7408 34.0874 65.8303 34.0874 63.4736H29.8203ZM21.3857 63.4736H17.1186C17.1186 65.8303 19.0291 67.7408 21.3857 67.7408V63.4736ZM21.3857 30.6777H25.6529V30.6777L21.3857 30.6777ZM3.64551 12.9385H-0.621628C-0.621628 15.2952 1.28883 17.2056 3.64551 17.2056V12.9385ZM3.64551 12.9336H7.91264C7.91264 10.5769 6.00218 8.66645 3.64551 8.66645V12.9336ZM-118.183 12.9336V8.66645C-120.539 8.66645 -122.45 10.5769 -122.45 12.9336H-118.183ZM-118.183 12.9385L-118.183 17.2056C-115.826 17.2056 -113.915 15.2951 -113.915 12.9385H-118.183ZM-135.922 30.6777L-140.189 30.6776V30.6777H-135.922ZM-135.922 63.4736V67.7408C-133.565 67.7408 -131.655 65.8303 -131.655 63.4736H-135.922ZM-144.356 63.4736H-148.624C-148.624 65.8303 -146.713 67.7408 -144.356 67.7408V63.4736ZM-144.356 30.6777H-140.089V30.6777L-144.356 30.6777ZM-162.097 12.9385H-166.364C-166.364 15.2951 -164.453 17.2056 -162.097 17.2056L-162.097 12.9385ZM-162.097 6.79492V2.52778C-164.453 2.52778 -166.364 4.43825 -166.364 6.79492H-162.097ZM47.5605 6.79492H51.8277C51.8277 4.43825 49.9172 2.52778 47.5605 2.52778V6.79492ZM47.5625 5.86328V10.1304C49.9192 10.1304 51.8296 8.21996 51.8296 5.86328H47.5625ZM-162.123 5.86328H-166.39C-166.39 8.21996 -164.48 10.1304 -162.123 10.1304V5.86328ZM-162.123 4.36035V0.0932121C-164.48 0.0932121 -166.39 2.00368 -166.39 4.36035L-162.123 4.36035ZM47.5625 4.36035H51.8296C51.8296 2.00368 49.9192 0.0932121 47.5625 0.0932121V4.36035ZM47.5605 12.9385V8.67134C35.4067 8.67134 25.5534 18.5235 25.5532 30.6776L29.8203 30.6777L34.0874 30.6778C34.0876 23.2373 40.1196 17.2056 47.5605 17.2056V12.9385ZM29.8203 30.6777H25.5532V63.4736H29.8203H34.0874V30.6777H29.8203ZM29.8203 63.4736V59.2065H21.3857V63.4736V67.7408H29.8203V63.4736ZM21.3857 63.4736H25.6529V30.6777H21.3857H17.1186V63.4736H21.3857ZM21.3857 30.6777L25.6529 30.6777C25.6526 18.5234 15.7994 8.67134 3.64551 8.67134V12.9385V17.2056C11.0865 17.2056 17.1185 23.2373 17.1186 30.6778L21.3857 30.6777ZM3.64551 12.9385H7.91264V12.9336H3.64551H-0.621628V12.9385H3.64551ZM3.64551 12.9336V8.66645H-118.183V12.9336V17.2007H3.64551V12.9336ZM-118.183 12.9336H-122.45V12.9385H-118.183H-113.915V12.9336H-118.183ZM-118.183 12.9385L-118.183 8.67134C-130.337 8.67158 -140.189 18.5238 -140.189 30.6776L-135.922 30.6777L-131.655 30.6778C-131.655 23.2372 -125.623 17.2058 -118.183 17.2056L-118.183 12.9385ZM-135.922 30.6777H-140.189V63.4736H-135.922H-131.655V30.6777H-135.922ZM-135.922 63.4736V59.2065H-144.356V63.4736V67.7408H-135.922V63.4736ZM-144.356 63.4736H-140.089V30.6777H-144.356H-148.624V63.4736H-144.356ZM-144.356 30.6777L-140.089 30.6777C-140.09 18.5235 -149.943 8.67149 -162.097 8.67134L-162.097 12.9385L-162.097 17.2056C-154.656 17.2057 -148.624 23.2374 -148.624 30.6778L-144.356 30.6777ZM-162.097 12.9385H-157.83V6.79492H-162.097H-166.364V12.9385H-162.097ZM-162.097 6.79492V11.0621H47.5605V6.79492V2.52778H-162.097V6.79492ZM47.5605 6.79492H43.2934V12.9385H47.5605H51.8277V6.79492H47.5605ZM47.5625 5.86328V1.59614H-162.123V5.86328V10.1304H47.5625V5.86328ZM-162.123 5.86328H-157.856V4.36035H-162.123H-166.39V5.86328H-162.123ZM-162.123 4.36035V8.62749H47.5625V4.36035V0.0932121H-162.123V4.36035ZM47.5625 4.36035H43.2954V5.86328H47.5625H51.8296V4.36035H47.5625Z" fill="#400500" mask="url(#path-7-outside-1_450_11759)"/>
|
||||
<mask id="path-9-outside-2_450_11759" maskUnits="userSpaceOnUse" x="-167.123" y="-0.639648" width="219" height="69" fill="black">
|
||||
<rect fill="white" x="-167.123" y="-0.639648" width="219" height="69"/>
|
||||
<path d="M47.5605 11.8717C47.5605 12.4609 47.0818 12.9351 46.4936 12.97C37.1929 13.5217 29.8205 21.2386 29.8203 30.6777V62.4068C29.8203 62.996 29.3427 63.4736 28.7535 63.4736H22.4525C21.8634 63.4736 21.3857 62.996 21.3857 62.4068V30.6777C21.3856 20.8812 13.4442 12.9398 3.64796 12.9385C3.64662 12.9385 3.64551 12.9374 3.64551 12.936C3.64551 12.9347 3.64441 12.9336 3.64307 12.9336H-118.18C-118.182 12.9336 -118.183 12.9347 -118.183 12.936C-118.183 12.9374 -118.184 12.9385 -118.185 12.9385C-127.981 12.94 -135.922 20.8813 -135.922 30.6777V62.4068C-135.922 62.996 -136.399 63.4736 -136.989 63.4736H-143.29C-143.879 63.4736 -144.356 62.996 -144.356 62.4068V30.6777C-144.357 21.2387 -151.729 13.5218 -161.03 12.97C-161.618 12.9352 -162.097 12.4609 -162.097 11.8717V7.86171C-162.097 7.27254 -161.619 6.79492 -161.03 6.79492H46.4938C47.0829 6.79492 47.5605 7.27254 47.5605 7.86171V11.8717ZM47.5625 5.11182C47.5625 5.52684 47.2261 5.86328 46.811 5.86328H-161.372C-161.787 5.86328 -162.123 5.52684 -162.123 5.11182C-162.123 4.69679 -161.787 4.36035 -161.372 4.36035H46.811C47.2261 4.36035 47.5625 4.69679 47.5625 5.11182Z"/>
|
||||
</mask>
|
||||
<path d="M47.5605 11.8717C47.5605 12.4609 47.0818 12.9351 46.4936 12.97C37.1929 13.5217 29.8205 21.2386 29.8203 30.6777V62.4068C29.8203 62.996 29.3427 63.4736 28.7535 63.4736H22.4525C21.8634 63.4736 21.3857 62.996 21.3857 62.4068V30.6777C21.3856 20.8812 13.4442 12.9398 3.64796 12.9385C3.64662 12.9385 3.64551 12.9374 3.64551 12.936C3.64551 12.9347 3.64441 12.9336 3.64307 12.9336H-118.18C-118.182 12.9336 -118.183 12.9347 -118.183 12.936C-118.183 12.9374 -118.184 12.9385 -118.185 12.9385C-127.981 12.94 -135.922 20.8813 -135.922 30.6777V62.4068C-135.922 62.996 -136.399 63.4736 -136.989 63.4736H-143.29C-143.879 63.4736 -144.356 62.996 -144.356 62.4068V30.6777C-144.357 21.2387 -151.729 13.5218 -161.03 12.97C-161.618 12.9352 -162.097 12.4609 -162.097 11.8717V7.86171C-162.097 7.27254 -161.619 6.79492 -161.03 6.79492H46.4938C47.0829 6.79492 47.5605 7.27254 47.5605 7.86171V11.8717ZM47.5625 5.11182C47.5625 5.52684 47.2261 5.86328 46.811 5.86328H-161.372C-161.787 5.86328 -162.123 5.52684 -162.123 5.11182C-162.123 4.69679 -161.787 4.36035 -161.372 4.36035H46.811C47.2261 4.36035 47.5625 4.69679 47.5625 5.11182Z" fill="url(#paint5_linear_450_11759)"/>
|
||||
<path d="M29.8203 30.6777L25.5532 30.6776V30.6777H29.8203ZM21.3857 30.6777H25.6529V30.6777L21.3857 30.6777ZM-135.922 30.6777L-140.189 30.6776V30.6777H-135.922ZM-144.356 30.6777H-140.089V30.6777L-144.356 30.6777ZM-118.185 12.9385L-118.184 17.2056L-118.185 12.9385ZM-161.03 12.97L-160.777 8.71039L-161.03 12.97ZM3.64796 12.9385L3.64738 17.2056L3.64796 12.9385ZM46.4936 12.97L46.241 8.71038L46.4936 12.97ZM46.4936 12.97L46.241 8.71038C34.6994 9.39498 25.5534 18.9663 25.5532 30.6776L29.8203 30.6777L34.0874 30.6778C34.0876 23.5108 39.6865 17.6484 46.7463 17.2297L46.4936 12.97ZM29.8203 30.6777H25.5532V62.4068H29.8203H34.0874V30.6777H29.8203ZM28.7535 63.4736V59.2065H22.4525V63.4736V67.7408H28.7535V63.4736ZM21.3857 62.4068H25.6529V30.6777H21.3857H17.1186V62.4068H21.3857ZM21.3857 30.6777L25.6529 30.6777C25.6526 18.5245 15.801 8.67298 3.64854 8.67134L3.64796 12.9385L3.64738 17.2056C11.0875 17.2066 17.1185 23.2379 17.1186 30.6778L21.3857 30.6777ZM3.64307 12.9336V8.66645H-118.18V12.9336V17.2007H3.64307V12.9336ZM-118.185 12.9385L-118.186 8.67134C-130.338 8.67322 -140.189 18.5248 -140.189 30.6776L-135.922 30.6777L-131.655 30.6778C-131.655 23.2378 -125.624 17.2068 -118.184 17.2056L-118.185 12.9385ZM-135.922 30.6777H-140.189V62.4068H-135.922H-131.655V30.6777H-135.922ZM-136.989 63.4736V59.2065H-143.29V63.4736V67.7408H-136.989V63.4736ZM-144.356 62.4068H-140.089V30.6777H-144.356H-148.624V62.4068H-144.356ZM-144.356 30.6777L-140.089 30.6777C-140.09 18.9663 -149.236 9.39512 -160.777 8.71039L-161.03 12.97L-161.282 17.2297C-154.223 17.6485 -148.624 23.511 -148.624 30.6778L-144.356 30.6777ZM-162.097 11.8717H-157.83V7.86171H-162.097H-166.364V11.8717H-162.097ZM-161.03 6.79492V11.0621H46.4938V6.79492V2.52778H-161.03V6.79492ZM47.5605 7.86171H43.2934V11.8717H47.5605H51.8277V7.86171H47.5605ZM46.811 5.86328V1.59614H-161.372V5.86328V10.1304H46.811V5.86328ZM-161.372 4.36035V8.62749H46.811V4.36035V0.0932121H-161.372V4.36035ZM46.811 4.36035V8.62749C44.8694 8.62749 43.2954 7.0535 43.2954 5.11182H47.5625H51.8296C51.8296 2.34009 49.5827 0.0932121 46.811 0.0932121V4.36035ZM-162.123 5.11182H-157.856C-157.856 7.05347 -159.43 8.62749 -161.372 8.62749V4.36035V0.0932121C-164.143 0.0932121 -166.39 2.34012 -166.39 5.11182H-162.123ZM-161.372 5.86328V1.59614C-159.43 1.59614 -157.856 3.17013 -157.856 5.11182H-162.123H-166.39C-166.39 7.88354 -164.143 10.1304 -161.372 10.1304V5.86328ZM46.811 5.86328V10.1304C49.5827 10.1304 51.8296 7.88351 51.8296 5.11182H47.5625H43.2954C43.2954 3.17016 44.8694 1.59614 46.811 1.59614V5.86328ZM46.4938 6.79492V11.0621C44.7263 11.0621 43.2934 9.62925 43.2934 7.86171H47.5605H51.8277C51.8277 4.91582 49.4396 2.52778 46.4938 2.52778V6.79492ZM-135.922 62.4068H-140.189C-140.189 60.6393 -138.756 59.2065 -136.989 59.2065V63.4736V67.7408C-134.043 67.7408 -131.655 65.3527 -131.655 62.4068H-135.922ZM-118.183 12.936H-122.45C-122.45 10.5484 -120.508 8.6717 -118.186 8.67134L-118.185 12.9385L-118.184 17.2056C-115.859 17.2053 -113.915 15.3263 -113.915 12.936H-118.183ZM-118.18 12.9336V8.66645C-120.542 8.66645 -122.45 10.5816 -122.45 12.936H-118.183H-113.915C-113.915 15.2878 -115.821 17.2007 -118.18 17.2007V12.9336ZM3.64551 12.936H7.91264C7.91264 10.578 6.00108 8.66645 3.64307 8.66645V12.9336V17.2007C1.28773 17.2007 -0.621628 15.2914 -0.621628 12.936H3.64551ZM-162.097 7.86171H-157.83C-157.83 9.62921 -159.262 11.0621 -161.03 11.0621V6.79492V2.52778C-163.976 2.52778 -166.364 4.91586 -166.364 7.86171H-162.097ZM22.4525 63.4736V59.2065C24.22 59.2065 25.6529 60.6393 25.6529 62.4068H21.3857H17.1186C17.1186 65.3527 19.5067 67.7408 22.4525 67.7408V63.4736ZM-161.03 12.97L-160.777 8.71039C-159.276 8.79943 -157.83 10.0401 -157.83 11.8717H-162.097H-166.364C-166.364 14.8817 -163.96 17.0709 -161.282 17.2297L-161.03 12.97ZM29.8203 62.4068H25.5532C25.5532 60.6393 26.986 59.2065 28.7535 59.2065V63.4736V67.7408C31.6994 67.7408 34.0874 65.3527 34.0874 62.4068H29.8203ZM3.64796 12.9385L3.64854 8.67134C5.96979 8.67165 7.91264 10.5472 7.91264 12.936H3.64551H-0.621628C-0.621628 15.3275 1.32344 17.2053 3.64738 17.2056L3.64796 12.9385ZM-143.29 63.4736V59.2065C-141.522 59.2065 -140.089 60.6393 -140.089 62.4068H-144.356H-148.624C-148.624 65.3527 -146.236 67.7408 -143.29 67.7408V63.4736ZM46.4936 12.97L46.7463 17.2297C49.4233 17.0709 51.8277 14.8817 51.8277 11.8717H47.5605H43.2934C43.2934 10.04 44.7402 8.7994 46.241 8.71038L46.4936 12.97Z" fill="#400500" mask="url(#path-9-outside-2_450_11759)"/>
|
||||
<mask id="path-11-outside-3_450_11759" maskUnits="userSpaceOnUse" x="-164.123" y="2.36035" width="213" height="63" fill="black">
|
||||
<rect fill="white" x="-164.123" y="2.36035" width="213" height="63"/>
|
||||
<path d="M47.5605 12.9385C37.7631 12.9385 29.8205 20.8804 29.8203 30.6777V63.4736H21.3857V30.6777C21.3856 20.8804 13.4429 12.9385 3.64551 12.9385V12.9336H-118.183V12.9385C-127.98 12.9387 -135.922 20.8805 -135.922 30.6777V63.4736H-144.356V30.6777C-144.357 20.8804 -152.299 12.9386 -162.097 12.9385V6.79492H47.5605V12.9385ZM47.5625 5.86328H-162.123V4.36035H47.5625V5.86328Z"/>
|
||||
</mask>
|
||||
<path d="M47.5605 12.9385V14.0053C48.1497 14.0053 48.6273 13.5276 48.6273 12.9385H47.5605ZM29.8203 30.6777L28.7535 30.6777V30.6777H29.8203ZM29.8203 63.4736V64.5404C30.4095 64.5404 30.8871 64.0628 30.8871 63.4736H29.8203ZM21.3857 63.4736H20.319C20.319 64.0628 20.7966 64.5404 21.3857 64.5404V63.4736ZM21.3857 30.6777H22.4525V30.6777L21.3857 30.6777ZM3.64551 12.9385H2.57872C2.57872 13.5276 3.05634 14.0053 3.64551 14.0053V12.9385ZM3.64551 12.9336H4.7123C4.7123 12.3444 4.23468 11.8668 3.64551 11.8668V12.9336ZM-118.183 12.9336V11.8668C-118.772 11.8668 -119.249 12.3444 -119.249 12.9336H-118.183ZM-118.183 12.9385L-118.183 14.0053C-117.593 14.0052 -117.116 13.5276 -117.116 12.9385H-118.183ZM-135.922 30.6777L-136.989 30.6777V30.6777H-135.922ZM-135.922 63.4736V64.5404C-135.333 64.5404 -134.855 64.0628 -134.855 63.4736H-135.922ZM-144.356 63.4736H-145.423C-145.423 64.0628 -144.946 64.5404 -144.356 64.5404V63.4736ZM-144.356 30.6777H-143.29L-143.29 30.6777L-144.356 30.6777ZM-162.097 12.9385H-163.163C-163.163 13.5276 -162.686 14.0053 -162.097 14.0053L-162.097 12.9385ZM-162.097 6.79492V5.72814C-162.686 5.72814 -163.163 6.20575 -163.163 6.79492H-162.097ZM47.5605 6.79492H48.6273C48.6273 6.20575 48.1497 5.72814 47.5605 5.72814V6.79492ZM47.5625 5.86328V6.93007C48.1517 6.93007 48.6293 6.45245 48.6293 5.86328H47.5625ZM-162.123 5.86328H-163.19C-163.19 6.45245 -162.712 6.93007 -162.123 6.93007V5.86328ZM-162.123 4.36035V3.29357C-162.712 3.29357 -163.19 3.77118 -163.19 4.36035H-162.123ZM47.5625 4.36035H48.6293C48.6293 3.77118 48.1517 3.29357 47.5625 3.29357V4.36035ZM47.5605 12.9385V11.8717C37.174 11.8717 28.7537 20.2911 28.7535 30.6777L29.8203 30.6777L30.8871 30.6778C30.8873 21.4696 38.3522 14.0053 47.5605 14.0053V12.9385ZM29.8203 30.6777H28.7535V63.4736H29.8203H30.8871V30.6777H29.8203ZM29.8203 63.4736V62.4068H21.3857V63.4736V64.5404H29.8203V63.4736ZM21.3857 63.4736H22.4525V30.6777H21.3857H20.319V63.4736H21.3857ZM21.3857 30.6777L22.4525 30.6777C22.4523 20.2911 14.032 11.8717 3.64551 11.8717V12.9385V14.0053C12.8538 14.0053 20.3188 21.4696 20.319 30.6778L21.3857 30.6777ZM3.64551 12.9385H4.7123V12.9336H3.64551H2.57872V12.9385H3.64551ZM3.64551 12.9336V11.8668H-118.183V12.9336V14.0004H3.64551V12.9336ZM-118.183 12.9336H-119.249V12.9385H-118.183H-117.116V12.9336H-118.183ZM-118.183 12.9385L-118.183 11.8717C-128.569 11.8719 -136.988 20.2913 -136.989 30.6777L-135.922 30.6777L-134.855 30.6778C-134.855 21.4697 -127.391 14.0054 -118.183 14.0053L-118.183 12.9385ZM-135.922 30.6777H-136.989V63.4736H-135.922H-134.855V30.6777H-135.922ZM-135.922 63.4736V62.4068H-144.356V63.4736V64.5404H-135.922V63.4736ZM-144.356 63.4736H-143.29V30.6777H-144.356H-145.423V63.4736H-144.356ZM-144.356 30.6777L-143.29 30.6777C-143.29 20.2912 -151.71 11.8718 -162.097 11.8717L-162.097 12.9385L-162.097 14.0053C-152.888 14.0054 -145.423 21.4697 -145.423 30.6778L-144.356 30.6777ZM-162.097 12.9385H-161.03V6.79492H-162.097H-163.163V12.9385H-162.097ZM-162.097 6.79492V7.86171H47.5605V6.79492V5.72814H-162.097V6.79492ZM47.5605 6.79492H46.4938V12.9385H47.5605H48.6273V6.79492H47.5605ZM47.5625 5.86328V4.7965H-162.123V5.86328V6.93007H47.5625V5.86328ZM-162.123 5.86328H-161.056V4.36035H-162.123H-163.19V5.86328H-162.123ZM-162.123 4.36035V5.42714H47.5625V4.36035V3.29357H-162.123V4.36035ZM47.5625 4.36035H46.4957V5.86328H47.5625H48.6293V4.36035H47.5625Z" fill="#040404" fill-opacity="0.5" mask="url(#path-11-outside-3_450_11759)"/>
|
||||
<g filter="url(#filter3_f_450_11759)" style="mix-blend-mode:color-dodge">
|
||||
<path d="M36.3008 65.6075C42.4871 65.6075 47.502 70.6224 47.502 76.8087V94.4102H-162.654V76.8087C-162.654 70.6224 -157.639 65.6075 -151.453 65.6075H36.3008Z" stroke="url(#paint6_linear_450_11759)" stroke-width="1.06678"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_f_450_11759)" style="mix-blend-mode:plus-lighter">
|
||||
<mask id="path-14-outside-4_450_11759" maskUnits="userSpaceOnUse" x="-164.123" y="3.33496" width="212" height="61" fill="black">
|
||||
<rect fill="white" x="-164.123" y="3.33496" width="212" height="61"/>
|
||||
<path d="M45.9014 5.33496V11.4834C36.1803 11.4836 28.2998 19.3649 28.2998 29.0859V62.9404H19.9307V29.0859C19.9307 19.7628 12.6825 12.1337 3.51367 11.5244H-119.734C-128.903 12.1336 -136.152 19.7628 -136.152 29.0859V62.9404H-144.521V29.0859C-144.521 19.3647 -152.402 11.4834 -162.123 11.4834V5.33496H45.9014Z"/>
|
||||
</mask>
|
||||
<path d="M45.9014 5.33496H46.9682V4.26818H45.9014V5.33496ZM45.9014 11.4834L45.9014 12.5502L46.9682 12.5502V11.4834H45.9014ZM28.2998 62.9404V64.0072H29.3666V62.9404H28.2998ZM19.9307 62.9404H18.8639V64.0072H19.9307V62.9404ZM3.51367 11.5244L3.5844 10.46L3.54907 10.4576H3.51367V11.5244ZM-119.734 11.5244V10.4576H-119.77L-119.805 10.46L-119.734 11.5244ZM-136.152 62.9404V64.0072H-135.086V62.9404H-136.152ZM-144.521 62.9404H-145.587V64.0072H-144.521V62.9404ZM-162.123 11.4834H-163.19V12.5502H-162.123V11.4834ZM-162.123 5.33496V4.26818H-163.19V5.33496H-162.123ZM45.9014 5.33496H44.8346V11.4834H45.9014H46.9682V5.33496H45.9014ZM45.9014 11.4834L45.9014 10.4166C35.5911 10.4168 27.233 18.7757 27.233 29.0859H28.2998H29.3666C29.3666 19.954 36.7695 12.5504 45.9014 12.5502L45.9014 11.4834ZM28.2998 29.0859H27.233V62.9404H28.2998H29.3666V29.0859H28.2998ZM28.2998 62.9404V61.8736H19.9307V62.9404V64.0072H28.2998V62.9404ZM19.9307 62.9404H20.9975V29.0859H19.9307H18.8639V62.9404H19.9307ZM19.9307 29.0859H20.9975C20.9975 19.1973 13.3099 11.1062 3.5844 10.46L3.51367 11.5244L3.44295 12.5889C12.0551 13.1611 18.8639 20.3284 18.8639 29.0859H19.9307ZM3.51367 11.5244V10.4576H-119.734V11.5244V12.5912H3.51367V11.5244ZM-119.734 11.5244L-119.805 10.46C-129.531 11.1061 -137.219 19.1972 -137.219 29.0859H-136.152H-135.086C-135.086 20.3284 -128.276 13.1611 -119.664 12.5889L-119.734 11.5244ZM-136.152 29.0859H-137.219V62.9404H-136.152H-135.086V29.0859H-136.152ZM-136.152 62.9404V61.8736H-144.521V62.9404V64.0072H-136.152V62.9404ZM-144.521 62.9404H-143.454V29.0859H-144.521H-145.587V62.9404H-144.521ZM-144.521 29.0859H-143.454C-143.454 18.7756 -151.813 10.4166 -162.123 10.4166V11.4834V12.5502C-152.991 12.5502 -145.587 19.9539 -145.587 29.0859H-144.521ZM-162.123 11.4834H-161.056V5.33496H-162.123H-163.19V11.4834H-162.123ZM-162.123 5.33496V6.40175H45.9014V5.33496V4.26818H-162.123V5.33496Z" fill="url(#paint7_linear_450_11759)" mask="url(#path-14-outside-4_450_11759)"/>
|
||||
</g>
|
||||
<g filter="url(#filter5_f_450_11759)" style="mix-blend-mode:plus-lighter">
|
||||
<mask id="path-16-outside-5_450_11759" maskUnits="userSpaceOnUse" x="-163.055" y="3.33496" width="211" height="61" fill="black">
|
||||
<rect fill="white" x="-163.055" y="3.33496" width="211" height="61"/>
|
||||
<path d="M44.8385 5.33496C45.4277 5.33496 45.9053 5.81258 45.9053 6.40175V10.4166C45.9053 11.0058 45.4264 11.48 44.8383 11.5154C35.6638 12.0668 28.3936 19.6821 28.3936 28.9951V61.8736C28.3936 62.4628 27.9159 62.9404 27.3268 62.9404H21.1342C20.545 62.9404 20.0674 62.4628 20.0674 61.8736V28.9951C20.0673 19.7198 12.8562 12.1305 3.73438 11.5244H-118.884C-128.006 12.1304 -135.217 19.7198 -135.217 28.9951V61.8736C-135.217 62.4628 -135.694 62.9404 -136.284 62.9404H-142.476C-143.065 62.9404 -143.543 62.4628 -143.543 61.8736V28.9951C-143.543 19.6821 -150.813 12.0668 -159.988 11.5154C-160.576 11.48 -161.055 11.0058 -161.055 10.4166V6.40175C-161.055 5.81258 -160.577 5.33496 -159.988 5.33496H44.8385Z"/>
|
||||
</mask>
|
||||
<path d="M28.3936 28.9951L27.3268 28.9951V28.9951H28.3936ZM20.0674 28.9951H21.1342V28.9951L20.0674 28.9951ZM3.73438 11.5244L3.8051 10.46L3.76978 10.4576H3.73438V11.5244ZM-118.884 11.5244V10.4576H-118.919L-118.955 10.46L-118.884 11.5244ZM-135.217 28.9951L-136.284 28.9951V28.9951H-135.217ZM-143.543 28.9951H-142.476L-142.476 28.9951L-143.543 28.9951ZM45.9053 6.40175H44.8385V10.4166H45.9053H46.9721V6.40175H45.9053ZM44.8383 11.5154L44.7743 10.4505C35.04 11.0356 27.3268 19.1144 27.3268 28.9951L28.3936 28.9951L29.4603 28.9951C29.4604 20.2498 36.2878 13.098 44.9023 12.5802L44.8383 11.5154ZM28.3936 28.9951H27.3268V61.8736H28.3936H29.4603V28.9951H28.3936ZM27.3268 62.9404V61.8736H21.1342V62.9404V64.0072H27.3268V62.9404ZM20.0674 61.8736H21.1342V28.9951H20.0674H19.0006V61.8736H20.0674ZM20.0674 28.9951L21.1342 28.9951C21.1341 19.1542 13.4836 11.103 3.8051 10.46L3.73438 11.5244L3.66365 12.5889C12.2289 13.1579 19.0005 20.2854 19.0006 28.9951L20.0674 28.9951ZM3.73438 11.5244V10.4576H-118.884V11.5244V12.5912H3.73438V11.5244ZM-118.884 11.5244L-118.955 10.46C-128.633 11.103 -136.284 19.1542 -136.284 28.9951L-135.217 28.9951L-134.15 28.9951C-134.15 20.2854 -127.378 13.1579 -118.813 12.5889L-118.884 11.5244ZM-135.217 28.9951H-136.284V61.8736H-135.217H-134.15V28.9951H-135.217ZM-136.284 62.9404V61.8736H-142.476V62.9404V64.0072H-136.284V62.9404ZM-143.543 61.8736H-142.476V28.9951H-143.543H-144.61V61.8736H-143.543ZM-143.543 28.9951L-142.476 28.9951C-142.476 19.1144 -150.189 11.0356 -159.924 10.4505L-159.988 11.5154L-160.052 12.5802C-151.437 13.098 -144.61 20.2498 -144.61 28.9951L-143.543 28.9951ZM-161.055 10.4166H-159.988V6.40175H-161.055H-162.121V10.4166H-161.055ZM-159.988 5.33496V6.40175H44.8385V5.33496V4.26818H-159.988V5.33496ZM-161.055 6.40175H-159.988V6.40175V5.33496V4.26818C-161.166 4.26818 -162.121 5.22341 -162.121 6.40175H-161.055ZM-142.476 62.9404V61.8736H-142.476H-143.543H-144.61C-144.61 63.052 -143.655 64.0072 -142.476 64.0072V62.9404ZM-135.217 61.8736H-136.284H-136.284V62.9404V64.0072C-135.105 64.0072 -134.15 63.052 -134.15 61.8736H-135.217ZM21.1342 62.9404V61.8736V61.8736H20.0674H19.0006C19.0006 63.052 19.9558 64.0072 21.1342 64.0072V62.9404ZM28.3936 61.8736H27.3268V61.8736V62.9404V64.0072C28.5051 64.0072 29.4603 63.052 29.4603 61.8736H28.3936ZM-159.988 11.5154L-159.924 10.4505C-159.949 10.449 -159.969 10.4383 -159.981 10.426C-159.986 10.4204 -159.988 10.4166 -159.988 10.4163C-159.988 10.4162 -159.988 10.4165 -159.988 10.4171C-159.988 10.4178 -159.988 10.4178 -159.988 10.4166H-161.055H-162.121C-162.121 11.6112 -161.161 12.5136 -160.052 12.5802L-159.988 11.5154ZM45.9053 10.4166H44.8385C44.8385 10.4178 44.8384 10.4178 44.8385 10.4171C44.8387 10.4165 44.8388 10.4162 44.8387 10.4163C44.8386 10.4166 44.837 10.4204 44.8315 10.426C44.8193 10.4383 44.7993 10.449 44.7743 10.4505L44.8383 11.5154L44.9023 12.5802C46.0117 12.5136 46.9721 11.6112 46.9721 10.4166H45.9053ZM45.9053 6.40175H46.9721C46.9721 5.22341 46.0168 4.26818 44.8385 4.26818V5.33496V6.40175V6.40175H45.9053Z" fill="url(#paint8_linear_450_11759)" mask="url(#path-16-outside-5_450_11759)"/>
|
||||
</g>
|
||||
<g filter="url(#filter6_f_450_11759)">
|
||||
<path d="M-164.256 13.8682V4.26712C-164.256 3.08878 -163.301 2.13354 -162.122 2.13354H48.0343C49.2127 2.13354 50.1679 3.08878 50.1679 4.26711V13.8682" stroke="url(#paint9_linear_450_11759)" stroke-width="1.06678"/>
|
||||
</g>
|
||||
<g filter="url(#filter7_f_450_11759)">
|
||||
<path d="M21.8999 63.4737V29.3366C21.8999 22.9359 16.0325 12.268 2.69773 12.268C-8.82355 12.268 -31.795 12.268 -42.6406 12.268" stroke="url(#paint10_linear_450_11759)" style="mix-blend-mode:plus-lighter" stroke-width="1.06678"/>
|
||||
</g>
|
||||
<g filter="url(#filter8_f_450_11759)">
|
||||
<path d="M-133.318 62.9403V26.9541C-132.963 22.5922 -129.585 15.4684 -119.45 13.8682H1.62993C11.231 14.4016 18.1651 18.2302 18.1651 31.3161V62.9403" stroke="url(#paint11_linear_450_11759)" stroke-width="1.06678"/>
|
||||
</g>
|
||||
<line x1="-148.197" y1="64.1475" x2="33.7176" y2="64.1475" stroke="#400500" stroke-width="1.06678"/>
|
||||
<g filter="url(#filter9_f_450_11759)">
|
||||
<path d="M22.4316 45.8718V62.4069H29.3657V45.8718" stroke="url(#paint12_linear_450_11759)" style="mix-blend-mode:plus-lighter" stroke-width="1.06678"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_450_11759" x="-132.027" y="17.1808" width="150.641" height="49.5774" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="1.06678" dy="1.06678"/>
|
||||
<feGaussianBlur stdDeviation="0.533392"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.49 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_450_11759"/>
|
||||
</filter>
|
||||
<filter id="filter1_ii_450_11759" x="-160.824" y="66.2541" width="208.147" height="27.7161" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="1.06678" dy="1.06678"/>
|
||||
<feGaussianBlur stdDeviation="0.533392"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.929412 0 0 0 0 0.556863 0 0 0 0 0.266667 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_450_11759"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-2.13357"/>
|
||||
<feGaussianBlur stdDeviation="0.533392"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.322115 0 0 0 0 0.142276 0 0 0 0 0.0263267 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="effect1_innerShadow_450_11759" result="effect2_innerShadow_450_11759"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_450_11759" x="-159.989" y="68.2742" width="205.889" height="25.6028" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11759"/>
|
||||
</filter>
|
||||
<filter id="filter3_f_450_11759" x="-164.254" y="64.0071" width="213.356" height="32.0035" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11759"/>
|
||||
</filter>
|
||||
<filter id="filter4_f_450_11759" x="-164.256" y="3.2014" width="212.292" height="61.8726" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11759"/>
|
||||
</filter>
|
||||
<filter id="filter5_f_450_11759" x="-163.188" y="3.2014" width="211.227" height="61.8726" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11759"/>
|
||||
</filter>
|
||||
<filter id="filter6_f_450_11759" x="-165.856" y="0.533374" width="217.624" height="14.4016" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11759"/>
|
||||
</filter>
|
||||
<filter id="filter7_f_450_11759" x="-43.7074" y="10.6678" width="67.2078" height="53.8726" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11759"/>
|
||||
</filter>
|
||||
<filter id="filter8_f_450_11759" x="-134.918" y="12.2681" width="154.684" height="51.739" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11759"/>
|
||||
</filter>
|
||||
<filter id="filter9_f_450_11759" x="20.8317" y="44.805" width="10.1336" height="19.2021" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11759"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_450_11759" x1="-57.2401" y1="17.1808" x2="-57.2401" y2="65.6915" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#442111"/>
|
||||
<stop offset="0.05" stop-color="#522510"/>
|
||||
<stop offset="1" stop-color="#090504"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_450_11759" x1="-134.698" y1="34.5814" x2="17.5471" y2="34.5814" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#803414"/>
|
||||
<stop offset="0.077962" stop-color="#803414" stop-opacity="0.25"/>
|
||||
<stop offset="0.939742" stop-color="#803414" stop-opacity="0.282099"/>
|
||||
<stop offset="1" stop-color="#803414"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_450_11759" x1="-57.2846" y1="68.3877" x2="-57.2846" y2="91.8366" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#AD571E"/>
|
||||
<stop offset="1" stop-color="#88441A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_450_11759" x1="-57.2846" y1="68.3877" x2="-57.2846" y2="91.8366" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#AD571E"/>
|
||||
<stop offset="1" stop-color="#88441A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_450_11759" x1="-57.0443" y1="70.4077" x2="-57.0443" y2="91.7434" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFC893"/>
|
||||
<stop offset="1" stop-color="#FFC893" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_450_11759" x1="-162.123" y1="33.9171" x2="47.5627" y2="33.9171" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFAC63"/>
|
||||
<stop offset="0.144231" stop-color="#FF7E0D"/>
|
||||
<stop offset="0.889423" stop-color="#FF7E0D"/>
|
||||
<stop offset="0.961538" stop-color="#EB5200"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_450_11759" x1="-57.5762" y1="66.1407" x2="-57.5762" y2="93.8771" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FDF7CC"/>
|
||||
<stop offset="1" stop-color="#FDF7CC" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_450_11759" x1="-58.1108" y1="5.33496" x2="-58.1108" y2="14.4016" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E5A76C"/>
|
||||
<stop offset="1" stop-color="#FFC893" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_450_11759" x1="-147.788" y1="12.268" x2="-91.993" y2="33.9432" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E5A76C"/>
|
||||
<stop offset="1" stop-color="#FFC893" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_450_11759" x1="-57.044" y1="2.13354" x2="-57.044" y2="13.8682" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#C17300"/>
|
||||
<stop offset="1" stop-color="#C17300" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_450_11759" x1="14.9658" y1="16.0018" x2="2.51952" y2="22.2738" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFC034"/>
|
||||
<stop offset="1" stop-color="#FFC034" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear_450_11759" x1="-57.5766" y1="13.8682" x2="-57.5766" y2="62.9403" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D59628"/>
|
||||
<stop offset="1" stop-color="#AD6E00" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint12_linear_450_11759" x1="24.5652" y1="61.8735" x2="24.0318" y2="36.2707" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFC034"/>
|
||||
<stop offset="1" stop-color="#FFC034" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_450_11759">
|
||||
<rect width="50" height="95" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_450_11759">
|
||||
<rect width="216.389" height="95" fill="white" transform="translate(-166.389)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,185 @@
|
||||
<svg width="50" height="95" viewBox="0 0 50 95" fill="none" shape-rendering="geometricPrecision" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_450_11683)">
|
||||
<g clip-path="url(#clip1_450_11683)">
|
||||
<g filter="url(#filter0_i_450_11683)">
|
||||
<path d="M34.3613 29.9823C34.3613 22.9122 40.0927 17.1808 47.1627 17.1808H171.134C178.204 17.1808 183.936 22.9122 183.936 29.9823V65.6915H34.3613V29.9823Z" fill="url(#paint0_linear_450_11683)"/>
|
||||
<path d="M34.3613 29.9823C34.3613 22.9122 40.0927 17.1808 47.1627 17.1808H171.134C178.204 17.1808 183.936 22.9122 183.936 29.9823V65.6915H34.3613V29.9823Z" fill="url(#paint1_linear_450_11683)"/>
|
||||
</g>
|
||||
<path d="M47.1631 17.7141H171.135C177.91 17.7143 183.402 23.2073 183.402 29.9826V65.1584H34.8945V29.9826C34.8945 23.2072 40.3876 17.7141 47.1631 17.7141Z" stroke="black" stroke-width="1.06678"/>
|
||||
<path d="M203.043 66.2539C208.935 66.2539 213.711 71.0302 213.711 76.9219V93.9707H4.49707V76.9219C4.49712 71.0303 9.27343 66.254 15.165 66.2539H203.043Z" fill="url(#paint2_linear_450_11683)" stroke="#400500" stroke-width="4.26714"/>
|
||||
<g filter="url(#filter1_ii_450_11683)">
|
||||
<path d="M6.63086 76.922C6.63086 72.2086 10.4518 68.3877 15.1651 68.3877H203.043C207.756 68.3877 211.577 72.2086 211.577 76.922V90.7699C211.577 91.359 211.1 91.8366 210.511 91.8366H7.69765C7.10848 91.8366 6.63086 91.359 6.63086 90.7699V76.922Z" fill="url(#paint3_linear_450_11683)"/>
|
||||
</g>
|
||||
<path d="M203.043 67.8545C208.051 67.8545 212.11 71.914 212.11 76.9219V90.7695C212.11 91.6532 211.394 92.37 210.511 92.3701H7.69727C6.81369 92.3699 6.09766 91.6532 6.09766 90.7695V76.9219C6.09771 71.914 10.1572 67.8545 15.165 67.8545H203.043Z" stroke="black" stroke-opacity="0.76" stroke-width="1.06678"/>
|
||||
<g filter="url(#filter2_f_450_11683)" style="mix-blend-mode:plus-lighter">
|
||||
<path d="M203.755 69.8745C207.584 69.8745 210.688 72.9787 210.688 76.8081V92.2769H8V76.8081C8.00017 72.9788 11.1042 69.8747 14.9336 69.8745H203.755Z" stroke="url(#paint4_linear_450_11683)" stroke-width="1.06678"/>
|
||||
</g>
|
||||
<mask id="path-7-outside-1_450_11683" maskUnits="userSpaceOnUse" x="-0.734375" y="-0.639648" width="219" height="69" fill="black">
|
||||
<rect fill="white" x="-0.734375" y="-0.639648" width="219" height="69"/>
|
||||
<path d="M213.949 12.9385C204.152 12.9385 196.209 20.8804 196.209 30.6777V63.4736H187.774V30.6777C187.774 20.8804 179.832 12.9385 170.034 12.9385V12.9336H48.2061V12.9385C38.4088 12.9387 30.467 20.8805 30.4668 30.6777V63.4736H22.0322V30.6777C22.032 20.8804 14.0893 12.9386 4.29199 12.9385V6.79492H213.949V12.9385ZM213.951 5.86328H4.26562V4.36035H213.951V5.86328Z"/>
|
||||
</mask>
|
||||
<path d="M213.949 12.9385C204.152 12.9385 196.209 20.8804 196.209 30.6777V63.4736H187.774V30.6777C187.774 20.8804 179.832 12.9385 170.034 12.9385V12.9336H48.2061V12.9385C38.4088 12.9387 30.467 20.8805 30.4668 30.6777V63.4736H22.0322V30.6777C22.032 20.8804 14.0893 12.9386 4.29199 12.9385V6.79492H213.949V12.9385ZM213.951 5.86328H4.26562V4.36035H213.951V5.86328Z" fill="#FF7700"/>
|
||||
<path d="M213.949 12.9385V17.2056C216.306 17.2056 218.216 15.2952 218.216 12.9385H213.949ZM196.209 30.6777L191.942 30.6776V30.6777H196.209ZM196.209 63.4736V67.7408C198.566 67.7408 200.476 65.8303 200.476 63.4736H196.209ZM187.774 63.4736H183.507C183.507 65.8303 185.418 67.7408 187.774 67.7408V63.4736ZM187.774 30.6777H192.042V30.6777L187.774 30.6777ZM170.034 12.9385H165.767C165.767 15.2952 167.678 17.2056 170.034 17.2056V12.9385ZM170.034 12.9336H174.301C174.301 10.5769 172.391 8.66645 170.034 8.66645V12.9336ZM48.2061 12.9336V8.66645C45.8494 8.66645 43.9389 10.5769 43.9389 12.9336H48.2061ZM48.2061 12.9385L48.2061 17.2056C50.5628 17.2056 52.4732 15.2951 52.4732 12.9385H48.2061ZM30.4668 30.6777L26.1997 30.6776V30.6777H30.4668ZM30.4668 63.4736V67.7408C32.8235 67.7408 34.7339 65.8303 34.7339 63.4736H30.4668ZM22.0322 63.4736H17.7651C17.7651 65.8303 19.6756 67.7408 22.0322 67.7408V63.4736ZM22.0322 30.6777H26.2994V30.6777L22.0322 30.6777ZM4.29199 12.9385H0.0248528C0.0248528 15.2951 1.93528 17.2056 4.29194 17.2056L4.29199 12.9385ZM4.29199 6.79492V2.52778C1.93532 2.52778 0.0248528 4.43825 0.0248528 6.79492H4.29199ZM213.949 6.79492H218.216C218.216 4.43825 216.306 2.52778 213.949 2.52778V6.79492ZM213.951 5.86328V10.1304C216.308 10.1304 218.218 8.21996 218.218 5.86328H213.951ZM4.26562 5.86328H-0.00151443C-0.00151443 8.21996 1.90895 10.1304 4.26563 10.1304L4.26562 5.86328ZM4.26562 4.36035V0.0932121C1.90895 0.0932121 -0.00151443 2.00368 -0.00151443 4.36035L4.26562 4.36035ZM213.951 4.36035H218.218C218.218 2.00368 216.308 0.0932121 213.951 0.0932121V4.36035ZM213.949 12.9385V8.67134C201.795 8.67134 191.942 18.5235 191.942 30.6776L196.209 30.6777L200.476 30.6778C200.476 23.2373 206.508 17.2056 213.949 17.2056V12.9385ZM196.209 30.6777H191.942V63.4736H196.209H200.476V30.6777H196.209ZM196.209 63.4736V59.2065H187.774V63.4736V67.7408H196.209V63.4736ZM187.774 63.4736H192.042V30.6777H187.774H183.507V63.4736H187.774ZM187.774 30.6777L192.042 30.6777C192.041 18.5234 182.188 8.67134 170.034 8.67134V12.9385V17.2056C177.475 17.2056 183.507 23.2373 183.507 30.6778L187.774 30.6777ZM170.034 12.9385H174.301V12.9336H170.034H165.767V12.9385H170.034ZM170.034 12.9336V8.66645H48.2061V12.9336V17.2007H170.034V12.9336ZM48.2061 12.9336H43.9389V12.9385H48.2061H52.4732V12.9336H48.2061ZM48.2061 12.9385L48.206 8.67134C36.0521 8.67158 26.1999 18.5238 26.1997 30.6776L30.4668 30.6777L34.7339 30.6778C34.7341 23.2372 40.7655 17.2058 48.2061 17.2056L48.2061 12.9385ZM30.4668 30.6777H26.1997V63.4736H30.4668H34.7339V30.6777H30.4668ZM30.4668 63.4736V59.2065H22.0322V63.4736V67.7408H30.4668V63.4736ZM22.0322 63.4736H26.2994V30.6777H22.0322H17.7651V63.4736H22.0322ZM22.0322 30.6777L26.2994 30.6777C26.2991 18.5235 16.4457 8.67149 4.29205 8.67134L4.29199 12.9385L4.29194 17.2056C11.7329 17.2057 17.7649 23.2374 17.7651 30.6778L22.0322 30.6777ZM4.29199 12.9385H8.55913V6.79492H4.29199H0.0248528V12.9385H4.29199ZM4.29199 6.79492V11.0621H213.949V6.79492V2.52778H4.29199V6.79492ZM213.949 6.79492H209.682V12.9385H213.949H218.216V6.79492H213.949ZM213.951 5.86328V1.59614H4.26562V5.86328V10.1304H213.951V5.86328ZM4.26562 5.86328H8.53276V4.36035H4.26562H-0.00151443V5.86328H4.26562ZM4.26562 4.36035V8.62749H213.951V4.36035V0.0932121H4.26562V4.36035ZM213.951 4.36035H209.684V5.86328H213.951H218.218V4.36035H213.951Z" fill="#400500" mask="url(#path-7-outside-1_450_11683)"/>
|
||||
<mask id="path-9-outside-2_450_11683" maskUnits="userSpaceOnUse" x="-0.734375" y="-0.639648" width="219" height="69" fill="black">
|
||||
<rect fill="white" x="-0.734375" y="-0.639648" width="219" height="69"/>
|
||||
<path d="M213.949 11.8717C213.949 12.4609 213.47 12.9351 212.882 12.97C203.582 13.5217 196.209 21.2386 196.209 30.6777V62.4068C196.209 62.996 195.731 63.4736 195.142 63.4736H188.841C188.252 63.4736 187.774 62.996 187.774 62.4068V30.6777C187.774 20.8812 179.833 12.9398 170.037 12.9385C170.035 12.9385 170.034 12.9374 170.034 12.936C170.034 12.9347 170.033 12.9336 170.032 12.9336H48.2085C48.2071 12.9336 48.2061 12.9347 48.2061 12.936C48.2061 12.9374 48.2049 12.9385 48.2036 12.9385C38.4075 12.94 30.467 20.8813 30.4668 30.6777V62.4068C30.4668 62.996 29.9892 63.4736 29.4 63.4736H23.099C22.5098 63.4736 22.0322 62.996 22.0322 62.4068V30.6777C22.032 21.2387 14.6595 13.5218 5.3589 12.97C4.77077 12.9352 4.29199 12.4609 4.29199 11.8717V7.86171C4.29199 7.27254 4.76961 6.79492 5.35878 6.79492H212.882C213.472 6.79492 213.949 7.27254 213.949 7.86171V11.8717ZM213.951 5.11182C213.951 5.52684 213.615 5.86328 213.2 5.86328H5.01709C4.60207 5.86328 4.26562 5.52684 4.26562 5.11182C4.26562 4.69679 4.60207 4.36035 5.01709 4.36035H213.2C213.615 4.36035 213.951 4.69679 213.951 5.11182Z"/>
|
||||
</mask>
|
||||
<path d="M213.949 11.8717C213.949 12.4609 213.47 12.9351 212.882 12.97C203.582 13.5217 196.209 21.2386 196.209 30.6777V62.4068C196.209 62.996 195.731 63.4736 195.142 63.4736H188.841C188.252 63.4736 187.774 62.996 187.774 62.4068V30.6777C187.774 20.8812 179.833 12.9398 170.037 12.9385C170.035 12.9385 170.034 12.9374 170.034 12.936C170.034 12.9347 170.033 12.9336 170.032 12.9336H48.2085C48.2071 12.9336 48.2061 12.9347 48.2061 12.936C48.2061 12.9374 48.2049 12.9385 48.2036 12.9385C38.4075 12.94 30.467 20.8813 30.4668 30.6777V62.4068C30.4668 62.996 29.9892 63.4736 29.4 63.4736H23.099C22.5098 63.4736 22.0322 62.996 22.0322 62.4068V30.6777C22.032 21.2387 14.6595 13.5218 5.3589 12.97C4.77077 12.9352 4.29199 12.4609 4.29199 11.8717V7.86171C4.29199 7.27254 4.76961 6.79492 5.35878 6.79492H212.882C213.472 6.79492 213.949 7.27254 213.949 7.86171V11.8717ZM213.951 5.11182C213.951 5.52684 213.615 5.86328 213.2 5.86328H5.01709C4.60207 5.86328 4.26562 5.52684 4.26562 5.11182C4.26562 4.69679 4.60207 4.36035 5.01709 4.36035H213.2C213.615 4.36035 213.951 4.69679 213.951 5.11182Z" fill="url(#paint5_linear_450_11683)"/>
|
||||
<path d="M196.209 30.6777L191.942 30.6776V30.6777H196.209ZM187.774 30.6777H192.042V30.6777L187.774 30.6777ZM30.4668 30.6777L26.1997 30.6776V30.6777H30.4668ZM22.0322 30.6777H26.2994V30.6777L22.0322 30.6777ZM48.2036 12.9385L48.2043 17.2056L48.2036 12.9385ZM5.3589 12.97L5.61162 8.71039L5.3589 12.97ZM170.037 12.9385L170.036 17.2056L170.037 12.9385ZM212.882 12.97L212.63 8.71038L212.882 12.97ZM212.882 12.97L212.63 8.71038C201.088 9.39498 191.942 18.9663 191.942 30.6776L196.209 30.6777L200.476 30.6778C200.476 23.5108 206.075 17.6484 213.135 17.2297L212.882 12.97ZM196.209 30.6777H191.942V62.4068H196.209H200.476V30.6777H196.209ZM195.142 63.4736V59.2065H188.841V63.4736V67.7408H195.142V63.4736ZM187.774 62.4068H192.042V30.6777H187.774H183.507V62.4068H187.774ZM187.774 30.6777L192.042 30.6777C192.041 18.5245 182.19 8.67298 170.037 8.67134L170.037 12.9385L170.036 17.2056C177.476 17.2066 183.507 23.2379 183.507 30.6778L187.774 30.6777ZM170.032 12.9336V8.66645H48.2085V12.9336V17.2007H170.032V12.9336ZM48.2036 12.9385L48.2029 8.67134C36.0505 8.67322 26.1999 18.5248 26.1997 30.6776L30.4668 30.6777L34.7339 30.6778C34.7341 23.2378 40.7645 17.2068 48.2043 17.2056L48.2036 12.9385ZM30.4668 30.6777H26.1997V62.4068H30.4668H34.7339V30.6777H30.4668ZM29.4 63.4736V59.2065H23.099V63.4736V67.7408H29.4V63.4736ZM22.0322 62.4068H26.2994V30.6777H22.0322H17.7651V62.4068H22.0322ZM22.0322 30.6777L26.2994 30.6777C26.2991 18.9663 17.153 9.39512 5.61162 8.71039L5.3589 12.97L5.10619 17.2297C12.166 17.6485 17.765 23.511 17.7651 30.6778L22.0322 30.6777ZM4.29199 11.8717H8.55913V7.86171H4.29199H0.0248528V11.8717H4.29199ZM5.35878 6.79492V11.0621H212.882V6.79492V2.52778H5.35878V6.79492ZM213.949 7.86171H209.682V11.8717H213.949H218.216V7.86171H213.949ZM213.2 5.86328V1.59614H5.01709V5.86328V10.1304H213.2V5.86328ZM5.01709 4.36035V8.62749H213.2V4.36035V0.0932121H5.01709V4.36035ZM213.2 4.36035V8.62749C211.258 8.62749 209.684 7.0535 209.684 5.11182H213.951H218.218C218.218 2.34009 215.971 0.0932121 213.2 0.0932121V4.36035ZM4.26562 5.11182H8.53276C8.53276 7.05347 6.95874 8.62749 5.01709 8.62749V4.36035V0.0932121C2.24539 0.0932121 -0.00151443 2.34012 -0.00151443 5.11182H4.26562ZM5.01709 5.86328V1.59614C6.95872 1.59614 8.53276 3.17013 8.53276 5.11182H4.26562H-0.00151443C-0.00151443 7.88354 2.24542 10.1304 5.01709 10.1304V5.86328ZM213.2 5.86328V10.1304C215.971 10.1304 218.218 7.88351 218.218 5.11182H213.951H209.684C209.684 3.17016 211.258 1.59614 213.2 1.59614V5.86328ZM212.882 6.79492V11.0621C211.115 11.0621 209.682 9.62925 209.682 7.86171H213.949H218.216C218.216 4.91582 215.828 2.52778 212.882 2.52778V6.79492ZM30.4668 62.4068H26.1997C26.1997 60.6393 27.6325 59.2065 29.4 59.2065V63.4736V67.7408C32.3459 67.7408 34.7339 65.3527 34.7339 62.4068H30.4668ZM48.2061 12.936H43.9389C43.9389 10.5484 45.8805 8.6717 48.2029 8.67134L48.2036 12.9385L48.2043 17.2056C50.5294 17.2053 52.4732 15.3263 52.4732 12.936H48.2061ZM48.2085 12.9336V8.66645C45.8469 8.66645 43.9389 10.5816 43.9389 12.936H48.2061H52.4732C52.4732 15.2878 50.5674 17.2007 48.2085 17.2007V12.9336ZM170.034 12.936H174.301C174.301 10.578 172.39 8.66645 170.032 8.66645V12.9336V17.2007C167.676 17.2007 165.767 15.2914 165.767 12.936H170.034ZM4.29199 7.86171H8.55913C8.55913 9.62921 7.12628 11.0621 5.35878 11.0621V6.79492V2.52778C2.41293 2.52778 0.0248528 4.91586 0.0248528 7.86171H4.29199ZM188.841 63.4736V59.2065C190.609 59.2065 192.042 60.6393 192.042 62.4068H187.774H183.507C183.507 65.3527 185.895 67.7408 188.841 67.7408V63.4736ZM5.3589 12.97L5.61162 8.71039C7.11239 8.79943 8.55913 10.0401 8.55913 11.8717H4.29199H0.0248528C0.0248528 14.8817 2.42916 17.0709 5.10619 17.2297L5.3589 12.97ZM196.209 62.4068H191.942C191.942 60.6393 193.375 59.2065 195.142 59.2065V63.4736V67.7408C198.088 67.7408 200.476 65.3527 200.476 62.4068H196.209ZM170.037 12.9385L170.037 8.67134C172.358 8.67165 174.301 10.5472 174.301 12.936H170.034H165.767C165.767 15.3275 167.712 17.2053 170.036 17.2056L170.037 12.9385ZM23.099 63.4736V59.2065C24.8665 59.2065 26.2994 60.6393 26.2994 62.4068H22.0322H17.7651C17.7651 65.3527 20.1532 67.7408 23.099 67.7408V63.4736ZM212.882 12.97L213.135 17.2297C215.812 17.0709 218.216 14.8817 218.216 11.8717H213.949H209.682C209.682 10.04 211.129 8.7994 212.63 8.71038L212.882 12.97Z" fill="#400500" mask="url(#path-9-outside-2_450_11683)"/>
|
||||
<mask id="path-11-outside-3_450_11683" maskUnits="userSpaceOnUse" x="2.26562" y="2.36035" width="213" height="63" fill="black">
|
||||
<rect fill="white" x="2.26562" y="2.36035" width="213" height="63"/>
|
||||
<path d="M213.949 12.9385C204.152 12.9385 196.209 20.8804 196.209 30.6777V63.4736H187.774V30.6777C187.774 20.8804 179.832 12.9385 170.034 12.9385V12.9336H48.2061V12.9385C38.4088 12.9387 30.467 20.8805 30.4668 30.6777V63.4736H22.0322V30.6777C22.032 20.8804 14.0893 12.9386 4.29199 12.9385V6.79492H213.949V12.9385ZM213.951 5.86328H4.26562V4.36035H213.951V5.86328Z"/>
|
||||
</mask>
|
||||
<path d="M213.949 12.9385V14.0053C214.538 14.0053 215.016 13.5276 215.016 12.9385H213.949ZM196.209 30.6777L195.142 30.6777V30.6777H196.209ZM196.209 63.4736V64.5404C196.798 64.5404 197.276 64.0628 197.276 63.4736H196.209ZM187.774 63.4736H186.708C186.708 64.0628 187.185 64.5404 187.774 64.5404V63.4736ZM187.774 30.6777H188.841V30.6777L187.774 30.6777ZM170.034 12.9385H168.967C168.967 13.5276 169.445 14.0053 170.034 14.0053V12.9385ZM170.034 12.9336H171.101C171.101 12.3444 170.623 11.8668 170.034 11.8668V12.9336ZM48.2061 12.9336V11.8668C47.6169 11.8668 47.1393 12.3444 47.1393 12.9336H48.2061ZM48.2061 12.9385L48.2061 14.0053C48.7952 14.0052 49.2728 13.5276 49.2728 12.9385H48.2061ZM30.4668 30.6777L29.4 30.6777V30.6777H30.4668ZM30.4668 63.4736V64.5404C31.056 64.5404 31.5336 64.0628 31.5336 63.4736H30.4668ZM22.0322 63.4736H20.9654C20.9654 64.0628 21.4431 64.5404 22.0322 64.5404V63.4736ZM22.0322 30.6777H23.099L23.099 30.6777L22.0322 30.6777ZM4.29199 12.9385H3.22521C3.22521 13.5276 3.70281 14.0053 4.29198 14.0053L4.29199 12.9385ZM4.29199 6.79492V5.72814C3.70282 5.72814 3.22521 6.20575 3.22521 6.79492H4.29199ZM213.949 6.79492H215.016C215.016 6.20575 214.538 5.72814 213.949 5.72814V6.79492ZM213.951 5.86328V6.93007C214.54 6.93007 215.018 6.45245 215.018 5.86328H213.951ZM4.26562 5.86328H3.19884C3.19884 6.45245 3.67646 6.93007 4.26562 6.93007V5.86328ZM4.26562 4.36035V3.29357C3.67646 3.29357 3.19884 3.77118 3.19884 4.36035H4.26562ZM213.951 4.36035H215.018C215.018 3.77118 214.54 3.29357 213.951 3.29357V4.36035ZM213.949 12.9385V11.8717C203.563 11.8717 195.142 20.2911 195.142 30.6777L196.209 30.6777L197.276 30.6778C197.276 21.4696 204.741 14.0053 213.949 14.0053V12.9385ZM196.209 30.6777H195.142V63.4736H196.209H197.276V30.6777H196.209ZM196.209 63.4736V62.4068H187.774V63.4736V64.5404H196.209V63.4736ZM187.774 63.4736H188.841V30.6777H187.774H186.708V63.4736H187.774ZM187.774 30.6777L188.841 30.6777C188.841 20.2911 180.421 11.8717 170.034 11.8717V12.9385V14.0053C179.242 14.0053 186.707 21.4696 186.708 30.6778L187.774 30.6777ZM170.034 12.9385H171.101V12.9336H170.034H168.967V12.9385H170.034ZM170.034 12.9336V11.8668H48.2061V12.9336V14.0004H170.034V12.9336ZM48.2061 12.9336H47.1393V12.9385H48.2061H49.2728V12.9336H48.2061ZM48.2061 12.9385L48.206 11.8717C37.8196 11.8719 29.4002 20.2913 29.4 30.6777L30.4668 30.6777L31.5336 30.6778C31.5338 21.4697 38.998 14.0054 48.2061 14.0053L48.2061 12.9385ZM30.4668 30.6777H29.4V63.4736H30.4668H31.5336V30.6777H30.4668ZM30.4668 63.4736V62.4068H22.0322V63.4736V64.5404H30.4668V63.4736ZM22.0322 63.4736H23.099V30.6777H22.0322H20.9654V63.4736H22.0322ZM22.0322 30.6777L23.099 30.6777C23.0988 20.2912 14.6784 11.8718 4.29201 11.8717L4.29199 12.9385L4.29198 14.0053C13.5002 14.0054 20.9653 21.4697 20.9654 30.6778L22.0322 30.6777ZM4.29199 12.9385H5.35878V6.79492H4.29199H3.22521V12.9385H4.29199ZM4.29199 6.79492V7.86171H213.949V6.79492V5.72814H4.29199V6.79492ZM213.949 6.79492H212.882V12.9385H213.949H215.016V6.79492H213.949ZM213.951 5.86328V4.7965H4.26562V5.86328V6.93007H213.951V5.86328ZM4.26562 5.86328H5.33241V4.36035H4.26562H3.19884V5.86328H4.26562ZM4.26562 4.36035V5.42714H213.951V4.36035V3.29357H4.26562V4.36035ZM213.951 4.36035H212.884V5.86328H213.951H215.018V4.36035H213.951Z" fill="#040404" fill-opacity="0.5" mask="url(#path-11-outside-3_450_11683)"/>
|
||||
<g filter="url(#filter3_f_450_11683)" style="mix-blend-mode:color-dodge">
|
||||
<path d="M202.689 65.6075C208.876 65.6075 213.891 70.6224 213.891 76.8087V94.4102H3.73438V76.8087C3.73438 70.6224 8.74927 65.6075 14.9355 65.6075H202.689Z" stroke="url(#paint6_linear_450_11683)" stroke-width="1.06678"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_f_450_11683)" style="mix-blend-mode:plus-lighter">
|
||||
<mask id="path-14-outside-4_450_11683" maskUnits="userSpaceOnUse" x="2.26562" y="3.33496" width="212" height="61" fill="black">
|
||||
<rect fill="white" x="2.26562" y="3.33496" width="212" height="61"/>
|
||||
<path d="M212.29 5.33496V11.4834C202.569 11.4836 194.688 19.3649 194.688 29.0859V62.9404H186.319V29.0859C186.319 19.7628 179.071 12.1337 169.902 11.5244H46.6543C37.4854 12.1336 30.2363 19.7628 30.2363 29.0859V62.9404H21.8682V29.0859C21.8682 19.3647 13.9868 11.4834 4.26562 11.4834V5.33496H212.29Z"/>
|
||||
</mask>
|
||||
<path d="M212.29 5.33496H213.357V4.26818H212.29V5.33496ZM212.29 11.4834L212.29 12.5502L213.357 12.5502V11.4834H212.29ZM194.688 62.9404V64.0072H195.755V62.9404H194.688ZM186.319 62.9404H185.253V64.0072H186.319V62.9404ZM169.902 11.5244L169.973 10.46L169.938 10.4576H169.902V11.5244ZM46.6543 11.5244V10.4576H46.6189L46.5836 10.46L46.6543 11.5244ZM30.2363 62.9404V64.0072H31.3031V62.9404H30.2363ZM21.8682 62.9404H20.8014V64.0072H21.8682V62.9404ZM4.26562 11.4834H3.19884V12.5502H4.26562V11.4834ZM4.26562 5.33496V4.26818H3.19884V5.33496H4.26562ZM212.29 5.33496H211.223V11.4834H212.29H213.357V5.33496H212.29ZM212.29 11.4834L212.29 10.4166C201.98 10.4168 193.622 18.7757 193.622 29.0859H194.688H195.755C195.755 19.954 203.158 12.5504 212.29 12.5502L212.29 11.4834ZM194.688 29.0859H193.622V62.9404H194.688H195.755V29.0859H194.688ZM194.688 62.9404V61.8736H186.319V62.9404V64.0072H194.688V62.9404ZM186.319 62.9404H187.386V29.0859H186.319H185.253V62.9404H186.319ZM186.319 29.0859H187.386C187.386 19.1973 179.699 11.1062 169.973 10.46L169.902 11.5244L169.832 12.5889C178.444 13.1611 185.253 20.3284 185.253 29.0859H186.319ZM169.902 11.5244V10.4576H46.6543V11.5244V12.5912H169.902V11.5244ZM46.6543 11.5244L46.5836 10.46C36.8581 11.1061 29.1695 19.1972 29.1695 29.0859H30.2363H31.3031C31.3031 20.3284 38.1127 13.1611 46.725 12.5889L46.6543 11.5244ZM30.2363 29.0859H29.1695V62.9404H30.2363H31.3031V29.0859H30.2363ZM30.2363 62.9404V61.8736H21.8682V62.9404V64.0072H30.2363V62.9404ZM21.8682 62.9404H22.9349V29.0859H21.8682H20.8014V62.9404H21.8682ZM21.8682 29.0859H22.9349C22.9349 18.7756 14.576 10.4166 4.26562 10.4166V11.4834V12.5502C13.3977 12.5502 20.8014 19.9539 20.8014 29.0859H21.8682ZM4.26562 11.4834H5.33241V5.33496H4.26562H3.19884V11.4834H4.26562ZM4.26562 5.33496V6.40175H212.29V5.33496V4.26818H4.26562V5.33496Z" fill="url(#paint7_linear_450_11683)" mask="url(#path-14-outside-4_450_11683)"/>
|
||||
</g>
|
||||
<g filter="url(#filter5_f_450_11683)" style="mix-blend-mode:plus-lighter">
|
||||
<mask id="path-16-outside-5_450_11683" maskUnits="userSpaceOnUse" x="3.33398" y="3.33496" width="211" height="61" fill="black">
|
||||
<rect fill="white" x="3.33398" y="3.33496" width="211" height="61"/>
|
||||
<path d="M211.227 5.33496C211.816 5.33496 212.294 5.81258 212.294 6.40175V10.4166C212.294 11.0058 211.815 11.48 211.227 11.5154C202.053 12.0668 194.782 19.6821 194.782 28.9951V61.8736C194.782 62.4628 194.305 62.9404 193.715 62.9404H187.523C186.934 62.9404 186.456 62.4628 186.456 61.8736V28.9951C186.456 19.7198 179.245 12.1305 170.123 11.5244H47.5049C38.383 12.1304 31.1719 19.7198 31.1719 28.9951V61.8736C31.1719 62.4628 30.6943 62.9404 30.1051 62.9404H23.9125C23.3233 62.9404 22.8457 62.4628 22.8457 61.8736V28.9951C22.8456 19.6821 15.5754 12.0668 6.40093 11.5154C5.81282 11.48 5.33398 11.0058 5.33398 10.4166V6.40175C5.33398 5.81258 5.8116 5.33496 6.40077 5.33496H211.227Z"/>
|
||||
</mask>
|
||||
<path d="M194.782 28.9951L193.715 28.9951V28.9951H194.782ZM186.456 28.9951H187.523V28.9951L186.456 28.9951ZM170.123 11.5244L170.194 10.46L170.158 10.4576H170.123V11.5244ZM47.5049 11.5244V10.4576H47.4695L47.4342 10.46L47.5049 11.5244ZM31.1719 28.9951L30.1051 28.9951V28.9951H31.1719ZM22.8457 28.9951H23.9125L23.9125 28.9951L22.8457 28.9951ZM212.294 6.40175H211.227V10.4166H212.294H213.361V6.40175H212.294ZM211.227 11.5154L211.163 10.4505C201.429 11.0356 193.716 19.1144 193.715 28.9951L194.782 28.9951L195.849 28.9951C195.849 20.2498 202.676 13.098 211.291 12.5802L211.227 11.5154ZM194.782 28.9951H193.715V61.8736H194.782H195.849V28.9951H194.782ZM193.715 62.9404V61.8736H187.523V62.9404V64.0072H193.715V62.9404ZM186.456 61.8736H187.523V28.9951H186.456H185.389V61.8736H186.456ZM186.456 28.9951L187.523 28.9951C187.523 19.1542 179.872 11.103 170.194 10.46L170.123 11.5244L170.052 12.5889C178.618 13.1579 185.389 20.2854 185.389 28.9951L186.456 28.9951ZM170.123 11.5244V10.4576H47.5049V11.5244V12.5912H170.123V11.5244ZM47.5049 11.5244L47.4342 10.46C37.7557 11.103 30.1052 19.1542 30.1051 28.9951L31.1719 28.9951L32.2387 28.9951C32.2387 20.2854 39.0103 13.1579 47.5756 12.5889L47.5049 11.5244ZM31.1719 28.9951H30.1051V61.8736H31.1719H32.2387V28.9951H31.1719ZM30.1051 62.9404V61.8736H23.9125V62.9404V64.0072H30.1051V62.9404ZM22.8457 61.8736H23.9125V28.9951H22.8457H21.7789V61.8736H22.8457ZM22.8457 28.9951L23.9125 28.9951C23.9124 19.1144 16.1993 11.0356 6.46493 10.4505L6.40093 11.5154L6.33693 12.5802C14.9515 13.098 21.7789 20.2498 21.7789 28.9951L22.8457 28.9951ZM5.33398 10.4166H6.40077V6.40175H5.33398H4.2672V10.4166H5.33398ZM6.40077 5.33496V6.40175H211.227V5.33496V4.26818H6.40077V5.33496ZM5.33398 6.40175H6.40077V6.40175V5.33496V4.26818C5.22243 4.26818 4.2672 5.22341 4.2672 6.40175H5.33398ZM23.9125 62.9404V61.8736H23.9125H22.8457H21.7789C21.7789 63.052 22.7341 64.0072 23.9125 64.0072V62.9404ZM31.1719 61.8736H30.1051H30.1051V62.9404V64.0072C31.2834 64.0072 32.2387 63.052 32.2387 61.8736H31.1719ZM187.523 62.9404V61.8736V61.8736H186.456H185.389C185.389 63.052 186.344 64.0072 187.523 64.0072V62.9404ZM194.782 61.8736H193.715V61.8736V62.9404V64.0072C194.894 64.0072 195.849 63.052 195.849 61.8736H194.782ZM6.40093 11.5154L6.46493 10.4505C6.44001 10.449 6.41994 10.4383 6.4078 10.426C6.40231 10.4204 6.40063 10.4166 6.40051 10.4163C6.40046 10.4162 6.40061 10.4165 6.40073 10.4171C6.40087 10.4178 6.40077 10.4178 6.40077 10.4166H5.33398H4.2672C4.2672 11.6112 5.22751 12.5136 6.33693 12.5802L6.40093 11.5154ZM212.294 10.4166H211.227C211.227 10.4178 211.227 10.4178 211.227 10.4171C211.227 10.4165 211.227 10.4162 211.227 10.4163C211.227 10.4166 211.226 10.4204 211.22 10.426C211.208 10.4383 211.188 10.449 211.163 10.4505L211.227 11.5154L211.291 12.5802C212.4 12.5136 213.361 11.6112 213.361 10.4166H212.294ZM212.294 6.40175H213.361C213.361 5.22341 212.406 4.26818 211.227 4.26818V5.33496V6.40175V6.40175H212.294Z" fill="url(#paint8_linear_450_11683)" mask="url(#path-16-outside-5_450_11683)"/>
|
||||
</g>
|
||||
<g filter="url(#filter6_f_450_11683)">
|
||||
<path d="M2.13281 13.8682V4.26712C2.13281 3.08878 3.08804 2.13354 4.26638 2.13354H214.423C215.601 2.13354 216.557 3.08878 216.557 4.26711V13.8682" stroke="url(#paint9_linear_450_11683)" stroke-width="1.06678"/>
|
||||
</g>
|
||||
<g filter="url(#filter7_f_450_11683)">
|
||||
<path d="M33.0703 62.9403V26.9541C33.4259 22.5922 36.8041 15.4684 46.9385 13.8682H168.019C177.62 14.4016 184.554 18.2302 184.554 31.3161V62.9403" stroke="url(#paint10_linear_450_11683)" stroke-width="1.06678"/>
|
||||
</g>
|
||||
<line x1="18.1914" y1="64.1475" x2="200.106" y2="64.1475" stroke="#400500" stroke-width="1.06678"/>
|
||||
<g filter="url(#filter8_f_450_11683)">
|
||||
<path d="M22.9355 45.8718V62.4069H29.8696V45.8718" stroke="url(#paint11_linear_450_11683)" style="mix-blend-mode:plus-lighter" stroke-width="1.06678"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_450_11683" x="34.3613" y="17.1808" width="150.641" height="49.5774" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="1.06678" dy="1.06678"/>
|
||||
<feGaussianBlur stdDeviation="0.533392"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.49 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_450_11683"/>
|
||||
</filter>
|
||||
<filter id="filter1_ii_450_11683" x="5.56445" y="66.2541" width="208.147" height="27.7161" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="1.06678" dy="1.06678"/>
|
||||
<feGaussianBlur stdDeviation="0.533392"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.929412 0 0 0 0 0.556863 0 0 0 0 0.266667 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_450_11683"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-2.13357"/>
|
||||
<feGaussianBlur stdDeviation="0.533392"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.322115 0 0 0 0 0.142276 0 0 0 0 0.0263267 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="effect1_innerShadow_450_11683" result="effect2_innerShadow_450_11683"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_450_11683" x="6.40001" y="68.2742" width="205.889" height="25.6028" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11683"/>
|
||||
</filter>
|
||||
<filter id="filter3_f_450_11683" x="2.13439" y="64.0071" width="213.356" height="32.0035" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11683"/>
|
||||
</filter>
|
||||
<filter id="filter4_f_450_11683" x="2.13243" y="3.2014" width="212.292" height="61.8726" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11683"/>
|
||||
</filter>
|
||||
<filter id="filter5_f_450_11683" x="3.20079" y="3.2014" width="211.227" height="61.8726" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11683"/>
|
||||
</filter>
|
||||
<filter id="filter6_f_450_11683" x="0.532825" y="0.533374" width="217.624" height="14.4016" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11683"/>
|
||||
</filter>
|
||||
<filter id="filter7_f_450_11683" x="31.4703" y="12.2681" width="154.684" height="51.739" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11683"/>
|
||||
</filter>
|
||||
<filter id="filter8_f_450_11683" x="21.3356" y="44.805" width="10.1336" height="19.2021" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11683"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_450_11683" x1="109.149" y1="17.1808" x2="109.149" y2="65.6915" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#442111"/>
|
||||
<stop offset="0.05" stop-color="#522510"/>
|
||||
<stop offset="1" stop-color="#090504"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_450_11683" x1="31.6904" y1="34.5814" x2="183.936" y2="34.5814" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#803414"/>
|
||||
<stop offset="0.077962" stop-color="#803414" stop-opacity="0.25"/>
|
||||
<stop offset="0.939742" stop-color="#803414" stop-opacity="0.282099"/>
|
||||
<stop offset="1" stop-color="#803414"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_450_11683" x1="109.104" y1="68.3877" x2="109.104" y2="91.8366" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#AD571E"/>
|
||||
<stop offset="1" stop-color="#88441A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_450_11683" x1="109.104" y1="68.3877" x2="109.104" y2="91.8366" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#AD571E"/>
|
||||
<stop offset="1" stop-color="#88441A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_450_11683" x1="109.344" y1="70.4077" x2="109.344" y2="91.7434" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFC893"/>
|
||||
<stop offset="1" stop-color="#FFC893" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_450_11683" x1="4.26562" y1="33.9171" x2="213.951" y2="33.9171" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFAC63"/>
|
||||
<stop offset="0.144231" stop-color="#FF7E0D"/>
|
||||
<stop offset="0.889423" stop-color="#FF7E0D"/>
|
||||
<stop offset="0.961538" stop-color="#EB5200"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_450_11683" x1="108.812" y1="66.1407" x2="108.812" y2="93.8771" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FDF7CC"/>
|
||||
<stop offset="1" stop-color="#FDF7CC" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_450_11683" x1="108.278" y1="5.33496" x2="108.278" y2="14.4016" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E5A76C"/>
|
||||
<stop offset="1" stop-color="#FFC893" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_450_11683" x1="18.6006" y1="12.268" x2="45.5373" y2="15.065" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E5A76C"/>
|
||||
<stop offset="1" stop-color="#FFC893" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_450_11683" x1="109.345" y1="2.13354" x2="109.345" y2="13.8682" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#C17300"/>
|
||||
<stop offset="1" stop-color="#C17300" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_450_11683" x1="108.812" y1="13.8682" x2="108.812" y2="62.9403" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D59628"/>
|
||||
<stop offset="1" stop-color="#AD6E00" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint11_linear_450_11683" x1="25.0691" y1="61.8735" x2="24.5357" y2="36.2707" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFC034"/>
|
||||
<stop offset="1" stop-color="#FFC034" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_450_11683">
|
||||
<rect width="50" height="95" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_450_11683">
|
||||
<rect width="216.389" height="95" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,173 @@
|
||||
<svg width="84" height="95" viewBox="0 0 84 95" fill="none" shape-rendering="geometricPrecision" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_450_11720)">
|
||||
<g clip-path="url(#clip1_450_11720)">
|
||||
<g filter="url(#filter0_i_450_11720)">
|
||||
<path d="M-449.396 29.9823C-449.396 22.9122 -443.665 17.1808 -436.595 17.1808H517.713C524.783 17.1808 530.514 22.9122 530.514 29.9823V65.6915H-449.396V29.9823Z" fill="url(#paint0_linear_450_11720)"/>
|
||||
<path d="M-449.396 29.9823C-449.396 22.9122 -443.665 17.1808 -436.595 17.1808H517.713C524.783 17.1808 530.514 22.9122 530.514 29.9823V65.6915H-449.396V29.9823Z" fill="url(#paint1_linear_450_11720)"/>
|
||||
</g>
|
||||
<path d="M-436.595 17.7141H517.713C524.488 17.7141 529.981 23.2072 529.981 29.9826V65.1584H-448.863V29.9826C-448.863 23.2072 -443.37 17.7141 -436.595 17.7141Z" stroke="black" stroke-width="1.06678"/>
|
||||
<path d="M703.07 66.2539C708.962 66.2539 713.738 71.0302 713.738 76.9219V93.9707H-633.2V76.9219C-633.2 71.0303 -628.424 66.254 -622.532 66.2539H703.07Z" fill="url(#paint2_linear_450_11720)" stroke="#400500" stroke-width="4.26714"/>
|
||||
<g filter="url(#filter1_ii_450_11720)">
|
||||
<path d="M-631.066 76.922C-631.066 72.2086 -627.245 68.3877 -622.532 68.3877H703.07C707.784 68.3877 711.605 72.2086 711.605 76.922V90.7699C711.605 91.359 711.127 91.8366 710.538 91.8366H-630C-630.589 91.8366 -631.066 91.359 -631.066 90.7699V76.922Z" fill="url(#paint3_linear_450_11720)"/>
|
||||
</g>
|
||||
<path d="M703.07 67.8545C708.078 67.8545 712.138 71.914 712.138 76.9219V90.7695C712.138 91.6532 711.422 92.37 710.538 92.3701H-630C-630.884 92.3699 -631.6 91.6532 -631.6 90.7695V76.9219C-631.6 71.914 -627.54 67.8545 -622.532 67.8545H703.07Z" stroke="black" stroke-opacity="0.76" stroke-width="1.06678"/>
|
||||
<g filter="url(#filter2_f_450_11720)" style="mix-blend-mode:plus-lighter">
|
||||
<path d="M695.89 69.8745C699.719 69.8747 702.823 72.9788 702.823 76.8081V92.2769H-619.137V76.8081C-619.137 72.9788 -616.032 69.8747 -612.203 69.8745H695.89Z" stroke="url(#paint4_linear_450_11720)" stroke-width="1.06678"/>
|
||||
</g>
|
||||
<mask id="path-7-outside-1_450_11720" maskUnits="userSpaceOnUse" x="-651.562" y="-0.639648" width="1383" height="69" fill="black">
|
||||
<rect fill="white" x="-651.562" y="-0.639648" width="1383" height="69"/>
|
||||
<path d="M727.146 12.9385H661.462C633.552 12.9385 610.926 35.5636 610.926 63.4736H555.668C555.668 35.5637 533.043 12.9386 505.133 12.9385H439.447V12.9336H-358.697V12.9385H-424.382C-452.292 12.9385 -474.918 35.5636 -474.918 63.4736H-530.176C-530.176 35.5637 -552.801 12.9386 -580.711 12.9385H-646.396V6.79492H727.146V12.9385ZM727.153 5.86328H-646.562V4.36035H727.153V5.86328Z"/>
|
||||
</mask>
|
||||
<path d="M727.146 12.9385H661.462C633.552 12.9385 610.926 35.5636 610.926 63.4736H555.668C555.668 35.5637 533.043 12.9386 505.133 12.9385H439.447V12.9336H-358.697V12.9385H-424.382C-452.292 12.9385 -474.918 35.5636 -474.918 63.4736H-530.176C-530.176 35.5637 -552.801 12.9386 -580.711 12.9385H-646.396V6.79492H727.146V12.9385ZM727.153 5.86328H-646.562V4.36035H727.153V5.86328Z" fill="#FF7700"/>
|
||||
<path d="M727.146 12.9385V17.2056C729.503 17.2056 731.414 15.2952 731.414 12.9385H727.146ZM610.926 63.4736V67.7408C613.282 67.7408 615.193 65.8303 615.193 63.4736L610.926 63.4736ZM555.668 63.4736L551.401 63.4736C551.401 65.8303 553.311 67.7408 555.668 67.7408V63.4736ZM505.133 12.9385V8.67134V8.67134V12.9385ZM439.447 12.9385H435.18C435.18 15.2952 437.091 17.2056 439.447 17.2056V12.9385ZM439.447 12.9336H443.714C443.714 10.5769 441.804 8.66645 439.447 8.66645V12.9336ZM-358.697 12.9336V8.66645C-361.054 8.66645 -362.964 10.5769 -362.964 12.9336H-358.697ZM-358.697 12.9385V17.2056C-356.341 17.2056 -354.43 15.2952 -354.43 12.9385H-358.697ZM-474.918 63.4736V67.7408C-472.561 67.7408 -470.651 65.8303 -470.651 63.4736L-474.918 63.4736ZM-530.176 63.4736L-534.443 63.4736C-534.443 65.8303 -532.532 67.7408 -530.176 67.7408V63.4736ZM-580.711 12.9385L-580.711 8.67134H-580.711V12.9385ZM-646.396 12.9385H-650.664C-650.664 15.2952 -648.753 17.2056 -646.396 17.2056V12.9385ZM-646.396 6.79492V2.52778C-648.753 2.52778 -650.664 4.43825 -650.664 6.79492H-646.396ZM727.146 6.79492H731.414C731.414 4.43825 729.503 2.52778 727.146 2.52778V6.79492ZM727.153 5.86328V10.1304C729.51 10.1304 731.42 8.21996 731.42 5.86328H727.153ZM-646.562 5.86328H-650.83C-650.83 8.21996 -648.919 10.1304 -646.562 10.1304V5.86328ZM-646.562 4.36035V0.0932121C-648.919 0.0932121 -650.83 2.00368 -650.83 4.36035L-646.562 4.36035ZM727.153 4.36035H731.42C731.42 2.00368 729.51 0.0932121 727.153 0.0932121V4.36035ZM727.146 12.9385V8.67134H661.462V12.9385V17.2056H727.146V12.9385ZM661.462 12.9385V8.67134C631.195 8.67134 606.659 33.2068 606.659 63.4736L610.926 63.4736L615.193 63.4736C615.193 37.9204 635.908 17.2056 661.462 17.2056V12.9385ZM610.926 63.4736V59.2065H555.668V63.4736V67.7408H610.926V63.4736ZM555.668 63.4736L559.935 63.4736C559.935 33.207 535.399 8.67147 505.133 8.67134V12.9385V17.2056C530.686 17.2057 551.401 37.9204 551.401 63.4736L555.668 63.4736ZM505.133 12.9385V8.67134H439.447V12.9385V17.2056H505.133V12.9385ZM439.447 12.9385H443.714V12.9336H439.447H435.18V12.9385H439.447ZM439.447 12.9336V8.66645H-358.697V12.9336V17.2007H439.447V12.9336ZM-358.697 12.9336H-362.964V12.9385H-358.697H-354.43V12.9336H-358.697ZM-358.697 12.9385V8.67134H-424.382V12.9385V17.2056H-358.697V12.9385ZM-424.382 12.9385V8.67134C-454.648 8.67134 -479.185 33.2068 -479.185 63.4736L-474.918 63.4736L-470.651 63.4736C-470.651 37.9204 -449.935 17.2056 -424.382 17.2056V12.9385ZM-474.918 63.4736V59.2065H-530.176V63.4736V67.7408H-474.918V63.4736ZM-530.176 63.4736L-525.909 63.4736C-525.909 33.207 -550.444 8.67148 -580.711 8.67134L-580.711 12.9385L-580.711 17.2056C-555.158 17.2057 -534.443 37.9204 -534.443 63.4736L-530.176 63.4736ZM-580.711 12.9385V8.67134H-646.396V12.9385V17.2056H-580.711V12.9385ZM-646.396 12.9385H-642.129V6.79492H-646.396H-650.664V12.9385H-646.396ZM-646.396 6.79492V11.0621H727.146V6.79492V2.52778H-646.396V6.79492ZM727.146 6.79492H722.879V12.9385H727.146H731.414V6.79492H727.146ZM727.153 5.86328V1.59614H-646.562V5.86328V10.1304H727.153V5.86328ZM-646.562 5.86328H-642.295V4.36035H-646.562H-650.83V5.86328H-646.562ZM-646.562 4.36035V8.62749H727.153V4.36035V0.0932121H-646.562V4.36035ZM727.153 4.36035H722.886V5.86328H727.153H731.42V4.36035H727.153Z" fill="#400500" mask="url(#path-7-outside-1_450_11720)"/>
|
||||
<mask id="path-9-outside-2_450_11720" maskUnits="userSpaceOnUse" x="-651.562" y="-0.639648" width="1383" height="69" fill="black">
|
||||
<rect fill="white" x="-651.562" y="-0.639648" width="1383" height="69"/>
|
||||
<path d="M727.146 11.8717C727.146 12.4609 726.669 12.9385 726.08 12.9385H661.462C633.91 12.9385 611.507 34.9874 610.937 62.4033C610.925 62.9923 610.448 63.4736 609.859 63.4736H556.735C556.146 63.4736 555.669 62.9923 555.657 62.4033C555.087 34.9875 532.685 12.9386 505.133 12.9385H439.45C439.448 12.9385 439.447 12.9374 439.447 12.936C439.447 12.9347 439.446 12.9336 439.445 12.9336H-358.695C-358.696 12.9336 -358.697 12.9347 -358.697 12.936C-358.697 12.9374 -358.698 12.9385 -358.7 12.9385H-424.382C-451.934 12.9385 -474.337 34.9874 -474.907 62.4033C-474.919 62.9923 -475.396 63.4736 -475.985 63.4736H-529.109C-529.698 63.4736 -530.175 62.9923 -530.187 62.4033C-530.757 34.9875 -553.159 12.9386 -580.711 12.9385H-645.33C-645.919 12.9385 -646.396 12.4609 -646.396 11.8717V7.86171C-646.396 7.27254 -645.919 6.79492 -645.33 6.79492H726.08C726.669 6.79492 727.146 7.27254 727.146 7.86171V11.8717ZM727.153 5.11182C727.153 5.52684 726.817 5.86328 726.402 5.86328H-645.811C-646.226 5.86328 -646.562 5.52684 -646.562 5.11182C-646.562 4.69679 -646.226 4.36035 -645.811 4.36035H726.402C726.817 4.36035 727.153 4.69679 727.153 5.11182Z"/>
|
||||
</mask>
|
||||
<path d="M727.146 11.8717C727.146 12.4609 726.669 12.9385 726.08 12.9385H661.462C633.91 12.9385 611.507 34.9874 610.937 62.4033C610.925 62.9923 610.448 63.4736 609.859 63.4736H556.735C556.146 63.4736 555.669 62.9923 555.657 62.4033C555.087 34.9875 532.685 12.9386 505.133 12.9385H439.45C439.448 12.9385 439.447 12.9374 439.447 12.936C439.447 12.9347 439.446 12.9336 439.445 12.9336H-358.695C-358.696 12.9336 -358.697 12.9347 -358.697 12.936C-358.697 12.9374 -358.698 12.9385 -358.7 12.9385H-424.382C-451.934 12.9385 -474.337 34.9874 -474.907 62.4033C-474.919 62.9923 -475.396 63.4736 -475.985 63.4736H-529.109C-529.698 63.4736 -530.175 62.9923 -530.187 62.4033C-530.757 34.9875 -553.159 12.9386 -580.711 12.9385H-645.33C-645.919 12.9385 -646.396 12.4609 -646.396 11.8717V7.86171C-646.396 7.27254 -645.919 6.79492 -645.33 6.79492H726.08C726.669 6.79492 727.146 7.27254 727.146 7.86171V11.8717ZM727.153 5.11182C727.153 5.52684 726.817 5.86328 726.402 5.86328H-645.811C-646.226 5.86328 -646.562 5.52684 -646.562 5.11182C-646.562 4.69679 -646.226 4.36035 -645.811 4.36035H726.402C726.817 4.36035 727.153 4.69679 727.153 5.11182Z" fill="url(#paint5_linear_450_11720)"/>
|
||||
<path d="M505.133 12.9385V8.67134V8.67134V12.9385ZM-580.711 12.9385L-580.711 8.67134H-580.711V12.9385ZM-474.907 62.4033L-479.173 62.3146L-474.907 62.4033ZM555.657 62.4033L551.391 62.4919L555.657 62.4033ZM610.937 62.4033L606.671 62.3146L610.937 62.4033ZM-530.187 62.4033L-534.453 62.4919L-530.187 62.4033ZM726.08 12.9385V8.67134H661.462V12.9385V17.2056H726.08V12.9385ZM661.462 12.9385V8.67134C631.583 8.67134 607.289 32.5819 606.671 62.3146L610.937 62.4033L615.203 62.4919C615.725 37.393 636.236 17.2056 661.462 17.2056V12.9385ZM609.859 63.4736V59.2065H556.735V63.4736V67.7408H609.859V63.4736ZM555.657 62.4033L559.923 62.3146C559.305 32.5821 535.012 8.67147 505.133 8.67134V12.9385V17.2056C530.358 17.2057 550.869 37.3929 551.391 62.4919L555.657 62.4033ZM505.133 12.9385V8.67134H439.45V12.9385V17.2056H505.133V12.9385ZM439.445 12.9336V8.66645H-358.695V12.9336V17.2007H439.445V12.9336ZM-358.7 12.9385V8.67134H-424.382V12.9385V17.2056H-358.7V12.9385ZM-424.382 12.9385V8.67134C-454.261 8.67134 -478.555 32.5819 -479.173 62.3146L-474.907 62.4033L-470.641 62.4919C-470.119 37.393 -449.607 17.2056 -424.382 17.2056V12.9385ZM-475.985 63.4736V59.2065H-529.109V63.4736V67.7408H-475.985V63.4736ZM-530.187 62.4033L-525.921 62.3146C-526.538 32.5821 -550.832 8.67148 -580.711 8.67134L-580.711 12.9385L-580.711 17.2056C-555.486 17.2057 -534.975 37.393 -534.453 62.4919L-530.187 62.4033ZM-580.711 12.9385V8.67134H-645.33V12.9385V17.2056H-580.711V12.9385ZM-646.396 11.8717H-642.129V7.86171H-646.396H-650.664V11.8717H-646.396ZM-645.33 6.79492V11.0621H726.08V6.79492V2.52778H-645.33V6.79492ZM727.146 7.86171H722.879V11.8717H727.146H731.414V7.86171H727.146ZM726.402 5.86328V1.59614H-645.811V5.86328V10.1304H726.402V5.86328ZM-645.811 4.36035V8.62749H726.402V4.36035V0.0932121H-645.811V4.36035ZM726.402 4.36035V8.62749C724.46 8.62749 722.886 7.05368 722.886 5.11182H727.153H731.42C731.42 2.33991 729.173 0.0932121 726.402 0.0932121V4.36035ZM-646.562 5.11182H-642.295C-642.295 7.05347 -643.869 8.62749 -645.811 8.62749V4.36035V0.0932121C-648.583 0.0932121 -650.83 2.34012 -650.83 5.11182H-646.562ZM-645.811 5.86328V1.59614C-643.87 1.59614 -642.295 3.16995 -642.295 5.11182H-646.562H-650.83C-650.83 7.88372 -648.582 10.1304 -645.811 10.1304V5.86328ZM726.402 5.86328V10.1304C729.174 10.1304 731.42 7.88351 731.42 5.11182H727.153H722.886C722.886 3.17016 724.46 1.59614 726.402 1.59614V5.86328ZM726.08 6.79492V11.0621C724.312 11.0621 722.879 9.62909 722.879 7.86171H727.146H731.414C731.414 4.91599 729.026 2.52778 726.08 2.52778V6.79492ZM-474.907 62.4033L-479.173 62.3146C-479.139 60.6582 -477.793 59.2065 -475.985 59.2065V63.4736V67.7408C-472.998 67.7408 -470.7 65.3264 -470.641 62.4919L-474.907 62.4033ZM-358.697 12.936H-362.964C-362.964 10.5807 -361.055 8.67134 -358.7 8.67134V12.9385V17.2056C-356.342 17.2056 -354.43 15.2941 -354.43 12.936H-358.697ZM-358.695 12.9336V8.66645C-361.087 8.66645 -362.964 10.6128 -362.964 12.936H-358.697H-354.43C-354.43 15.2566 -356.305 17.2007 -358.695 17.2007V12.9336ZM439.447 12.936H443.714C443.714 10.578 441.803 8.66645 439.445 8.66645V12.9336V17.2007C437.089 17.2007 435.18 15.2914 435.18 12.936H439.447ZM-646.396 7.86171H-642.129C-642.129 9.62921 -643.562 11.0621 -645.33 11.0621V6.79492V2.52778C-648.276 2.52778 -650.664 4.91586 -650.664 7.86171H-646.396ZM556.735 63.4736V59.2065C558.543 59.2065 559.889 60.6582 559.923 62.3146L555.657 62.4033L551.391 62.4919C551.45 65.3264 553.748 67.7408 556.735 67.7408V63.4736ZM-645.33 12.9385V8.67134C-643.562 8.67134 -642.129 10.1042 -642.129 11.8717H-646.396H-650.664C-650.664 14.8176 -648.276 17.2056 -645.33 17.2056V12.9385ZM610.937 62.4033L606.671 62.3146C606.705 60.6582 608.051 59.2065 609.859 59.2065V63.4736V67.7408C612.846 67.7408 615.144 65.3264 615.203 62.4919L610.937 62.4033ZM439.45 12.9385V8.67134C441.801 8.67134 443.714 10.5769 443.714 12.936H439.447H435.18C435.18 15.2978 437.095 17.2056 439.45 17.2056V12.9385ZM-529.109 63.4736V59.2065C-527.301 59.2065 -525.955 60.6582 -525.921 62.3146L-530.187 62.4033L-534.453 62.4919C-534.394 65.3264 -532.096 67.7408 -529.109 67.7408V63.4736ZM726.08 12.9385V17.2056C729.026 17.2056 731.414 14.8175 731.414 11.8717H727.146H722.879C722.879 10.1042 724.312 8.67134 726.08 8.67134V12.9385Z" fill="#400500" mask="url(#path-9-outside-2_450_11720)"/>
|
||||
<mask id="path-11-outside-3_450_11720" maskUnits="userSpaceOnUse" x="-648.562" y="2.36035" width="1377" height="63" fill="black">
|
||||
<rect fill="white" x="-648.562" y="2.36035" width="1377" height="63"/>
|
||||
<path d="M727.146 12.9385H661.462C633.552 12.9385 610.926 35.5636 610.926 63.4736H555.668C555.668 35.5637 533.043 12.9386 505.133 12.9385H439.447V12.9336H-358.697V12.9385H-424.382C-452.292 12.9385 -474.918 35.5636 -474.918 63.4736H-530.176C-530.176 35.5637 -552.801 12.9386 -580.711 12.9385H-646.396V6.79492H727.146V12.9385ZM727.153 5.86328H-646.562V4.36035H727.153V5.86328Z"/>
|
||||
</mask>
|
||||
<path d="M727.146 12.9385V14.0053C727.736 14.0053 728.213 13.5276 728.213 12.9385H727.146ZM610.926 63.4736V64.5404C611.515 64.5404 611.993 64.0628 611.993 63.4736H610.926ZM555.668 63.4736H554.601C554.601 64.0628 555.079 64.5404 555.668 64.5404V63.4736ZM505.133 12.9385V11.8717V11.8717V12.9385ZM439.447 12.9385H438.38C438.38 13.5276 438.858 14.0053 439.447 14.0053V12.9385ZM439.447 12.9336H440.514C440.514 12.3444 440.036 11.8668 439.447 11.8668V12.9336ZM-358.697 12.9336V11.8668C-359.286 11.8668 -359.764 12.3444 -359.764 12.9336H-358.697ZM-358.697 12.9385V14.0053C-358.108 14.0053 -357.63 13.5276 -357.63 12.9385H-358.697ZM-474.918 63.4736V64.5404C-474.329 64.5404 -473.851 64.0628 -473.851 63.4736H-474.918ZM-530.176 63.4736H-531.243C-531.243 64.0628 -530.765 64.5404 -530.176 64.5404V63.4736ZM-580.711 12.9385L-580.711 11.8717H-580.711V12.9385ZM-646.396 12.9385H-647.463C-647.463 13.5276 -646.986 14.0053 -646.396 14.0053V12.9385ZM-646.396 6.79492V5.72814C-646.986 5.72814 -647.463 6.20575 -647.463 6.79492H-646.396ZM727.146 6.79492H728.213C728.213 6.20575 727.736 5.72814 727.146 5.72814V6.79492ZM727.153 5.86328V6.93007C727.742 6.93007 728.22 6.45245 728.22 5.86328H727.153ZM-646.562 5.86328H-647.629C-647.629 6.45245 -647.152 6.93007 -646.562 6.93007V5.86328ZM-646.562 4.36035V3.29357C-647.152 3.29357 -647.629 3.77118 -647.629 4.36035H-646.562ZM727.153 4.36035H728.22C728.22 3.77118 727.742 3.29357 727.153 3.29357V4.36035ZM727.146 12.9385V11.8717H661.462V12.9385V14.0053H727.146V12.9385ZM661.462 12.9385V11.8717C632.963 11.8717 609.859 34.9744 609.859 63.4736H610.926H611.993C611.993 36.1528 634.141 14.0053 661.462 14.0053V12.9385ZM610.926 63.4736V62.4068H555.668V63.4736V64.5404H610.926V63.4736ZM555.668 63.4736H556.735C556.735 34.9745 533.632 11.8718 505.133 11.8717V12.9385V14.0053C532.454 14.0054 554.601 36.1529 554.601 63.4736H555.668ZM505.133 12.9385V11.8717H439.447V12.9385V14.0053H505.133V12.9385ZM439.447 12.9385H440.514V12.9336H439.447H438.38V12.9385H439.447ZM439.447 12.9336V11.8668H-358.697V12.9336V14.0004H439.447V12.9336ZM-358.697 12.9336H-359.764V12.9385H-358.697H-357.63V12.9336H-358.697ZM-358.697 12.9385V11.8717H-424.382V12.9385V14.0053H-358.697V12.9385ZM-424.382 12.9385V11.8717C-452.881 11.8717 -475.985 34.9744 -475.985 63.4736H-474.918H-473.851C-473.851 36.1528 -451.703 14.0053 -424.382 14.0053V12.9385ZM-474.918 63.4736V62.4068H-530.176V63.4736V64.5404H-474.918V63.4736ZM-530.176 63.4736H-529.109C-529.109 34.9745 -552.212 11.8718 -580.711 11.8717L-580.711 12.9385L-580.711 14.0053C-553.39 14.0054 -531.243 36.1529 -531.243 63.4736H-530.176ZM-580.711 12.9385V11.8717H-646.396V12.9385V14.0053H-580.711V12.9385ZM-646.396 12.9385H-645.33V6.79492H-646.396H-647.463V12.9385H-646.396ZM-646.396 6.79492V7.86171H727.146V6.79492V5.72814H-646.396V6.79492ZM727.146 6.79492H726.08V12.9385H727.146H728.213V6.79492H727.146ZM727.153 5.86328V4.7965H-646.562V5.86328V6.93007H727.153V5.86328ZM-646.562 5.86328H-645.496V4.36035H-646.562H-647.629V5.86328H-646.562ZM-646.562 4.36035V5.42714H727.153V4.36035V3.29357H-646.562V4.36035ZM727.153 4.36035H726.087V5.86328H727.153H728.22V4.36035H727.153Z" fill="#040404" fill-opacity="0.5" mask="url(#path-11-outside-3_450_11720)"/>
|
||||
<g filter="url(#filter3_f_450_11720)" style="mix-blend-mode:color-dodge">
|
||||
<path d="M712.599 65.6074C718.785 65.6074 723.8 70.6223 723.8 76.8086V94.4102H-647.082V76.8086C-647.082 70.6223 -642.067 65.6074 -635.881 65.6074H712.599Z" stroke="url(#paint6_linear_450_11720)" stroke-width="1.06678"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_f_450_11720)" style="mix-blend-mode:plus-lighter">
|
||||
<mask id="path-14-outside-4_450_11720" maskUnits="userSpaceOnUse" x="-648.562" y="3.33496" width="1366" height="61" fill="black">
|
||||
<rect fill="white" x="-648.562" y="3.33496" width="1366" height="61"/>
|
||||
<path d="M716.281 5.33496V11.4834H652.423C624.004 11.4834 600.966 34.5219 600.966 62.9404H546.139C546.139 34.5219 523.1 11.4834 494.682 11.4834H469.869V11.5244H-427.018C-454.484 12.6027 -476.42 35.2098 -476.42 62.9404H-531.247C-531.247 34.5219 -554.286 11.4834 -582.704 11.4834H-646.562V5.33496H716.281Z"/>
|
||||
</mask>
|
||||
<path d="M716.281 5.33496H717.348V4.26818H716.281V5.33496ZM716.281 11.4834V12.5502H717.348V11.4834H716.281ZM600.966 62.9404V64.0072H602.033V62.9404H600.966ZM546.139 62.9404H545.072V64.0072H546.139V62.9404ZM469.869 11.4834V10.4166H468.802V11.4834H469.869ZM469.869 11.5244V12.5912H470.936V11.5244H469.869ZM-427.018 11.5244V10.4576H-427.039L-427.059 10.4585L-427.018 11.5244ZM-476.42 62.9404V64.0072H-475.353V62.9404H-476.42ZM-531.247 62.9404H-532.314V64.0072H-531.247V62.9404ZM-646.562 11.4834H-647.629V12.5502H-646.562V11.4834ZM-646.562 5.33496V4.26818H-647.629V5.33496H-646.562ZM716.281 5.33496H715.214V11.4834H716.281H717.348V5.33496H716.281ZM716.281 11.4834V10.4166H652.423V11.4834V12.5502H716.281V11.4834ZM652.423 11.4834V10.4166C623.415 10.4166 599.899 33.9327 599.899 62.9404H600.966H602.033C602.033 35.111 624.593 12.5502 652.423 12.5502V11.4834ZM600.966 62.9404V61.8736H546.139V62.9404V64.0072H600.966V62.9404ZM546.139 62.9404H547.205C547.205 33.9327 523.689 10.4166 494.682 10.4166V11.4834V12.5502C522.511 12.5502 545.072 35.111 545.072 62.9404H546.139ZM494.682 11.4834V10.4166H469.869V11.4834V12.5502H494.682V11.4834ZM469.869 11.4834H468.802V11.5244H469.869H470.936V11.4834H469.869ZM469.869 11.5244V10.4576H-427.018V11.5244V12.5912H469.869V11.5244ZM-427.018 11.5244L-427.059 10.4585C-455.096 11.5591 -477.487 34.6346 -477.487 62.9404H-476.42H-475.353C-475.353 35.7849 -453.872 13.6462 -426.976 12.5904L-427.018 11.5244ZM-476.42 62.9404V61.8736H-531.247V62.9404V64.0072H-476.42V62.9404ZM-531.247 62.9404H-530.18C-530.18 33.9327 -553.696 10.4166 -582.704 10.4166V11.4834V12.5502C-554.875 12.5502 -532.314 35.111 -532.314 62.9404H-531.247ZM-582.704 11.4834V10.4166H-646.562V11.4834V12.5502H-582.704V11.4834ZM-646.562 11.4834H-645.496V5.33496H-646.562H-647.629V11.4834H-646.562ZM-646.562 5.33496V6.40175H716.281V5.33496V4.26818H-646.562V5.33496Z" fill="url(#paint7_linear_450_11720)" mask="url(#path-14-outside-4_450_11720)"/>
|
||||
</g>
|
||||
<g filter="url(#filter5_f_450_11720)" style="mix-blend-mode:plus-lighter">
|
||||
<mask id="path-16-outside-5_450_11720" maskUnits="userSpaceOnUse" x="-641.564" y="3.33496" width="1359" height="61" fill="black">
|
||||
<rect fill="white" x="-641.564" y="3.33496" width="1359" height="61"/>
|
||||
<path d="M715.234 5.33496C715.823 5.33496 716.301 5.81258 716.301 6.40175V10.4166C716.301 11.0058 715.823 11.4834 715.234 11.4834H653.033C624.972 11.4834 602.158 33.9456 601.588 61.87C601.576 62.4591 601.099 62.9404 600.51 62.9404H548.097C547.508 62.9404 547.031 62.4591 547.019 61.87C546.45 33.9457 523.635 11.4835 495.574 11.4834H471.174C471.162 11.4834 471.153 11.4926 471.153 11.5039C471.153 11.5152 471.144 11.5244 471.133 11.5244H-420.893C-448.004 12.5891 -469.727 34.6305 -470.283 61.8702C-470.295 62.4592 -470.772 62.9404 -471.361 62.9404H-523.774C-524.363 62.9404 -524.84 62.4591 -524.852 61.87C-525.422 33.9456 -548.236 11.4835 -576.297 11.4834H-638.498C-639.087 11.4834 -639.564 11.0058 -639.564 10.4166V6.40175C-639.564 5.81258 -639.087 5.33496 -638.498 5.33496H715.234Z"/>
|
||||
</mask>
|
||||
<path d="M495.574 11.4834V10.4166V10.4166V11.4834ZM-420.893 11.5244V10.4576H-420.914L-420.934 10.4585L-420.893 11.5244ZM-576.297 11.4834L-576.297 10.4166H-576.297V11.4834ZM-524.852 61.87L-525.918 61.8918V61.8918L-524.852 61.87ZM-470.283 61.8702L-471.35 61.8484L-470.283 61.8702ZM547.019 61.87L545.953 61.8918L547.019 61.87ZM601.588 61.87L600.521 61.8482L601.588 61.87ZM716.301 6.40175H715.234V10.4166H716.301H717.368V6.40175H716.301ZM715.234 11.4834V10.4166H653.033V11.4834V12.5502H715.234V11.4834ZM653.033 11.4834V10.4166C624.39 10.4166 601.103 33.3445 600.521 61.8482L601.588 61.87L602.655 61.8918C603.213 34.5467 625.554 12.5502 653.033 12.5502V11.4834ZM600.51 62.9404V61.8736H548.097V62.9404V64.0072H600.51V62.9404ZM547.019 61.87L548.086 61.8482C547.504 33.3445 524.217 10.4167 495.574 10.4166V11.4834V12.5502C523.053 12.5503 545.395 34.5468 545.953 61.8918L547.019 61.87ZM495.574 11.4834V10.4166H471.174V11.4834V12.5502H495.574V11.4834ZM471.133 11.5244V10.4576H-420.893V11.5244V12.5912H471.133V11.5244ZM-420.893 11.5244L-420.934 10.4585C-448.609 11.5452 -470.782 34.0433 -471.35 61.8484L-470.283 61.8702L-469.216 61.8919C-468.672 35.2177 -447.4 13.6329 -420.851 12.5904L-420.893 11.5244ZM-471.361 62.9404V61.8736H-523.774V62.9404V64.0072H-471.361V62.9404ZM-524.852 61.87L-523.785 61.8482C-524.367 33.3445 -547.654 10.4167 -576.297 10.4166L-576.297 11.4834L-576.297 12.5502C-548.818 12.5503 -526.476 34.5467 -525.918 61.8918L-524.852 61.87ZM-576.297 11.4834V10.4166H-638.498V11.4834V12.5502H-576.297V11.4834ZM-639.564 10.4166H-638.498V6.40175H-639.564H-640.631V10.4166H-639.564ZM-638.498 5.33496V6.40175H715.234V5.33496V4.26818H-638.498V5.33496ZM-639.564 6.40175H-638.498V6.40175V5.33496V4.26818C-639.676 4.26818 -640.631 5.22341 -640.631 6.40175H-639.564ZM-523.774 62.9404V61.8736C-523.775 61.8736 -523.775 61.8736 -523.774 61.8738C-523.773 61.874 -523.772 61.8742 -523.772 61.8744C-523.771 61.8746 -523.773 61.874 -523.776 61.8712C-523.778 61.8683 -523.781 61.8646 -523.783 61.8608C-523.784 61.8576 -523.785 61.8539 -523.785 61.8482L-524.852 61.87L-525.918 61.8918C-525.895 63.0425 -524.963 64.0072 -523.774 64.0072V62.9404ZM-470.283 61.8702L-471.35 61.8484C-471.35 61.854 -471.351 61.8578 -471.352 61.8609C-471.354 61.8647 -471.356 61.8683 -471.359 61.8712C-471.362 61.874 -471.364 61.8746 -471.363 61.8744C-471.363 61.8742 -471.362 61.8739 -471.361 61.8738C-471.36 61.8736 -471.36 61.8736 -471.361 61.8736V62.9404V64.0072C-470.172 64.0072 -469.24 63.0429 -469.216 61.8919L-470.283 61.8702ZM471.153 11.5039H470.087C470.087 10.9261 470.555 10.4576 471.133 10.4576V11.5244V12.5912C471.733 12.5912 472.22 12.1044 472.22 11.5039H471.153ZM548.097 62.9404V61.8736C548.096 61.8736 548.097 61.8736 548.097 61.8738C548.098 61.874 548.099 61.8742 548.099 61.8744C548.1 61.8746 548.098 61.874 548.096 61.8712C548.093 61.8683 548.09 61.8646 548.089 61.8608C548.087 61.8576 548.086 61.8539 548.086 61.8482L547.019 61.87L545.953 61.8918C545.976 63.0425 546.909 64.0072 548.097 64.0072V62.9404ZM601.588 61.87L600.521 61.8482C600.521 61.8539 600.52 61.8576 600.519 61.8608C600.517 61.8646 600.515 61.8683 600.512 61.8712C600.509 61.874 600.507 61.8746 600.508 61.8744C600.508 61.8742 600.509 61.874 600.51 61.8738C600.511 61.8736 600.511 61.8736 600.51 61.8736V62.9404V64.0072C601.699 64.0072 602.631 63.0425 602.655 61.8918L601.588 61.87ZM471.174 11.4834V10.4166C470.573 10.4166 470.087 10.9034 470.087 11.5039H471.153H472.22C472.22 12.0818 471.752 12.5502 471.174 12.5502V11.4834ZM-638.498 11.4834V10.4166H-638.498H-639.564H-640.631C-640.631 11.5949 -639.676 12.5502 -638.498 12.5502V11.4834ZM716.301 10.4166H715.234V10.4166V11.4834V12.5502C716.412 12.5502 717.368 11.595 717.368 10.4166H716.301ZM716.301 6.40175H717.368C717.368 5.22343 716.412 4.26818 715.234 4.26818V5.33496V6.40175V6.40175H716.301Z" fill="url(#paint8_linear_450_11720)" mask="url(#path-16-outside-5_450_11720)"/>
|
||||
</g>
|
||||
<g filter="url(#filter6_f_450_11720)">
|
||||
<path d="M-660.535 13.8682V4.26712C-660.535 3.08878 -659.58 2.13354 -658.402 2.13354H742.091C743.269 2.13354 744.225 3.08878 744.225 4.26711V13.8682" stroke="url(#paint9_linear_450_11720)" stroke-width="1.06678"/>
|
||||
</g>
|
||||
<g filter="url(#filter7_f_450_11720)">
|
||||
<path d="M-457.854 62.9403V26.9541C-455.524 22.5921 -433.393 15.4683 -366.998 13.8682H426.237C489.136 14.4016 534.564 18.2301 534.564 31.316V62.9403" stroke="url(#paint10_linear_450_11720)" stroke-width="1.06678"/>
|
||||
</g>
|
||||
<line x1="-555.33" y1="64.1475" x2="636.453" y2="64.1475" stroke="#400500" stroke-width="1.06678"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_450_11720" x="-449.396" y="17.1808" width="980.977" height="49.5774" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="1.06678" dy="1.06678"/>
|
||||
<feGaussianBlur stdDeviation="0.533392"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.49 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_450_11720"/>
|
||||
</filter>
|
||||
<filter id="filter1_ii_450_11720" x="-632.133" y="66.2541" width="1345.87" height="27.7161" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="1.06678" dy="1.06678"/>
|
||||
<feGaussianBlur stdDeviation="0.533392"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.929412 0 0 0 0 0.556863 0 0 0 0 0.266667 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_450_11720"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-2.13357"/>
|
||||
<feGaussianBlur stdDeviation="0.533392"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.322115 0 0 0 0 0.142276 0 0 0 0 0.0263267 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="effect1_innerShadow_450_11720" result="effect2_innerShadow_450_11720"/>
|
||||
</filter>
|
||||
<filter id="filter2_f_450_11720" x="-620.737" y="68.2742" width="1325.16" height="25.6028" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11720"/>
|
||||
</filter>
|
||||
<filter id="filter3_f_450_11720" x="-648.682" y="64.0071" width="1374.08" height="32.0036" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11720"/>
|
||||
</filter>
|
||||
<filter id="filter4_f_450_11720" x="-648.696" y="3.2014" width="1367.11" height="61.8726" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11720"/>
|
||||
</filter>
|
||||
<filter id="filter5_f_450_11720" x="-641.698" y="3.2014" width="1360.13" height="61.8726" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11720"/>
|
||||
</filter>
|
||||
<filter id="filter6_f_450_11720" x="-662.135" y="0.533374" width="1407.96" height="14.4016" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11720"/>
|
||||
</filter>
|
||||
<filter id="filter7_f_450_11720" x="-459.454" y="12.2678" width="995.618" height="51.7392" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="0.533392" result="effect1_foregroundBlur_450_11720"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_450_11720" x1="40.559" y1="17.1808" x2="40.559" y2="65.6915" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#442111"/>
|
||||
<stop offset="0.05" stop-color="#522510"/>
|
||||
<stop offset="1" stop-color="#090504"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_450_11720" x1="-466.895" y1="34.5814" x2="530.514" y2="34.5816" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#803414"/>
|
||||
<stop offset="0.077962" stop-color="#803414" stop-opacity="0.25"/>
|
||||
<stop offset="0.939742" stop-color="#803414" stop-opacity="0.282099"/>
|
||||
<stop offset="1" stop-color="#803414"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_450_11720" x1="40.2691" y1="68.3877" x2="40.2691" y2="91.8366" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#AD571E"/>
|
||||
<stop offset="1" stop-color="#88441A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_450_11720" x1="40.2691" y1="68.3877" x2="40.2691" y2="91.8366" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#AD571E"/>
|
||||
<stop offset="1" stop-color="#88441A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_450_11720" x1="41.8433" y1="70.4077" x2="41.8433" y2="91.7434" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFC893"/>
|
||||
<stop offset="1" stop-color="#FFC893" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_450_11720" x1="-646.562" y1="33.917" x2="727.153" y2="33.917" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFAC63"/>
|
||||
<stop offset="0.144231" stop-color="#FF7E0D"/>
|
||||
<stop offset="0.889423" stop-color="#FF7E0D"/>
|
||||
<stop offset="0.961538" stop-color="#EB5200"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_450_11720" x1="38.3589" y1="66.1406" x2="38.3589" y2="93.877" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FDF7CC"/>
|
||||
<stop offset="1" stop-color="#FDF7CC" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_450_11720" x1="34.8594" y1="5.33496" x2="34.8594" y2="14.4016" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E5A76C"/>
|
||||
<stop offset="1" stop-color="#FFC893" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_450_11720" x1="-552.65" y1="12.268" x2="-496.387" y2="155.461" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E5A76C"/>
|
||||
<stop offset="1" stop-color="#FFC893" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint9_linear_450_11720" x1="41.8447" y1="2.13354" x2="41.8447" y2="13.8682" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#C17300"/>
|
||||
<stop offset="1" stop-color="#C17300" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint10_linear_450_11720" x1="38.3552" y1="13.8682" x2="38.3552" y2="62.9403" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D59628"/>
|
||||
<stop offset="1" stop-color="#AD6E00" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_450_11720">
|
||||
<rect width="84" height="95" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_450_11720">
|
||||
<rect width="688" height="95" fill="white" transform="translate(-302.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 32 KiB |
Loaded 100 of 486 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user