@qijenchen/design-system 0.1.0-beta.54 → 0.1.0-beta.55

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.
@@ -29,7 +29,7 @@ export declare const aspectRatioMeta: {
29
29
  readonly family: null;
30
30
  readonly variants: {};
31
31
  readonly sizes: {};
32
- readonly states: readonly ["default", "hover", "active", "focus-visible", "disabled"];
32
+ readonly states: readonly ["default"];
33
33
  readonly tokens: {
34
34
  readonly bg: readonly [];
35
35
  readonly fg: readonly [];
@@ -1 +1 @@
1
- {"version":3,"file":"aspect-ratio.d.ts","sourceRoot":"","sources":["../../../src/components/AspectRatio/aspect-ratio.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,oBAAoB,MAAM,8BAA8B,CAAA;AAEpE;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,MAAM,gBAAgB,GAAG,KAAK,CAAC,wBAAwB,CAAC,OAAO,oBAAoB,CAAC,IAAI,CAAC,CAAA;AAK/F,QAAA,MAAM,WAAW,iKAGoD,CAAA;AAKrE,eAAO,MAAM,eAAe;;;;;;;;;;;CAelB,CAAA;AAEV,OAAO,EAAE,WAAW,EAAE,CAAA"}
1
+ {"version":3,"file":"aspect-ratio.d.ts","sourceRoot":"","sources":["../../../src/components/AspectRatio/aspect-ratio.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,oBAAoB,MAAM,8BAA8B,CAAA;AAEpE;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,MAAM,gBAAgB,GAAG,KAAK,CAAC,wBAAwB,CAAC,OAAO,oBAAoB,CAAC,IAAI,CAAC,CAAA;AAK/F,QAAA,MAAM,WAAW,iKAGoD,CAAA;AAKrE,eAAO,MAAM,eAAe;;;;;;;;;;;CAiBlB,CAAA;AAEV,OAAO,EAAE,WAAW,EAAE,CAAA"}
@@ -6,10 +6,12 @@ AspectRatio.displayName = "AspectRatio";
6
6
  const aspectRatioMeta = {
7
7
  component: "AspectRatio",
8
8
  family: null,
9
- // non-family composite / overlay / layout
9
+ // self-contained primitive(對齊 spec frontmatter self-contained + L22;非 Family 1-4)
10
10
  variants: {},
11
11
  sizes: {},
12
- states: ["default", "hover", "active", "focus-visible", "disabled"],
12
+ // 2026-06-05 fix(deep-audit):純 structural container,無互動狀態(spec.md L101「無 hover/focus/active/disabled」)。
13
+ // 原 ['default','hover','active','focus-visible','disabled'] 是 Phase 1 mechanical placeholder,與 spec 矛盾。
14
+ states: ["default"],
13
15
  tokens: {
14
16
  bg: [],
15
17
  fg: [],
@@ -1 +1 @@
1
- {"version":3,"file":"aspect-ratio.js","sources":["../../../src/components/AspectRatio/aspect-ratio.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'\n\n/**\n * AspectRatio — 固定長寬比容器(Radix AspectRatio primitive 薄包裝)\n *\n * 世界級對照:shadcn `AspectRatio` / Ant 無獨立元件(CSS 方案)/ Material 無\n *\n * ── 為什麼需要 ──\n * CSS `aspect-ratio` 屬性雖然現代瀏覽器都支援,但 Radix primitive 提供 SSR-safe\n * padding-bottom 方案 + consistent API,避免邊緣 bug(image 未載入時容器坍塌 /\n * content-fit 差異)。保 safe + 一致視覺。\n *\n * ── 標準 ratio(DS 慣例) ──\n * 16/9 — 寬螢幕影片、onboarding feature tour 截圖(Coachmark media 預設)\n * 4/3 — 老電視 / 照片基本 ratio、產品 thumbnail\n * 1/1 — Avatar / 方形貼文預覽 / icon preview\n * 3/4 — 直式照片(人物 portrait)\n * 21/9 — Ultra-wide banner(hero section)\n *\n * Consumer 傳 `ratio={n/m}` 數字計算(如 16/9 = 1.7777)。\n *\n * ── 常見消費者 ──\n * Coachmark media / Carousel item image / Card thumbnail(未來)/ Chart preview\n */\n\nexport type AspectRatioProps = React.ComponentPropsWithoutRef<typeof AspectRatioPrimitive.Root>\n\n// shadcn canonical:顯式 forwardRef + displayName(雖 Radix primitive 已 forwardRef,\n// 此 wrapper 確保本 DS 每個 named export 在 Inspector 顯示正確 displayName,\n// 且 props passthrough + ref 行為在 code 層面明確可見)\nconst AspectRatio = React.forwardRef<\n React.ElementRef<typeof AspectRatioPrimitive.Root>,\n AspectRatioProps\n>((props, ref) => <AspectRatioPrimitive.Root ref={ref} {...props} />)\nAspectRatio.displayName = 'AspectRatio'\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 aspectRatioMeta = {\n component: 'AspectRatio',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: [],\n fg: [],\n ring: [],\n },\n} as const\n\nexport { AspectRatio }\n"],"names":[],"mappings":";;;AAgCA,MAAM,cAAc,MAAM,WAGxB,CAAC,OAAO,QAAQ,oBAAC,qBAAqB,MAArB,EAA0B,KAAW,GAAG,OAAO,CAAE;AACpE,YAAY,cAAc;AAInB,MAAM,kBAAkB;AAAA,EAC7B,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,WAAW,SAAS,UAAU,iBAAiB,UAAU;AAAA,EAClE,QAAQ;AAAA,IACN,IAAI,CAAA;AAAA,IACJ,IAAI,CAAA;AAAA,IACJ,MAAM,CAAA;AAAA,EAAC;AAEX;"}
1
+ {"version":3,"file":"aspect-ratio.js","sources":["../../../src/components/AspectRatio/aspect-ratio.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'\n\n/**\n * AspectRatio — 固定長寬比容器(Radix AspectRatio primitive 薄包裝)\n *\n * 世界級對照:shadcn `AspectRatio` / Ant 無獨立元件(CSS 方案)/ Material 無\n *\n * ── 為什麼需要 ──\n * CSS `aspect-ratio` 屬性雖然現代瀏覽器都支援,但 Radix primitive 提供 SSR-safe\n * padding-bottom 方案 + consistent API,避免邊緣 bug(image 未載入時容器坍塌 /\n * content-fit 差異)。保 safe + 一致視覺。\n *\n * ── 標準 ratio(DS 慣例) ──\n * 16/9 — 寬螢幕影片、onboarding feature tour 截圖(Coachmark media 預設)\n * 4/3 — 老電視 / 照片基本 ratio、產品 thumbnail\n * 1/1 — Avatar / 方形貼文預覽 / icon preview\n * 3/4 — 直式照片(人物 portrait)\n * 21/9 — Ultra-wide banner(hero section)\n *\n * Consumer 傳 `ratio={n/m}` 數字計算(如 16/9 = 1.7777)。\n *\n * ── 常見消費者 ──\n * Coachmark media / Carousel item image / Card thumbnail(未來)/ Chart preview\n */\n\nexport type AspectRatioProps = React.ComponentPropsWithoutRef<typeof AspectRatioPrimitive.Root>\n\n// shadcn canonical:顯式 forwardRef + displayName(雖 Radix primitive 已 forwardRef,\n// 此 wrapper 確保本 DS 每個 named export 在 Inspector 顯示正確 displayName,\n// 且 props passthrough + ref 行為在 code 層面明確可見)\nconst AspectRatio = React.forwardRef<\n React.ElementRef<typeof AspectRatioPrimitive.Root>,\n AspectRatioProps\n>((props, ref) => <AspectRatioPrimitive.Root ref={ref} {...props} />)\nAspectRatio.displayName = 'AspectRatio'\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 aspectRatioMeta = {\n component: 'AspectRatio',\n family: null, // self-contained primitive(對齊 spec frontmatter self-contained + L22;非 Family 1-4)\n variants: {\n\n },\n sizes: {\n\n },\n // 2026-06-05 fix(deep-audit):純 structural container,無互動狀態(spec.md L101「無 hover/focus/active/disabled」)。\n // 原 ['default','hover','active','focus-visible','disabled'] 是 Phase 1 mechanical placeholder,與 spec 矛盾。\n states: ['default'],\n tokens: {\n bg: [],\n fg: [],\n ring: [],\n },\n} as const\n\nexport { AspectRatio }\n"],"names":[],"mappings":";;;AAgCA,MAAM,cAAc,MAAM,WAGxB,CAAC,OAAO,QAAQ,oBAAC,qBAAqB,MAArB,EAA0B,KAAW,GAAG,OAAO,CAAE;AACpE,YAAY,cAAc;AAInB,MAAM,kBAAkB;AAAA,EAC7B,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA;AAAA;AAAA,EAKP,QAAQ,CAAC,SAAS;AAAA,EAClB,QAAQ;AAAA,IACN,IAAI,CAAA;AAAA,IACJ,IAAI,CAAA;AAAA,IACJ,MAAM,CAAA;AAAA,EAAC;AAEX;"}
@@ -32,7 +32,7 @@ export interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
32
32
  icon?: LucideIcon;
33
33
  /** Icon / text fallback 的背景色,預設 neutral */
34
34
  color?: ColorKey;
35
- /** 深底白字模式(step-6 背景 + 白色前景,warning 例外),預設 false */
35
+ /** 深底模式(step-6 背景 + on-emphasis 配對前景;亮色 hue yellow/amber/orange/lime 用深字 --on-emphasis-dark),預設 false */
36
36
  solid?: boolean;
37
37
  /**
38
38
  * 在線狀態指示器(presence),顯示在 avatar **右下角**。
@@ -1 +1 @@
1
- {"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../../src/components/Avatar/avatar.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,0CAA0C,CAAA;AAMrH;;;;;;;;;;;;;;;;GAgBG;AAQH,KAAK,QAAQ,GAAG,gBAAgB,CAAA;AA6DhC,MAAM,WAAW,WAAY,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;IACvE,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,aAAa;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0BAA0B;IAC1B,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,QAAQ,CAAA;IAChB,mDAAmD;IACnD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IAC/C;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B;AAiMD,MAAM,WAAW,UAAU;IACzB,aAAa;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,2CAA2C;IAC3C,KAAK,CAAC,EAAE,QAAQ,CAAA;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B;AAID,eAAO,MAAM,UAAU;;;;;;;;;;;CAeb,CAAA;AAGV,QAAA,MAAM,MAAM,+GAA0B,CAAA;AAEtC,OAAO,EAAE,MAAM,EAAE,CAAA"}
1
+ {"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../../src/components/Avatar/avatar.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,0CAA0C,CAAA;AAMrH;;;;;;;;;;;;;;;;GAgBG;AAQH,KAAK,QAAQ,GAAG,gBAAgB,CAAA;AA6DhC,MAAM,WAAW,WAAY,SAAQ,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC;IACvE,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,aAAa;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0BAA0B;IAC1B,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,QAAQ,CAAA;IAChB,yGAAyG;IACzG,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IAC/C;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B;AAiMD,MAAM,WAAW,UAAU;IACzB,aAAa;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,2CAA2C;IAC3C,KAAK,CAAC,EAAE,QAAQ,CAAA;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B;AAID,eAAO,MAAM,UAAU;;;;;;;;;;;CAeb,CAAA;AAGV,QAAA,MAAM,MAAM,+GAA0B,CAAA;AAEtC,OAAO,EAAE,MAAM,EAAE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"avatar.js","sources":["../../../src/components/Avatar/avatar.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport { User } from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { CAT_SUBTLE_TOKENS, CAT_SOLID_TOKENS, type CategoricalColor } from '@/design-system/tokens/categorical-color'\nimport { HoverCard, HoverCardTrigger, HoverCardContent } from '@/design-system/components/HoverCard/hover-card'\nimport { HOVER_DELAY_RICH_MS, HOVER_DELAY_CLOSE_MS } from '@/design-system/tokens/motion/motion'\nimport { Badge } from '@/design-system/components/Badge/badge'\nimport { useFieldContext, useTableIsScrolling } from '@/design-system/components/Field/field-context'\n\n/**\n * Avatar — 頭像元件\n *\n * 三種內容模式(按優先順序):\n * 1. src → 圖片\n * 2. icon → icon 在底色圓/方形內\n * 3. alt → 取首字作文字 fallback\n * 4. 都沒有 → 預設 User icon\n *\n * ── 尺寸 ──\n * size 接受任意 px 值,icon 自動 = round_even(size × 0.6)\n * 文字 fallback 字體 = size × 0.5\n *\n * ── 形狀 ──\n * circle(預設)→ rounded-full,用於人物\n * square → rounded-md (4px),用於實體(專案、組織、App)\n */\n\n// ── 色彩 ──\n// **消費 categorical-color SSOT**(CAT_SUBTLE_TOKENS / CAT_SOLID_TOKENS,key X 一律對 `--color-X-*`,\n// 1:1 零 offset)。subtle=step-1 底 + step-7 字;solid=step-6 全色底 + on-emphasis 字\n//(亮 hue yellow/amber/orange/lime 用 --on-emphasis-dark 深字;green 白字例外)。neutral 非色相,自處理(subtle=muted、solid=neutral-9)。\n// 2026-06-04 修:原 `red` 誤接 `--color-deep-orange-*`(red=品牌紅 hue-25 ≠ deep-orange);\n// 改消費 SSOT 後 red→`--color-red-*`,並補齊全 12 色相。\ntype ColorKey = CategoricalColor\ntype VariantKey = 'subtle' | 'solid'\n\nconst COLOR_MAP: Record<VariantKey, Record<ColorKey, { bg: string; text: string }>> = {\n subtle: {\n neutral: { bg: 'var(--muted)', text: 'var(--foreground)' },\n ...CAT_SUBTLE_TOKENS,\n },\n solid: {\n neutral: { bg: 'var(--color-neutral-9)', text: 'var(--inverse-fg)' },\n ...CAT_SOLID_TOKENS,\n },\n}\n\n// ── Icon size: round to nearest even, ≈ 60% ──\nfunction getIconSize(avatarSize: number): number {\n return Math.round((avatarSize * 0.6) / 2) * 2\n}\n\n// ── Text fallback: first character ──\nfunction getInitial(text: string): string {\n return text.trim().charAt(0).toUpperCase()\n}\n\n// Semantic presence tokens — 見 color/semantic.css\n// Module-level constant(2026-04-22 D3 perf audit):從 render body 移到 module scope,\n// 避免每次 Avatar render 都 re-declare 此 4-entry object(Low impact 但渲染大量 avatars 時累積可觀)\nconst STATUS_DOT_COLOR: Record<string, string> = {\n online: 'var(--status-online)',\n away: 'var(--status-away)',\n busy: 'var(--status-busy)',\n offline: 'var(--status-offline)',\n}\n\n// ── useDocumentTheme(2026-04-23;M3 Portal 逃脫防線,scope verified 2026-04-25)──\n// 讀 `<html data-theme>` 並 observe mutation。用於 Avatar hoverCard ProfileCard:\n// Portal 後的 HoverCardContent 會繼承 trigger subtree theme(如 OverflowIndicator\n// dark tooltip 內部),造成 ProfileCard 被污染成 dark。顯式 bind app-level theme\n// 確保 ProfileCard 永遠跟 app 本身 theme 一致(light-in-light-app / dark-in-dark-app)。\n//\n// 範圍 audit 2026-04-25:觀察對象是 `document.documentElement` 自有 DOM,非 3rd-party\n// lib 內部(不屬 M2 scope);attributeFilter 限定 `data-theme` 單一 attr,re-render 成本\n// 為每次全站 theme 切換 × Avatar 數量,可接受。\nfunction useDocumentTheme(): string | null {\n const [theme, setTheme] = React.useState<string | null>(() =>\n typeof document !== 'undefined' ? document.documentElement.getAttribute('data-theme') : null,\n )\n React.useEffect(() => {\n if (typeof document === 'undefined') return\n const root = document.documentElement\n const update = () => setTheme(root.getAttribute('data-theme'))\n update()\n const obs = new MutationObserver(update)\n obs.observe(root, { attributes: true, attributeFilter: ['data-theme'] })\n return () => obs.disconnect()\n }, [])\n return theme\n}\n\n// ── Component ──\n\nexport interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {\n /** 尺寸:number (px) 或 'fill'(填滿父容器,由父層決定大小)。預設 32 */\n size?: number | 'fill'\n /** 形狀:circle(人物)或 square(實體),預設 circle */\n shape?: 'circle' | 'square'\n /** 圖片 URL */\n src?: string\n /** 替代文字(圖片失敗時取首字作 fallback) */\n alt?: string\n /** Icon 模式(LucideIcon) */\n icon?: LucideIcon\n /** Icon / text fallback 的背景色,預設 neutral */\n color?: ColorKey\n /** 深底白字模式(step-6 背景 + 白色前景,warning 例外),預設 false */\n solid?: boolean\n /**\n * 在線狀態指示器(presence),顯示在 avatar **右下角**。\n * 世界級對照:Slack / Teams / Discord — `online` 是最廣泛被理解的術語。\n * 位置語義:右下 = \"此人的 presence\"(使用者聚焦於「這個人是誰 + 現在 在不在」)。\n */\n status?: 'online' | 'away' | 'busy' | 'offline'\n /**\n * 未讀 / 通知計數 badge,顯示在 avatar **右上角**。\n * 世界級對照:chat app(iMessage / Slack thread / LINE / WhatsApp)一律右上角。\n * 位置語義:右上 = \"關於此對話的新事件數量\"(使用者聚焦於「有多少未處理」);\n * 與右下的 presence 共存不衝突(不同角、不同語義)。\n * `> 99` 自動顯示 \"99+\"(交給內部 Badge 的 `max` 行為)。\n */\n badgeCount?: number\n /**\n * 傳入 HoverCard 內容(如 ProfileCard),hover avatar 時自動顯示。\n * 只有人員 avatar 需要傳;實體 avatar(專案、組織)不傳。\n */\n hoverCard?: React.ReactNode\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\n// 2026-05-13 (a) perf fix part-2(per codex Layer C Roadmap rich-cell dominant + user 拍 Path (a)):\n// `React.memo` wrap forwardRef Avatar — Roadmap 13 columns 含 person/multiPerson,每 row 多 avatar\n// × HoverCard subtree + useDocumentTheme observer = 重渲染 hotspot。memo shallow-equal props,\n// HoverCard / themeRef stable across scroll 時 skip re-render。對齊 codex Profile Plan step 5\n// (filter Avatar/PeoplePicker/FieldSurfaceProvider remounts)。\n// code-quality-allow: long-function — size × shape × color × solid × status × badgeCount × hoverCard × img-fallback 多軸 prop 組合,拆 sub-fn 會跨 fn 傳 imgError state + isTableScrolling observer 結果\nconst AvatarInner = React.forwardRef<HTMLDivElement, AvatarProps>(\n ({ size = 32, shape = 'circle', src, alt, icon: Icon, color = 'neutral', solid = false, status, badgeCount, hoverCard, className, style, ...props }, ref) => {\n const [imgError, setImgError] = React.useState(false)\n const documentTheme = useDocumentTheme()\n const isTableScrolling = useTableIsScrolling()\n // 2026-05-13 R3.5(per codex Q3 verdict + user 拍「想盡辦法 auto-handle prereq」):\n // Avatar self-dim when in disabled Field wrapper context(取代既有 wrapper opacity-disabled blanket\n // 逃生艙 — color.spec.md:729 specific-disabled-color canonical)。\n // Scope narrowest:`fieldCtx?.mode === 'disabled' && fieldCtx?.hasFieldWrapper === true`,標準 Field\n // 家族 wrapper disabled 時才 dim;**沒包在 Field wrapper 內的 standalone Avatar**(ProfileCard / FileItem /\n // HoverCard / Dialog 等 display 場景)**backward compat 不變**。對齊 avatar.spec.md「Avatar 在 disabled\n // 元件內 host-controlled opacity」canonical — 升級成「Avatar self-managed via fieldCtx」。\n const fieldCtx = useFieldContext()\n const isDisabledInField = fieldCtx?.mode === 'disabled' && fieldCtx?.hasFieldWrapper === true\n const isFill = size === 'fill'\n // Fill 模式下 icon 用 60% 寬高、text 用 50cqi(container query inline-size);\n // 數字模式下用既有 px 計算\n const numSize = isFill ? 32 : (size as number)\n const iconPx = getIconSize(numSize)\n const fontSizePx = Math.round(numSize * 0.5)\n const variantKey: VariantKey = solid ? 'solid' : 'subtle'\n const colors = COLOR_MAP[variantKey]?.[color] ?? COLOR_MAP.subtle.neutral\n const radius = shape === 'circle' ? '9999px' : '4px'\n\n // 決定內容\n const showImage = src && !imgError\n const showIcon = !showImage && (Icon || (!alt))\n const showText = !showImage && !showIcon && alt\n\n const FallbackIcon = Icon ?? User\n\n // Status dot 尺寸:avatar 的 28%(Slack / Teams / Discord 世界級平均),\n // clamp [8, 16] — floor 8 保小 avatar 仍可辨識但不喧賓奪主(10 floor 會讓 24px\n // avatar 的 dot 占 42% 太大);ceiling 16 防大 avatar dot 過度放大\n const dotSize = isFill ? 10 : Math.max(8, Math.min(16, Math.round(numSize * 0.28)))\n // Border ring 在 surface 上分離 dot 與 avatar,dotSize ≥ 12 時升階到 3px 保持視覺比例\n const dotBorder = dotSize >= 12 ? 3 : 2\n\n const avatarEl = (\n <div\n className={cn(\n 'inline-flex items-center justify-center shrink-0 overflow-hidden select-none',\n isFill && 'w-full h-full',\n // 2026-05-13 R3.5 self-dim:Avatar 在 disabled Field wrapper context 內自 dim\n // (取代 field-wrapper.tsx default/bare/naked disabled blanket opacity-disabled 逃生艙)\n isDisabledInField && 'opacity-disabled',\n )}\n style={{\n ...(isFill\n ? { containerType: 'inline-size' as React.CSSProperties['containerType'] }\n : { width: numSize, height: numSize }),\n borderRadius: radius,\n backgroundColor: showImage ? undefined : colors.bg,\n color: showImage ? undefined : colors.text,\n }}\n data-avatar-size={isFill ? 'fill' : numSize}\n role={!showImage && alt && !hoverCard ? 'img' : undefined}\n aria-label={!showImage && alt && !hoverCard ? alt : undefined}\n >\n {showImage && (\n <img\n src={src}\n alt={alt ?? ''}\n className=\"w-full h-full object-cover\"\n onError={() => setImgError(true)}\n />\n )}\n {showIcon && (\n isFill\n ? <FallbackIcon className=\"w-[60%] h-[60%]\" aria-hidden />\n : <FallbackIcon size={iconPx} aria-hidden />\n )}\n {showText && (\n <span\n className=\"font-medium leading-none\"\n style={{ fontSize: isFill ? '50cqi' : fontSizePx }}\n aria-hidden\n >\n {getInitial(alt!)}\n </span>\n )}\n </div>\n )\n\n const hasOverlay = status || typeof badgeCount === 'number'\n // Keyboard access canonical(D4 UX audit 2026-04-22 finding):Avatar with `hoverCard`\n // 需 keyboard 可達 — Radix `HoverCardTrigger asChild` 不自動加 tabIndex,non-focusable\n // `<div>` 會讓 keyboard-only user 無法 reach ProfileCard popover(WCAG 2.1.1 / 4.1.2 違反)。\n // 解:當 `hoverCard` 存在時,wrapper `<div>` 變 focusable(`tabIndex=0` + `role=\"button\"` +\n // `aria-haspopup=\"dialog\"` + focus-visible ring)。若無 hoverCard 則維持純展示 `<div>`。\n const focusableProps = hoverCard\n ? {\n tabIndex: 0,\n role: 'button' as const,\n 'aria-haspopup': 'dialog' as const,\n 'aria-label': alt ?? 'View profile',\n }\n : {}\n // 2026-05-31:focus ring 圓角跟隨 shape(circle→rounded-full / square→rounded-md 對齊 body radius L173),\n // 原寫死 rounded-full 會讓方形 avatar(實體)配 hoverCard 時出現圓形 ring。hoverCard 為通用行為(任意內容),\n // 方形 avatar 合法可配(內容非 ProfileCard 而已),故 ring 必跟形狀。\n const focusableClass = hoverCard\n ? cn('focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1', shape === 'circle' ? 'rounded-full' : 'rounded-md')\n : ''\n const baseEl = !hasOverlay\n ? <div ref={ref} className={cn('inline-flex shrink-0', focusableClass, className)} style={style} {...focusableProps} {...props}>{avatarEl}</div>\n : (\n <div ref={ref} className={cn('relative inline-flex shrink-0', focusableClass, className)} style={style} {...focusableProps} {...props}>\n {avatarEl}\n {/* Status dot:bottom-right(presence — 世界級對照 Slack / Teams / Discord),\n 落在 circle avatar 圓周 45° 位置 / square avatar 右下直角;\n border ring 用 surface 色讓 dot 從 avatar 邊界視覺分離。\n a11y:`aria-hidden` — presence 資訊整合到 parent avatar 的 aria-label\n (world-class Slack 做法),避免多 `role=\"status\"` 造成 screen reader 洪水 */}\n {status && (\n <span\n className=\"absolute block rounded-full\"\n style={{\n width: dotSize,\n height: dotSize,\n bottom: 0,\n right: 0,\n backgroundColor: STATUS_DOT_COLOR[status],\n boxShadow: `0 0 0 ${dotBorder}px var(--surface-raised, var(--canvas))`,\n }}\n aria-hidden\n />\n )}\n {/* Count badge:top-right(chat 未讀 / 通知計數 — 世界級對照 iMessage /\n Slack thread / LINE / WhatsApp)。消費 DS Badge(critical variant),\n 再加 ring 與 avatar 分離 */}\n {typeof badgeCount === 'number' && badgeCount > 0 && (\n <Badge\n variant=\"critical\"\n count={badgeCount}\n max={99}\n className=\"absolute -top-1 -right-1\"\n style={{\n boxShadow: `0 0 0 2px var(--surface-raised, var(--canvas))`,\n }}\n aria-label={`${badgeCount} unread`}\n />\n )}\n </div>\n )\n\n // 2026-05-13 (c) scroll-defer perf(per user 拍 Path (c) + codex Q3 verdict):\n // DataTable scrolling 期間跳 HoverCard wrapper(Portal + useDocumentTheme observer 是\n // Roadmap 重渲 hotspot,per codex Layer C 分析)。scroll 結束 → context flips false →\n // re-render 接回完整 HoverCard tree(ProfileCard 仍可 hover 顯示)。\n // 對齊 AG Grid `deferRender` for slow React cell components / MUI X DataGrid scroll-defer。\n if (!hoverCard || isTableScrolling) return baseEl\n\n return (\n <HoverCard openDelay={HOVER_DELAY_RICH_MS} closeDelay={HOVER_DELAY_CLOSE_MS}>\n <HoverCardTrigger asChild>\n {baseEl}\n </HoverCardTrigger>\n {/* HoverCardContent canonical(2026-04-23):\n - 無 inner padding(consumer ProfileCard 自帶 `px-4 py-3` chrome)\n - `overflow-hidden` + `rounded-lg` → child(ProfileCard)圓角裁切\n - **不設 max-height**:ProfileCard 自己消費 `--radix-hover-card-content-available-height`\n 自約束高度 + 內部 ScrollArea 處理捲動\n - `data-theme={documentTheme}`:ProfileCard 永遠跟隨 **app-level theme**(從 `<html data-theme>`\n 動態讀),不受 trigger subtree theme 污染。範例:Avatar 位於 OverflowIndicator 的 dark\n tooltip 內,其 Portal 會繼承該 subtree dark theme → ProfileCard 變全黑。顯式設回 app theme\n 確保 ProfileCard 永遠 light-in-light-app / dark-in-dark-app。 */}\n <HoverCardContent\n data-theme={documentTheme ?? undefined}\n className=\"bg-surface-raised rounded-lg border border-border overflow-hidden\"\n style={{ boxShadow: 'var(--elevation-200)' }}\n >\n {hoverCard}\n </HoverCardContent>\n </HoverCard>\n )\n }\n)\nAvatarInner.displayName = 'AvatarInner'\n\n// ── AvatarData ─────────────────────────────────────────────────────────────\n// 資料型別,讓 consumer 傳資料而非 ReactNode。\n// 接收端內部用 Avatar 元件渲染,統一控制尺寸與 fallback。\n\nexport interface AvatarData {\n /** 圖片 URL */\n src?: string\n /** 替代文字(圖片失敗時取首字作 fallback) */\n alt: string\n /** Icon / text fallback 的背景色,預設 neutral */\n color?: ColorKey\n /**\n * Person avatar hover ProfileCard(DS-wide canonical,person avatar 預設必有,見 avatar.spec.md)。\n * Entity avatar(專案 / 組織 logo)不帶 → consumer 不傳 hoverCard 即豁免。\n * 所有消費 AvatarData 的 primitive(MenuItem / DropdownMenu / SelectMenu / SelectionItem / ProfileCard)\n * 需 forward 此 prop 到內部 <Avatar hoverCard={avatar.hoverCard} />。\n */\n hoverCard?: React.ReactNode\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 avatarMeta = {\n component: 'Avatar',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-surface-raised'],\n fg: ['--foreground', '--on-emphasis'],\n ring: ['ring-ring'],\n },\n} as const\n\nAvatarInner.displayName = 'Avatar'\nconst Avatar = React.memo(AvatarInner)\n\nexport { Avatar }\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"avatar.js","sources":["../../../src/components/Avatar/avatar.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport { User } from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { CAT_SUBTLE_TOKENS, CAT_SOLID_TOKENS, type CategoricalColor } from '@/design-system/tokens/categorical-color'\nimport { HoverCard, HoverCardTrigger, HoverCardContent } from '@/design-system/components/HoverCard/hover-card'\nimport { HOVER_DELAY_RICH_MS, HOVER_DELAY_CLOSE_MS } from '@/design-system/tokens/motion/motion'\nimport { Badge } from '@/design-system/components/Badge/badge'\nimport { useFieldContext, useTableIsScrolling } from '@/design-system/components/Field/field-context'\n\n/**\n * Avatar — 頭像元件\n *\n * 三種內容模式(按優先順序):\n * 1. src → 圖片\n * 2. icon → icon 在底色圓/方形內\n * 3. alt → 取首字作文字 fallback\n * 4. 都沒有 → 預設 User icon\n *\n * ── 尺寸 ──\n * size 接受任意 px 值,icon 自動 = round_even(size × 0.6)\n * 文字 fallback 字體 = size × 0.5\n *\n * ── 形狀 ──\n * circle(預設)→ rounded-full,用於人物\n * square → rounded-md (4px),用於實體(專案、組織、App)\n */\n\n// ── 色彩 ──\n// **消費 categorical-color SSOT**(CAT_SUBTLE_TOKENS / CAT_SOLID_TOKENS,key X 一律對 `--color-X-*`,\n// 1:1 零 offset)。subtle=step-1 底 + step-7 字;solid=step-6 全色底 + on-emphasis 字\n//(亮 hue yellow/amber/orange/lime 用 --on-emphasis-dark 深字;green 白字例外)。neutral 非色相,自處理(subtle=muted、solid=neutral-9)。\n// 2026-06-04 修:原 `red` 誤接 `--color-deep-orange-*`(red=品牌紅 hue-25 ≠ deep-orange);\n// 改消費 SSOT 後 red→`--color-red-*`,並補齊全 12 色相。\ntype ColorKey = CategoricalColor\ntype VariantKey = 'subtle' | 'solid'\n\nconst COLOR_MAP: Record<VariantKey, Record<ColorKey, { bg: string; text: string }>> = {\n subtle: {\n neutral: { bg: 'var(--muted)', text: 'var(--foreground)' },\n ...CAT_SUBTLE_TOKENS,\n },\n solid: {\n neutral: { bg: 'var(--color-neutral-9)', text: 'var(--inverse-fg)' },\n ...CAT_SOLID_TOKENS,\n },\n}\n\n// ── Icon size: round to nearest even, ≈ 60% ──\nfunction getIconSize(avatarSize: number): number {\n return Math.round((avatarSize * 0.6) / 2) * 2\n}\n\n// ── Text fallback: first character ──\nfunction getInitial(text: string): string {\n return text.trim().charAt(0).toUpperCase()\n}\n\n// Semantic presence tokens — 見 color/semantic.css\n// Module-level constant(2026-04-22 D3 perf audit):從 render body 移到 module scope,\n// 避免每次 Avatar render 都 re-declare 此 4-entry object(Low impact 但渲染大量 avatars 時累積可觀)\nconst STATUS_DOT_COLOR: Record<string, string> = {\n online: 'var(--status-online)',\n away: 'var(--status-away)',\n busy: 'var(--status-busy)',\n offline: 'var(--status-offline)',\n}\n\n// ── useDocumentTheme(2026-04-23;M3 Portal 逃脫防線,scope verified 2026-04-25)──\n// 讀 `<html data-theme>` 並 observe mutation。用於 Avatar hoverCard ProfileCard:\n// Portal 後的 HoverCardContent 會繼承 trigger subtree theme(如 OverflowIndicator\n// dark tooltip 內部),造成 ProfileCard 被污染成 dark。顯式 bind app-level theme\n// 確保 ProfileCard 永遠跟 app 本身 theme 一致(light-in-light-app / dark-in-dark-app)。\n//\n// 範圍 audit 2026-04-25:觀察對象是 `document.documentElement` 自有 DOM,非 3rd-party\n// lib 內部(不屬 M2 scope);attributeFilter 限定 `data-theme` 單一 attr,re-render 成本\n// 為每次全站 theme 切換 × Avatar 數量,可接受。\nfunction useDocumentTheme(): string | null {\n const [theme, setTheme] = React.useState<string | null>(() =>\n typeof document !== 'undefined' ? document.documentElement.getAttribute('data-theme') : null,\n )\n React.useEffect(() => {\n if (typeof document === 'undefined') return\n const root = document.documentElement\n const update = () => setTheme(root.getAttribute('data-theme'))\n update()\n const obs = new MutationObserver(update)\n obs.observe(root, { attributes: true, attributeFilter: ['data-theme'] })\n return () => obs.disconnect()\n }, [])\n return theme\n}\n\n// ── Component ──\n\nexport interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {\n /** 尺寸:number (px) 或 'fill'(填滿父容器,由父層決定大小)。預設 32 */\n size?: number | 'fill'\n /** 形狀:circle(人物)或 square(實體),預設 circle */\n shape?: 'circle' | 'square'\n /** 圖片 URL */\n src?: string\n /** 替代文字(圖片失敗時取首字作 fallback) */\n alt?: string\n /** Icon 模式(LucideIcon) */\n icon?: LucideIcon\n /** Icon / text fallback 的背景色,預設 neutral */\n color?: ColorKey\n /** 深底模式(step-6 背景 + on-emphasis 配對前景;亮色 hue yellow/amber/orange/lime 用深字 --on-emphasis-dark),預設 false */\n solid?: boolean\n /**\n * 在線狀態指示器(presence),顯示在 avatar **右下角**。\n * 世界級對照:Slack / Teams / Discord — `online` 是最廣泛被理解的術語。\n * 位置語義:右下 = \"此人的 presence\"(使用者聚焦於「這個人是誰 + 現在 在不在」)。\n */\n status?: 'online' | 'away' | 'busy' | 'offline'\n /**\n * 未讀 / 通知計數 badge,顯示在 avatar **右上角**。\n * 世界級對照:chat app(iMessage / Slack thread / LINE / WhatsApp)一律右上角。\n * 位置語義:右上 = \"關於此對話的新事件數量\"(使用者聚焦於「有多少未處理」);\n * 與右下的 presence 共存不衝突(不同角、不同語義)。\n * `> 99` 自動顯示 \"99+\"(交給內部 Badge 的 `max` 行為)。\n */\n badgeCount?: number\n /**\n * 傳入 HoverCard 內容(如 ProfileCard),hover avatar 時自動顯示。\n * 只有人員 avatar 需要傳;實體 avatar(專案、組織)不傳。\n */\n hoverCard?: React.ReactNode\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\n// 2026-05-13 (a) perf fix part-2(per codex Layer C Roadmap rich-cell dominant + user 拍 Path (a)):\n// `React.memo` wrap forwardRef Avatar — Roadmap 13 columns 含 person/multiPerson,每 row 多 avatar\n// × HoverCard subtree + useDocumentTheme observer = 重渲染 hotspot。memo shallow-equal props,\n// HoverCard / themeRef stable across scroll 時 skip re-render。對齊 codex Profile Plan step 5\n// (filter Avatar/PeoplePicker/FieldSurfaceProvider remounts)。\n// code-quality-allow: long-function — size × shape × color × solid × status × badgeCount × hoverCard × img-fallback 多軸 prop 組合,拆 sub-fn 會跨 fn 傳 imgError state + isTableScrolling observer 結果\nconst AvatarInner = React.forwardRef<HTMLDivElement, AvatarProps>(\n ({ size = 32, shape = 'circle', src, alt, icon: Icon, color = 'neutral', solid = false, status, badgeCount, hoverCard, className, style, ...props }, ref) => {\n const [imgError, setImgError] = React.useState(false)\n const documentTheme = useDocumentTheme()\n const isTableScrolling = useTableIsScrolling()\n // 2026-05-13 R3.5(per codex Q3 verdict + user 拍「想盡辦法 auto-handle prereq」):\n // Avatar self-dim when in disabled Field wrapper context(取代既有 wrapper opacity-disabled blanket\n // 逃生艙 — color.spec.md:729 specific-disabled-color canonical)。\n // Scope narrowest:`fieldCtx?.mode === 'disabled' && fieldCtx?.hasFieldWrapper === true`,標準 Field\n // 家族 wrapper disabled 時才 dim;**沒包在 Field wrapper 內的 standalone Avatar**(ProfileCard / FileItem /\n // HoverCard / Dialog 等 display 場景)**backward compat 不變**。對齊 avatar.spec.md「Avatar 在 disabled\n // 元件內 host-controlled opacity」canonical — 升級成「Avatar self-managed via fieldCtx」。\n const fieldCtx = useFieldContext()\n const isDisabledInField = fieldCtx?.mode === 'disabled' && fieldCtx?.hasFieldWrapper === true\n const isFill = size === 'fill'\n // Fill 模式下 icon 用 60% 寬高、text 用 50cqi(container query inline-size);\n // 數字模式下用既有 px 計算\n const numSize = isFill ? 32 : (size as number)\n const iconPx = getIconSize(numSize)\n const fontSizePx = Math.round(numSize * 0.5)\n const variantKey: VariantKey = solid ? 'solid' : 'subtle'\n const colors = COLOR_MAP[variantKey]?.[color] ?? COLOR_MAP.subtle.neutral\n const radius = shape === 'circle' ? '9999px' : '4px'\n\n // 決定內容\n const showImage = src && !imgError\n const showIcon = !showImage && (Icon || (!alt))\n const showText = !showImage && !showIcon && alt\n\n const FallbackIcon = Icon ?? User\n\n // Status dot 尺寸:avatar 的 28%(Slack / Teams / Discord 世界級平均),\n // clamp [8, 16] — floor 8 保小 avatar 仍可辨識但不喧賓奪主(10 floor 會讓 24px\n // avatar 的 dot 占 42% 太大);ceiling 16 防大 avatar dot 過度放大\n const dotSize = isFill ? 10 : Math.max(8, Math.min(16, Math.round(numSize * 0.28)))\n // Border ring 在 surface 上分離 dot 與 avatar,dotSize ≥ 12 時升階到 3px 保持視覺比例\n const dotBorder = dotSize >= 12 ? 3 : 2\n\n const avatarEl = (\n <div\n className={cn(\n 'inline-flex items-center justify-center shrink-0 overflow-hidden select-none',\n isFill && 'w-full h-full',\n // 2026-05-13 R3.5 self-dim:Avatar 在 disabled Field wrapper context 內自 dim\n // (取代 field-wrapper.tsx default/bare/naked disabled blanket opacity-disabled 逃生艙)\n isDisabledInField && 'opacity-disabled',\n )}\n style={{\n ...(isFill\n ? { containerType: 'inline-size' as React.CSSProperties['containerType'] }\n : { width: numSize, height: numSize }),\n borderRadius: radius,\n backgroundColor: showImage ? undefined : colors.bg,\n color: showImage ? undefined : colors.text,\n }}\n data-avatar-size={isFill ? 'fill' : numSize}\n role={!showImage && alt && !hoverCard ? 'img' : undefined}\n aria-label={!showImage && alt && !hoverCard ? alt : undefined}\n >\n {showImage && (\n <img\n src={src}\n alt={alt ?? ''}\n className=\"w-full h-full object-cover\"\n onError={() => setImgError(true)}\n />\n )}\n {showIcon && (\n isFill\n ? <FallbackIcon className=\"w-[60%] h-[60%]\" aria-hidden />\n : <FallbackIcon size={iconPx} aria-hidden />\n )}\n {showText && (\n <span\n className=\"font-medium leading-none\"\n style={{ fontSize: isFill ? '50cqi' : fontSizePx }}\n aria-hidden\n >\n {getInitial(alt!)}\n </span>\n )}\n </div>\n )\n\n const hasOverlay = status || typeof badgeCount === 'number'\n // Keyboard access canonical(D4 UX audit 2026-04-22 finding):Avatar with `hoverCard`\n // 需 keyboard 可達 — Radix `HoverCardTrigger asChild` 不自動加 tabIndex,non-focusable\n // `<div>` 會讓 keyboard-only user 無法 reach ProfileCard popover(WCAG 2.1.1 / 4.1.2 違反)。\n // 解:當 `hoverCard` 存在時,wrapper `<div>` 變 focusable(`tabIndex=0` + `role=\"button\"` +\n // `aria-haspopup=\"dialog\"` + focus-visible ring)。若無 hoverCard 則維持純展示 `<div>`。\n const focusableProps = hoverCard\n ? {\n tabIndex: 0,\n role: 'button' as const,\n 'aria-haspopup': 'dialog' as const,\n 'aria-label': alt ?? 'View profile',\n }\n : {}\n // 2026-05-31:focus ring 圓角跟隨 shape(circle→rounded-full / square→rounded-md 對齊 body radius L173),\n // 原寫死 rounded-full 會讓方形 avatar(實體)配 hoverCard 時出現圓形 ring。hoverCard 為通用行為(任意內容),\n // 方形 avatar 合法可配(內容非 ProfileCard 而已),故 ring 必跟形狀。\n const focusableClass = hoverCard\n ? cn('focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1', shape === 'circle' ? 'rounded-full' : 'rounded-md')\n : ''\n const baseEl = !hasOverlay\n ? <div ref={ref} className={cn('inline-flex shrink-0', focusableClass, className)} style={style} {...focusableProps} {...props}>{avatarEl}</div>\n : (\n <div ref={ref} className={cn('relative inline-flex shrink-0', focusableClass, className)} style={style} {...focusableProps} {...props}>\n {avatarEl}\n {/* Status dot:bottom-right(presence — 世界級對照 Slack / Teams / Discord),\n 落在 circle avatar 圓周 45° 位置 / square avatar 右下直角;\n border ring 用 surface 色讓 dot 從 avatar 邊界視覺分離。\n a11y:`aria-hidden` — presence 資訊整合到 parent avatar 的 aria-label\n (world-class Slack 做法),避免多 `role=\"status\"` 造成 screen reader 洪水 */}\n {status && (\n <span\n className=\"absolute block rounded-full\"\n style={{\n width: dotSize,\n height: dotSize,\n bottom: 0,\n right: 0,\n backgroundColor: STATUS_DOT_COLOR[status],\n boxShadow: `0 0 0 ${dotBorder}px var(--surface-raised, var(--canvas))`,\n }}\n aria-hidden\n />\n )}\n {/* Count badge:top-right(chat 未讀 / 通知計數 — 世界級對照 iMessage /\n Slack thread / LINE / WhatsApp)。消費 DS Badge(critical variant),\n 再加 ring 與 avatar 分離 */}\n {typeof badgeCount === 'number' && badgeCount > 0 && (\n <Badge\n variant=\"critical\"\n count={badgeCount}\n max={99}\n className=\"absolute -top-1 -right-1\"\n style={{\n boxShadow: `0 0 0 2px var(--surface-raised, var(--canvas))`,\n }}\n aria-label={`${badgeCount} unread`}\n />\n )}\n </div>\n )\n\n // 2026-05-13 (c) scroll-defer perf(per user 拍 Path (c) + codex Q3 verdict):\n // DataTable scrolling 期間跳 HoverCard wrapper(Portal + useDocumentTheme observer 是\n // Roadmap 重渲 hotspot,per codex Layer C 分析)。scroll 結束 → context flips false →\n // re-render 接回完整 HoverCard tree(ProfileCard 仍可 hover 顯示)。\n // 對齊 AG Grid `deferRender` for slow React cell components / MUI X DataGrid scroll-defer。\n if (!hoverCard || isTableScrolling) return baseEl\n\n return (\n <HoverCard openDelay={HOVER_DELAY_RICH_MS} closeDelay={HOVER_DELAY_CLOSE_MS}>\n <HoverCardTrigger asChild>\n {baseEl}\n </HoverCardTrigger>\n {/* HoverCardContent canonical(2026-04-23):\n - 無 inner padding(consumer ProfileCard 自帶 `px-4 py-3` chrome)\n - `overflow-hidden` + `rounded-lg` → child(ProfileCard)圓角裁切\n - **不設 max-height**:ProfileCard 自己消費 `--radix-hover-card-content-available-height`\n 自約束高度 + 內部 ScrollArea 處理捲動\n - `data-theme={documentTheme}`:ProfileCard 永遠跟隨 **app-level theme**(從 `<html data-theme>`\n 動態讀),不受 trigger subtree theme 污染。範例:Avatar 位於 OverflowIndicator 的 dark\n tooltip 內,其 Portal 會繼承該 subtree dark theme → ProfileCard 變全黑。顯式設回 app theme\n 確保 ProfileCard 永遠 light-in-light-app / dark-in-dark-app。 */}\n <HoverCardContent\n data-theme={documentTheme ?? undefined}\n className=\"bg-surface-raised rounded-lg border border-border overflow-hidden\"\n style={{ boxShadow: 'var(--elevation-200)' }}\n >\n {hoverCard}\n </HoverCardContent>\n </HoverCard>\n )\n }\n)\nAvatarInner.displayName = 'AvatarInner'\n\n// ── AvatarData ─────────────────────────────────────────────────────────────\n// 資料型別,讓 consumer 傳資料而非 ReactNode。\n// 接收端內部用 Avatar 元件渲染,統一控制尺寸與 fallback。\n\nexport interface AvatarData {\n /** 圖片 URL */\n src?: string\n /** 替代文字(圖片失敗時取首字作 fallback) */\n alt: string\n /** Icon / text fallback 的背景色,預設 neutral */\n color?: ColorKey\n /**\n * Person avatar hover ProfileCard(DS-wide canonical,person avatar 預設必有,見 avatar.spec.md)。\n * Entity avatar(專案 / 組織 logo)不帶 → consumer 不傳 hoverCard 即豁免。\n * 所有消費 AvatarData 的 primitive(MenuItem / DropdownMenu / SelectMenu / SelectionItem / ProfileCard)\n * 需 forward 此 prop 到內部 <Avatar hoverCard={avatar.hoverCard} />。\n */\n hoverCard?: React.ReactNode\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 avatarMeta = {\n component: 'Avatar',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-surface-raised'],\n fg: ['--foreground', '--on-emphasis'],\n ring: ['ring-ring'],\n },\n} as const\n\nAvatarInner.displayName = 'Avatar'\nconst Avatar = React.memo(AvatarInner)\n\nexport { Avatar }\n"],"names":[],"mappings":"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"}
@@ -18,7 +18,7 @@ export declare const badgeMeta: {
18
18
  readonly family: 3;
19
19
  readonly variants: {
20
20
  readonly critical: {
21
- readonly purpose: "紅底白字(bg-notification)";
21
+ readonly purpose: "deep-orange 底白字(bg-notification = deep-orange-6)";
22
22
  };
23
23
  readonly high: {
24
24
  readonly purpose: "藍底白字(bg-info)";
@@ -47,7 +47,7 @@ const badgeMeta = {
47
47
  component: "Badge",
48
48
  family: 3,
49
49
  variants: {
50
- critical: { purpose: "紅底白字(bg-notification)" },
50
+ critical: { purpose: "deep-orange 底白字(bg-notification = deep-orange-6)" },
51
51
  high: { purpose: "藍底白字(bg-info)" },
52
52
  medium: { purpose: "淺藍底藍字(bg-info-subtle)" },
53
53
  low: { purpose: "使用者切 tab 才看,不需搶注意力" }
@@ -1 +1 @@
1
- {"version":3,"file":"badge.js","sources":["../../../src/components/Badge/badge.tsx"],"sourcesContent":["import * as React from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@/lib/utils'\n\n// ── Badge(notification count indicator)────────────────────────────────────\n// 通知計數指示器,用於未讀數量、待辦計數等。\n//\n// 兩種模式:\n// count — 16px 高,10px 字,font-medium。個位數正圓,多位數膠囊。\n// dot — 6×6px 純色圓點,無文字。\n//\n// 四個層級(由 passive 到 urgent):\n// low(預設) — 灰底灰字(neutral-3 + neutral-7),被動計數\n// medium — 淺藍底藍字(bg-info-subtle + text-info-text),可延後看\n// high — 藍底白字(bg-info),有感影響的待辦\n// critical — 紅底白字(bg-notification),立即處理\n//\n// 規則:default low, escalate with reason。見 badge.spec.md「選 level 的流程」。\n\nconst badgeVariants = cva(\n // 2026-05-23 Path B revert:icon-as-text / numeric-in-circle 用 leading-none canonical(對齊 Material Avatar `line-height: 1` / Polaris Badge / Carbon Tag 共識)。\n // 視覺等效驗證:Badge container 顯式 `h-4 (16px)` + `flex items-center` 主導,text 被 items-center 置中,line-height 1.0 vs 1.3 視覺零差別。\n // user 2026-05-23「或是其實根本不用分?」+「照你建議」path B:統一 leading-none,不分。\n // 廢除 2026-05-21 F1 fix 的 leading-compact migration(那次 anchor「不影響高度的話就改」core constraint 是高度不變,Path B 同樣滿足且更簡單)。\n 'inline-flex items-center justify-center rounded-full leading-none',\n {\n variants: {\n variant: {\n critical: 'bg-notification text-on-emphasis',\n high: 'bg-info text-on-emphasis',\n medium: 'bg-info-subtle text-info-text',\n low: 'bg-secondary text-fg-muted',\n },\n dot: {\n true: 'w-1.5 h-1.5',\n false: 'min-w-4 h-4 px-1 text-[10px] font-medium',\n },\n },\n defaultVariants: {\n variant: 'low',\n dot: false,\n },\n }\n)\n\nexport interface BadgeProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'children'>,\n Omit<VariantProps<typeof badgeVariants>, 'dot'> {\n /** dot 模式:6×6px 純色圓點,無文字 */\n dot?: boolean\n /** 顯示的數量(dot 模式下忽略) */\n count?: number\n /** 數量上限,超過時顯示 \"max+\"(例:max=99 → \"99+\") */\n max?: number\n}\n\nconst Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(\n ({ variant, dot = false, count, max, className, role, ...props }, ref) => {\n const display = dot ? null : (\n max != null && count != null && count > max ? `${max}+` : `${count}`\n )\n\n // a11y(2026-04-25 axe aria-prohibited-attr fix):\n // span default 無 role → 不接 aria-label(WCAG 禁止)。Badge 是通知指示器,\n // `role=\"status\"` 語意正確(live region 可播報計數變化)且允許 aria-label。\n // Consumer 可 override(傳 role=\"img\" / role={undefined})。\n return (\n <span\n ref={ref}\n role={role ?? 'status'}\n className={cn(badgeVariants({ variant, dot }), className)}\n {...props}\n >\n {display}\n </span>\n )\n }\n)\nBadge.displayName = 'Badge'\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 badgeMeta = {\n component: 'Badge',\n family: 3,\n variants: {\n critical: { purpose: '紅底白字(bg-notification)' },\n high: { purpose: '藍底白字(bg-info)' },\n medium: { purpose: '淺藍底藍字(bg-info-subtle)' },\n low: { purpose: '使用者切 tab 才看,不需搶注意力' },\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-info', 'bg-info-subtle', 'bg-notification', 'bg-secondary'],\n fg: ['text-fg-muted', 'text-info-text'],\n ring: [],\n },\n defaultVariant: 'low',\n} as const\n\nexport { Badge, badgeVariants }\n"],"names":[],"mappings":";;;;AAmBA,MAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKpB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,UAAU;AAAA,QACV,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,KAAK;AAAA,MAAA;AAAA,MAEP,KAAK;AAAA,QACH,MAAM;AAAA,QACN,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,KAAK;AAAA,IAAA;AAAA,EACP;AAEJ;AAaA,MAAM,QAAQ,MAAM;AAAA,EAClB,CAAC,EAAE,SAAS,MAAM,OAAO,OAAO,KAAK,WAAW,MAAM,GAAG,MAAA,GAAS,QAAQ;AACxE,UAAM,UAAU,MAAM,OACpB,OAAO,QAAQ,SAAS,QAAQ,QAAQ,MAAM,GAAG,GAAG,MAAM,GAAG,KAAK;AAOpE,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAM,QAAQ;AAAA,QACd,WAAW,GAAG,cAAc,EAAE,SAAS,IAAA,CAAK,GAAG,SAAS;AAAA,QACvD,GAAG;AAAA,QAEH,UAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AACF;AACA,MAAM,cAAc;AAIb,MAAM,YAAY;AAAA,EACvB,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,UAAU;AAAA,IACR,UAAU,EAAE,SAAS,wBAAA;AAAA,IACrB,MAAM,EAAE,SAAS,gBAAA;AAAA,IACjB,QAAQ,EAAE,SAAS,wBAAA;AAAA,IACnB,KAAK,EAAE,SAAS,qBAAA;AAAA,EAAqB;AAAA,EAEvC,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,WAAW,SAAS,UAAU,iBAAiB,UAAU;AAAA,EAClE,QAAQ;AAAA,IACN,IAAI,CAAC,WAAW,kBAAkB,mBAAmB,cAAc;AAAA,IACnE,IAAI,CAAC,iBAAiB,gBAAgB;AAAA,IACtC,MAAM,CAAA;AAAA,EAAC;AAAA,EAET,gBAAgB;AAClB;"}
1
+ {"version":3,"file":"badge.js","sources":["../../../src/components/Badge/badge.tsx"],"sourcesContent":["import * as React from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@/lib/utils'\n\n// ── Badge(notification count indicator)────────────────────────────────────\n// 通知計數指示器,用於未讀數量、待辦計數等。\n//\n// 兩種模式:\n// count — 16px 高,10px 字,font-medium。個位數正圓,多位數膠囊。\n// dot — 6×6px 純色圓點,無文字。\n//\n// 四個層級(由 passive 到 urgent):\n// low(預設) — 灰底灰字(neutral-3 + neutral-7),被動計數\n// medium — 淺藍底藍字(bg-info-subtle + text-info-text),可延後看\n// high — 藍底白字(bg-info),有感影響的待辦\n// critical — deep-orange 底白字(bg-notification = --color-deep-orange-6,hue 38;非 categorical red hue 25),立即處理\n//\n// 規則:default low, escalate with reason。見 badge.spec.md「選 level 的流程」。\n\nconst badgeVariants = cva(\n // 2026-05-23 Path B revert:icon-as-text / numeric-in-circle 用 leading-none canonical(對齊 Material Avatar `line-height: 1` / Polaris Badge / Carbon Tag 共識)。\n // 視覺等效驗證:Badge container 顯式 `h-4 (16px)` + `flex items-center` 主導,text 被 items-center 置中,line-height 1.0 vs 1.3 視覺零差別。\n // user 2026-05-23「或是其實根本不用分?」+「照你建議」path B:統一 leading-none,不分。\n // 廢除 2026-05-21 F1 fix 的 leading-compact migration(那次 anchor「不影響高度的話就改」core constraint 是高度不變,Path B 同樣滿足且更簡單)。\n 'inline-flex items-center justify-center rounded-full leading-none',\n {\n variants: {\n variant: {\n critical: 'bg-notification text-on-emphasis',\n high: 'bg-info text-on-emphasis',\n medium: 'bg-info-subtle text-info-text',\n low: 'bg-secondary text-fg-muted',\n },\n dot: {\n true: 'w-1.5 h-1.5',\n false: 'min-w-4 h-4 px-1 text-[10px] font-medium',\n },\n },\n defaultVariants: {\n variant: 'low',\n dot: false,\n },\n }\n)\n\nexport interface BadgeProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'children'>,\n Omit<VariantProps<typeof badgeVariants>, 'dot'> {\n /** dot 模式:6×6px 純色圓點,無文字 */\n dot?: boolean\n /** 顯示的數量(dot 模式下忽略) */\n count?: number\n /** 數量上限,超過時顯示 \"max+\"(例:max=99 → \"99+\") */\n max?: number\n}\n\nconst Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(\n ({ variant, dot = false, count, max, className, role, ...props }, ref) => {\n const display = dot ? null : (\n max != null && count != null && count > max ? `${max}+` : `${count}`\n )\n\n // a11y(2026-04-25 axe aria-prohibited-attr fix):\n // span default 無 role → 不接 aria-label(WCAG 禁止)。Badge 是通知指示器,\n // `role=\"status\"` 語意正確(live region 可播報計數變化)且允許 aria-label。\n // Consumer 可 override(傳 role=\"img\" / role={undefined})。\n return (\n <span\n ref={ref}\n role={role ?? 'status'}\n className={cn(badgeVariants({ variant, dot }), className)}\n {...props}\n >\n {display}\n </span>\n )\n }\n)\nBadge.displayName = 'Badge'\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 badgeMeta = {\n component: 'Badge',\n family: 3,\n variants: {\n critical: { purpose: 'deep-orange 底白字(bg-notification = deep-orange-6)' },\n high: { purpose: '藍底白字(bg-info)' },\n medium: { purpose: '淺藍底藍字(bg-info-subtle)' },\n low: { purpose: '使用者切 tab 才看,不需搶注意力' },\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-info', 'bg-info-subtle', 'bg-notification', 'bg-secondary'],\n fg: ['text-fg-muted', 'text-info-text'],\n ring: [],\n },\n defaultVariant: 'low',\n} as const\n\nexport { Badge, badgeVariants }\n"],"names":[],"mappings":";;;;AAmBA,MAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKpB;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,UAAU;AAAA,QACV,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,KAAK;AAAA,MAAA;AAAA,MAEP,KAAK;AAAA,QACH,MAAM;AAAA,QACN,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,IAEF,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,KAAK;AAAA,IAAA;AAAA,EACP;AAEJ;AAaA,MAAM,QAAQ,MAAM;AAAA,EAClB,CAAC,EAAE,SAAS,MAAM,OAAO,OAAO,KAAK,WAAW,MAAM,GAAG,MAAA,GAAS,QAAQ;AACxE,UAAM,UAAU,MAAM,OACpB,OAAO,QAAQ,SAAS,QAAQ,QAAQ,MAAM,GAAG,GAAG,MAAM,GAAG,KAAK;AAOpE,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAM,QAAQ;AAAA,QACd,WAAW,GAAG,cAAc,EAAE,SAAS,IAAA,CAAK,GAAG,SAAS;AAAA,QACvD,GAAG;AAAA,QAEH,UAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AACF;AACA,MAAM,cAAc;AAIb,MAAM,YAAY;AAAA,EACvB,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,UAAU;AAAA,IACR,UAAU,EAAE,SAAS,mDAAA;AAAA,IACrB,MAAM,EAAE,SAAS,gBAAA;AAAA,IACjB,QAAQ,EAAE,SAAS,wBAAA;AAAA,IACnB,KAAK,EAAE,SAAS,qBAAA;AAAA,EAAqB;AAAA,EAEvC,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,WAAW,SAAS,UAAU,iBAAiB,UAAU;AAAA,EAClE,QAAQ;AAAA,IACN,IAAI,CAAC,WAAW,kBAAkB,mBAAmB,cAAc;AAAA,IACnE,IAAI,CAAC,iBAAiB,gBAAgB;AAAA,IACtC,MAAM,CAAA;AAAA,EAAC;AAAA,EAET,gBAAgB;AAClB;"}
@@ -1 +1 @@
1
- {"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/components/Button/button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAK9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,QAAA,MAAM,cAAc;;;;;8EAgKnB,CAAA;AAKD,UAAU,uBAAuB;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AACD,QAAA,MAAM,kBAAkB,wCAAmD,CAAA;AAI3E,MAAM,WAAW,WACf,SAAQ,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EACnD,IAAI,CAAC,YAAY,CAAC,OAAO,cAAc,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACjE;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;IAChF,mCAAmC;IACnC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,UAAU,GAAG,SAAS,CAAA;IACpC,sDAAsD;IACtD,SAAS,CAAC,EAAE,UAAU,CAAA;IACtB,oCAAoC;IACpC,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,6DAA6D;IAC7D,OAAO,CAAC,EAAE,UAAU,CAAA;IACpB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc;IACd,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAwBD,QAAA,MAAM,MAAM,uFAqKX,CAAA;AAGD;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwBb,CAAA;AAEV,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAA"}
1
+ {"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/components/Button/button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAK9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,QAAA,MAAM,cAAc;;;;;8EAgKnB,CAAA;AAKD,UAAU,uBAAuB;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AACD,QAAA,MAAM,kBAAkB,wCAAmD,CAAA;AAI3E,MAAM,WAAW,WACf,SAAQ,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EACnD,IAAI,CAAC,YAAY,CAAC,OAAO,cAAc,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACjE;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;IAChF,mCAAmC;IACnC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,UAAU,GAAG,SAAS,CAAA;IACpC,sDAAsD;IACtD,SAAS,CAAC,EAAE,UAAU,CAAA;IACtB,oCAAoC;IACpC,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,6DAA6D;IAC7D,OAAO,CAAC,EAAE,UAAU,CAAA;IACpB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc;IACd,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAwBD,QAAA,MAAM,MAAM,uFAsKX,CAAA;AAGD;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwBb,CAAA;AAEV,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAA"}
@@ -186,6 +186,8 @@ const Button = React.forwardRef(
186
186
  loading = false,
187
187
  fullWidth = false,
188
188
  pressed,
189
+ pressedTone,
190
+ // 2026-06-05 fix(deep-audit P0):原漏 destructure → 落入 ...props 噴到 DOM + 永遠用 cva default
189
191
  children,
190
192
  disabled,
191
193
  ...props
@@ -218,7 +220,7 @@ const Button = React.forwardRef(
218
220
  Comp,
219
221
  {
220
222
  className: cn(
221
- buttonVariants({ variant: resolvedVariant, danger: resolvedDanger, size: resolvedSize, className }),
223
+ buttonVariants({ variant: resolvedVariant, danger: resolvedDanger, size: resolvedSize, pressedTone, className }),
222
224
  // iconOnly 鐵律:padding-free + aspect-square + flex-center (Polaris idiom)
223
225
  // 0 magic-number 0 公式自動正方形。詳 ICON_ONLY_BASE rationale。
224
226
  resolvedIconOnly && ICON_ONLY_BASE,
@@ -1 +1 @@
1
- {"version":3,"file":"button.js","sources":["../../../src/components/Button/button.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\n// code-quality-allow: file-size — foundational composite(Button + iconOnly + danger + loading + asChild + dismiss + pressedTone)— 跨 7 axis variant 集中 SSOT 一處,拆分會 fragment cva variant catalog。當前 528 < cap 800。\nimport * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { CircularProgress } from '@/design-system/components/CircularProgress/circular-progress'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { useFieldContext } from '@/design-system/components/Field/field-context'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/design-system/components/Tooltip/tooltip'\n\n/**\n * Button — shadcn 風格,橋接設計系統 token\n *\n * ── Variants ──\n * primary 主要操作,藍底白字\n * secondary 次要品牌操作,藍框藍字;正面 vs 負面並存時用於正面那個\n * tertiary 一般輔助操作,灰框灰字,hover 轉藍(最常用的非主要按鈕)\n * text 無底色無邊框,hover 顯示灰底(工具列、密集 UI)\n * link 外觀像連結的按鈕(本質仍是 button)\n *\n * ── danger prop ──\n * danger 套用在任何 variant 上,將顏色改為危險色(紅色)\n *\n * <Button variant=\"primary\" danger>永久刪除</Button> → 紅底白字(立即不可逆)\n * <Button variant=\"secondary\" danger>移至垃圾桶</Button> → 紅框紅字(點下去還可反悔)\n *\n * ── pressed prop(toggle)──\n * pressed Toggle 按下狀態(持續 on/off),寫入 aria-pressed + data-state\n * 僅 secondary / tertiary / text 三個 variant 支援 toggle 視覺:\n * - secondary + pressed → primary-subtle 底、primary 字、透明邊框\n * - tertiary + pressed → primary-subtle 底、primary 字、透明邊框(同 secondary 按下視覺)\n * - text + pressed → neutral-selected 底,hover 反向變淺,:active 深一階\n * primary / link 傳入 pressed 無視覺效果(語意不符)\n *\n * ── Sizes(預設 md)──\n * xs h-field-xs(24px 固定),不隨 density 縮放\n * sm h-field-sm,md=28px / lg=32px\n * md h-field-md,md=32px / lg=36px ← 預設(跟 Field/Input 對齊)\n * lg h-field-lg,md=36px / lg=40px\n * icon-only 不是獨立尺寸 — 加 iconOnly prop 讓任何尺寸變正方形\n *\n * ── 內部結構 ──\n * [startIcon?] [label] [badge? + endIcon?]\n *\n * ── 用法範例 ──\n * <Button startIcon={Plus}>新增</Button>\n * <Button variant=\"tertiary\">取消</Button>\n * <Button variant=\"primary\" danger>永久刪除</Button>\n * <Button variant=\"text\" pressed={isPinned} startIcon={Pin} aria-label=\"釘選\" iconOnly />\n * <Button badge={<Badge count={3} />} endIcon={ChevronDown}>通知</Button>\n * <Button size=\"sm\" iconOnly startIcon={Plus} aria-label=\"新增\" />\n * <Button iconOnly startIcon={Bell} aria-label=\"通知 (3 則)\"\n * overlayBadge={<Badge count={3} />} /> ← badge 自動貼 icon 右上角\n *\n * ── asChild ──\n * <Button asChild><Link to=\"/home\">回首頁</Link></Button>\n */\nconst buttonVariants = cva(\n [\n 'inline-flex items-center justify-center',\n 'whitespace-nowrap font-medium',\n 'border border-transparent',\n 'transition-colors duration-150',\n 'cursor-pointer select-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n // 2026-05-12 Round 4.5 fix(codex M31 Layer C 抓):`aria-disabled` visual 分支補(per WAI-ARIA APG —\n // aria-disabled 給語意 + visual,但不 suppress functionality;functionality 由 consumer 阻 e.g.\n // RowDragHandle listeners 只在 canDrag spread)。**故意不加** `aria-disabled:pointer-events-none`\n // — Round 4 RowDragHandle Tooltip flicker fix root cause:aria-disabled buttons 必保 pointer events\n // 讓 Radix Tooltip pointerenter 通過。HTML `disabled` 保 PE:none(完全 inactive)。\n 'disabled:pointer-events-none',\n // 2026-05-12 fix v2(playwright pixel-quantified verify 抓 opacity=1 不生效):\n // `opacity-disabled` 是 custom Tailwind v4 `@utility`(opacity.css:21),variant prefix\n // `aria-disabled:` 跟 custom @utility 不 compose(`aria-disabled:cursor-not-allowed` 標準\n // utility 能 work,custom 不行)。改 arbitrary value `opacity-[var(--opacity-disabled)]`\n // 繞 custom @utility 限制(Tailwind v4 arbitrary value 直接生 `opacity: var(--opacity-disabled)`)。\n 'aria-disabled:opacity-[var(--opacity-disabled)]',\n 'rounded-md',\n // Defensive:SVG 不被 flex shrink 擠扁(防 inner-area 計算誤差導致 icon 被擠成\n // width<intrinsic 的 asymmetric 顯示)。詳 ICON_ONLY_PX 段 rationale。\n '[&_svg]:shrink-0',\n ],\n {\n variants: {\n variant: {\n primary: [\n 'bg-primary text-on-emphasis',\n 'hover:bg-primary-hover',\n 'active:bg-primary-active',\n 'disabled:bg-disabled disabled:text-fg-disabled disabled:border-transparent',\n ],\n secondary: [\n 'bg-surface text-primary border-primary',\n 'hover:text-primary-hover hover:border-primary-hover',\n 'active:text-primary-active active:border-primary-active',\n // Overlay trigger active(asChild Popover/DropdownMenu trigger)— 維持 hover 樣式\n // (canonical 對齊 inline-action.spec.md:trigger 維持 host hover 直到 overlay 關閉)\n 'data-[state=open]:text-primary-hover data-[state=open]:border-primary-hover',\n 'disabled:bg-transparent disabled:text-fg-disabled disabled:border-border',\n // 2026-05-21 v12:Toggle pressed 視覺移到 compoundVariants(variant × pressedTone),\n // 同時支援 emphasis(藍底)/ neutral(灰底)兩 tone。詳 cva.compoundVariants 段。\n ],\n tertiary: [\n 'bg-surface text-foreground border-border',\n 'hover:text-primary-hover hover:border-primary-hover',\n 'active:text-primary-active active:border-primary-active',\n // Overlay trigger active — 維持 hover 樣式(同 secondary 邏輯)\n 'data-[state=open]:text-primary-hover data-[state=open]:border-primary-hover',\n 'disabled:bg-transparent disabled:text-fg-disabled disabled:border-border',\n ],\n text: [\n 'bg-transparent text-foreground border-transparent',\n 'hover:bg-neutral-hover',\n 'active:bg-neutral-active',\n // Overlay trigger active — 維持 hover 樣式(canonical 2026-05-02 改:hover token,\n // 不另開 selected 4% — 對齊 shadcn/Radix/Material 狀態極簡派,跨 host 一致)\n 'data-[state=open]:bg-neutral-hover',\n 'disabled:bg-transparent disabled:text-fg-disabled',\n ],\n link: [\n 'bg-transparent text-primary border-transparent',\n 'hover:text-primary-hover',\n 'active:text-primary-active',\n 'disabled:text-fg-disabled',\n ],\n },\n danger: {\n true: '', // 實際樣式由 compoundVariants 提供\n },\n /**\n * 2026-05-21 v12 — pressed visual tone(per user「我認同這一個方向,然後預設emphasis」):\n * emphasis = 淡藍底(toolbar functional toggle / 篩選啟用 / 面板開關 — Figma toolbar /\n * Linear toolbar / Material ToggleButton 共識)\n * neutral = 灰底(sidebar/contextual nav row pressed — Linear / Notion / VS Code Activity Bar 共識)\n * 預設 `emphasis` per user directive。實際樣式由 compoundVariants(variant × pressedTone)套用。\n * 只在 secondary / tertiary / text variant 觸發 toggle 視覺;primary / link 無視覺效果。\n */\n pressedTone: {\n emphasis: '',\n neutral: '',\n },\n size: {\n xs: 'h-field-xs px-2 text-caption leading-compact gap-0',\n sm: 'h-field-sm px-3 min-w-14 text-body leading-compact gap-1',\n md: 'h-field-md px-3 min-w-16 text-body leading-compact gap-1',\n lg: 'h-field-lg px-3 min-w-20 text-body-lg leading-compact gap-1',\n },\n },\n compoundVariants: [\n // primary + danger → 紅底白字(立即不可逆操作)\n {\n variant: 'primary',\n danger: true,\n class: [\n 'bg-error text-on-emphasis border-transparent',\n 'hover:bg-error-hover',\n 'active:bg-error-active',\n ],\n },\n // secondary + danger → 紅框紅字(有確認步驟的危險操作)\n {\n variant: 'secondary',\n danger: true,\n class: [\n 'bg-surface text-error border-error',\n 'hover:text-error-hover hover:border-error-hover',\n 'active:text-error-active active:border-error-active',\n ],\n },\n // text + danger → 紅字,hover 灰底\n {\n variant: 'text',\n danger: true,\n class: [\n 'text-error',\n 'hover:bg-neutral-hover hover:text-error-hover',\n 'active:bg-neutral-active active:text-error-active',\n ],\n },\n // ── 2026-05-21 v12 Toggle pressed(variant × pressedTone)──────────────────\n // 視覺由 data-[state=on] + aria-pressed(Radix overlay trigger fallback)觸發\n //\n // emphasis tone:藍底(primary-subtle / primary 字)— functional toggle\n {\n variant: ['secondary', 'tertiary', 'text'],\n pressedTone: 'emphasis',\n class: [\n 'data-[state=on]:bg-primary-subtle data-[state=on]:text-primary data-[state=on]:border-transparent',\n 'data-[state=on]:hover:text-primary-hover',\n 'data-[state=on]:active:text-primary-active',\n 'data-[state=on]:disabled:bg-disabled data-[state=on]:disabled:text-fg-disabled data-[state=on]:disabled:border-transparent',\n // aria-pressed fallback(Radix overlay trigger override data-state 時仍生效)\n 'aria-pressed:bg-primary-subtle aria-pressed:text-primary aria-pressed:border-transparent',\n 'aria-pressed:hover:text-primary-hover',\n ],\n },\n // neutral tone:灰底(neutral-selected family)— sidebar/contextual nav pressed\n {\n variant: ['secondary', 'tertiary', 'text'],\n pressedTone: 'neutral',\n class: [\n 'data-[state=on]:bg-neutral-selected data-[state=on]:text-foreground data-[state=on]:border-transparent',\n 'data-[state=on]:hover:bg-neutral-selected-hover',\n 'data-[state=on]:active:bg-neutral-selected-active',\n 'data-[state=on]:disabled:bg-transparent data-[state=on]:disabled:text-fg-disabled',\n // aria-pressed fallback\n 'aria-pressed:bg-neutral-selected aria-pressed:text-foreground aria-pressed:border-transparent',\n 'aria-pressed:hover:bg-neutral-selected-hover',\n ],\n },\n ],\n defaultVariants: {\n variant: 'primary',\n size: 'md',\n pressedTone: 'emphasis',\n },\n }\n)\n\n// ── ButtonGroup Context ──────────────────────────────────────────────────────\n// ButtonGroup provides this context; Button reads it for fullWidth injection.\n// Context lives here (not in button-group.tsx) so there is no circular import.\ninterface ButtonGroupContextValue {\n fullWidth?: boolean\n}\nconst ButtonGroupContext = React.createContext<ButtonGroupContextValue>({})\n\ntype InternalVariant = VariantProps<typeof buttonVariants>['variant']\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n Omit<VariantProps<typeof buttonVariants>, 'variant' | 'danger'> {\n /**\n * 將樣式套用至子元件(e.g. React Router Link)。\n *\n * ⚠️ **icon-only + asChild 警告**:當 `iconOnly={true}` 且 `asChild={true}` 時,內建\n * Tooltip wrapper 不啟動(Radix Slot 不接受多 child)。consumer 必須**自管 child 的 `aria-label`**\n * 給 screen reader,並視需要自行包 `<Tooltip>`(對齊 Polaris / Radix asChild idiom)。\n * Button 不會主動補 tooltip — 否則會破壞 Slot 單 child 規則。\n */\n asChild?: boolean\n /**\n * 按鈕視覺強調等級。\n * `destructive` / `ghost` 為 shadcn 內部 compat,請勿在應用程式碼中直接使用。\n */\n variant?: 'primary' | 'secondary' | 'tertiary' | 'text' | 'link' | (string & {})\n /** 套用危險色(紅色)。可與任何 variant 組合使用。 */\n danger?: boolean\n /**\n * Toggle 按下狀態(持續 on/off)。設定時 Button 變為 toggle:\n * - 自動寫入 `aria-pressed` + `data-state=\"on\" | \"off\"`\n * - 樣式由 variant × `pressedTone` 的 compoundVariants 套用\n * - 僅 secondary / tertiary / text 有 toggle 視覺;primary / link 傳入無效果\n *\n * 不傳此 prop 時 Button 就是一般按鈕,不帶 aria-pressed。\n */\n pressed?: boolean\n /**\n * Pressed 視覺色調(2026-05-21 v12 加):\n * - `'emphasis'`(預設)→ 淡藍底 / primary 字(對齊 Figma toolbar / Linear toolbar /\n * Material ToggleButton 共識 — toolbar functional toggle / 篩選啟用 / 面板開關)\n * - `'neutral'` → 灰底 / foreground 字(對齊 Linear / Notion / VS Code Activity Bar\n * 共識 — sidebar/contextual nav row pressed)\n *\n * 僅在 `pressed` 啟用且 variant ∈ {secondary, tertiary, text} 時生效。\n * 跨 toggle context 維持單一 prop API,consumer 視語意選 tone 不另開 variant。\n */\n pressedTone?: 'emphasis' | 'neutral'\n /** 左側 icon(LucideIcon),最多一個,loading 時自動替換為 spinner */\n startIcon?: LucideIcon\n /** 右側 badge(ReactNode),通常傳入計數指示器 */\n badge?: React.ReactNode\n /**\n * Overlay badge(iconOnly 專用)。接收 `<Badge>` 元素,Button 內部**自動定位在 startIcon 右上角**——\n * badge 中心對齊 icon 的 top-right corner(Material BadgedBox / iOS App icon canonical),不是按鈕邊緣。\n * 解決手刻 `relative + absolute -top-1 -right-1` 讓 badge 飄在按鈕 chrome 右上的問題。\n *\n * 世界級對照:Material BadgedBox、iOS App Icon、Ant Badge wrap icon,badge 相對於**視覺重心**(icon)。\n * 只在 `iconOnly=true` 時生效;非 iconOnly 時應該用 inline `badge` prop 放 suffix 位置。\n */\n overlayBadge?: React.ReactNode\n /** 右側 icon(LucideIcon),放在 badge 右邊,通常用於 ChevronDown 等方向指示 */\n endIcon?: LucideIcon\n /** Icon-only 模式:移除 padding,變為正方形(必須同時設定 aria-label) */\n iconOnly?: boolean\n /**\n * Dismiss 視覺類(X close only canonical)。專用於 **X(close)icon 的 dismiss 語意** —\n * 「關閉 surface / 忽略訊息」。**不適用 Trash / Delete / Clear / Remove 等 destructive / clear 操作**。\n *\n * 自動套用:\n * - `variant=\"text\"`(強制 override 其他 variant)\n * - `iconOnly=true`(強制)\n * - Icon 色 override:`fg-muted` → hover `foreground`(跟 Inline Action dismiss 視覺一致)\n *\n * 典型 case:Dialog / Sheet / Popover / Alert / Toast / Coachmark 的 **chrome corner close X**\n * (action group region — corner 可多 action,close 左側加 Separator + refresh / share 等)。\n *\n * 非 dismiss(**不套此 prop**):\n * - Trash / Delete → destructive action,Button 用一般 variant 或 Inline Action(按 row size 判)\n * - Clear → 欄位清空,用 Inline Action\n * - Remove → collection 移除,用一般 Button / Inline Action\n *\n * 詳見 button.spec.md「Dismiss 視覺類」段 + patterns/element-anatomy/item-anatomy.spec.md\n * 「Dismiss canonical — X close only」段。\n */\n dismiss?: boolean\n /** 載入中狀態:startIcon 替換為 spinner,自動 disabled;badge / endIcon 維持顯示以避免 layout shift */\n loading?: boolean\n /** 撐滿父容器寬度 */\n fullWidth?: boolean\n}\n\n/**\n * Icon-only padding — calc `(field-height - icon-size) / 2` per size。\n *\n * 設計:startIcon 到左邊距離 = padding = `(height - icon) / 2`。\n * 純 icon-only 時 width = 2*padding + icon = height → **自然正方形**,不需要 aspect-square。\n * 有 suffix(badge / endIcon)時 width = 2*pad + icon + gap + suffix > height → **自然長方形**。\n * StartIcon 到左邊距離始終不變,形狀自動適應內容。\n *\n * 用 CSS var 讓 density 切換時 padding 自動跟著算(field-height 會變)。\n */\n// IconOnly 用 padding-free + aspect-square + flex-center 的 Polaris/Atlassian idiom\n// (M17 SSOT 必可傳播 — 取代 4 個 size 的 magic-number 公式):\n// - aspect-square 鎖 width=height(來自 h-field-X)\n// - p-0 移除 px-3 (label 模式) override\n// - flex justify-center items-center(base 已有)→ SVG 自動視覺置中\n// 結果:0 magic number,0 公式,0 border-deduction,任何 size / icon size 都自然正方形。\n// World-class 對照:Polaris + Atlassian iconOnly 走 padding-free 派(Material/Ant 走\n// padding-based)。我們選 padding-free 因 SSOT 性更強(SegmentedControl / Tag dismiss\n// 等 host 全可共用同 utility class,無需各自抄公式)。詳 button.spec.md「iconOnly 鐵律」。\nconst ICON_ONLY_BASE = 'aspect-square p-0 min-w-0 gap-0'\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n className,\n variant: variantProp,\n danger: dangerProp,\n size,\n asChild = false,\n startIcon: StartIcon,\n badge,\n overlayBadge,\n endIcon: EndIcon,\n iconOnly = false,\n dismiss = false,\n loading = false,\n fullWidth = false,\n pressed,\n children,\n disabled,\n ...props\n },\n ref\n ) => {\n // ── FieldContext:在 Field 內時自動讀 size,讓 Button 跟 Input 同高 ──\n const fieldCtx = useFieldContext?.()\n const resolvedSize = size ?? (fieldCtx?.size as typeof size) ?? 'md'\n\n // ── Dismiss 視覺類 override(2026-04-22 cross-implementation dimming canonical) ──\n // dismiss=true 強制:variant=\"text\" + iconOnly=true + icon 色弱化(fg-muted → hover foreground)\n // 跟 Inline Action dismiss 視覺一致。詳見 button.spec.md「Dismiss 視覺類」。\n const resolvedIconOnly = iconOnly || dismiss\n\n // ── Dev-mode warning:overlayBadge 只適用 iconOnly ──\n // 有 label 的 Button 傳入 overlayBadge 會被忽略(只 render icon / 不渲染 overlay slot),\n // 靜默忽略會讓 consumer 誤以為「傳了但位置錯」。Dev mode 印 warning 引導改用 `badge` prop\n // (inline 位置,跟 label 並列)或改 `iconOnly`。Spec SSOT:badge.spec.md「Overlay 適用元件」。\n if (process.env.NODE_ENV !== 'production' && overlayBadge && !iconOnly) {\n // eslint-disable-next-line no-console\n console.warn(\n '[DS Button] `overlayBadge` 只適用於 `iconOnly` Button。有 label 的 Button 請改用 `badge` prop(inline 位置,跟 label 並列),或移除 label 改為 iconOnly。SSOT:badge.spec.md「Overlay 適用元件 canonical」節。'\n )\n }\n\n // 2026-05-23 deep-audit Phase A.4 Decision 1(user verbatim「決策ㄧ照你建議做」):dev-warn `iconOnly + (endIcon|badge)` 並用。\n // Spec SSOT:button.spec.md「iconOnly 嚴格定義為『只有一個 icon,正方形』,不可與 endIcon 或 badge 並用」。\n // 例外:overlayBadge 跟 iconOnly 並用 canonical(L372 反向 warn 已 cover)。\n // 不擋 image canonical「icon + 下拉指示 = 不加 iconOnly + startIcon + endIcon + aria-label」(那 case iconOnly=false 不 trigger)。\n if (process.env.NODE_ENV !== 'production' && iconOnly && (EndIcon !== undefined || badge != null)) {\n // eslint-disable-next-line no-console\n console.warn(\n '[DS Button] `iconOnly` 嚴格定義為「只有一個 icon,正方形」,不可與 `endIcon` 或 `badge` 並用。若需 icon + 下拉指示 → 不加 iconOnly + startIcon + endIcon + aria-label;若需 icon + 角標 → iconOnly + overlayBadge。SSOT:button.spec.md `iconOnly 的邊界` 節 + `badge.spec.md` Overlay 適用元件 canonical。'\n )\n }\n\n // shadcn compat:AlertDialog、Toast 等元件內部會傳入這些 alias,\n // 在此靜默轉換,不暴露到型別或自動完成。\n // dismiss=true 強制 variant=text(dismiss canonical);override 其他 variant 傳入。\n const resolvedVariant: InternalVariant =\n dismiss ? 'text' :\n (variantProp as string) === 'destructive' ? 'primary' :\n (variantProp as string) === 'ghost' ? 'text' :\n (variantProp as InternalVariant) ?? 'primary'\n\n const resolvedDanger = dangerProp || (variantProp as string) === 'destructive'\n\n // ButtonGroup context:vertical group 自動注入 fullWidth\n const groupCtx = React.useContext(ButtonGroupContext)\n const resolvedFullWidth = fullWidth || !!groupCtx.fullWidth\n\n const Comp = asChild ? Slot : 'button'\n const iconSize = resolvedSize === 'lg' ? 20 : 16\n\n // loading 行為:spinner 永遠在 prefix 位置\n // 有 prefix icon → icon 換成 spinner(同位置,零 layout shift)\n // 無 prefix icon → spinner 加在文字左邊(按鈕略微變寬,可接受)\n const hasSuffix = badge != null || EndIcon !== undefined\n\n // icon-only 自動 tooltip:從 props 提取 aria-label,同時保留在 DOM\n const { 'aria-label': ariaLabel, ...restProps } = props\n\n // Toggle 狀態:pressed 定義時自動寫入 aria-pressed + data-state。\n // 未定義時不寫入任何 toggle 屬性(按鈕為一般 action button)。\n // 樣式由 cva 的 data-[state=on] 分支套用——secondary/tertiary 走 primary-subtle,\n // text 走 neutral-selected family;primary/link 不定義 on 分支,傳入無效果。\n const toggleAttrs =\n pressed === undefined\n ? {}\n : { 'aria-pressed': pressed, 'data-state': pressed ? 'on' : 'off' }\n\n // Chrome-unbounded marker(2026-04-22 v5 canonical):button 若無視覺邊界(text variant 或 dismiss),\n // 標記 data-unbounded=\"true\"。SurfaceHeader 透過 [&_[data-unbounded]]:my-[...] 套負 margin\n // 讓 layout 佔位縮到 24(chrome-header-height 幾何)— button native size 與 touch target 不變。\n // 詳 overlay-surface.spec.md「Chrome dismiss size canonical」\n const unboundedAttr =\n resolvedVariant === 'text' || dismiss ? { 'data-unbounded': 'true' } : {}\n\n const buttonEl = (\n <Comp\n className={cn(\n buttonVariants({ variant: resolvedVariant, danger: resolvedDanger, size: resolvedSize, className }),\n // iconOnly 鐵律:padding-free + aspect-square + flex-center (Polaris idiom)\n // 0 magic-number 0 公式自動正方形。詳 ICON_ONLY_BASE rationale。\n resolvedIconOnly && ICON_ONLY_BASE,\n // Dismiss 視覺弱化:override Button text variant 預設 foreground 為 fg-muted → hover foreground\n // 跟 Inline Action dismiss 視覺一致(cross-implementation dimming canonical)\n dismiss && 'text-fg-muted hover:text-foreground',\n resolvedFullWidth && 'w-full',\n )}\n ref={ref}\n type=\"button\"\n disabled={disabled || loading}\n aria-busy={loading || undefined}\n aria-label={ariaLabel}\n {...toggleAttrs}\n {...unboundedAttr}\n {...restProps}\n >\n {loading ? (\n <CircularProgress size={iconSize} className=\"text-current\" />\n ) : StartIcon ? (\n resolvedIconOnly && overlayBadge ? (\n // Overlay badge canonical:wrapper 貼 icon 尺寸,badge 中心對齊 icon top-right corner\n // (Material BadgedBox / iOS App icon),不是 button chrome 角。\n //\n // CSS 細節(2026-04-20 bug fix):用 `inline-block` + 明確 width/height + `leading-none`\n // 避免 `inline-flex` span 在 Button flex container 內被撐高/撐寬(inline-flex 的 span\n // 在某些瀏覽器下會把絕對定位子元素的 translate 計算基準搞錯,造成 badge 噴飛、Button\n // aspect-square 失效)。明確給 span width/height = iconSize 鎖住 positioning context。\n <span\n className=\"relative inline-block leading-none shrink-0 pointer-events-none\"\n style={{ width: iconSize, height: iconSize }}\n >\n <StartIcon size={iconSize} aria-hidden />\n <span className=\"absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 pointer-events-auto\">\n {overlayBadge}\n </span>\n </span>\n ) : (\n <StartIcon size={iconSize} aria-hidden />\n )\n ) : null}\n {children != null && <span className=\"px-1\">{children}</span>}\n {hasSuffix && (\n <span className=\"inline-flex items-center gap-1\">\n {badge}\n {EndIcon && <EndIcon size={iconSize} aria-hidden />}\n </span>\n )}\n </Comp>\n )\n\n // icon-only + aria-label → 自動包 Tooltip(tooltip 是元件保證的行為)\n // 不建立獨立 TooltipProvider——依賴全域 Provider,\n // 這樣所有 tooltip 共享同一組 delay 參數和 warm-up 機制\n if (resolvedIconOnly && typeof ariaLabel === 'string' && !asChild) {\n return (\n <Tooltip>\n <TooltipTrigger asChild>{buttonEl}</TooltipTrigger>\n <TooltipContent>{ariaLabel}</TooltipContent>\n </Tooltip>\n )\n }\n\n return buttonEl\n }\n)\nButton.displayName = 'Button'\n\n/**\n * componentMeta — Story Auto-Compile 系統消費的結構化 canonical\n * (見 .claude/planning/story-auto-compile.md Phase 1)\n *\n * compile-stories.mjs 讀本 export + spec.md frontmatter 產出\n * anatomy.stories.tsx 的 variant/size/state/token 矩陣 canonical section。\n *\n * Keys 必跟 buttonVariants cva + spec frontmatter 對齊(compile-time 驗證)。\n */\nexport const buttonMeta = {\n component: 'Button',\n family: 3, // Pill Layout\n variants: {\n primary: { purpose: '主要 action / CTA' },\n secondary: { purpose: '次要 action(陪襯 primary)' },\n tertiary: { purpose: '第三級 action(tool-like)' },\n text: { purpose: '文字樣式 action(low emphasis / toolbar)' },\n link: { purpose: '內文連結(inline reading)' },\n },\n sizes: {\n xs: { fieldHeight: 24, iconSize: 16, typography: 'body' },\n sm: { fieldHeight: 28, iconSize: 16, typography: 'body' },\n md: { fieldHeight: 32, iconSize: 16, typography: 'body' },\n lg: { fieldHeight: 40, iconSize: 20, typography: 'body-lg' },\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['--primary', '--primary-hover', '--primary-active', '--bg-disabled'],\n fg: ['--on-emphasis', '--fg-disabled'],\n ring: ['--ring'],\n },\n defaultVariant: 'primary',\n defaultSize: 'md',\n} as const\n\nexport { Button, buttonVariants, ButtonGroupContext }\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"button.js","sources":["../../../src/components/Button/button.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\n// code-quality-allow: file-size — foundational composite(Button + iconOnly + danger + loading + asChild + dismiss + pressedTone)— 跨 7 axis variant 集中 SSOT 一處,拆分會 fragment cva variant catalog。當前 528 < cap 800。\nimport * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { CircularProgress } from '@/design-system/components/CircularProgress/circular-progress'\nimport type { LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { useFieldContext } from '@/design-system/components/Field/field-context'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/design-system/components/Tooltip/tooltip'\n\n/**\n * Button — shadcn 風格,橋接設計系統 token\n *\n * ── Variants ──\n * primary 主要操作,藍底白字\n * secondary 次要品牌操作,藍框藍字;正面 vs 負面並存時用於正面那個\n * tertiary 一般輔助操作,灰框灰字,hover 轉藍(最常用的非主要按鈕)\n * text 無底色無邊框,hover 顯示灰底(工具列、密集 UI)\n * link 外觀像連結的按鈕(本質仍是 button)\n *\n * ── danger prop ──\n * danger 套用在任何 variant 上,將顏色改為危險色(紅色)\n *\n * <Button variant=\"primary\" danger>永久刪除</Button> → 紅底白字(立即不可逆)\n * <Button variant=\"secondary\" danger>移至垃圾桶</Button> → 紅框紅字(點下去還可反悔)\n *\n * ── pressed prop(toggle)──\n * pressed Toggle 按下狀態(持續 on/off),寫入 aria-pressed + data-state\n * 僅 secondary / tertiary / text 三個 variant 支援 toggle 視覺:\n * - secondary + pressed → primary-subtle 底、primary 字、透明邊框\n * - tertiary + pressed → primary-subtle 底、primary 字、透明邊框(同 secondary 按下視覺)\n * - text + pressed → neutral-selected 底,hover 反向變淺,:active 深一階\n * primary / link 傳入 pressed 無視覺效果(語意不符)\n *\n * ── Sizes(預設 md)──\n * xs h-field-xs(24px 固定),不隨 density 縮放\n * sm h-field-sm,md=28px / lg=32px\n * md h-field-md,md=32px / lg=36px ← 預設(跟 Field/Input 對齊)\n * lg h-field-lg,md=36px / lg=40px\n * icon-only 不是獨立尺寸 — 加 iconOnly prop 讓任何尺寸變正方形\n *\n * ── 內部結構 ──\n * [startIcon?] [label] [badge? + endIcon?]\n *\n * ── 用法範例 ──\n * <Button startIcon={Plus}>新增</Button>\n * <Button variant=\"tertiary\">取消</Button>\n * <Button variant=\"primary\" danger>永久刪除</Button>\n * <Button variant=\"text\" pressed={isPinned} startIcon={Pin} aria-label=\"釘選\" iconOnly />\n * <Button badge={<Badge count={3} />} endIcon={ChevronDown}>通知</Button>\n * <Button size=\"sm\" iconOnly startIcon={Plus} aria-label=\"新增\" />\n * <Button iconOnly startIcon={Bell} aria-label=\"通知 (3 則)\"\n * overlayBadge={<Badge count={3} />} /> ← badge 自動貼 icon 右上角\n *\n * ── asChild ──\n * <Button asChild><Link to=\"/home\">回首頁</Link></Button>\n */\nconst buttonVariants = cva(\n [\n 'inline-flex items-center justify-center',\n 'whitespace-nowrap font-medium',\n 'border border-transparent',\n 'transition-colors duration-150',\n 'cursor-pointer select-none disabled:cursor-not-allowed aria-disabled:cursor-not-allowed',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n // 2026-05-12 Round 4.5 fix(codex M31 Layer C 抓):`aria-disabled` visual 分支補(per WAI-ARIA APG —\n // aria-disabled 給語意 + visual,但不 suppress functionality;functionality 由 consumer 阻 e.g.\n // RowDragHandle listeners 只在 canDrag spread)。**故意不加** `aria-disabled:pointer-events-none`\n // — Round 4 RowDragHandle Tooltip flicker fix root cause:aria-disabled buttons 必保 pointer events\n // 讓 Radix Tooltip pointerenter 通過。HTML `disabled` 保 PE:none(完全 inactive)。\n 'disabled:pointer-events-none',\n // 2026-05-12 fix v2(playwright pixel-quantified verify 抓 opacity=1 不生效):\n // `opacity-disabled` 是 custom Tailwind v4 `@utility`(opacity.css:21),variant prefix\n // `aria-disabled:` 跟 custom @utility 不 compose(`aria-disabled:cursor-not-allowed` 標準\n // utility 能 work,custom 不行)。改 arbitrary value `opacity-[var(--opacity-disabled)]`\n // 繞 custom @utility 限制(Tailwind v4 arbitrary value 直接生 `opacity: var(--opacity-disabled)`)。\n 'aria-disabled:opacity-[var(--opacity-disabled)]',\n 'rounded-md',\n // Defensive:SVG 不被 flex shrink 擠扁(防 inner-area 計算誤差導致 icon 被擠成\n // width<intrinsic 的 asymmetric 顯示)。詳 ICON_ONLY_PX 段 rationale。\n '[&_svg]:shrink-0',\n ],\n {\n variants: {\n variant: {\n primary: [\n 'bg-primary text-on-emphasis',\n 'hover:bg-primary-hover',\n 'active:bg-primary-active',\n 'disabled:bg-disabled disabled:text-fg-disabled disabled:border-transparent',\n ],\n secondary: [\n 'bg-surface text-primary border-primary',\n 'hover:text-primary-hover hover:border-primary-hover',\n 'active:text-primary-active active:border-primary-active',\n // Overlay trigger active(asChild Popover/DropdownMenu trigger)— 維持 hover 樣式\n // (canonical 對齊 inline-action.spec.md:trigger 維持 host hover 直到 overlay 關閉)\n 'data-[state=open]:text-primary-hover data-[state=open]:border-primary-hover',\n 'disabled:bg-transparent disabled:text-fg-disabled disabled:border-border',\n // 2026-05-21 v12:Toggle pressed 視覺移到 compoundVariants(variant × pressedTone),\n // 同時支援 emphasis(藍底)/ neutral(灰底)兩 tone。詳 cva.compoundVariants 段。\n ],\n tertiary: [\n 'bg-surface text-foreground border-border',\n 'hover:text-primary-hover hover:border-primary-hover',\n 'active:text-primary-active active:border-primary-active',\n // Overlay trigger active — 維持 hover 樣式(同 secondary 邏輯)\n 'data-[state=open]:text-primary-hover data-[state=open]:border-primary-hover',\n 'disabled:bg-transparent disabled:text-fg-disabled disabled:border-border',\n ],\n text: [\n 'bg-transparent text-foreground border-transparent',\n 'hover:bg-neutral-hover',\n 'active:bg-neutral-active',\n // Overlay trigger active — 維持 hover 樣式(canonical 2026-05-02 改:hover token,\n // 不另開 selected 4% — 對齊 shadcn/Radix/Material 狀態極簡派,跨 host 一致)\n 'data-[state=open]:bg-neutral-hover',\n 'disabled:bg-transparent disabled:text-fg-disabled',\n ],\n link: [\n 'bg-transparent text-primary border-transparent',\n 'hover:text-primary-hover',\n 'active:text-primary-active',\n 'disabled:text-fg-disabled',\n ],\n },\n danger: {\n true: '', // 實際樣式由 compoundVariants 提供\n },\n /**\n * 2026-05-21 v12 — pressed visual tone(per user「我認同這一個方向,然後預設emphasis」):\n * emphasis = 淡藍底(toolbar functional toggle / 篩選啟用 / 面板開關 — Figma toolbar /\n * Linear toolbar / Material ToggleButton 共識)\n * neutral = 灰底(sidebar/contextual nav row pressed — Linear / Notion / VS Code Activity Bar 共識)\n * 預設 `emphasis` per user directive。實際樣式由 compoundVariants(variant × pressedTone)套用。\n * 只在 secondary / tertiary / text variant 觸發 toggle 視覺;primary / link 無視覺效果。\n */\n pressedTone: {\n emphasis: '',\n neutral: '',\n },\n size: {\n xs: 'h-field-xs px-2 text-caption leading-compact gap-0',\n sm: 'h-field-sm px-3 min-w-14 text-body leading-compact gap-1',\n md: 'h-field-md px-3 min-w-16 text-body leading-compact gap-1',\n lg: 'h-field-lg px-3 min-w-20 text-body-lg leading-compact gap-1',\n },\n },\n compoundVariants: [\n // primary + danger → 紅底白字(立即不可逆操作)\n {\n variant: 'primary',\n danger: true,\n class: [\n 'bg-error text-on-emphasis border-transparent',\n 'hover:bg-error-hover',\n 'active:bg-error-active',\n ],\n },\n // secondary + danger → 紅框紅字(有確認步驟的危險操作)\n {\n variant: 'secondary',\n danger: true,\n class: [\n 'bg-surface text-error border-error',\n 'hover:text-error-hover hover:border-error-hover',\n 'active:text-error-active active:border-error-active',\n ],\n },\n // text + danger → 紅字,hover 灰底\n {\n variant: 'text',\n danger: true,\n class: [\n 'text-error',\n 'hover:bg-neutral-hover hover:text-error-hover',\n 'active:bg-neutral-active active:text-error-active',\n ],\n },\n // ── 2026-05-21 v12 Toggle pressed(variant × pressedTone)──────────────────\n // 視覺由 data-[state=on] + aria-pressed(Radix overlay trigger fallback)觸發\n //\n // emphasis tone:藍底(primary-subtle / primary 字)— functional toggle\n {\n variant: ['secondary', 'tertiary', 'text'],\n pressedTone: 'emphasis',\n class: [\n 'data-[state=on]:bg-primary-subtle data-[state=on]:text-primary data-[state=on]:border-transparent',\n 'data-[state=on]:hover:text-primary-hover',\n 'data-[state=on]:active:text-primary-active',\n 'data-[state=on]:disabled:bg-disabled data-[state=on]:disabled:text-fg-disabled data-[state=on]:disabled:border-transparent',\n // aria-pressed fallback(Radix overlay trigger override data-state 時仍生效)\n 'aria-pressed:bg-primary-subtle aria-pressed:text-primary aria-pressed:border-transparent',\n 'aria-pressed:hover:text-primary-hover',\n ],\n },\n // neutral tone:灰底(neutral-selected family)— sidebar/contextual nav pressed\n {\n variant: ['secondary', 'tertiary', 'text'],\n pressedTone: 'neutral',\n class: [\n 'data-[state=on]:bg-neutral-selected data-[state=on]:text-foreground data-[state=on]:border-transparent',\n 'data-[state=on]:hover:bg-neutral-selected-hover',\n 'data-[state=on]:active:bg-neutral-selected-active',\n 'data-[state=on]:disabled:bg-transparent data-[state=on]:disabled:text-fg-disabled',\n // aria-pressed fallback\n 'aria-pressed:bg-neutral-selected aria-pressed:text-foreground aria-pressed:border-transparent',\n 'aria-pressed:hover:bg-neutral-selected-hover',\n ],\n },\n ],\n defaultVariants: {\n variant: 'primary',\n size: 'md',\n pressedTone: 'emphasis',\n },\n }\n)\n\n// ── ButtonGroup Context ──────────────────────────────────────────────────────\n// ButtonGroup provides this context; Button reads it for fullWidth injection.\n// Context lives here (not in button-group.tsx) so there is no circular import.\ninterface ButtonGroupContextValue {\n fullWidth?: boolean\n}\nconst ButtonGroupContext = React.createContext<ButtonGroupContextValue>({})\n\ntype InternalVariant = VariantProps<typeof buttonVariants>['variant']\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n Omit<VariantProps<typeof buttonVariants>, 'variant' | 'danger'> {\n /**\n * 將樣式套用至子元件(e.g. React Router Link)。\n *\n * ⚠️ **icon-only + asChild 警告**:當 `iconOnly={true}` 且 `asChild={true}` 時,內建\n * Tooltip wrapper 不啟動(Radix Slot 不接受多 child)。consumer 必須**自管 child 的 `aria-label`**\n * 給 screen reader,並視需要自行包 `<Tooltip>`(對齊 Polaris / Radix asChild idiom)。\n * Button 不會主動補 tooltip — 否則會破壞 Slot 單 child 規則。\n */\n asChild?: boolean\n /**\n * 按鈕視覺強調等級。\n * `destructive` / `ghost` 為 shadcn 內部 compat,請勿在應用程式碼中直接使用。\n */\n variant?: 'primary' | 'secondary' | 'tertiary' | 'text' | 'link' | (string & {})\n /** 套用危險色(紅色)。可與任何 variant 組合使用。 */\n danger?: boolean\n /**\n * Toggle 按下狀態(持續 on/off)。設定時 Button 變為 toggle:\n * - 自動寫入 `aria-pressed` + `data-state=\"on\" | \"off\"`\n * - 樣式由 variant × `pressedTone` 的 compoundVariants 套用\n * - 僅 secondary / tertiary / text 有 toggle 視覺;primary / link 傳入無效果\n *\n * 不傳此 prop 時 Button 就是一般按鈕,不帶 aria-pressed。\n */\n pressed?: boolean\n /**\n * Pressed 視覺色調(2026-05-21 v12 加):\n * - `'emphasis'`(預設)→ 淡藍底 / primary 字(對齊 Figma toolbar / Linear toolbar /\n * Material ToggleButton 共識 — toolbar functional toggle / 篩選啟用 / 面板開關)\n * - `'neutral'` → 灰底 / foreground 字(對齊 Linear / Notion / VS Code Activity Bar\n * 共識 — sidebar/contextual nav row pressed)\n *\n * 僅在 `pressed` 啟用且 variant ∈ {secondary, tertiary, text} 時生效。\n * 跨 toggle context 維持單一 prop API,consumer 視語意選 tone 不另開 variant。\n */\n pressedTone?: 'emphasis' | 'neutral'\n /** 左側 icon(LucideIcon),最多一個,loading 時自動替換為 spinner */\n startIcon?: LucideIcon\n /** 右側 badge(ReactNode),通常傳入計數指示器 */\n badge?: React.ReactNode\n /**\n * Overlay badge(iconOnly 專用)。接收 `<Badge>` 元素,Button 內部**自動定位在 startIcon 右上角**——\n * badge 中心對齊 icon 的 top-right corner(Material BadgedBox / iOS App icon canonical),不是按鈕邊緣。\n * 解決手刻 `relative + absolute -top-1 -right-1` 讓 badge 飄在按鈕 chrome 右上的問題。\n *\n * 世界級對照:Material BadgedBox、iOS App Icon、Ant Badge wrap icon,badge 相對於**視覺重心**(icon)。\n * 只在 `iconOnly=true` 時生效;非 iconOnly 時應該用 inline `badge` prop 放 suffix 位置。\n */\n overlayBadge?: React.ReactNode\n /** 右側 icon(LucideIcon),放在 badge 右邊,通常用於 ChevronDown 等方向指示 */\n endIcon?: LucideIcon\n /** Icon-only 模式:移除 padding,變為正方形(必須同時設定 aria-label) */\n iconOnly?: boolean\n /**\n * Dismiss 視覺類(X close only canonical)。專用於 **X(close)icon 的 dismiss 語意** —\n * 「關閉 surface / 忽略訊息」。**不適用 Trash / Delete / Clear / Remove 等 destructive / clear 操作**。\n *\n * 自動套用:\n * - `variant=\"text\"`(強制 override 其他 variant)\n * - `iconOnly=true`(強制)\n * - Icon 色 override:`fg-muted` → hover `foreground`(跟 Inline Action dismiss 視覺一致)\n *\n * 典型 case:Dialog / Sheet / Popover / Alert / Toast / Coachmark 的 **chrome corner close X**\n * (action group region — corner 可多 action,close 左側加 Separator + refresh / share 等)。\n *\n * 非 dismiss(**不套此 prop**):\n * - Trash / Delete → destructive action,Button 用一般 variant 或 Inline Action(按 row size 判)\n * - Clear → 欄位清空,用 Inline Action\n * - Remove → collection 移除,用一般 Button / Inline Action\n *\n * 詳見 button.spec.md「Dismiss 視覺類」段 + patterns/element-anatomy/item-anatomy.spec.md\n * 「Dismiss canonical — X close only」段。\n */\n dismiss?: boolean\n /** 載入中狀態:startIcon 替換為 spinner,自動 disabled;badge / endIcon 維持顯示以避免 layout shift */\n loading?: boolean\n /** 撐滿父容器寬度 */\n fullWidth?: boolean\n}\n\n/**\n * Icon-only padding — calc `(field-height - icon-size) / 2` per size。\n *\n * 設計:startIcon 到左邊距離 = padding = `(height - icon) / 2`。\n * 純 icon-only 時 width = 2*padding + icon = height → **自然正方形**,不需要 aspect-square。\n * 有 suffix(badge / endIcon)時 width = 2*pad + icon + gap + suffix > height → **自然長方形**。\n * StartIcon 到左邊距離始終不變,形狀自動適應內容。\n *\n * 用 CSS var 讓 density 切換時 padding 自動跟著算(field-height 會變)。\n */\n// IconOnly 用 padding-free + aspect-square + flex-center 的 Polaris/Atlassian idiom\n// (M17 SSOT 必可傳播 — 取代 4 個 size 的 magic-number 公式):\n// - aspect-square 鎖 width=height(來自 h-field-X)\n// - p-0 移除 px-3 (label 模式) override\n// - flex justify-center items-center(base 已有)→ SVG 自動視覺置中\n// 結果:0 magic number,0 公式,0 border-deduction,任何 size / icon size 都自然正方形。\n// World-class 對照:Polaris + Atlassian iconOnly 走 padding-free 派(Material/Ant 走\n// padding-based)。我們選 padding-free 因 SSOT 性更強(SegmentedControl / Tag dismiss\n// 等 host 全可共用同 utility class,無需各自抄公式)。詳 button.spec.md「iconOnly 鐵律」。\nconst ICON_ONLY_BASE = 'aspect-square p-0 min-w-0 gap-0'\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n className,\n variant: variantProp,\n danger: dangerProp,\n size,\n asChild = false,\n startIcon: StartIcon,\n badge,\n overlayBadge,\n endIcon: EndIcon,\n iconOnly = false,\n dismiss = false,\n loading = false,\n fullWidth = false,\n pressed,\n pressedTone, // 2026-06-05 fix(deep-audit P0):原漏 destructure → 落入 ...props 噴到 DOM + 永遠用 cva default\n children,\n disabled,\n ...props\n },\n ref\n ) => {\n // ── FieldContext:在 Field 內時自動讀 size,讓 Button 跟 Input 同高 ──\n const fieldCtx = useFieldContext?.()\n const resolvedSize = size ?? (fieldCtx?.size as typeof size) ?? 'md'\n\n // ── Dismiss 視覺類 override(2026-04-22 cross-implementation dimming canonical) ──\n // dismiss=true 強制:variant=\"text\" + iconOnly=true + icon 色弱化(fg-muted → hover foreground)\n // 跟 Inline Action dismiss 視覺一致。詳見 button.spec.md「Dismiss 視覺類」。\n const resolvedIconOnly = iconOnly || dismiss\n\n // ── Dev-mode warning:overlayBadge 只適用 iconOnly ──\n // 有 label 的 Button 傳入 overlayBadge 會被忽略(只 render icon / 不渲染 overlay slot),\n // 靜默忽略會讓 consumer 誤以為「傳了但位置錯」。Dev mode 印 warning 引導改用 `badge` prop\n // (inline 位置,跟 label 並列)或改 `iconOnly`。Spec SSOT:badge.spec.md「Overlay 適用元件」。\n if (process.env.NODE_ENV !== 'production' && overlayBadge && !iconOnly) {\n // eslint-disable-next-line no-console\n console.warn(\n '[DS Button] `overlayBadge` 只適用於 `iconOnly` Button。有 label 的 Button 請改用 `badge` prop(inline 位置,跟 label 並列),或移除 label 改為 iconOnly。SSOT:badge.spec.md「Overlay 適用元件 canonical」節。'\n )\n }\n\n // 2026-05-23 deep-audit Phase A.4 Decision 1(user verbatim「決策ㄧ照你建議做」):dev-warn `iconOnly + (endIcon|badge)` 並用。\n // Spec SSOT:button.spec.md「iconOnly 嚴格定義為『只有一個 icon,正方形』,不可與 endIcon 或 badge 並用」。\n // 例外:overlayBadge 跟 iconOnly 並用 canonical(L372 反向 warn 已 cover)。\n // 不擋 image canonical「icon + 下拉指示 = 不加 iconOnly + startIcon + endIcon + aria-label」(那 case iconOnly=false 不 trigger)。\n if (process.env.NODE_ENV !== 'production' && iconOnly && (EndIcon !== undefined || badge != null)) {\n // eslint-disable-next-line no-console\n console.warn(\n '[DS Button] `iconOnly` 嚴格定義為「只有一個 icon,正方形」,不可與 `endIcon` 或 `badge` 並用。若需 icon + 下拉指示 → 不加 iconOnly + startIcon + endIcon + aria-label;若需 icon + 角標 → iconOnly + overlayBadge。SSOT:button.spec.md `iconOnly 的邊界` 節 + `badge.spec.md` Overlay 適用元件 canonical。'\n )\n }\n\n // shadcn compat:AlertDialog、Toast 等元件內部會傳入這些 alias,\n // 在此靜默轉換,不暴露到型別或自動完成。\n // dismiss=true 強制 variant=text(dismiss canonical);override 其他 variant 傳入。\n const resolvedVariant: InternalVariant =\n dismiss ? 'text' :\n (variantProp as string) === 'destructive' ? 'primary' :\n (variantProp as string) === 'ghost' ? 'text' :\n (variantProp as InternalVariant) ?? 'primary'\n\n const resolvedDanger = dangerProp || (variantProp as string) === 'destructive'\n\n // ButtonGroup context:vertical group 自動注入 fullWidth\n const groupCtx = React.useContext(ButtonGroupContext)\n const resolvedFullWidth = fullWidth || !!groupCtx.fullWidth\n\n const Comp = asChild ? Slot : 'button'\n const iconSize = resolvedSize === 'lg' ? 20 : 16\n\n // loading 行為:spinner 永遠在 prefix 位置\n // 有 prefix icon → icon 換成 spinner(同位置,零 layout shift)\n // 無 prefix icon → spinner 加在文字左邊(按鈕略微變寬,可接受)\n const hasSuffix = badge != null || EndIcon !== undefined\n\n // icon-only 自動 tooltip:從 props 提取 aria-label,同時保留在 DOM\n const { 'aria-label': ariaLabel, ...restProps } = props\n\n // Toggle 狀態:pressed 定義時自動寫入 aria-pressed + data-state。\n // 未定義時不寫入任何 toggle 屬性(按鈕為一般 action button)。\n // 樣式由 cva 的 data-[state=on] 分支套用——secondary/tertiary 走 primary-subtle,\n // text 走 neutral-selected family;primary/link 不定義 on 分支,傳入無效果。\n const toggleAttrs =\n pressed === undefined\n ? {}\n : { 'aria-pressed': pressed, 'data-state': pressed ? 'on' : 'off' }\n\n // Chrome-unbounded marker(2026-04-22 v5 canonical):button 若無視覺邊界(text variant 或 dismiss),\n // 標記 data-unbounded=\"true\"。SurfaceHeader 透過 [&_[data-unbounded]]:my-[...] 套負 margin\n // 讓 layout 佔位縮到 24(chrome-header-height 幾何)— button native size 與 touch target 不變。\n // 詳 overlay-surface.spec.md「Chrome dismiss size canonical」\n const unboundedAttr =\n resolvedVariant === 'text' || dismiss ? { 'data-unbounded': 'true' } : {}\n\n const buttonEl = (\n <Comp\n className={cn(\n buttonVariants({ variant: resolvedVariant, danger: resolvedDanger, size: resolvedSize, pressedTone, className }),\n // iconOnly 鐵律:padding-free + aspect-square + flex-center (Polaris idiom)\n // 0 magic-number 0 公式自動正方形。詳 ICON_ONLY_BASE rationale。\n resolvedIconOnly && ICON_ONLY_BASE,\n // Dismiss 視覺弱化:override Button text variant 預設 foreground 為 fg-muted → hover foreground\n // 跟 Inline Action dismiss 視覺一致(cross-implementation dimming canonical)\n dismiss && 'text-fg-muted hover:text-foreground',\n resolvedFullWidth && 'w-full',\n )}\n ref={ref}\n type=\"button\"\n disabled={disabled || loading}\n aria-busy={loading || undefined}\n aria-label={ariaLabel}\n {...toggleAttrs}\n {...unboundedAttr}\n {...restProps}\n >\n {loading ? (\n <CircularProgress size={iconSize} className=\"text-current\" />\n ) : StartIcon ? (\n resolvedIconOnly && overlayBadge ? (\n // Overlay badge canonical:wrapper 貼 icon 尺寸,badge 中心對齊 icon top-right corner\n // (Material BadgedBox / iOS App icon),不是 button chrome 角。\n //\n // CSS 細節(2026-04-20 bug fix):用 `inline-block` + 明確 width/height + `leading-none`\n // 避免 `inline-flex` span 在 Button flex container 內被撐高/撐寬(inline-flex 的 span\n // 在某些瀏覽器下會把絕對定位子元素的 translate 計算基準搞錯,造成 badge 噴飛、Button\n // aspect-square 失效)。明確給 span width/height = iconSize 鎖住 positioning context。\n <span\n className=\"relative inline-block leading-none shrink-0 pointer-events-none\"\n style={{ width: iconSize, height: iconSize }}\n >\n <StartIcon size={iconSize} aria-hidden />\n <span className=\"absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 pointer-events-auto\">\n {overlayBadge}\n </span>\n </span>\n ) : (\n <StartIcon size={iconSize} aria-hidden />\n )\n ) : null}\n {children != null && <span className=\"px-1\">{children}</span>}\n {hasSuffix && (\n <span className=\"inline-flex items-center gap-1\">\n {badge}\n {EndIcon && <EndIcon size={iconSize} aria-hidden />}\n </span>\n )}\n </Comp>\n )\n\n // icon-only + aria-label → 自動包 Tooltip(tooltip 是元件保證的行為)\n // 不建立獨立 TooltipProvider——依賴全域 Provider,\n // 這樣所有 tooltip 共享同一組 delay 參數和 warm-up 機制\n if (resolvedIconOnly && typeof ariaLabel === 'string' && !asChild) {\n return (\n <Tooltip>\n <TooltipTrigger asChild>{buttonEl}</TooltipTrigger>\n <TooltipContent>{ariaLabel}</TooltipContent>\n </Tooltip>\n )\n }\n\n return buttonEl\n }\n)\nButton.displayName = 'Button'\n\n/**\n * componentMeta — Story Auto-Compile 系統消費的結構化 canonical\n * (見 .claude/planning/story-auto-compile.md Phase 1)\n *\n * compile-stories.mjs 讀本 export + spec.md frontmatter 產出\n * anatomy.stories.tsx 的 variant/size/state/token 矩陣 canonical section。\n *\n * Keys 必跟 buttonVariants cva + spec frontmatter 對齊(compile-time 驗證)。\n */\nexport const buttonMeta = {\n component: 'Button',\n family: 3, // Pill Layout\n variants: {\n primary: { purpose: '主要 action / CTA' },\n secondary: { purpose: '次要 action(陪襯 primary)' },\n tertiary: { purpose: '第三級 action(tool-like)' },\n text: { purpose: '文字樣式 action(low emphasis / toolbar)' },\n link: { purpose: '內文連結(inline reading)' },\n },\n sizes: {\n xs: { fieldHeight: 24, iconSize: 16, typography: 'body' },\n sm: { fieldHeight: 28, iconSize: 16, typography: 'body' },\n md: { fieldHeight: 32, iconSize: 16, typography: 'body' },\n lg: { fieldHeight: 40, iconSize: 20, typography: 'body-lg' },\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['--primary', '--primary-hover', '--primary-active', '--bg-disabled'],\n fg: ['--on-emphasis', '--fg-disabled'],\n ring: ['--ring'],\n },\n defaultVariant: 'primary',\n defaultSize: 'md',\n} as const\n\nexport { Button, buttonVariants, ButtonGroupContext }\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"progress-bar.js","sources":["../../../src/components/ProgressBar/progress-bar.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport * as ProgressPrimitive from '@radix-ui/react-progress'\nimport { CircleCheck, XCircle } from 'lucide-react'\nimport { cn } from '@/lib/utils'\n\n/**\n * ProgressBar — 水平進度條(determinate)\n *\n * 世界級對照:Material `LinearProgress` / Ant `Progress` / Polaris `ProgressBar` /\n * shadcn `Progress`(皆為 Radix Progress primitive 的包裝)。本 DS 命名為\n * `ProgressBar`(linear)以和 `CircularProgress`(circular)做清楚區分。\n *\n * ── 與 CircularProgress 的分界 ──\n * CircularProgress = circular 兩態(indeterminate 旋轉 / determinate arc),inline 小空間\n * ProgressBar = linear determinate(0-100% 已知量化),頁面級大區塊 / 表單 wizard / quota\n * 兩者視覺與語意都不同,consumer 依「形狀(linear / circular)+ 是否量化」擇一。\n *\n * ── 與 FileItem 的分界(2026-04-20 user 決策) ──\n * **檔案上傳 UI 走 `FileItem`,不直接消費 ProgressBar**。FileItem 是檔案上傳情境的\n * canonical consumer-facing primitive(檔名 / icon / 進度 / status / actions 一條龍);\n * FileItem 內部可能消費 ProgressBar(engineering 實作細節),consumer 不用也不該自己\n * 組合 raw ProgressBar + 檔名 + action 來做上傳列表。世界級對照:Ant Design 的 `Upload`\n * vs `Progress`(Upload.List 內部用 Progress,consumer 不直接用 Progress 做上傳 UI)。\n *\n * ── 3 狀態,單一 size ──\n * status: inProgress(進行中藍) / success(完成綠) / error(失敗紅)\n * ^ 命名理由:`status` 是 lifecycle(在途 / 終態),不是視覺 emphasis 階。前身\n * `primary` 會撞 Button `variant=\"primary\"`(emphasis 最高階),改用世界級\n * lifecycle 慣例(Polaris `inProgress` / Ant Progress `active`)。\n *\n * **單一高度 4px**(2026-04-20 user 決策):對齊 Material 3 / Carbon / Ant 慣例\n * (固定 4dp/px 不給 size 選項)。4px 是 linear progress 的業界 canonical;若需\n * 視覺強調改用 CircularProgress + label 或改做 full-width hero 版型,不靠 size\n * 階梯撐。移除前的 sm(2)/ md(4)/ lg(6)刻度差太小,無實質視覺區分。\n *\n * ── affix(右側附加) ──\n * `affix=\"value\"` → 顯示 `{value}%` 文字\n * `affix=\"status-icon\"` → 顯示狀態 icon(success ✓ / error ✗;inProgress 時無 icon)\n * `affix={<custom />}` → consumer 客製\n * 不傳 → 純 bar\n */\n\nconst DEFAULT_TRACK_H = 4 // 預設 4px(見 docblock「單一高度」)\nconst STATUS_FILL = {\n inProgress: 'bg-primary',\n success: 'bg-success',\n error: 'bg-error',\n} as const\nconst STATUS_ICON = {\n success: { Icon: CircleCheck, className: 'text-success' },\n error: { Icon: XCircle, className: 'text-error' },\n} as const\n\nexport interface ProgressBarProps extends Omit<React.ComponentProps<typeof ProgressPrimitive.Root>, 'value'> {\n /** 當前進度 0-100 */\n value: number\n /** 狀態色(lifecycle,非 emphasis 階) */\n status?: 'inProgress' | 'success' | 'error'\n /** 右側附加 */\n affix?: 'value' | 'status-icon' | React.ReactNode\n /**\n * Track 高度(px)override。**預設 4**(對齊 Material / Carbon / Ant canonical)。\n *\n * **非公開 size 階**——這不是給 consumer 自由選擇粗細的 API,而是給**內部另一個\n * DS 元件(FileItem)在極密集 row layout 下壓低到 2px** 的逃生艙。Consumer 端\n * 一律走預設 4px。未來若新 primitive(例如 health-bar 型 hero progress)需要\n * 較高的 track,再評估是否擴公開 API。\n *\n * 世界級對照:Ant `<Progress>` 有 `strokeWidth` 原生 prop;本 DS 只暴露給內部\n * 元件使用,不在 public API 宣傳,避免 consumer 重新陷入「選哪個 size」的判斷負擔。\n */\n height?: number\n}\n\nconst ProgressBar = React.forwardRef<HTMLDivElement, ProgressBarProps>(\n (\n {\n value,\n status = 'inProgress',\n affix,\n height,\n className,\n ...props\n },\n ref,\n ) => {\n const clampedValue = Math.max(0, Math.min(100, value))\n const fillColor = STATUS_FILL[status]\n const trackH = height ?? DEFAULT_TRACK_H\n\n // Affix 渲染\n let affixNode: React.ReactNode = null\n if (affix === 'value') {\n affixNode = (\n <span className=\"text-caption text-foreground tabular-nums shrink-0\">\n {Math.round(clampedValue)}%\n </span>\n )\n } else if (affix === 'status-icon') {\n const s = status !== 'inProgress' ? STATUS_ICON[status] : null\n if (s) affixNode = <s.Icon size={16} className={cn('shrink-0', s.className)} aria-hidden />\n } else if (React.isValidElement(affix) || typeof affix === 'string' || typeof affix === 'number') {\n affixNode = affix\n }\n\n const bar = (\n <ProgressPrimitive.Root\n ref={ref}\n value={clampedValue}\n max={100}\n className={cn(\n 'relative overflow-hidden rounded-full bg-secondary w-full',\n className,\n )}\n style={{ height: trackH }}\n {...props}\n >\n <ProgressPrimitive.Indicator\n className={cn('h-full rounded-full transition-all duration-300', fillColor)}\n style={{ width: `${clampedValue}%` }}\n />\n </ProgressPrimitive.Root>\n )\n\n if (!affixNode) return bar\n\n return (\n <div className=\"flex items-center gap-2 w-full\">\n <div className=\"flex-1 min-w-0\">{bar}</div>\n {affixNode}\n </div>\n )\n },\n)\nProgressBar.displayName = 'ProgressBar'\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 progressBarMeta = {\n component: 'ProgressBar',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-error', 'bg-primary', 'bg-secondary', 'bg-success'],\n fg: ['text-foreground'],\n ring: [],\n },\n} as const\n\nexport { ProgressBar }\n"],"names":[],"mappings":";;;;;AA2CA,MAAM,kBAAkB;AACxB,MAAM,cAAc;AAAA,EAClB,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,OAAO;AACT;AACA,MAAM,cAAc;AAAA,EAClB,SAAS,EAAE,MAAM,aAAa,WAAW,eAAA;AAAA,EACzC,OAAS,EAAE,MAAM,SAAa,WAAW,aAAA;AAC3C;AAuBA,MAAM,cAAc,MAAM;AAAA,EACxB,CACE;AAAA,IACE;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,eAAe,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,CAAC;AACrD,UAAM,YAAY,YAAY,MAAM;AACpC,UAAM,SAAS,UAAU;AAGzB,QAAI,YAA6B;AACjC,QAAI,UAAU,SAAS;AACrB,kBACE,qBAAC,QAAA,EAAK,WAAU,sDACb,UAAA;AAAA,QAAA,KAAK,MAAM,YAAY;AAAA,QAAE;AAAA,MAAA,GAC5B;AAAA,IAEJ,WAAW,UAAU,eAAe;AAClC,YAAM,IAAI,WAAW,eAAe,YAAY,MAAM,IAAI;AAC1D,UAAI,EAAG,aAAY,oBAAC,EAAE,MAAF,EAAO,MAAM,IAAI,WAAW,GAAG,YAAY,EAAE,SAAS,GAAG,eAAW,MAAC;AAAA,IAC3F,WAAW,MAAM,eAAe,KAAK,KAAK,OAAO,UAAU,YAAY,OAAO,UAAU,UAAU;AAChG,kBAAY;AAAA,IACd;AAEA,UAAM,MACJ;AAAA,MAAC,kBAAkB;AAAA,MAAlB;AAAA,QACC;AAAA,QACA,OAAO;AAAA,QACP,KAAK;AAAA,QACL,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QAAA;AAAA,QAEF,OAAO,EAAE,QAAQ,OAAA;AAAA,QAChB,GAAG;AAAA,QAEJ,UAAA;AAAA,UAAC,kBAAkB;AAAA,UAAlB;AAAA,YACC,WAAW,GAAG,mDAAmD,SAAS;AAAA,YAC1E,OAAO,EAAE,OAAO,GAAG,YAAY,IAAA;AAAA,UAAI;AAAA,QAAA;AAAA,MACrC;AAAA,IAAA;AAIJ,QAAI,CAAC,UAAW,QAAO;AAEvB,WACE,qBAAC,OAAA,EAAI,WAAU,kCACb,UAAA;AAAA,MAAA,oBAAC,OAAA,EAAI,WAAU,kBAAkB,UAAA,KAAI;AAAA,MACpC;AAAA,IAAA,GACH;AAAA,EAEJ;AACF;AACA,YAAY,cAAc;AAInB,MAAM,kBAAkB;AAAA,EAC7B,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,WAAW,SAAS,UAAU,iBAAiB,UAAU;AAAA,EAClE,QAAQ;AAAA,IACN,IAAI,CAAC,YAAY,cAAc,gBAAgB,YAAY;AAAA,IAC3D,IAAI,CAAC,iBAAiB;AAAA,IACtB,MAAM,CAAA;AAAA,EAAC;AAEX;"}
1
+ {"version":3,"file":"progress-bar.js","sources":["../../../src/components/ProgressBar/progress-bar.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved.\nimport * as React from 'react'\nimport * as ProgressPrimitive from '@radix-ui/react-progress'\nimport { CircleCheck, XCircle } from 'lucide-react'\nimport { cn } from '@/lib/utils'\n\n/**\n * ProgressBar — 水平進度條(determinate)\n *\n * 世界級對照:Material `LinearProgress` / Ant `Progress` / Polaris `ProgressBar` /\n * shadcn `Progress`(皆為 Radix Progress primitive 的包裝)。本 DS 命名為\n * `ProgressBar`(linear)以和 `CircularProgress`(circular)做清楚區分。\n *\n * ── 與 CircularProgress 的分界 ──\n * CircularProgress = circular 兩態(indeterminate 旋轉 / determinate arc),inline 小空間\n * ProgressBar = linear determinate(0-100% 已知量化),頁面級大區塊 / 表單 wizard / quota\n * 兩者視覺與語意都不同,consumer 依「形狀(linear / circular)+ 是否量化」擇一。\n *\n * ── 與 FileItem 的分界(2026-04-20 user 決策) ──\n * **檔案上傳 UI 走 `FileItem`,不直接消費 ProgressBar**。FileItem 是檔案上傳情境的\n * canonical consumer-facing primitive(檔名 / icon / 進度 / status / actions 一條龍);\n * FileItem 內部可能消費 ProgressBar(engineering 實作細節),consumer 不用也不該自己\n * 組合 raw ProgressBar + 檔名 + action 來做上傳列表。世界級對照:Ant Design 的 `Upload`\n * vs `Progress`(Upload.List 內部用 Progress,consumer 不直接用 Progress 做上傳 UI)。\n *\n * ── 3 狀態,單一 size ──\n * status: inProgress(進行中藍) / success(完成綠) / error(失敗 deep-orange,bg-error=--color-deep-orange-6)\n * ^ 命名理由:`status` 是 lifecycle(在途 / 終態),不是視覺 emphasis 階。前身\n * `primary` 會撞 Button `variant=\"primary\"`(emphasis 最高階),改用世界級\n * lifecycle 慣例(Polaris `inProgress` / Ant Progress `active`)。\n *\n * **單一高度 4px**(2026-04-20 user 決策):對齊 Material 3 / Carbon / Ant 慣例\n * (固定 4dp/px 不給 size 選項)。4px 是 linear progress 的業界 canonical;若需\n * 視覺強調改用 CircularProgress + label 或改做 full-width hero 版型,不靠 size\n * 階梯撐。移除前的 sm(2)/ md(4)/ lg(6)刻度差太小,無實質視覺區分。\n *\n * ── affix(右側附加) ──\n * `affix=\"value\"` → 顯示 `{value}%` 文字\n * `affix=\"status-icon\"` → 顯示狀態 icon(success ✓ / error ✗;inProgress 時無 icon)\n * `affix={<custom />}` → consumer 客製\n * 不傳 → 純 bar\n */\n\nconst DEFAULT_TRACK_H = 4 // 預設 4px(見 docblock「單一高度」)\nconst STATUS_FILL = {\n inProgress: 'bg-primary',\n success: 'bg-success',\n error: 'bg-error',\n} as const\nconst STATUS_ICON = {\n success: { Icon: CircleCheck, className: 'text-success' },\n error: { Icon: XCircle, className: 'text-error' },\n} as const\n\nexport interface ProgressBarProps extends Omit<React.ComponentProps<typeof ProgressPrimitive.Root>, 'value'> {\n /** 當前進度 0-100 */\n value: number\n /** 狀態色(lifecycle,非 emphasis 階) */\n status?: 'inProgress' | 'success' | 'error'\n /** 右側附加 */\n affix?: 'value' | 'status-icon' | React.ReactNode\n /**\n * Track 高度(px)override。**預設 4**(對齊 Material / Carbon / Ant canonical)。\n *\n * **非公開 size 階**——這不是給 consumer 自由選擇粗細的 API,而是給**內部另一個\n * DS 元件(FileItem)在極密集 row layout 下壓低到 2px** 的逃生艙。Consumer 端\n * 一律走預設 4px。未來若新 primitive(例如 health-bar 型 hero progress)需要\n * 較高的 track,再評估是否擴公開 API。\n *\n * 世界級對照:Ant `<Progress>` 有 `strokeWidth` 原生 prop;本 DS 只暴露給內部\n * 元件使用,不在 public API 宣傳,避免 consumer 重新陷入「選哪個 size」的判斷負擔。\n */\n height?: number\n}\n\nconst ProgressBar = React.forwardRef<HTMLDivElement, ProgressBarProps>(\n (\n {\n value,\n status = 'inProgress',\n affix,\n height,\n className,\n ...props\n },\n ref,\n ) => {\n const clampedValue = Math.max(0, Math.min(100, value))\n const fillColor = STATUS_FILL[status]\n const trackH = height ?? DEFAULT_TRACK_H\n\n // Affix 渲染\n let affixNode: React.ReactNode = null\n if (affix === 'value') {\n affixNode = (\n <span className=\"text-caption text-foreground tabular-nums shrink-0\">\n {Math.round(clampedValue)}%\n </span>\n )\n } else if (affix === 'status-icon') {\n const s = status !== 'inProgress' ? STATUS_ICON[status] : null\n if (s) affixNode = <s.Icon size={16} className={cn('shrink-0', s.className)} aria-hidden />\n } else if (React.isValidElement(affix) || typeof affix === 'string' || typeof affix === 'number') {\n affixNode = affix\n }\n\n const bar = (\n <ProgressPrimitive.Root\n ref={ref}\n value={clampedValue}\n max={100}\n className={cn(\n 'relative overflow-hidden rounded-full bg-secondary w-full',\n className,\n )}\n style={{ height: trackH }}\n {...props}\n >\n <ProgressPrimitive.Indicator\n className={cn('h-full rounded-full transition-all duration-300', fillColor)}\n style={{ width: `${clampedValue}%` }}\n />\n </ProgressPrimitive.Root>\n )\n\n if (!affixNode) return bar\n\n return (\n <div className=\"flex items-center gap-2 w-full\">\n <div className=\"flex-1 min-w-0\">{bar}</div>\n {affixNode}\n </div>\n )\n },\n)\nProgressBar.displayName = 'ProgressBar'\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 progressBarMeta = {\n component: 'ProgressBar',\n family: null, // non-family composite / overlay / layout\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-error', 'bg-primary', 'bg-secondary', 'bg-success'],\n fg: ['text-foreground'],\n ring: [],\n },\n} as const\n\nexport { ProgressBar }\n"],"names":[],"mappings":";;;;;AA2CA,MAAM,kBAAkB;AACxB,MAAM,cAAc;AAAA,EAClB,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,OAAO;AACT;AACA,MAAM,cAAc;AAAA,EAClB,SAAS,EAAE,MAAM,aAAa,WAAW,eAAA;AAAA,EACzC,OAAS,EAAE,MAAM,SAAa,WAAW,aAAA;AAC3C;AAuBA,MAAM,cAAc,MAAM;AAAA,EACxB,CACE;AAAA,IACE;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,eAAe,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,CAAC;AACrD,UAAM,YAAY,YAAY,MAAM;AACpC,UAAM,SAAS,UAAU;AAGzB,QAAI,YAA6B;AACjC,QAAI,UAAU,SAAS;AACrB,kBACE,qBAAC,QAAA,EAAK,WAAU,sDACb,UAAA;AAAA,QAAA,KAAK,MAAM,YAAY;AAAA,QAAE;AAAA,MAAA,GAC5B;AAAA,IAEJ,WAAW,UAAU,eAAe;AAClC,YAAM,IAAI,WAAW,eAAe,YAAY,MAAM,IAAI;AAC1D,UAAI,EAAG,aAAY,oBAAC,EAAE,MAAF,EAAO,MAAM,IAAI,WAAW,GAAG,YAAY,EAAE,SAAS,GAAG,eAAW,MAAC;AAAA,IAC3F,WAAW,MAAM,eAAe,KAAK,KAAK,OAAO,UAAU,YAAY,OAAO,UAAU,UAAU;AAChG,kBAAY;AAAA,IACd;AAEA,UAAM,MACJ;AAAA,MAAC,kBAAkB;AAAA,MAAlB;AAAA,QACC;AAAA,QACA,OAAO;AAAA,QACP,KAAK;AAAA,QACL,WAAW;AAAA,UACT;AAAA,UACA;AAAA,QAAA;AAAA,QAEF,OAAO,EAAE,QAAQ,OAAA;AAAA,QAChB,GAAG;AAAA,QAEJ,UAAA;AAAA,UAAC,kBAAkB;AAAA,UAAlB;AAAA,YACC,WAAW,GAAG,mDAAmD,SAAS;AAAA,YAC1E,OAAO,EAAE,OAAO,GAAG,YAAY,IAAA;AAAA,UAAI;AAAA,QAAA;AAAA,MACrC;AAAA,IAAA;AAIJ,QAAI,CAAC,UAAW,QAAO;AAEvB,WACE,qBAAC,OAAA,EAAI,WAAU,kCACb,UAAA;AAAA,MAAA,oBAAC,OAAA,EAAI,WAAU,kBAAkB,UAAA,KAAI;AAAA,MACpC;AAAA,IAAA,GACH;AAAA,EAEJ;AACF;AACA,YAAY,cAAc;AAInB,MAAM,kBAAkB;AAAA,EAC7B,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA,EAGV,OAAO,CAAA;AAAA,EAGP,QAAQ,CAAC,WAAW,SAAS,UAAU,iBAAiB,UAAU;AAAA,EAClE,QAAQ;AAAA,IACN,IAAI,CAAC,YAAY,cAAc,gBAAgB,YAAY;AAAA,IAC3D,IAAI,CAAC,iBAAiB;AAAA,IACtB,MAAM,CAAA;AAAA,EAAC;AAEX;"}
@@ -12,7 +12,7 @@ export interface TagProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'pr
12
12
  avatar?: React.ReactNode;
13
13
  /** 可移除——Tag 自動渲染 dismiss 按鈕並控制尺寸與互動樣式 */
14
14
  onDismiss?: () => void;
15
- /** 深底白字模式(step-6 背景 + 白色前景,warning 例外) */
15
+ /** 深底模式(step-6 背景 + on-emphasis 配對前景;亮色 hue yellow/amber/orange/lime 用深字 --on-emphasis-dark,green 白字例外) */
16
16
  solid?: boolean;
17
17
  /**
18
18
  * 2026-05-15 Q3 真 SSOT fix(per user verbatim「同空間兩判斷點」+「不要冰山一角」):
@@ -1 +1 @@
1
- {"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../../src/components/Tag/tag.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAwB9C,QAAA,MAAM,WAAW;;;8EAuBhB,CAAA;AAWD,MAAM,WAAW,QACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,EACpE,YAAY,CAAC,OAAO,WAAW,CAAC;IAClC,qDAAqD;IACrD,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,sCAAsC;IACtC,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACxB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAkHD,QAAA,MAAM,GAAG,iFAAuD,CAAA;AAKhE,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCV,CAAA;AAEV,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,CAAA"}
1
+ {"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../../src/components/Tag/tag.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAwB9C,QAAA,MAAM,WAAW;;;8EAuBhB,CAAA;AAWD,MAAM,WAAW,QACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,EACpE,YAAY,CAAC,OAAO,WAAW,CAAC;IAClC,qDAAqD;IACrD,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,sCAAsC;IACtC,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACxB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,2GAA2G;IAC3G,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAkHD,QAAA,MAAM,GAAG,iFAAuD,CAAA;AAKhE,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCV,CAAA;AAEV,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"tag.js","sources":["../../../src/components/Tag/tag.tsx"],"sourcesContent":["import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { X } from \"lucide-react\"\nimport type { LucideIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Tooltip, TooltipTrigger, TooltipContent } from \"@/design-system/components/Tooltip/tooltip\"\nimport { ItemInlineActionButton } from \"@/design-system/patterns/element-anatomy/item-anatomy\"\nimport { CAT_SUBTLE, CAT_SOLID, CAT_INTERACT } from \"@/design-system/tokens/categorical-color\"\n\n// ── Tag(inline label)─────────────────────────────────────────────────────\n// 用於分類標籤、狀態標記、多選已選值。\n//\n// 三種尺寸(子元件補齊原則——消費端直接透傳 size,不做 mapping):\n// sm — 20px 高, 12px 字, 4px tag-px, font-medium(配 field sm)\n// md — 24px 高, 14px 字, 4px tag-px, font-normal(配 field md)— 預設\n// lg — 24px = md alias(配 field lg,子元件補齊原則)\n//\n// 截斷:max-w-40(160px),超出時文字 truncate + 自動 tooltip。\n// 用 Canvas measureText 偵測截斷(scrollWidth 在 flex 內不可靠)。\n\nlet _measureCtx: CanvasRenderingContext2D | null = null\nfunction getMeasureCtx() {\n if (!_measureCtx) _measureCtx = document.createElement('canvas').getContext('2d')\n return _measureCtx\n}\n\nconst tagVariants = cva(\n \"inline-flex items-center rounded-md border border-transparent transition-colors cursor-text\",\n {\n variants: {\n // color:categorical 色相(裝飾性分類,非語意狀態)。**消費 categorical-color SSOT**——\n // key X 一律對 `--color-X-*`(1:1,零 offset)。neutral 非色相(無 hue),用 secondary 底自處理。\n // 2026-06-04 修:原 `red` 誤接 `--color-deep-orange-*`(red=品牌紅 hue-25 ≠ deep-orange hue-38\n // ≠ 語意 --error〔= deep-orange〕);改消費 SSOT 後 red→`--color-red-*`,並補齊全 12 色相。\n color: {\n neutral: \"bg-secondary text-foreground\",\n ...CAT_SUBTLE,\n },\n size: {\n sm: \"h-5 px-1 text-caption font-medium\",\n md: \"h-6 px-1 text-body font-normal\",\n lg: \"h-6 px-1 text-body font-normal\",\n },\n },\n defaultVariants: {\n color: \"neutral\",\n size: \"md\",\n },\n }\n)\n\n// ── Solid variant 色彩(step-6 底 + on-emphasis 配對文字,消費 categorical-color SSOT)──\n// 白字 --on-emphasis(夠深的 hue)/ 深字 --on-emphasis-dark(亮 hue:yellow/amber/orange/lime);green 白字例外。\n// **消費 categorical-color SSOT**(CAT_SOLID,1:1 色相)。neutral 非色相,用 neutral-9\n// + --inverse-fg(light=白字, dark=深字,自動反轉)自處理。\nconst SOLID_CLASSES: Record<string, string> = {\n neutral: 'bg-[var(--color-neutral-9)] text-inverse-fg',\n ...CAT_SOLID,\n}\n\nexport interface TagProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, 'prefix' | 'color'>,\n VariantProps<typeof tagVariants> {\n /** 左側 icon(LucideIcon),由 Tag 統一 16px。與 avatar 互斥。 */\n icon?: LucideIcon\n /** 左側 avatar(ReactNode),與 icon 互斥。 */\n avatar?: React.ReactNode\n /** 可移除——Tag 自動渲染 dismiss 按鈕並控制尺寸與互動樣式 */\n onDismiss?: () => void\n /** 深底白字模式(step-6 背景 + 白色前景,warning 例外) */\n solid?: boolean\n /**\n * 2026-05-15 Q3 真 SSOT fix(per user verbatim「同空間兩判斷點」+「不要冰山一角」):\n * Tag 寬度由 parent constrain,不套預設 max-w-40(160px)。用於 cell-as-input narrow cell\n * (< 160px)時 Tag fit cell 寬度 + truncate ellipsis,而非 160px 後被 cell `overflow-hidden`\n * 硬切。對齊「同 cell width → 同 overflow 判斷」SSOT。Default false 保 backward compat\n * (wrap layout / pill rail 等仍受 160px 保護)。\n */\n unbounded?: boolean\n}\n\n// ── Solid dismiss hover/active bg ──\n// **消費 categorical-color SSOT**(CAT_INTERACT,semantic --{hue}-hover/active token),\n// 跟 --primary-hover/active 同模式:solid 色彩 shade change(hover 較亮 step、active 較暗 step),\n// 在 semantic 層做 dark mode swap 確保方向跨 mode 一致。\n// neutral 特例:bg 是 neutral-9 隨 mode 反轉,用 --inverse-neutral-* 鏡射,自處理。\nconst SOLID_DISMISS_HOVER: Record<string, { hover: string; active: string }> = {\n neutral: { hover: 'var(--inverse-neutral-hover)', active: 'var(--inverse-neutral-active)' },\n ...CAT_INTERACT,\n}\n\n// ── Dismiss(internal)────────────────────────────────────────────────────\n// 走 `ItemInlineActionButton`(item-anatomy SSOT)+ `hoverBgClassName` override prop\n// (2026-05-01 整合,消除原 Tag 自刻 `<button>` 繞 DS infra 的 tech debt)。\n//\n// 視覺對齊:`size=\"md\"` → icon 16 / hover-bg 18,跟 Tag 既有手刻幾何完全相等。\n// Solid variant(blue/green/red 等)透過 `hoverBgClassName` 套色相 override token;\n// Subtle variant 落用 ItemInlineActionButton 預設 neutral-hover。\n// 圖標色繼承 Tag 文字色 → `text-current` 三態覆寫。\n\nfunction TagDismiss({ onDismiss, label, solid, color }: { onDismiss: () => void; label: string; solid?: boolean; color?: string }) {\n const solidColors = solid && color ? SOLID_DISMISS_HOVER[color] : undefined\n\n return (\n <ItemInlineActionButton\n icon={X}\n size=\"md\"\n onClick={(e) => { e.stopPropagation(); onDismiss() }}\n aria-label={`移除 ${label}`}\n style={solidColors ? ({ '--dismiss-hover': solidColors.hover, '--dismiss-active': solidColors.active } as React.CSSProperties) : undefined}\n hoverBgClassName={\n solidColors\n ? 'group-hover/action:bg-[var(--dismiss-hover)] group-active/action:bg-[var(--dismiss-active)]'\n : undefined\n }\n // Override default fg-muted → 繼承 Tag 文字色(label 同色)\n className=\"text-current hover:text-current active:text-current\"\n />\n )\n}\n\nfunction TagInner(\n { className, color, size, icon: Icon, avatar, onDismiss, solid, unbounded = false, children, style, ...props }: TagProps,\n forwardedRef: React.ForwardedRef<HTMLDivElement>,\n) {\n const solidClass = solid ? SOLID_CLASSES[color ?? 'neutral'] : undefined\n const ownRef = React.useRef<HTMLDivElement | null>(null)\n const [isTruncated, setIsTruncated] = React.useState(false)\n\n React.useLayoutEffect(() => {\n const el = ownRef.current\n if (!el) return\n const ctx = getMeasureCtx()\n const check = () => {\n const textSpan = el.querySelector('[data-tag-text]')\n if (!textSpan || !ctx) return\n const text = textSpan.textContent || ''\n const cs = getComputedStyle(textSpan)\n ctx.font = `${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`\n const textWidth = ctx.measureText(text).width\n const padL = parseFloat(cs.paddingLeft) || 0\n const padR = parseFloat(cs.paddingRight) || 0\n const needed = textWidth + padL + padR\n setIsTruncated(needed > (textSpan as HTMLElement).clientWidth + 1)\n }\n check()\n const obs = new ResizeObserver(check)\n obs.observe(el)\n return () => obs.disconnect()\n }, [children])\n\n const label = typeof children === 'string' ? children : ''\n\n const tag = (\n <div\n ref={(el) => {\n ownRef.current = el\n if (typeof forwardedRef === 'function') forwardedRef(el)\n else if (forwardedRef) (forwardedRef as React.MutableRefObject<HTMLDivElement | null>).current = el\n }}\n data-tag-root=\"\"\n className={cn(tagVariants({ color, size }), solidClass, 'w-fit min-w-0 overflow-hidden', className)}\n // 2026-05-18 Round 5 fix(per Codex M31 Round 5 verdict + user 拍板「那就開始做」):\n // 用 CSS var `--combobox-tag-area-inline-size`(由 Combobox useOverflowCount JS-injected)取代\n // `min(100%, 10rem)` cyclic percentage。CSS Sizing 3 §5.2.1:percentage 在 indefinite containing\n // block 退化為 initial value → Round 4 的 100% 沒 enforce。改 explicit px 值(JS measured)避此 trap。\n // unbounded=true:cap = inject 寬(回 cell-as-input narrow cell 原 behavior)\n // default:cap = min(inject 寬, 160px)— 兩 cap 取小。fallback(無 var,Form context 等)= 100% / 10rem。\n style={{\n maxWidth: unbounded\n ? 'var(--combobox-tag-area-inline-size, 100%)'\n : 'min(var(--combobox-tag-area-inline-size, 10rem), 10rem)',\n ...style,\n }}\n {...props}\n >\n {Icon && <Icon size={16} aria-hidden />}\n {avatar && <span className=\"shrink-0 w-4 h-4 rounded-full overflow-hidden inline-grid place-content-center [&>*]:w-full [&>*]:h-full\">{avatar}</span>}\n <span data-tag-text=\"\" className=\"px-1 truncate min-w-0\">{children}</span>\n {onDismiss && <TagDismiss onDismiss={onDismiss} label={label} solid={solid} color={color ?? 'neutral'} />}\n </div>\n )\n\n if (!isTruncated) return tag\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>{tag}</TooltipTrigger>\n <TooltipContent>{children}</TooltipContent>\n </Tooltip>\n )\n}\n\nconst Tag = React.forwardRef<HTMLDivElement, TagProps>(TagInner)\nTag.displayName = 'Tag'\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 tagMeta = {\n component: 'Tag',\n family: 3,\n // categorical 色相(裝飾性分類,非語意狀態)。**1:1 對 `--color-{hue}-*` primitive,零 offset**。\n // 不對應語意 token——語意狀態(error/info/success/warning)走 Notice / Alert 等狀態元件,\n // 不是 Tag 色相。2026-06-04 修:移除原「red 對應 --error / blue 對應 --info ...」誤導框架\n // (red = 品牌紅 hue-25,跟語意 --error〔= deep-orange〕無關)。\n variants: {\n neutral: { purpose: '通用分類、草稿、無特定語義(secondary 底)' },\n blue: { purpose: 'categorical 色相(--color-blue-*)' },\n green: { purpose: 'categorical 色相(--color-green-*)' },\n 'deep-orange': { purpose: 'categorical 色相(--color-deep-orange-*,hue 38)' },\n yellow: { purpose: 'categorical 色相(--color-yellow-*,淺底深字)' },\n red: { purpose: 'categorical 色相(--color-red-*,品牌紅家族 hue 25;≠ 語意 --error)' },\n orange: { purpose: 'categorical 色相(--color-orange-*)' },\n amber: { purpose: 'categorical 色相(--color-amber-*,淺底深字)' },\n lime: { purpose: 'categorical 色相(--color-lime-*)' },\n turquoise: { purpose: 'categorical 色相(--color-turquoise-*)' },\n indigo: { purpose: 'categorical 色相(--color-indigo-*)' },\n purple: { purpose: 'categorical 色相(--color-purple-*)' },\n magenta: { purpose: 'categorical 色相(--color-magenta-*)' },\n },\n sizes: {\n // Tag 尺寸不引用 field-height token(spec.md:180/241——Tag 與 Field 尺寸獨立)。\n // height = Tag 自身高度(cva h-5/h-6/h-6 = 20/24/24,lg = md alias)。\n // iconSize 全尺寸統一 16(tag.tsx:195 硬寫 size={16})。\n sm: { height: 20, iconSize: 16, typography: 'caption' },\n md: { height: 24, iconSize: 16, typography: 'body' },\n lg: { height: 24, iconSize: 16, typography: 'body' },\n },\n // Tag 為純展示 indicator,無互動 state(spec.md:249-256「為何無 StateBehavior」)。\n // 唯一行為 dismiss 屬 Inline Action pattern,非 Tag 自有 state。\n states: ['default'],\n tokens: {\n bg: ['bg-neutral-active', 'bg-neutral-hover', 'bg-secondary', 'bg-transparent'],\n fg: ['text-foreground', 'text-inverse-fg'],\n ring: [],\n },\n defaultVariant: 'neutral',\n defaultSize: 'md',\n} as const\n\nexport { Tag, tagVariants }\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tag.js","sources":["../../../src/components/Tag/tag.tsx"],"sourcesContent":["import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { X } from \"lucide-react\"\nimport type { LucideIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Tooltip, TooltipTrigger, TooltipContent } from \"@/design-system/components/Tooltip/tooltip\"\nimport { ItemInlineActionButton } from \"@/design-system/patterns/element-anatomy/item-anatomy\"\nimport { CAT_SUBTLE, CAT_SOLID, CAT_INTERACT } from \"@/design-system/tokens/categorical-color\"\n\n// ── Tag(inline label)─────────────────────────────────────────────────────\n// 用於分類標籤、狀態標記、多選已選值。\n//\n// 三種尺寸(子元件補齊原則——消費端直接透傳 size,不做 mapping):\n// sm — 20px 高, 12px 字, 4px tag-px, font-medium(配 field sm)\n// md — 24px 高, 14px 字, 4px tag-px, font-normal(配 field md)— 預設\n// lg — 24px = md alias(配 field lg,子元件補齊原則)\n//\n// 截斷:max-w-40(160px),超出時文字 truncate + 自動 tooltip。\n// 用 Canvas measureText 偵測截斷(scrollWidth 在 flex 內不可靠)。\n\nlet _measureCtx: CanvasRenderingContext2D | null = null\nfunction getMeasureCtx() {\n if (!_measureCtx) _measureCtx = document.createElement('canvas').getContext('2d')\n return _measureCtx\n}\n\nconst tagVariants = cva(\n \"inline-flex items-center rounded-md border border-transparent transition-colors cursor-text\",\n {\n variants: {\n // color:categorical 色相(裝飾性分類,非語意狀態)。**消費 categorical-color SSOT**——\n // key X 一律對 `--color-X-*`(1:1,零 offset)。neutral 非色相(無 hue),用 secondary 底自處理。\n // 2026-06-04 修:原 `red` 誤接 `--color-deep-orange-*`(red=品牌紅 hue-25 ≠ deep-orange hue-38\n // ≠ 語意 --error〔= deep-orange〕);改消費 SSOT 後 red→`--color-red-*`,並補齊全 12 色相。\n color: {\n neutral: \"bg-secondary text-foreground\",\n ...CAT_SUBTLE,\n },\n size: {\n sm: \"h-5 px-1 text-caption font-medium\",\n md: \"h-6 px-1 text-body font-normal\",\n lg: \"h-6 px-1 text-body font-normal\",\n },\n },\n defaultVariants: {\n color: \"neutral\",\n size: \"md\",\n },\n }\n)\n\n// ── Solid variant 色彩(step-6 底 + on-emphasis 配對文字,消費 categorical-color SSOT)──\n// 白字 --on-emphasis(夠深的 hue)/ 深字 --on-emphasis-dark(亮 hue:yellow/amber/orange/lime);green 白字例外。\n// **消費 categorical-color SSOT**(CAT_SOLID,1:1 色相)。neutral 非色相,用 neutral-9\n// + --inverse-fg(light=白字, dark=深字,自動反轉)自處理。\nconst SOLID_CLASSES: Record<string, string> = {\n neutral: 'bg-[var(--color-neutral-9)] text-inverse-fg',\n ...CAT_SOLID,\n}\n\nexport interface TagProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, 'prefix' | 'color'>,\n VariantProps<typeof tagVariants> {\n /** 左側 icon(LucideIcon),由 Tag 統一 16px。與 avatar 互斥。 */\n icon?: LucideIcon\n /** 左側 avatar(ReactNode),與 icon 互斥。 */\n avatar?: React.ReactNode\n /** 可移除——Tag 自動渲染 dismiss 按鈕並控制尺寸與互動樣式 */\n onDismiss?: () => void\n /** 深底模式(step-6 背景 + on-emphasis 配對前景;亮色 hue yellow/amber/orange/lime 用深字 --on-emphasis-dark,green 白字例外) */\n solid?: boolean\n /**\n * 2026-05-15 Q3 真 SSOT fix(per user verbatim「同空間兩判斷點」+「不要冰山一角」):\n * Tag 寬度由 parent constrain,不套預設 max-w-40(160px)。用於 cell-as-input narrow cell\n * (< 160px)時 Tag fit cell 寬度 + truncate ellipsis,而非 160px 後被 cell `overflow-hidden`\n * 硬切。對齊「同 cell width → 同 overflow 判斷」SSOT。Default false 保 backward compat\n * (wrap layout / pill rail 等仍受 160px 保護)。\n */\n unbounded?: boolean\n}\n\n// ── Solid dismiss hover/active bg ──\n// **消費 categorical-color SSOT**(CAT_INTERACT,semantic --{hue}-hover/active token),\n// 跟 --primary-hover/active 同模式:solid 色彩 shade change(hover 較亮 step、active 較暗 step),\n// 在 semantic 層做 dark mode swap 確保方向跨 mode 一致。\n// neutral 特例:bg 是 neutral-9 隨 mode 反轉,用 --inverse-neutral-* 鏡射,自處理。\nconst SOLID_DISMISS_HOVER: Record<string, { hover: string; active: string }> = {\n neutral: { hover: 'var(--inverse-neutral-hover)', active: 'var(--inverse-neutral-active)' },\n ...CAT_INTERACT,\n}\n\n// ── Dismiss(internal)────────────────────────────────────────────────────\n// 走 `ItemInlineActionButton`(item-anatomy SSOT)+ `hoverBgClassName` override prop\n// (2026-05-01 整合,消除原 Tag 自刻 `<button>` 繞 DS infra 的 tech debt)。\n//\n// 視覺對齊:`size=\"md\"` → icon 16 / hover-bg 18,跟 Tag 既有手刻幾何完全相等。\n// Solid variant(blue/green/red 等)透過 `hoverBgClassName` 套色相 override token;\n// Subtle variant 落用 ItemInlineActionButton 預設 neutral-hover。\n// 圖標色繼承 Tag 文字色 → `text-current` 三態覆寫。\n\nfunction TagDismiss({ onDismiss, label, solid, color }: { onDismiss: () => void; label: string; solid?: boolean; color?: string }) {\n const solidColors = solid && color ? SOLID_DISMISS_HOVER[color] : undefined\n\n return (\n <ItemInlineActionButton\n icon={X}\n size=\"md\"\n onClick={(e) => { e.stopPropagation(); onDismiss() }}\n aria-label={`移除 ${label}`}\n style={solidColors ? ({ '--dismiss-hover': solidColors.hover, '--dismiss-active': solidColors.active } as React.CSSProperties) : undefined}\n hoverBgClassName={\n solidColors\n ? 'group-hover/action:bg-[var(--dismiss-hover)] group-active/action:bg-[var(--dismiss-active)]'\n : undefined\n }\n // Override default fg-muted → 繼承 Tag 文字色(label 同色)\n className=\"text-current hover:text-current active:text-current\"\n />\n )\n}\n\nfunction TagInner(\n { className, color, size, icon: Icon, avatar, onDismiss, solid, unbounded = false, children, style, ...props }: TagProps,\n forwardedRef: React.ForwardedRef<HTMLDivElement>,\n) {\n const solidClass = solid ? SOLID_CLASSES[color ?? 'neutral'] : undefined\n const ownRef = React.useRef<HTMLDivElement | null>(null)\n const [isTruncated, setIsTruncated] = React.useState(false)\n\n React.useLayoutEffect(() => {\n const el = ownRef.current\n if (!el) return\n const ctx = getMeasureCtx()\n const check = () => {\n const textSpan = el.querySelector('[data-tag-text]')\n if (!textSpan || !ctx) return\n const text = textSpan.textContent || ''\n const cs = getComputedStyle(textSpan)\n ctx.font = `${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`\n const textWidth = ctx.measureText(text).width\n const padL = parseFloat(cs.paddingLeft) || 0\n const padR = parseFloat(cs.paddingRight) || 0\n const needed = textWidth + padL + padR\n setIsTruncated(needed > (textSpan as HTMLElement).clientWidth + 1)\n }\n check()\n const obs = new ResizeObserver(check)\n obs.observe(el)\n return () => obs.disconnect()\n }, [children])\n\n const label = typeof children === 'string' ? children : ''\n\n const tag = (\n <div\n ref={(el) => {\n ownRef.current = el\n if (typeof forwardedRef === 'function') forwardedRef(el)\n else if (forwardedRef) (forwardedRef as React.MutableRefObject<HTMLDivElement | null>).current = el\n }}\n data-tag-root=\"\"\n className={cn(tagVariants({ color, size }), solidClass, 'w-fit min-w-0 overflow-hidden', className)}\n // 2026-05-18 Round 5 fix(per Codex M31 Round 5 verdict + user 拍板「那就開始做」):\n // 用 CSS var `--combobox-tag-area-inline-size`(由 Combobox useOverflowCount JS-injected)取代\n // `min(100%, 10rem)` cyclic percentage。CSS Sizing 3 §5.2.1:percentage 在 indefinite containing\n // block 退化為 initial value → Round 4 的 100% 沒 enforce。改 explicit px 值(JS measured)避此 trap。\n // unbounded=true:cap = inject 寬(回 cell-as-input narrow cell 原 behavior)\n // default:cap = min(inject 寬, 160px)— 兩 cap 取小。fallback(無 var,Form context 等)= 100% / 10rem。\n style={{\n maxWidth: unbounded\n ? 'var(--combobox-tag-area-inline-size, 100%)'\n : 'min(var(--combobox-tag-area-inline-size, 10rem), 10rem)',\n ...style,\n }}\n {...props}\n >\n {Icon && <Icon size={16} aria-hidden />}\n {avatar && <span className=\"shrink-0 w-4 h-4 rounded-full overflow-hidden inline-grid place-content-center [&>*]:w-full [&>*]:h-full\">{avatar}</span>}\n <span data-tag-text=\"\" className=\"px-1 truncate min-w-0\">{children}</span>\n {onDismiss && <TagDismiss onDismiss={onDismiss} label={label} solid={solid} color={color ?? 'neutral'} />}\n </div>\n )\n\n if (!isTruncated) return tag\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>{tag}</TooltipTrigger>\n <TooltipContent>{children}</TooltipContent>\n </Tooltip>\n )\n}\n\nconst Tag = React.forwardRef<HTMLDivElement, TagProps>(TagInner)\nTag.displayName = 'Tag'\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 tagMeta = {\n component: 'Tag',\n family: 3,\n // categorical 色相(裝飾性分類,非語意狀態)。**1:1 對 `--color-{hue}-*` primitive,零 offset**。\n // 不對應語意 token——語意狀態(error/info/success/warning)走 Notice / Alert 等狀態元件,\n // 不是 Tag 色相。2026-06-04 修:移除原「red 對應 --error / blue 對應 --info ...」誤導框架\n // (red = 品牌紅 hue-25,跟語意 --error〔= deep-orange〕無關)。\n variants: {\n neutral: { purpose: '通用分類、草稿、無特定語義(secondary 底)' },\n blue: { purpose: 'categorical 色相(--color-blue-*)' },\n green: { purpose: 'categorical 色相(--color-green-*)' },\n 'deep-orange': { purpose: 'categorical 色相(--color-deep-orange-*,hue 38)' },\n yellow: { purpose: 'categorical 色相(--color-yellow-*,淺底深字)' },\n red: { purpose: 'categorical 色相(--color-red-*,品牌紅家族 hue 25;≠ 語意 --error)' },\n orange: { purpose: 'categorical 色相(--color-orange-*)' },\n amber: { purpose: 'categorical 色相(--color-amber-*,淺底深字)' },\n lime: { purpose: 'categorical 色相(--color-lime-*)' },\n turquoise: { purpose: 'categorical 色相(--color-turquoise-*)' },\n indigo: { purpose: 'categorical 色相(--color-indigo-*)' },\n purple: { purpose: 'categorical 色相(--color-purple-*)' },\n magenta: { purpose: 'categorical 色相(--color-magenta-*)' },\n },\n sizes: {\n // Tag 尺寸不引用 field-height token(spec.md:180/241——Tag 與 Field 尺寸獨立)。\n // height = Tag 自身高度(cva h-5/h-6/h-6 = 20/24/24,lg = md alias)。\n // iconSize 全尺寸統一 16(tag.tsx:195 硬寫 size={16})。\n sm: { height: 20, iconSize: 16, typography: 'caption' },\n md: { height: 24, iconSize: 16, typography: 'body' },\n lg: { height: 24, iconSize: 16, typography: 'body' },\n },\n // Tag 為純展示 indicator,無互動 state(spec.md:249-256「為何無 StateBehavior」)。\n // 唯一行為 dismiss 屬 Inline Action pattern,非 Tag 自有 state。\n states: ['default'],\n tokens: {\n bg: ['bg-neutral-active', 'bg-neutral-hover', 'bg-secondary', 'bg-transparent'],\n fg: ['text-foreground', 'text-inverse-fg'],\n ring: [],\n },\n defaultVariant: 'neutral',\n defaultSize: 'md',\n} as const\n\nexport { Tag, tagVariants }\n"],"names":[],"mappings":"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"}
@@ -7,7 +7,7 @@
7
7
  "scripts/composition-fidelity-visual-diff.mjs",
8
8
  "product-workspace apps/template/src/AllDsComponents.stories.tsx (ImportSmoke portal → DS Storybook link)"
9
9
  ],
10
- "generatedAt": "2026-06-04T11:55:56.921Z"
10
+ "generatedAt": "2026-06-05T00:36:13.509Z"
11
11
  },
12
12
  "components": {
13
13
  "accordion": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qijenchen/design-system",
3
- "version": "0.1.0-beta.54",
3
+ "version": "0.1.0-beta.55",
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",
@@ -40,14 +40,16 @@ AspectRatio.displayName = 'AspectRatio'
40
40
  // Phase 2 fill needed: purpose descriptions + when rationale + world-class refs
41
41
  export const aspectRatioMeta = {
42
42
  component: 'AspectRatio',
43
- family: null, // non-family composite / overlay / layout
43
+ family: null, // self-contained primitive(對齊 spec frontmatter self-contained + L22;非 Family 1-4)
44
44
  variants: {
45
45
 
46
46
  },
47
47
  sizes: {
48
48
 
49
49
  },
50
- states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],
50
+ // 2026-06-05 fix(deep-audit):純 structural container,無互動狀態(spec.md L101「無 hover/focus/active/disabled」)。
51
+ // 原 ['default','hover','active','focus-visible','disabled'] 是 Phase 1 mechanical placeholder,與 spec 矛盾。
52
+ states: ['default'],
51
53
  tokens: {
52
54
  bg: [],
53
55
  fg: [],
@@ -107,7 +107,7 @@ export interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
107
107
  icon?: LucideIcon
108
108
  /** Icon / text fallback 的背景色,預設 neutral */
109
109
  color?: ColorKey
110
- /** 深底白字模式(step-6 背景 + 白色前景,warning 例外),預設 false */
110
+ /** 深底模式(step-6 背景 + on-emphasis 配對前景;亮色 hue yellow/amber/orange/lime 用深字 --on-emphasis-dark),預設 false */
111
111
  solid?: boolean
112
112
  /**
113
113
  * 在線狀態指示器(presence),顯示在 avatar **右下角**。
@@ -69,7 +69,7 @@ Badge 的 level **反映內容的緊急程度,不是 container 的視覺權重
69
69
  | `low`(起點)★ cva default | 灰底灰字(neutral-3 + neutral-7) | **無影響**——只是「目前有這麼多」的資訊,不讀也沒關係 |
70
70
  | `medium` | 淺藍底藍字(bg-info-subtle) | **輕微不便**——少了資訊但不影響主要流程 |
71
71
  | `high` | 藍底白字(bg-info) | **有感影響**——工作會堆積、待辦會過期,但不是立即傷害 |
72
- | `critical` | 紅底白字(bg-notification) | **直接傷害**——錯過訊息會造成資料遺失 / 錯過機會 / 帳戶問題 |
72
+ | `critical` | deep-orange 底白字(bg-notification = `--color-deep-orange-6`,hue 38;非 categorical red hue 25) | **直接傷害**——錯過訊息會造成資料遺失 / 錯過機會 / 帳戶問題 |
73
73
 
74
74
  ### 選 level 的流程
75
75
 
@@ -13,7 +13,7 @@ import { cn } from '@/lib/utils'
13
13
  // low(預設) — 灰底灰字(neutral-3 + neutral-7),被動計數
14
14
  // medium — 淺藍底藍字(bg-info-subtle + text-info-text),可延後看
15
15
  // high — 藍底白字(bg-info),有感影響的待辦
16
- // critical — 紅底白字(bg-notification),立即處理
16
+ // critical — deep-orange 底白字(bg-notification = --color-deep-orange-6,hue 38;非 categorical red hue 25),立即處理
17
17
  //
18
18
  // 規則:default low, escalate with reason。見 badge.spec.md「選 level 的流程」。
19
19
 
@@ -84,7 +84,7 @@ export const badgeMeta = {
84
84
  component: 'Badge',
85
85
  family: 3,
86
86
  variants: {
87
- critical: { purpose: '紅底白字(bg-notification)' },
87
+ critical: { purpose: 'deep-orange 底白字(bg-notification = deep-orange-6)' },
88
88
  high: { purpose: '藍底白字(bg-info)' },
89
89
  medium: { purpose: '淺藍底藍字(bg-info-subtle)' },
90
90
  low: { purpose: '使用者切 tab 才看,不需搶注意力' },
@@ -350,6 +350,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
350
350
  loading = false,
351
351
  fullWidth = false,
352
352
  pressed,
353
+ pressedTone, // 2026-06-05 fix(deep-audit P0):原漏 destructure → 落入 ...props 噴到 DOM + 永遠用 cva default
353
354
  children,
354
355
  disabled,
355
356
  ...props
@@ -432,7 +433,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
432
433
  const buttonEl = (
433
434
  <Comp
434
435
  className={cn(
435
- buttonVariants({ variant: resolvedVariant, danger: resolvedDanger, size: resolvedSize, className }),
436
+ buttonVariants({ variant: resolvedVariant, danger: resolvedDanger, size: resolvedSize, pressedTone, className }),
436
437
  // iconOnly 鐵律:padding-free + aspect-square + flex-center (Polaris idiom)
437
438
  // 0 magic-number 0 公式自動正方形。詳 ICON_ONLY_BASE rationale。
438
439
  resolvedIconOnly && ICON_ONLY_BASE,
@@ -24,7 +24,7 @@ import { cn } from '@/lib/utils'
24
24
  * vs `Progress`(Upload.List 內部用 Progress,consumer 不直接用 Progress 做上傳 UI)。
25
25
  *
26
26
  * ── 3 狀態,單一 size ──
27
- * status: inProgress(進行中藍) / success(完成綠) / error(失敗紅)
27
+ * status: inProgress(進行中藍) / success(完成綠) / error(失敗 deep-orange,bg-error=--color-deep-orange-6)
28
28
  * ^ 命名理由:`status` 是 lifecycle(在途 / 終態),不是視覺 emphasis 階。前身
29
29
  * `primary` 會撞 Button `variant="primary"`(emphasis 最高階),改用世界級
30
30
  * lifecycle 慣例(Polaris `inProgress` / Ant Progress `active`)。
@@ -68,7 +68,7 @@ export interface TagProps
68
68
  avatar?: React.ReactNode
69
69
  /** 可移除——Tag 自動渲染 dismiss 按鈕並控制尺寸與互動樣式 */
70
70
  onDismiss?: () => void
71
- /** 深底白字模式(step-6 背景 + 白色前景,warning 例外) */
71
+ /** 深底模式(step-6 背景 + on-emphasis 配對前景;亮色 hue yellow/amber/orange/lime 用深字 --on-emphasis-dark,green 白字例外) */
72
72
  solid?: boolean
73
73
  /**
74
74
  * 2026-05-15 Q3 真 SSOT fix(per user verbatim「同空間兩判斷點」+「不要冰山一角」):