style(storybook): refine manager sidebar, LNB inset and shared story frame

- Manager sidebar labels and sort order for the grouped pages; LNB 1-depth
  text start 12px (token, baseline and LNB doc updated)
- Shared story frame: drop the divider under section titles, side margin
  max 60px -> 30px; Foundation color/typography spacing
- Pseudo-state mirroring also pins :focus-visible via data-sb-focus while
  keeping the previous clones for compound rules (Slider Focus + Drag)
This commit is contained in:
shkim4 committed 2026-11-13 17:57:34 +09:00
1 parent 7ff852952a
commit fe9c115cf9
22 files changed
+100 -83

No files matched your search

+35 -39
View File
@@ -16,7 +16,8 @@
--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. */
2026-11-13 compact 180px LNB: 14px side gutters, 40px / 36px rows, 6px selected radius and a 12px join
(was 18px, the LNB specimen value) so the curves stay in proportion to the narrower column. Not a new token. */
@font-face {
font-family: 'Hanmac Manager Noto Sans KR';
src: url('/fonts/NotoSansKR-variable.ttf') format('truetype');
@@ -36,7 +37,7 @@
}
#storybook-sidebar-region {
--hm-manager-join: 18px;
--hm-manager-join: 12px;
color: var(--color-fg-on-green-primary);
background: linear-gradient(90deg,
var(--hm-green-default) 0%, var(--hm-green-light-1) 30%,
@@ -46,7 +47,7 @@
font-family: var(--hm-manager-font);
}
#storybook-sidebar-region [data-radix-scroll-area-content] > div {
padding: 28px 0 28px 20px;
padding: 20px 0 20px 14px;
}
/* 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.
@@ -61,13 +62,17 @@
background: var(--hm-fg-on-green-secondary);
}
#storybook-sidebar-region .sidebar-header {
margin: 0 20px 0px 0;
margin: 0 14px 0px 0;
padding: 0;
color: var(--color-fg-on-green-primary);
}
/* The Settings button is hidden, so the title uses the full header width. */
#storybook-sidebar-region .sidebar-header > div { margin-right: 0; }
#storybook-sidebar-region .sidebar-header > div a {
/* 2026-11-13: centred on the 180px LNB (the header box is already symmetric, 14px each side);
Storybook's -4px link inline margin is dropped so the link spans exactly that box. */
justify-content: center;
margin-inline: 0;
color: var(--color-fg-on-green-primary);
font: 700 16px/30px var(--hm-manager-font);
white-space: normal;
@@ -113,27 +118,18 @@
#storybook-explorer-tree .sidebar-subheading {
position: relative;
margin: 0;
padding: 0 20px 0 0;
min-height: 48px;
padding: 0 14px 0 0;
min-height: 40px;
color: var(--hm-fg-on-green-secondary);
}
#storybook-explorer-tree .sidebar-subheading[data-hm-section]:not([data-hm-section='0']) {
margin-top: 24px;
}
#storybook-explorer-tree .sidebar-subheading[data-hm-section]:not([data-hm-section='0'])::before {
content: '';
position: absolute;
top: -12px;
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);
margin-top: 0;
}
/* 2026-11-13: the divider above each group was removed. */
#storybook-explorer-tree .sidebar-subheading > button {
min-height: 48px;
min-height: 40px;
color: inherit;
font: 700 14px/28px var(--hm-manager-font);
font: 700 14px/24px var(--hm-manager-font);
letter-spacing: 0;
text-transform: none;
}
@@ -141,7 +137,7 @@
position: relative;
margin: 0;
padding: 0;
min-height: 48px;
min-height: 40px;
overflow: visible;
border: 0;
border-radius: 0;
@@ -155,51 +151,51 @@
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;
border-radius: 6px 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;
border-radius: 0 0 0 6px;
padding-bottom: 6px;
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: 48px;
gap: 8px;
min-height: 40px;
height: auto;
width: 100%;
margin: 0;
padding: 10px 32px 10px 18px;
padding: 8px 14px 8px 10px;
border: 0;
border-radius: 4px 0 0 4px;
background: transparent;
color: inherit;
font: 700 14px/28px var(--hm-manager-font);
font: 700 14px/24px var(--hm-manager-font);
text-align: left;
text-decoration: none;
white-space: normal;
overflow: visible;
text-overflow: clip;
}
/* Sub rows (depth 2+): 42px (7px + 28px line + 7px), shorter than the 48px top-level rows. */
/* Sub rows (depth 2+): 36px (6px + 24px line + 6px), shorter than the 40px top-level rows (2026-11-13 compact 180px LNB). */
#storybook-explorer-tree .sidebar-item:is([data-hm-depth='2'], [data-hm-depth='3'], [data-hm-depth='4']) {
min-height: 42px;
min-height: 36px;
}
#storybook-explorer-tree .sidebar-item:is([data-hm-depth='2'], [data-hm-depth='3'], [data-hm-depth='4']) > :is(a[id], button[id]:not([data-testid])) {
min-height: 42px;
padding-block: 7px;
min-height: 36px;
padding-block: 6px;
}
#storybook-explorer-tree .sidebar-item[data-hm-depth='2'] > :is(a[id], button[id]:not([data-testid])) {
margin-left: 32px;
width: calc(100% - 32px);
margin-left: 26px;
width: calc(100% - 26px);
}
#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);
margin-left: 36px;
width: calc(100% - 36px);
}
/* Menu item labels and section heading labels (.sidebar-subheading) are both 14px / 700. */
#storybook-explorer-tree .sidebar-item .hm-manager-label {
@@ -246,9 +242,9 @@
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. */
slot. Deeper rows retain their 26px / 36px 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;
padding-left: 26px;
}
#storybook-explorer-tree .sidebar-item[data-hm-current-parent='true'],
#storybook-explorer-tree .sidebar-subheading[data-hm-current-parent='true'] {
@@ -261,7 +257,7 @@
z-index: 1;
background: var(--color-bg-content);
color: var(--color-action-primary);
border-radius: 8px 0 0 8px;
border-radius: 6px 0 0 6px;
}
/* Keep the keyboard skip link without reserving a 1px flex column.
That column exposed the green branch background at the selected item's end. */
@@ -300,7 +296,7 @@
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);
top: calc(100% - 6px);
}
/* Auxiliary status/create actions are separate from selected row geometry.
Hide only these controls; the Story link and native group toggles stay intact. */
+10 -6
View File
@@ -29,6 +29,9 @@ toolbarStyle.textContent = `
document.head.appendChild(toolbarStyle)
// Sidebar display names only. Story titles, Story IDs and storySort stay English; items not listed keep item.name.
// 2026-11-13 grouped pages: 버튼 (Button + Toggle), 선택 컨트롤 (Checkbox / Radio Group / Switch), 시트·드로어 (Sheet + Drawer),
// 툴팁·호버 카드 (Tooltip + Hover Card), 접기·펼치기 (Accordion + Collapsible), 날짜 선택 (Date Picker + Calendar). The absorbed
// components keep their own labels for the hidden (verification) Stories shown by the sidebar tag filter.
const koreanLabels = {
foundation: '기본 요소',
components: '컴포넌트',
@@ -43,6 +46,7 @@ const koreanLabels = {
'components-form-input': '입력창',
'components-form-textarea': '텍스트 영역',
'components-form-select': '선택 목록',
'components-form-selection-controls': '선택 컨트롤',
'components-form-checkbox': '체크박스',
'components-form-radio-group': '라디오 버튼',
'components-form-switch': '스위치',
@@ -55,12 +59,12 @@ const koreanLabels = {
'components-navigation-menubar': '메뉴바',
'components-overlay-dialog': '대화상자',
'components-overlay-alert-dialog': '확인·경고 대화상자',
'components-overlay-sheet': '시트 패널',
'components-overlay-sheet': '시트·드로어',
'components-overlay-drawer': '드로어',
'components-overlay-dropdown-menu': '드롭다운 메뉴',
'components-overlay-dropdown-menu': '팝업 메뉴',
'components-overlay-context-menu': '컨텍스트 메뉴',
'components-overlay-popover': '팝오버',
'components-overlay-tooltip': '툴팁',
'components-overlay-tooltip': '툴팁·호버 카드',
'components-overlay-hover-card': '호버 카드',
'components-data-display-card': '카드',
'components-data-display-table': '테이블',
@@ -72,7 +76,7 @@ const koreanLabels = {
'components-data-display-aspect-ratio': '화면 비율',
'components-data-display-carousel': '캐러셀',
'components-data-display-scroll-area': '스크롤 영역',
'components-disclosure-accordion': '아코디언',
'components-disclosure-accordion': '접기·펼치기',
'components-disclosure-collapsible': '접기·펼치기',
'components-disclosure-resizable': '크기 조절',
'components-search-date-command': '명령어 검색',
@@ -86,9 +90,9 @@ const koreanLabels = {
'patterns-data-data-table': '데이터 테이블',
'patterns-data-filter-bar': '필터 바',
'patterns-layout': '화면 구성',
'patterns-layout-gnb': '전체 메뉴 (GNB)',
'patterns-layout-gnb': '상단 메뉴',
'patterns-layout-dashboard': '대시보드',
'patterns-layout-lnb': '좌측 메뉴 (LNB)',
'patterns-layout-lnb': '좌측 메뉴',
'patterns-layout-dashboard-layout': '대시보드',
'patterns-layout-page-header': '페이지 헤더',
chart: '차트',
+4 -3
View File
@@ -29,7 +29,8 @@ const preview: Preview = {
"Components",
[
"Form",
["Button", "Input", "Textarea", "Select", "Checkbox", "Radio Group", "Switch", "Slider", "Toggle", "Toggle Group", "Label"],
// "Selection Controls" (2026-11-13) takes the place of Checkbox / Radio Group / Switch, now hidden from the sidebar.
["Button", "Input", "Textarea", "Select", "Selection Controls", "Checkbox", "Radio Group", "Switch", "Slider", "Toggle", "Toggle Group", "Label"],
"Navigation",
["Tabs", "Breadcrumb", "Pagination", "Navigation Menu", "Menubar"],
"Overlay",
@@ -48,9 +49,9 @@ const preview: Preview = {
"Chart",
["Bar Chart", "Line Chart", "Donut Chart", "Pie Chart", "Radar Chart", "Area Chart", "Radial Chart"],
"SW",
["Header", ["SW Header", "*"], "Viewer", "Step Header", "Popup", "하단 명령어바", ["SW 하단 명령어바", "*"], "Theme"],
["Header", ["SW Header", "*"], "Viewer", "Step Header", "Popup", "하단 명령어바", ["SW 명령어바", "*"], "Theme"],
"조직도",
["개요", "부서별 색상", "전체 구성", "확장 조직도"],
["가족사 현황", "부서별 색상", "전체 구성", "확장 조직도"],
"노선도",
"*",
// With includeNames, keeps the Loader group after the Runtime Contract Stories (its place without names).
+16 -7
View File
@@ -1,19 +1,28 @@
// 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
// Pins :hover, :active and :focus-visible visuals for state Stories (Storybook only, never loaded by the app).
// Every rule that uses :hover, :active or :focus-visible 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.
// 2026-11-13: :focus-visible → data-sb-focus, so a Focus specimen can show its ring without holding the page focus (only one
// element can be focused; e.g. Selection Controls focuses the Checkbox and pins the Radio Group / Switch rings).
const HOVER = "data-sb-hover"
const ACTIVE = "data-sb-active"
const FOCUS = "data-sb-focus"
const PSEUDO = /:hover\b|:active\b|:focus-visible\b/
// A negated :focus-visible (:not(:focus-visible)) would match every element without the attribute, so such a rule keeps its
// :focus-visible and only its :hover / :active (if any) are swapped.
const NEGATED_FOCUS = /:not\([^)]*:focus-visible/
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,
)
if (PSEUDO.test(rule.selectorText)) {
// `base` keeps :focus-visible (the pre-2026-11-13 clone), so real focus + data-sb-hover / data-sb-active still matches
// a compound rule such as :focus-visible:active (Slider Focus + Drag). `full` also swaps :focus-visible.
const base = rule.cssText.replace(/:hover\b/g, `[${HOVER}]`).replace(/:active\b/g, `[${ACTIVE}]`)
const full = NEGATED_FOCUS.test(rule.selectorText) ? base : base.replace(/:focus-visible\b/g, `[${FOCUS}]`)
if (full !== base) list.insertRule(full, i + 1)
if (base !== rule.cssText) list.insertRule(base, i + 1)
}
} else if ("cssRules" in rule) {
mirror(rule as CSSGroupingRule)