@qijenchen/design-system 0.1.0-beta.65 → 0.1.0-beta.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,10 +6,13 @@ import * as React from 'react';
6
6
  /**
7
7
  * Notice — Toast / Alert 共用的視覺佈局層
8
8
  *
9
- * ── Typography: md tier ──
10
- * title: text-body (14px) leading-compact — 有 description 時加 font-medium
11
- * description: text-body (14px) leading-compact + text-fg-secondary (neutral-8)
9
+ * ── Typography: Family 2 reading-md(2026-06-15 user 拍板,off-grid 偏移收斂)──
10
+ * title: text-body (14px) default leading 1.5 — 有 description 時加 font-medium
11
+ * description: text-body (14px) default leading 1.5 + text-fg-secondary (neutral-8)
12
12
  * 14px 配 14px — 視覺層級靠 font-weight + color 區分,不靠 font-size。
13
+ * **行高 = reading 預設 1.5**(非 compact):Notice 是 Family 2 reading consumer,
14
+ * ItemContent 預設 mode='reading' + gap token `--item-gap-label-desc-reading` 名實相符。
15
+ * 原 `leading-compact`(1.3)= reading-gap + scanning-行高 混搭的 off-grid 偏移,已移除。
13
16
  *
14
17
  * ── Padding(固定,不隨 density 變) ──
15
18
  * px = px-4(16px)
@@ -1 +1 @@
1
- {"version":3,"file":"notice.d.ts","sourceRoot":"","sources":["../../../src/components/Notice/notice.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAM9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;AAUhF,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAM3D,CAAA;AAQD,MAAM,WAAW,WACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAC3D,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAA;IACzB;;OAEG;IACH,WAAW,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,KAAK,CAAA;CAC7C;AAED,QAAA,MAAM,MAAM,oFA0DX,CAAA;AA+BD,wBAAgB,eAAe,IAAI,MAAM,GAAG,OAAO,CAGlD;AAID,eAAO,MAAM,UAAU;;;;;;;;;;;CAeb,CAAA;AAEV,OAAO,EAAE,MAAM,EAAE,CAAA"}
1
+ {"version":3,"file":"notice.d.ts","sourceRoot":"","sources":["../../../src/components/Notice/notice.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAM9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;AAUhF,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAM3D,CAAA;AAYD,MAAM,WAAW,WACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAC3D,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAA;IACzB;;OAEG;IACH,WAAW,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,KAAK,CAAA;CAC7C;AAED,QAAA,MAAM,MAAM,oFA8DX,CAAA;AA+BD,wBAAgB,eAAe,IAAI,MAAM,GAAG,OAAO,CAGlD;AAID,eAAO,MAAM,UAAU;;;;;;;;;;;CAeb,CAAA;AAEV,OAAO,EAAE,MAAM,EAAE,CAAA"}
@@ -3,7 +3,7 @@ import * as React from "react";
3
3
  import { XCircle, TriangleAlert, CircleCheck, Info, X } from "lucide-react";
4
4
  import { cn } from "../../lib/utils.js";
5
5
  import { Button } from "../Button/button.js";
6
- import { ItemPrefix, ItemContent } from "../../patterns/element-anatomy/item-anatomy.js";
6
+ import { ItemPrefix, ItemContent, ItemSuffix } from "../../patterns/element-anatomy/item-anatomy.js";
7
7
  const VARIANT_ICON = {
8
8
  neutral: null,
9
9
  info: Info,
@@ -20,7 +20,11 @@ const SUBTLE_ICON_COLOR = {
20
20
  };
21
21
  const NOTICE_LAYOUT = [
22
22
  "flex items-start gap-2 w-full",
23
- "text-body leading-compact",
23
+ // 2026-06-15 user 拍板:Notice = 乾淨 Family 2 reading-md consumer。
24
+ // 原 `leading-compact`(1.3)是 off-grid 偏移(reading gap token + scanning 行高混搭、未文件化),
25
+ // 已移除 → label/desc 走 text-body reading 預設 1.5,與 ItemContent 預設 mode='reading' + gap
26
+ // token `--item-gap-label-desc-reading` 名實相符。詳 notice.spec.md「Typography」段。
27
+ "text-body",
24
28
  "px-4 py-3"
25
29
  ].join(" ");
26
30
  const Notice = React.forwardRef(
@@ -54,8 +58,12 @@ const Notice = React.forwardRef(
54
58
  labelClassName: description ? "font-medium" : void 0
55
59
  }
56
60
  ),
57
- (endContent || dismissible) && // @row-slot-handcraft-allow: Notice 是非-row alert(非 item-anatomy row),此 end slot 是 Notice 自身 layout 的 dismiss/endContent 容器,不是 row prefix/suffix → 不消費 ItemPrefix/ItemSuffix
58
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 shrink-0 h-[1lh]", children: [
61
+ (endContent || dismissible) && // 2026-06-15 user 拍板:消費 ItemSuffix primitive(原手刻 div + @row-slot-handcraft-allow
62
+ // 已移除)。item-anatomy.spec.md Notice action/dismiss 對應到 suffix slot;ItemSuffix
63
+ // base geometry(h-[1lh] shrink-0 ml-auto flex items-center gap-2)正是此處所需,hoverReveal
64
+ // 預設 false 故無 row inline-action 機制干擾。內裝 dismiss = Button iconOnly dismiss xs(banner
65
+ // family canonical,overlay-surface.spec.md「Chrome dismiss size canonical」)。
66
+ /* @__PURE__ */ jsxs(ItemSuffix, { children: [
59
67
  endContent,
60
68
  dismissible && /* @__PURE__ */ jsx(
61
69
  Button,
@@ -1 +1 @@
1
- {"version":3,"file":"notice.js","sources":["../../../src/components/Notice/notice.tsx"],"sourcesContent":["/**\n * @internal — DS-internal 單元(per `.claude/rules/ui-development.md` Public vs Internal canonical;spec frontmatter `isInternal`)。\n * 不進 root barrel front-door;由 Alert / Toast wrap 消費,end-user app 請用 wrapper 元件。\n */\nimport * as React from 'react'\nimport { X as XIcon, Info, CircleCheck, TriangleAlert, XCircle, type LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/design-system/components/Button/button'\nimport { ItemContent, ItemPrefix } from '@/design-system/patterns/element-anatomy/item-anatomy'\n\n/**\n * Notice — Toast / Alert 共用的視覺佈局層\n *\n * ── Typography: md tier ──\n * title: text-body (14px) leading-compact — 有 description 時加 font-medium\n * description: text-body (14px) leading-compact + text-fg-secondary (neutral-8)\n * 14px 配 14px — 視覺層級靠 font-weight + color 區分,不靠 font-size。\n *\n * ── Padding(固定,不隨 density 變) ──\n * px = px-4(16px)\n * py = py-3(12px)\n * gap = gap-2(8px)\n * Toast/Alert 是浮動通知,不是工作區域元件——density 控制表單/選單的緊湊度,\n * 通知的尺寸應該固定,不隨 density 縮放。\n *\n * ── Icon: md tier ──\n * icon size: 16px(ICON_SIZE.md)\n *\n * ── Dismiss X(chrome corner close,Cat 3 Action group region)──\n * 用 Button iconOnly dismiss **size=\"xs\"** — 非 Inline Action、非自刻 button。\n * Rationale(Notification banner family canonical):\n * - Notice / Alert / Toast 屬 **notification banner family**(ephemeral、px-4 py-3 固定不隨 density),\n * dismiss 是邊角小 affordance,xs 視覺不搶眼不跟 content 競爭。見 `overlay-surface.spec.md`\n * 「Chrome dismiss size canonical」(overlay header 走 sm native + 負 margin trick;xs 只用於 notification banner family)\n * - Close 左側可加 refresh / share(action group region),皆統一 xs\n * - `dismiss` prop 自動套 variant=\"text\" + fg-muted override\n * SSOT:patterns/element-anatomy/inline-action.spec.md「Dismiss canonical — X close only」\n * + components/Alert/alert.spec.md「Chrome corner close X canonical」。\n */\n\nexport type NoticeVariant = 'neutral' | 'info' | 'success' | 'warning' | 'error'\n\nconst VARIANT_ICON: Record<NoticeVariant, LucideIcon | null> = {\n neutral: null,\n info: Info,\n success: CircleCheck,\n warning: TriangleAlert,\n error: XCircle,\n}\n\nexport const SUBTLE_ICON_COLOR: Record<NoticeVariant, string> = {\n neutral: 'text-fg-muted',\n info: 'text-info-text',\n success: 'text-success-text',\n warning: 'text-warning-text',\n error: 'text-error-text',\n}\n\nconst NOTICE_LAYOUT = [\n 'flex items-start gap-2 w-full',\n 'text-body leading-compact',\n 'px-4 py-3',\n].join(' ')\n\nexport interface NoticeProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {\n variant?: NoticeVariant\n title: React.ReactNode\n description?: React.ReactNode\n endContent?: React.ReactNode\n dismissible?: boolean\n onDismiss?: () => void\n /** ARIA label for the dismiss button. Override for i18n. Default: \"關閉通知\" */\n dismissAriaLabel?: string\n iconClassName?: string\n /**\n * ARIA role 由 wrapping consumer 決定(Alert / Toast / 自管 host),Notice 預設不帶 role。\n * Notice 是 layout primitive,Alert / Toast 是 live region 擁有者——避免 nested live region\n * 造成 screen reader 重複朗讀。明文傳遞才覆寫。\n */\n role?: 'status' | 'alert'\n /**\n * 對應 role 的 aria-live 策略,wrapping consumer 決定;Notice 預設 undefined 不帶 live region。\n */\n 'aria-live'?: 'polite' | 'assertive' | 'off'\n}\n\nconst Notice = React.forwardRef<HTMLDivElement, NoticeProps>(\n (\n {\n variant = 'neutral',\n title,\n description,\n endContent,\n dismissible = true,\n onDismiss,\n dismissAriaLabel = '關閉通知', // i18n-allow: DS default; consumer override via dismissAriaLabel prop\n iconClassName,\n className,\n ...props\n },\n ref,\n ) => {\n const StatusIcon = VARIANT_ICON[variant]\n\n return (\n <div\n ref={ref}\n className={cn(NOTICE_LAYOUT, className)}\n {...props}\n >\n {StatusIcon && (\n <ItemPrefix>\n <StatusIcon size={16} className={cn('shrink-0', iconClassName)} aria-hidden />\n </ItemPrefix>\n )}\n\n {/* Title + description 消費 ItemContent primitive(SSOT)。\n Label 有 desc 時 font-medium(Notice idiom:title 跟 desc 對照時 title 要更重)。 */}\n <ItemContent\n label={title}\n description={description}\n labelClassName={description ? 'font-medium' : undefined}\n />\n\n {(endContent || dismissible) && (\n // @row-slot-handcraft-allow: Notice 是非-row alert(非 item-anatomy row),此 end slot 是 Notice 自身 layout 的 dismiss/endContent 容器,不是 row prefix/suffix → 不消費 ItemPrefix/ItemSuffix\n <div className=\"flex items-center gap-2 shrink-0 h-[1lh]\">\n {endContent}\n {dismissible && (\n <Button\n data-dismiss\n iconOnly\n dismiss\n size=\"xs\"\n startIcon={XIcon}\n aria-label={dismissAriaLabel}\n onClick={onDismiss}\n />\n )}\n </div>\n )}\n </div>\n )\n },\n)\nNotice.displayName = 'Notice'\n\n// Singleton MutationObserver + subscription fan-out(2026-04-22 D3 perf audit):\n// 先前每個 useInverseTheme consumer(Alert / Toast / Notice instance 等)各建一個 MO,\n// N 個 Notice = N 個 observers。singleton 共用一個 MO + pub/sub 讓 theme swap 只做一次 DOM read。\nlet themeObserverStarted = false\nconst themeSubscribers = new Set<() => void>()\n\nfunction getInverseTheme(): 'dark' | 'light' {\n if (typeof document === 'undefined') return 'dark'\n const current = document.documentElement.getAttribute('data-theme') ?? 'light'\n return current === 'dark' ? 'light' : 'dark'\n}\n\nfunction startThemeObserver() {\n if (themeObserverStarted || typeof document === 'undefined') return\n themeObserverStarted = true\n const root = document.documentElement\n const observer = new MutationObserver(() => {\n themeSubscribers.forEach((cb) => cb())\n })\n observer.observe(root, { attributes: true, attributeFilter: ['data-theme'] })\n}\n\nfunction subscribe(cb: () => void): () => void {\n startThemeObserver()\n themeSubscribers.add(cb)\n return () => themeSubscribers.delete(cb)\n}\n\nexport function useInverseTheme(): 'dark' | 'light' {\n // useSyncExternalStore canonical (React 18+):單一 external source 被 N consumers 訂閱\n return React.useSyncExternalStore(subscribe, getInverseTheme, getInverseTheme)\n}\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const noticeMeta = {\n component: 'Notice',\n family: 2, // Family 2(List item layout)消費者 — 對齊 notice.spec.md frontmatter family: 2 + body「Layout Family」段\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default'], // 無互動 layout primitive(spec「邊界案例」:不擁有 disabled;hover / focus 屬內嵌 dismiss Button)\n tokens: {\n bg: [],\n fg: ['text-error-text', 'text-fg-muted', 'text-fg-secondary', 'text-info-text', 'text-success-text', 'text-warning-text'],\n ring: [],\n },\n} as const\n\nexport { Notice }\n"],"names":["XIcon"],"mappings":";;;;;;AA0CA,MAAM,eAAyD;AAAA,EAC7D,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAO;AACT;AAEO,MAAM,oBAAmD;AAAA,EAC9D,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAO;AACT;AAEA,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AAyBV,MAAM,SAAS,MAAM;AAAA,EACnB,CACE;AAAA,IACE,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd;AAAA,IACA,mBAAmB;AAAA;AAAA,IACnB;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,aAAa,aAAa,OAAO;AAEvC,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,WAAW,GAAG,eAAe,SAAS;AAAA,QACrC,GAAG;AAAA,QAEH,UAAA;AAAA,UAAA,cACC,oBAAC,YAAA,EACC,UAAA,oBAAC,YAAA,EAAW,MAAM,IAAI,WAAW,GAAG,YAAY,aAAa,GAAG,eAAW,MAAC,GAC9E;AAAA,UAKF;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,OAAO;AAAA,cACP;AAAA,cACA,gBAAgB,cAAc,gBAAgB;AAAA,YAAA;AAAA,UAAA;AAAA,WAG9C,cAAc;AAAA,UAEd,qBAAC,OAAA,EAAI,WAAU,4CACZ,UAAA;AAAA,YAAA;AAAA,YACA,eACC;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,gBAAY;AAAA,gBACZ,UAAQ;AAAA,gBACR,SAAO;AAAA,gBACP,MAAK;AAAA,gBACL,WAAWA;AAAAA,gBACX,cAAY;AAAA,gBACZ,SAAS;AAAA,cAAA;AAAA,YAAA;AAAA,UACX,EAAA,CAEJ;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AACF;AACA,OAAO,cAAc;AAKrB,IAAI,uBAAuB;AAC3B,MAAM,uCAAuB,IAAA;AAE7B,SAAS,kBAAoC;AAC3C,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,UAAU,SAAS,gBAAgB,aAAa,YAAY,KAAK;AACvE,SAAO,YAAY,SAAS,UAAU;AACxC;AAEA,SAAS,qBAAqB;AAC5B,MAAI,wBAAwB,OAAO,aAAa,YAAa;AAC7D,yBAAuB;AACvB,QAAM,OAAO,SAAS;AACtB,QAAM,WAAW,IAAI,iBAAiB,MAAM;AAC1C,qBAAiB,QAAQ,CAAC,OAAO,GAAA,CAAI;AAAA,EACvC,CAAC;AACD,WAAS,QAAQ,MAAM,EAAE,YAAY,MAAM,iBAAiB,CAAC,YAAY,GAAG;AAC9E;AAEA,SAAS,UAAU,IAA4B;AAC7C,qBAAA;AACA,mBAAiB,IAAI,EAAE;AACvB,SAAO,MAAM,iBAAiB,OAAO,EAAE;AACzC;AAEO,SAAS,kBAAoC;AAElD,SAAO,MAAM,qBAAqB,WAAW,iBAAiB,eAAe;AAC/E;AAIO,MAAM,aAAa;AAAA,EACxB,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,SAAS;AAAA;AAAA,EAClB,QAAQ;AAAA,IACN,IAAI,CAAA;AAAA,IACJ,IAAI,CAAC,mBAAmB,iBAAiB,qBAAqB,kBAAkB,qBAAqB,mBAAmB;AAAA,IACxH,MAAM,CAAA;AAAA,EAAC;AAEX;"}
1
+ {"version":3,"file":"notice.js","sources":["../../../src/components/Notice/notice.tsx"],"sourcesContent":["/**\n * @internal — DS-internal 單元(per `.claude/rules/ui-development.md` Public vs Internal canonical;spec frontmatter `isInternal`)。\n * 不進 root barrel front-door;由 Alert / Toast wrap 消費,end-user app 請用 wrapper 元件。\n */\nimport * as React from 'react'\nimport { X as XIcon, Info, CircleCheck, TriangleAlert, XCircle, type LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/design-system/components/Button/button'\nimport { ItemContent, ItemPrefix, ItemSuffix } from '@/design-system/patterns/element-anatomy/item-anatomy'\n\n/**\n * Notice — Toast / Alert 共用的視覺佈局層\n *\n * ── Typography: Family 2 reading-md(2026-06-15 user 拍板,off-grid 偏移收斂)──\n * title: text-body (14px) default leading 1.5 — 有 description 時加 font-medium\n * description: text-body (14px) default leading 1.5 + text-fg-secondary (neutral-8)\n * 14px 配 14px — 視覺層級靠 font-weight + color 區分,不靠 font-size。\n * **行高 = reading 預設 1.5**(非 compact):Notice 是 Family 2 reading consumer,\n * ItemContent 預設 mode='reading' + gap token `--item-gap-label-desc-reading` 名實相符。\n * 原 `leading-compact`(1.3)= reading-gap + scanning-行高 混搭的 off-grid 偏移,已移除。\n *\n * ── Padding(固定,不隨 density 變) ──\n * px = px-4(16px)\n * py = py-3(12px)\n * gap = gap-2(8px)\n * Toast/Alert 是浮動通知,不是工作區域元件——density 控制表單/選單的緊湊度,\n * 通知的尺寸應該固定,不隨 density 縮放。\n *\n * ── Icon: md tier ──\n * icon size: 16px(ICON_SIZE.md)\n *\n * ── Dismiss X(chrome corner close,Cat 3 Action group region)──\n * 用 Button iconOnly dismiss **size=\"xs\"** — 非 Inline Action、非自刻 button。\n * Rationale(Notification banner family canonical):\n * - Notice / Alert / Toast 屬 **notification banner family**(ephemeral、px-4 py-3 固定不隨 density),\n * dismiss 是邊角小 affordance,xs 視覺不搶眼不跟 content 競爭。見 `overlay-surface.spec.md`\n * 「Chrome dismiss size canonical」(overlay header 走 sm native + 負 margin trick;xs 只用於 notification banner family)\n * - Close 左側可加 refresh / share(action group region),皆統一 xs\n * - `dismiss` prop 自動套 variant=\"text\" + fg-muted override\n * SSOT:patterns/element-anatomy/inline-action.spec.md「Dismiss canonical — X close only」\n * + components/Alert/alert.spec.md「Chrome corner close X canonical」。\n */\n\nexport type NoticeVariant = 'neutral' | 'info' | 'success' | 'warning' | 'error'\n\nconst VARIANT_ICON: Record<NoticeVariant, LucideIcon | null> = {\n neutral: null,\n info: Info,\n success: CircleCheck,\n warning: TriangleAlert,\n error: XCircle,\n}\n\nexport const SUBTLE_ICON_COLOR: Record<NoticeVariant, string> = {\n neutral: 'text-fg-muted',\n info: 'text-info-text',\n success: 'text-success-text',\n warning: 'text-warning-text',\n error: 'text-error-text',\n}\n\nconst NOTICE_LAYOUT = [\n 'flex items-start gap-2 w-full',\n // 2026-06-15 user 拍板:Notice = 乾淨 Family 2 reading-md consumer。\n // 原 `leading-compact`(1.3)是 off-grid 偏移(reading gap token + scanning 行高混搭、未文件化),\n // 已移除 → label/desc 走 text-body reading 預設 1.5,與 ItemContent 預設 mode='reading' + gap\n // token `--item-gap-label-desc-reading` 名實相符。詳 notice.spec.md「Typography」段。\n 'text-body',\n 'px-4 py-3',\n].join(' ')\n\nexport interface NoticeProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {\n variant?: NoticeVariant\n title: React.ReactNode\n description?: React.ReactNode\n endContent?: React.ReactNode\n dismissible?: boolean\n onDismiss?: () => void\n /** ARIA label for the dismiss button. Override for i18n. Default: \"關閉通知\" */\n dismissAriaLabel?: string\n iconClassName?: string\n /**\n * ARIA role 由 wrapping consumer 決定(Alert / Toast / 自管 host),Notice 預設不帶 role。\n * Notice 是 layout primitive,Alert / Toast 是 live region 擁有者——避免 nested live region\n * 造成 screen reader 重複朗讀。明文傳遞才覆寫。\n */\n role?: 'status' | 'alert'\n /**\n * 對應 role 的 aria-live 策略,wrapping consumer 決定;Notice 預設 undefined 不帶 live region。\n */\n 'aria-live'?: 'polite' | 'assertive' | 'off'\n}\n\nconst Notice = React.forwardRef<HTMLDivElement, NoticeProps>(\n (\n {\n variant = 'neutral',\n title,\n description,\n endContent,\n dismissible = true,\n onDismiss,\n dismissAriaLabel = '關閉通知', // i18n-allow: DS default; consumer override via dismissAriaLabel prop\n iconClassName,\n className,\n ...props\n },\n ref,\n ) => {\n const StatusIcon = VARIANT_ICON[variant]\n\n return (\n <div\n ref={ref}\n className={cn(NOTICE_LAYOUT, className)}\n {...props}\n >\n {StatusIcon && (\n <ItemPrefix>\n <StatusIcon size={16} className={cn('shrink-0', iconClassName)} aria-hidden />\n </ItemPrefix>\n )}\n\n {/* Title + description 消費 ItemContent primitive(SSOT)。\n Label 有 desc 時 font-medium(Notice idiom:title 跟 desc 對照時 title 要更重)。 */}\n <ItemContent\n label={title}\n description={description}\n labelClassName={description ? 'font-medium' : undefined}\n />\n\n {(endContent || dismissible) && (\n // 2026-06-15 user 拍板:消費 ItemSuffix primitive(原手刻 div + @row-slot-handcraft-allow\n // 已移除)。item-anatomy.spec.md 把 Notice 的 action/dismiss 對應到 suffix slot;ItemSuffix\n // base geometry(h-[1lh] shrink-0 ml-auto flex items-center gap-2)正是此處所需,hoverReveal\n // 預設 false 故無 row inline-action 機制干擾。內裝 dismiss = Button iconOnly dismiss xs(banner\n // family canonical,overlay-surface.spec.md「Chrome dismiss size canonical」)。\n <ItemSuffix>\n {endContent}\n {dismissible && (\n <Button\n data-dismiss\n iconOnly\n dismiss\n size=\"xs\"\n startIcon={XIcon}\n aria-label={dismissAriaLabel}\n onClick={onDismiss}\n />\n )}\n </ItemSuffix>\n )}\n </div>\n )\n },\n)\nNotice.displayName = 'Notice'\n\n// Singleton MutationObserver + subscription fan-out(2026-04-22 D3 perf audit):\n// 先前每個 useInverseTheme consumer(Alert / Toast / Notice instance 等)各建一個 MO,\n// N 個 Notice = N 個 observers。singleton 共用一個 MO + pub/sub 讓 theme swap 只做一次 DOM read。\nlet themeObserverStarted = false\nconst themeSubscribers = new Set<() => void>()\n\nfunction getInverseTheme(): 'dark' | 'light' {\n if (typeof document === 'undefined') return 'dark'\n const current = document.documentElement.getAttribute('data-theme') ?? 'light'\n return current === 'dark' ? 'light' : 'dark'\n}\n\nfunction startThemeObserver() {\n if (themeObserverStarted || typeof document === 'undefined') return\n themeObserverStarted = true\n const root = document.documentElement\n const observer = new MutationObserver(() => {\n themeSubscribers.forEach((cb) => cb())\n })\n observer.observe(root, { attributes: true, attributeFilter: ['data-theme'] })\n}\n\nfunction subscribe(cb: () => void): () => void {\n startThemeObserver()\n themeSubscribers.add(cb)\n return () => themeSubscribers.delete(cb)\n}\n\nexport function useInverseTheme(): 'dark' | 'light' {\n // useSyncExternalStore canonical (React 18+):單一 external source 被 N consumers 訂閱\n return React.useSyncExternalStore(subscribe, getInverseTheme, getInverseTheme)\n}\n\n// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)\n// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs\nexport const noticeMeta = {\n component: 'Notice',\n family: 2, // Family 2(List item layout)消費者 — 對齊 notice.spec.md frontmatter family: 2 + body「Layout Family」段\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default'], // 無互動 layout primitive(spec「邊界案例」:不擁有 disabled;hover / focus 屬內嵌 dismiss Button)\n tokens: {\n bg: [],\n fg: ['text-error-text', 'text-fg-muted', 'text-fg-secondary', 'text-info-text', 'text-success-text', 'text-warning-text'],\n ring: [],\n },\n} as const\n\nexport { Notice }\n"],"names":["XIcon"],"mappings":";;;;;;AA6CA,MAAM,eAAyD;AAAA,EAC7D,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAO;AACT;AAEO,MAAM,oBAAmD;AAAA,EAC9D,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAO;AACT;AAEA,MAAM,gBAAgB;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AAyBV,MAAM,SAAS,MAAM;AAAA,EACnB,CACE;AAAA,IACE,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd;AAAA,IACA,mBAAmB;AAAA;AAAA,IACnB;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,aAAa,aAAa,OAAO;AAEvC,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,WAAW,GAAG,eAAe,SAAS;AAAA,QACrC,GAAG;AAAA,QAEH,UAAA;AAAA,UAAA,cACC,oBAAC,YAAA,EACC,UAAA,oBAAC,YAAA,EAAW,MAAM,IAAI,WAAW,GAAG,YAAY,aAAa,GAAG,eAAW,MAAC,GAC9E;AAAA,UAKF;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,OAAO;AAAA,cACP;AAAA,cACA,gBAAgB,cAAc,gBAAgB;AAAA,YAAA;AAAA,UAAA;AAAA,WAG9C,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA,+BAMb,YAAA,EACE,UAAA;AAAA,YAAA;AAAA,YACA,eACC;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,gBAAY;AAAA,gBACZ,UAAQ;AAAA,gBACR,SAAO;AAAA,gBACP,MAAK;AAAA,gBACL,WAAWA;AAAAA,gBACX,cAAY;AAAA,gBACZ,SAAS;AAAA,cAAA;AAAA,YAAA;AAAA,UACX,EAAA,CAEJ;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AACF;AACA,OAAO,cAAc;AAKrB,IAAI,uBAAuB;AAC3B,MAAM,uCAAuB,IAAA;AAE7B,SAAS,kBAAoC;AAC3C,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,UAAU,SAAS,gBAAgB,aAAa,YAAY,KAAK;AACvE,SAAO,YAAY,SAAS,UAAU;AACxC;AAEA,SAAS,qBAAqB;AAC5B,MAAI,wBAAwB,OAAO,aAAa,YAAa;AAC7D,yBAAuB;AACvB,QAAM,OAAO,SAAS;AACtB,QAAM,WAAW,IAAI,iBAAiB,MAAM;AAC1C,qBAAiB,QAAQ,CAAC,OAAO,GAAA,CAAI;AAAA,EACvC,CAAC;AACD,WAAS,QAAQ,MAAM,EAAE,YAAY,MAAM,iBAAiB,CAAC,YAAY,GAAG;AAC9E;AAEA,SAAS,UAAU,IAA4B;AAC7C,qBAAA;AACA,mBAAiB,IAAI,EAAE;AACvB,SAAO,MAAM,iBAAiB,OAAO,EAAE;AACzC;AAEO,SAAS,kBAAoC;AAElD,SAAO,MAAM,qBAAqB,WAAW,iBAAiB,eAAe;AAC/E;AAIO,MAAM,aAAa;AAAA,EACxB,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,SAAS;AAAA;AAAA,EAClB,QAAQ;AAAA,IACN,IAAI,CAAA;AAAA,IACJ,IAAI,CAAC,mBAAmB,iBAAiB,qBAAqB,kBAAkB,qBAAqB,mBAAmB;AAAA,IACxH,MAAM,CAAA;AAAA,EAAC;AAEX;"}
@@ -36,6 +36,12 @@
36
36
  "regex": "<SidebarHeader[^>]*>[[:space:]]*<div[^>]*flex-col",
37
37
  "severity": "block",
38
38
  "rationale": "SidebarHeader brand 必單行(Fixed-h chrome 不可成長,sidebar.spec.md SidebarHeader 段 2026-06-12 明文)。flex-col 直接子層 = logo name 下加副標的結構簽名 — shadcn TeamSwitcher demo prior 滲漏;plan/org 資訊歸 workspace switcher dropdown row,非常駐 header"
39
+ },
40
+ {
41
+ "id": "sidebar-footer-two-line",
42
+ "regex": "<SidebarFooter[^>]*>.{0,400}(text-xs|leading-tight)",
43
+ "severity": "block",
44
+ "rationale": "SidebarFooter user 區必單行(名字 only;職稱/email 等身份資訊歸 ItemAvatar hoverCard 的 ProfileCard,sidebar.stories.tsx UserFooter canonical)。shadcn nav-user 的「name + email text-xs 兩行」是強 LLM 先驗,且 text-xs 違反 chrome typography canonical(sidebar.spec『chrome 內 text 一律 text-body-lg』);400 字元窗限制 greediness"
39
45
  }
40
46
  ]
41
47
  },
@@ -88,6 +94,12 @@
88
94
  "regex": "<ChromeHeader[^>]*>.{0,160}<span[[:space:]]+className=\"[^\"]*flex-1[^\"]*\">",
89
95
  "severity": "block",
90
96
  "rationale": "ChromeHeader title 該用 <h1 className=\"text-body-lg font-medium\"> 緊鄰 SidebarTrigger,不該用 <span flex-1> 撐空間造成 toggle + title 巨大間距"
97
+ },
98
+ {
99
+ "id": "chromeheader-subtitle-second-line",
100
+ "regex": "<(ChromeHeader|PageHeader)[^>]*>[[:space:]]*<div[^>]*flex-col",
101
+ "severity": "block",
102
+ "rationale": "Chrome 家族永遠 single-row(header-canonical.spec.md:47,剛性 --chrome-header-height);title 下加副標/description = LLM 先驗回填(Polaris Page subtitle / Tailwind UI page heading 先驗),頁面說明文字歸 content 區,非 chrome。flex-col 直接子層 = 兩行結構簽名"
91
103
  }
92
104
  ]
93
105
  }
@@ -188,7 +188,7 @@ User 2026-05-15 verbatim 抓「DS 深度稽核漏 storybook content quality」+
188
188
  | 68 | **Stories-vs-spec canonical drift**(2026-05-27 codify per user「DS 自己 stories 教錯 = consumer 抄 stories 抄錯」root cause)| 對每 component `*.stories.tsx` + `*.anatomy.stories.tsx` + `*.principles.stories.tsx` grep:含 `<SidebarHeader>` 內 `<ItemAvatar>` / 同類 spec 明文禁止的 pattern → 違反(DS 教 consumer 錯 pattern,類似 2026-05-27 sidebar 3 stories 在 WorkspaceBrand 用 ItemAvatar 違反 chrome header canonical)。**主防線 = R8 registry**(`.claude/references/story-baseline-registry.json` requiredHelpers / antiPatterns + `check_story_invariants.sh` R8/R9 機械攔)+ `check_chrome_header_avatar_canonical.sh` / `check_sidebar_menu_button_implicit_wrap.sh` 具體錨例 hook;`// @canonical-pattern:` / `// @anti-pattern:` 文字 marker **降格為建議性輔助**(2026-06-12 R2 拍板 — 全庫僅 sidebar.stories 2 處採用且無機械強制 = 紙防線,不再「必加」;存在時 audit 順驗其與 spec 一致)。配 Dim 53(spec-vs-code)+ 本 dim(spec-vs-stories)雙向 drift verify;系統性 stories drift audit run via design-system-audit Dim 68 batch sweep |
189
189
  | 69 | **Consumer no-DS-catalog enforcement**(2026-05-27 M31 codex synthesis per user「眼不見為淨」+「做產品真的能使用跟 ds repo 一模一樣的元件做產品嗎?」)| 對 consumer `apps/**/*.stories.tsx` grep:basename 為 `EveryDsComponent` / `AllDsComponents` / catalog naming + `Object.keys(DS).map` iterate-render + mass hand-mock(≥5 distinct `<DS.X>` 單 file)→ 違反(DS catalog 是 DS Storybook 唯一 SSOT,consumer 重寫必 drift,2026-05-27 錨例 7 bug)。Hook `check_consumer_app_invariants.sh(r1,2026-06-11 merge)` PostToolUse Write/Edit BLOCKER,escape `// @consumer-catalog-allow:`。允許 portal proxy(iframe to DS Storybook)|
190
190
  | 70 | **Consumer @story-baseline enforcement**(2026-05-27 M31 codex synthesis)| 對 consumer `apps/**/*.stories.tsx` grep:用高風險 DS primitive(DataTable / Dialog / Sheet / Popover / DropdownMenu / Tooltip / HoverCard / LinkInput / RadioGroup / CircularProgress / AppShell / Sidebar)但無 `// @story-baseline: <DS-story-path>#<exportName>` marker → 違反(consumer 必 reference DS canonical story 才 enable visual diff CI)。Hook `check_consumer_app_invariants.sh(r2,2026-06-11 merge)` PostToolUse BLOCKER,escape `// @story-baseline-allow:`。SSOT mapping → `ds-story-manifest.json`(DS package ship)|
191
- | 71 | **Consumer DS primitive misuse anti-pattern**(2026-05-27 per user「做產品真的要能使用跟 ds repo 一模一樣的元件」)| 對 consumer `apps/**/*.{tsx,ts}` production + stories grep:`<CircularProgress size={N}>` literal number 覆蓋 default 24 / `<RadioGroupItem>` 無 `<SelectionItem control={...}>` wrap / `<DataTable columns={[single-col]}>` minimal / `<LinkInput placeholder=...>` 無 `value` prop / `<Empty title=...>` 無 icon AND 無 description / Overlay story 無 `defaultOpen` 視覺 snapshot 看不到 content / **硬寫色值-字級-shadow 繞 token(`bg-[#hex]` / `text-[14px]` / `shadow-md`,2026-06-02 Pattern 8,CF conformance-model 主防線)**。Hook `check_consumer_app_invariants.sh(r3,2026-06-11 merge)` BLOCKER。Per-violation cite spec.md file:line。Escape `// @ds-misuse-allow:` |
191
+ | 71 | **Consumer DS primitive misuse anti-pattern**(2026-05-27 per user「做產品真的要能使用跟 ds repo 一模一樣的元件」)| 對 consumer `apps/**/*.{tsx,ts}` production + stories grep:`<CircularProgress size={N}>` literal number 覆蓋 default 24 / `<RadioGroupItem>` 無 `<SelectionItem control={...}>` wrap / `<DataTable columns={[single-col]}>` minimal / `<LinkInput placeholder=...>` 無 `value` prop / `<Empty title=...>` 無 icon AND 無 description / Overlay story 無 `defaultOpen` 視覺 snapshot 看不到 content / **硬寫色值-字級-shadow 繞 token(`bg-[#hex]` / `text-[14px]` / `shadow-md`,2026-06-02 Pattern 8,CF conformance-model 主防線)** / **AppShell slot 餵 raw element(`sidebar={<div>}` 等,2026-06-12 Pattern 9,app-shell.spec:296-299)**。**判斷層補掃(2026-06-12 fork 四不像 + 先驗滲漏 anchor;hook 零誤判原則不上 regex)**:(a) page-level entry 手拼 raw 殼而無 `<DS.AppShell>`(對照 app-shell.spec「何時用/不用」表;愈不用 DS 愈不觸 hook = 反直覺洞)(b) 手刻 right panel(`w-[Npx] border-l` div,該走 `aside={<AppShellAside>}`)(c) ChromeHeader/頁頂 tabs 沒走 `withTabs` lockstep(header-canonical.spec「withTabs 連動」段)(d) BulkActionBar 內 primary 鈕 / 浮層 shadow(bulk-action-bar.spec 禁止事項 — regex 無法 bound 到 actions slot,判斷層接)(e) FileViewer toolbar 加鈕不走 `onCapabilitiesChange`(file-viewer.spec:183-194;R9 skip-list 豁免該家族故無寫入網)(f) chrome 面(ChromeHeader/SidebarFooter)title 下第二行(registry 簽名涵蓋 stories;fork production code 由本 dim 接)。Hook `check_consumer_app_invariants.sh(r3,2026-06-11 merge)` BLOCKER。Per-violation cite spec.md file:line。Escape `// @ds-misuse-allow:` |
192
192
  | 72 | **DS API surface tightening**(2026-05-27 — 治標 vs 治本)| Hook 71 偵測 anti-pattern 是 lint 層攔截;治本要 DS API design 強到 misuse 即 fail tsc。Audit:逐 component review API surface — `size?: number` 該改 `'sm'\|'md'\|'lg'` enum / `columns: Column[]` 該加 min length runtime check / `title` + `description` 該有 type-level XOR / Overlay primitive `defaultOpen` 該 require explicit。配套 codify in `tightening-roadmap.md`(若存在;不存在則以 `props-naming.md` + 各 spec API 段為準,對齊 audit-prompts.md dim 72),分 quarter ship。對應 Dim 71 是攔當前 misuse,本 dim 是消除未來 misuse 可能 |
193
193
  | 73 | **Full-story visual+interaction sweep enforce**(2026-05-27 codex M31 P0 finding)| Audit report JSON `storyResults.length === manifest.totalStories`(916)。Sample < 916 = reject(per user「不准抽樣」)。Hook `check_full_story_visual_interaction_sweep.sh` PostToolUse audit-report.json BLOCKER。Escape `"_sampling_allowed": "<rationale>"`(極罕見)|
194
194
  | 74 | **Overlay open/focus/Escape probe**(2026-05-27 codex M31 P0 finding + user 7-bug 錨點「overlay 沒彈出」)| Consumer story 用 Tooltip / Popover / Dialog / Sheet / DropdownMenu / HoverCard Trigger 必含 `defaultOpen` OR `open={true}` OR `play()` interaction click。Trigger-only catalog = reject(visual snapshot 看不到 content)。Hook `check_overlay_open_focus_escape_probe.sh` BLOCKER。HoverCard exception via `@story-trait-allow: missing-opensnapshot` per codex |
@@ -34,6 +34,7 @@ import {
34
34
  SidebarMenuItem,
35
35
  SidebarMenuButton,
36
36
  SidebarTrigger,
37
+ ChromeHeader,
37
38
  TooltipProvider,
38
39
  Avatar,
39
40
  ItemAvatar,
@@ -92,15 +93,19 @@ function AppSidebar() {
92
93
  )
93
94
  }
94
95
 
95
- // ── PageHeader(對齊 DS canonical chrome-header pattern:fixed --chrome-header-height + SidebarTrigger + title)──
96
+ // ── PageHeader(消費 DS ChromeHeader primitive,2026-06-12 修:原手刻 <header className=...>
97
+ // 繞過 header-canonical 全部機械簽名 + 違反「消費 primitive 不 hand-craft」canonical;
98
+ // 對齊 _demo-helpers.tsx PageHeader 同款消費形)──
96
99
  // SidebarTrigger 必有(primary-sidebar mode 的 menu toggle 入口,⌘B keyboard shortcut)
97
- function PageHeader({ title, rightSlot }: { title: string; rightSlot?: React.ReactNode }) {
100
+ // rightSlot JSX.Element(非 React.ReactNode):workspace app 自帶 @types/react 副本與
101
+ // DS 套件 .d.ts 解析的 ReactNode 版本不相容(bigint 差異)— Element 兩邊皆可指派
102
+ function PageHeader({ title, rightSlot }: { title: string; rightSlot?: JSX.Element }) {
98
103
  return (
99
- <header className="flex items-center gap-2 h-[var(--chrome-header-height)] px-[var(--layout-space-loose)] bg-surface border-b border-divider">
104
+ <ChromeHeader className="bg-surface">
100
105
  <SidebarTrigger />
101
106
  <h1 className="text-body-lg font-medium flex-1 truncate">{title}</h1>
102
107
  {rightSlot}
103
- </header>
108
+ </ChromeHeader>
104
109
  )
105
110
  }
106
111
 
package/llms-full.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # @qijenchen/design-system — 完整設計參考(llms-full)
2
2
 
3
- > 全 component / pattern 的 variants / sizes / 禁止事項。build-time 從 spec.md frontmatter 生成,禁手改。v0.1.0-beta.65
3
+ > 全 component / pattern 的 variants / sizes / 禁止事項。build-time 從 spec.md frontmatter 生成,禁手改。v0.1.0-beta.67
4
4
 
5
5
  # Components
6
6
 
package/llms.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # @qijenchen/design-system
2
2
 
3
3
  > World-class React design system(Radix/shadcn + Tailwind v4 + 自訂 design token)。
4
- > 54 components + 4 public patterns + design tokens。v0.1.0-beta.65
4
+ > 54 components + 4 public patterns + design tokens。v0.1.0-beta.67
5
5
 
6
6
  本檔由 source(spec.md frontmatter + Storybook index)build-time 自動生成,**禁手改**(CI --check drift gate 守)。
7
7
  每元件 / pattern 的完整 variants / sizes / 禁止事項 全文見 [llms-full.txt](./llms-full.txt)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qijenchen/design-system",
3
- "version": "0.1.0-beta.65",
3
+ "version": "0.1.0-beta.67",
4
4
  "private": false,
5
5
  "description": "World-class design system — components, patterns, tokens, hooks (single source of truth for team distribution).",
6
6
  "type": "module",
@@ -297,6 +297,7 @@ Main 內塞什麼(table / field / card / page header / list)的 layout + spacing
297
297
  - ❌ 禁:`sidebar={<div>...</div>}`(應傳 `<Sidebar>` primitive,確保視覺 SSOT。型別上收 ReactNode、不做機械強制 — React 型別強制易被 wrapper 包一層繞過,世界級 shell 元件皆收 ReactNode;本約定靠 story 示範 + audit 把關。2026-06-10 user 拍板 2a:措辭「必」→「應」對齊 code 真實)
298
298
  - ❌ 禁:`header={<header>...</header>}`(應傳 `<ChromeHeader>` 或消費 `header-canonical` 派生 header;同上,型別不機械強制)
299
299
  - ❌ 禁:AppShell.Main 自加 padding(違反 layoutSpace 規則 1B)
300
+ - ❌ 禁:AppShellAside header 加第二行 / actions(header = 單行 title + close X,API typed `title: string` 結構鎖;說明文字歸 aside body — 2026-06-12 fork-drift 防線)
300
301
  - ✅ 必:`layout` mode 在 product 啟動時固定,**不在 runtime 切換**(切換 = product 角色變動 = 應該重 mount app)
301
302
 
302
303
  ---
@@ -122,14 +122,14 @@ export const Overview: Story = {
122
122
  <tr>
123
123
  <Td mono>title</Td>
124
124
  <Td>單行必填</Td>
125
- <Td mono>text-body leading-compact</Td>
125
+ <Td mono>text-body(reading 1.5)</Td>
126
126
  <Td mono>14px / 有 desc 時 font-medium</Td>
127
127
  </tr>
128
128
  <tr>
129
129
  <Td mono>description</Td>
130
130
  <Td>可選輔助文字</Td>
131
131
  <Td mono>text-fg-secondary</Td>
132
- <Td mono>14px leading-compact</Td>
132
+ <Td mono>14px(reading 1.5)</Td>
133
133
  </tr>
134
134
  <tr>
135
135
  <Td mono>endContent</Td>
@@ -265,7 +265,7 @@ function NoticeInspector() {
265
265
  │ h-[1lh] min-w-0 flex-1 h-[1lh] │
266
266
  └──────────────────────────────────────────────────────────────────┘
267
267
 
268
- text-body = 14px leading-compact = 1.3
268
+ text-body = 14px reading 行高 = 1.5(Family 2 reading-md,2026-06-15 收斂)
269
269
  固定 md tier — 不隨 density 變(通知是跨 density 一致的訊息載體)`}
270
270
  </pre>
271
271
  </div>
@@ -350,7 +350,7 @@ text-body = 14px leading-compact = 1.3
350
350
  </tr>
351
351
  <tr>
352
352
  <Td mono>title font</Td>
353
- <Td mono>text-body · leading-compact</Td>
353
+ <Td mono>text-body · reading 1.5 · font-medium</Td>
354
354
  </tr>
355
355
  <tr>
356
356
  <Td mono>desc font</Td>
@@ -29,11 +29,15 @@ Notice 是純視覺 primitive,不是獨立使用的元件。消費者:
29
29
 
30
30
  ## Typography
31
31
 
32
- md tier,固定不隨 density 變:
33
- - title: `text-body`(14px)`leading-compact`(1.3)— 有 description 時加 `font-medium`
34
- - description: `text-body`(14px)`leading-compact` + `text-fg-secondary`(neutral-8)
32
+ **Family 2 reading-md consumer(2026-06-15 user 拍板,off-grid 偏移收斂)**,md tier 固定不隨 density 變:
33
+ - title: `text-body`(14px)**default leading 1.5** 有 description 時加 `font-medium`
34
+ - description: `text-body`(14px)**default leading 1.5** + `text-fg-secondary`(neutral-8)
35
+ - label↔desc gap: `--item-gap-label-desc-reading`(ItemContent 預設 mode=`reading` size=`md` 自動選)
36
+ - icon: 16px(`ICON_SIZE.md`,item-anatomy.tsx)
35
37
 
36
- 相同 body 字級,層級靠 font-weight / color 區分。
38
+ 相同 body 字級,層級靠 font-weight / color 區分(不靠 font-size,亦不靠 compact 行高)。
39
+
40
+ > **為何 reading-md 而非 compact(2026-06-15 codify,根治 off-grid 偏移)**:Notice 同時服務 title-only(多數)與 title+desc、Alert(banner)與 Toast(snackbar)。原本 root 套 `leading-compact`(1.3) 形成「reading gap token + scanning 行高」混搭,落在 ItemContent 4 個合法 mode(reading-md/reading-lg/scanning-md/scanning-lg)**之外**的第五種組合(label 14 + desc 14 + 行高 1.3)且未文件化 → 違反 `item-anatomy.spec.md`「偏離 canonical 必明文 rationale」。收斂為純 **reading-md**:title-only 14px 對齊 Ant/Material title-only norm(scanning-lg 的 16px 過大);desc 維持 14px 可讀(scanning-md 的 12px caption 對通知訊息過小);title↔desc 階層由 `font-medium` 承載已足夠。對照 FileItem(顯式 `mode="scanning"` + 文件化 = 正確消費範例,file-item.tsx:13/214)。
37
41
 
38
42
  ## Padding(固定)
39
43
 
@@ -6,15 +6,18 @@ import * as React from 'react'
6
6
  import { X as XIcon, Info, CircleCheck, TriangleAlert, XCircle, type LucideIcon } from 'lucide-react'
7
7
  import { cn } from '@/lib/utils'
8
8
  import { Button } from '@/design-system/components/Button/button'
9
- import { ItemContent, ItemPrefix } from '@/design-system/patterns/element-anatomy/item-anatomy'
9
+ import { ItemContent, ItemPrefix, ItemSuffix } from '@/design-system/patterns/element-anatomy/item-anatomy'
10
10
 
11
11
  /**
12
12
  * Notice — Toast / Alert 共用的視覺佈局層
13
13
  *
14
- * ── Typography: md tier ──
15
- * title: text-body (14px) leading-compact — 有 description 時加 font-medium
16
- * description: text-body (14px) leading-compact + text-fg-secondary (neutral-8)
14
+ * ── Typography: Family 2 reading-md(2026-06-15 user 拍板,off-grid 偏移收斂)──
15
+ * title: text-body (14px) default leading 1.5 — 有 description 時加 font-medium
16
+ * description: text-body (14px) default leading 1.5 + text-fg-secondary (neutral-8)
17
17
  * 14px 配 14px — 視覺層級靠 font-weight + color 區分,不靠 font-size。
18
+ * **行高 = reading 預設 1.5**(非 compact):Notice 是 Family 2 reading consumer,
19
+ * ItemContent 預設 mode='reading' + gap token `--item-gap-label-desc-reading` 名實相符。
20
+ * 原 `leading-compact`(1.3)= reading-gap + scanning-行高 混搭的 off-grid 偏移,已移除。
18
21
  *
19
22
  * ── Padding(固定,不隨 density 變) ──
20
23
  * px = px-4(16px)
@@ -58,7 +61,11 @@ export const SUBTLE_ICON_COLOR: Record<NoticeVariant, string> = {
58
61
 
59
62
  const NOTICE_LAYOUT = [
60
63
  'flex items-start gap-2 w-full',
61
- 'text-body leading-compact',
64
+ // 2026-06-15 user 拍板:Notice = 乾淨 Family 2 reading-md consumer。
65
+ // 原 `leading-compact`(1.3)是 off-grid 偏移(reading gap token + scanning 行高混搭、未文件化),
66
+ // 已移除 → label/desc 走 text-body reading 預設 1.5,與 ItemContent 預設 mode='reading' + gap
67
+ // token `--item-gap-label-desc-reading` 名實相符。詳 notice.spec.md「Typography」段。
68
+ 'text-body',
62
69
  'px-4 py-3',
63
70
  ].join(' ')
64
71
 
@@ -124,8 +131,12 @@ const Notice = React.forwardRef<HTMLDivElement, NoticeProps>(
124
131
  />
125
132
 
126
133
  {(endContent || dismissible) && (
127
- // @row-slot-handcraft-allow: Notice 是非-row alert(非 item-anatomy row),此 end slot 是 Notice 自身 layout 的 dismiss/endContent 容器,不是 row prefix/suffix → 不消費 ItemPrefix/ItemSuffix
128
- <div className="flex items-center gap-2 shrink-0 h-[1lh]">
134
+ // 2026-06-15 user 拍板:消費 ItemSuffix primitive(原手刻 div + @row-slot-handcraft-allow
135
+ // 已移除)。item-anatomy.spec.md Notice 的 action/dismiss 對應到 suffix slot;ItemSuffix
136
+ // base geometry(h-[1lh] shrink-0 ml-auto flex items-center gap-2)正是此處所需,hoverReveal
137
+ // 預設 false 故無 row inline-action 機制干擾。內裝 dismiss = Button iconOnly dismiss xs(banner
138
+ // family canonical,overlay-surface.spec.md「Chrome dismiss size canonical」)。
139
+ <ItemSuffix>
129
140
  {endContent}
130
141
  {dismissible && (
131
142
  <Button
@@ -138,7 +149,7 @@ const Notice = React.forwardRef<HTMLDivElement, NoticeProps>(
138
149
  onClick={onDismiss}
139
150
  />
140
151
  )}
141
- </div>
152
+ </ItemSuffix>
142
153
  )}
143
154
  </div>
144
155
  )
@@ -128,7 +128,9 @@ SidebarProvider ← 全域 context(open 狀態、cookie、快捷
128
128
  - **Density**(md/lg)是**全域 UI 設定**,影響所有 `--field-height-*` tokens 的實際值
129
129
  - 兩者**獨立**:size="md" 在 density="md" 下是 32px row、在 density="lg" 下自動變 36px row
130
130
 
131
- **Chrome header 不跟 size 變**:`SidebarHeader` / `SidebarFooter` 固定用 `var(--chrome-header-height)`,只跟 **density** 連動,不跟 `size` prop 變——因為 header 是結構槽位,不是 row
131
+ **Chrome header 不跟 size 變**:`SidebarHeader` 固定用 `var(--chrome-header-height)`,只跟 **density** 連動,不跟 `size` prop 變——因為 header 是結構槽位,不是 row。**SidebarFooter 修正(2026-06-12 doc-to-code)**:footer 實作自 2026-04-14 起為 content-based(`flex flex-col py-2`,sidebar.tsx SidebarFooter,行為同 SidebarGroup),**非** fixed-h——單行高度由下方內容紀律保證,非結構鎖。
132
+
133
+ **SidebarFooter user 區必單行(2026-06-12 明文,fork drift 防線)**:名字單行(`text-body-lg` truncate);職稱/email/工號等身份資訊歸 `ItemAvatar hoverCard` 的 ProfileCard(UserFooter canonical,sidebar.stories.tsx)。shadcn nav-user 的「name + email `text-xs` 兩行」是 demo 慣例非本 DS canonical(`text-xs` 同時違反下方 chrome typography 一律 `text-body-lg` 條);registry antiPattern `sidebar-footer-two-line` 機械攔。
132
134
 
133
135
  **SidebarHeader brand 必單行(2026-06-12 明文,fork drift anchor)**:Avatar 24 + 名稱單行,**無副標 slot**——Fixed-h chrome 結構上不可成長;plan/org 等第二行資訊歸 workspace switcher dropdown row,對齊 Slack/Notion/Linear 現行版單行。shadcn TeamSwitcher demo 的「Acme Inc + Enterprise」兩行是 demo 慣例非本 DS canonical(fork repo AI 曾以訓練先驗在 spec 沉默處回填此副標)。Registry antiPattern `sidebar-header-subtitle-second-line` 機械攔(flex-col 直接子層簽名)。
134
136
 
@@ -205,13 +207,13 @@ Gmail / Linear 的標準版型:上方 SidebarGroup 放扁平主導覽(Sideba
205
207
 
206
208
  ## Chrome header / footer 高度:density-responsive shared token
207
209
 
208
- `SidebarHeader` 和 `SidebarFooter` 的高度 = `var(--chrome-header-height)`,**不是寫死**,也**不是完全 content-based**。這個 token:
210
+ `SidebarHeader` 的高度 = `var(--chrome-header-height)`,**不是寫死**,也**不是完全 content-based**(SidebarFooter 為 content-based,見上方 2026-06-12 修正)。這個 token:
209
211
 
210
212
  > **跨家族 SSOT pointer**:本元件 SidebarHeader 屬 **Chrome header(Fixed-h)家族**;border / padding / dismiss size / withTabs(tabs 進 header 時 border auto-suppress + tabs size 對應 + flush stack)的跨家族視覺契約 SSOT 詳 `patterns/header-canonical/header-canonical.spec.md`。本節僅 codify Sidebar 特有的高度 + token rationale。
211
213
 
212
214
  - md density: `3rem`(48px)
213
215
  - lg density: `3.5rem`(56px)
214
- - **跨元件共享**:sidebar header、sidebar footer、主內容 page header 都用同一個 token
216
+ - **跨元件共享**:sidebar header、主內容 page header 都用同一個 token(sidebar footer 為 content-based,見上方 2026-06-12 修正)
215
217
  - **隨 density 同步變**
216
218
 
217
219
  ### 為什麼是這個設計(不是 h-12 寫死 或 py-3 純 padding)
@@ -55,7 +55,9 @@
55
55
 
56
56
  **用途**:頁面上的閱讀式單列。使用者讀取內容、需要 description 多行、looser density。
57
57
 
58
- **結構**:`[larger icon/avatar 20-24px] [content: label + multi-line description OK] [suffix action/button/counter]`,reading typography(default leading),字體略大於 Family 1
58
+ **結構**:`[icon/avatar 16-20px(`ICON_SIZE` sm/md=16, lg=20)] [content: label + multi-line description OK] [suffix action/button/counter]`,reading typography(default leading 1.5)。
59
+
60
+ > **2026-06-15 修**:本行原寫 icon「20-24px」,與 item-anatomy.tsx 的 `ICON_SIZE = {sm:16, md:16, lg:20}` 常數打架(24px 不存在於常數,md 是 16 非 20+)。已對齊**實作真值 ICON_SIZE**。Family 2 與 Family 1 的 icon **px 值相同**(皆走 ICON_SIZE);兩 family 的差別在 **typography 行高 + density**(reading 1.5 / scanning 1.3)+ description 多行能力,**不在 icon px**。
59
61
 
60
62
  **消費者**:
61
63
 
@@ -791,7 +793,7 @@ Block 模式時 checkbox 不在 label 第一行——它跟 avatar 在同一高
791
793
  | Consumer | Primitive 消費 | Rationale |
792
794
  |----------|--------------|-----------|
793
795
  | **FileItem**(rich + compact) | ✅ ItemContent + ItemPrefix(hover action 為 Button xs iconOnly,非 ItemInlineActionButton——2026-04-23 user 統一,見 file-item.tsx)| 純 row-item layout,完美 fit |
794
- | **Notice / Alert / Toast** | ✅ ItemContent + ItemPrefix | title+desc 標準 row-item 結構 |
796
+ | **Notice / Alert / Toast** | ✅ ItemContent + ItemPrefix + ItemSuffix | 乾淨 reading-md consumer(2026-06-15 收斂:原 root `leading-compact` off-grid 偏移已移除、suffix 改消費 ItemSuffix;詳 notice.spec.md Typography 段)|
795
797
  | **ProfileCard** | ✅ ItemContent(+ `labelTruncate=false` + `labelClassName` escape hatch) | 偏離 rationale:card context 用 `text-body-lg font-medium`,非一般 body label |
796
798
  | **MenuItem** | ✅ ItemContent(`mode="scanning"` + `size="md"\|"lg"` 正交)+ `itemPrefixAlignVariants` cva SSOT | Content 用 ItemContent 配合 size-aware scanning mode(sm/md = caption / lg = body-compact);label+desc clamp 透過 className escape hatch(MenuItem 特化 labelMaxLines / descMaxLines 語意) |
797
799
  | **Sidebar / TreeView** | ✅ ItemPrefix(+ 其他 primitives) | Label-only(無 description),不需 ItemContent |
@@ -288,6 +288,7 @@ ChromeHeader / SurfaceHeader 新增 `tabsSlot?: ReactNode` prop。提供時自
288
288
  - ❌ Hardcode `h-16` / `h-14` / `h-12` 自寫 chrome header 高度 — 必 `--chrome-header-height`
289
289
  - ❌ Header 內 close X 用 `size="md"` 或 `size="lg"`(real grep 100% sm)
290
290
  - ❌ Tabs default 覆寫回 `size="md"`(W5 + W6)— cva default 已是 sm,不該寫回 md
291
+ - ❌ ChromeHeader children 塞 `flex-col` 第二行(title + 副標/description)— Chrome 家族永遠 single-row(上方「為什麼兩家族」段);頁面說明文字歸 content 區 page heading,非 chrome(2026-06-12 fork-drift 防線,同 sidebar.spec.md SidebarHeader 必單行條;registry antiPattern `chromeheader-subtitle-second-line` 機械攔)
291
292
 
292
293
  ---
293
294