@qijenchen/design-system 0.1.0-beta.66 → 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;"}
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.66
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.66
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.66",
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",
@@ -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
  )
@@ -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 |