@qijenchen/design-system 0.1.0-beta.89 → 0.1.0-beta.90

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.
@@ -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;AAyB9C,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,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,iLAAiL;IACjL,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,0IAA0I;IAC1I,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,2GAA2G;IAC3G,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAuID,QAAA,MAAM,GAAG,iFAAuD,CAAA;AAKhE,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4CV,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;AAyB9C,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,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,iLAAiL;IACjL,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,0IAA0I;IAC1I,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,2GAA2G;IAC3G,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AA2ID,QAAA,MAAM,GAAG,iFAAuD,CAAA;AAKhE,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4CV,CAAA;AAEV,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,CAAA"}
@@ -118,8 +118,7 @@ function TagInner({ className, color, size, icon: Icon, avatar, onRemove, remove
118
118
  ]
119
119
  }
120
120
  );
121
- if (!isTruncated) return tag;
122
- return /* @__PURE__ */ jsxs(Tooltip, { children: [
121
+ return /* @__PURE__ */ jsxs(Tooltip, { open: isTruncated ? void 0 : false, children: [
123
122
  /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: tag }),
124
123
  /* @__PURE__ */ jsx(TooltipContent, { children })
125
124
  ] });
@@ -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 { useTruncated } from \"@/design-system/hooks/use-truncated\"\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 自動渲染 remove 按鈕並控制尺寸與互動樣式(從集合移除 item) */\n onRemove?: () => void\n /** remove 按鈕的 aria-label 目標名(a11y)。children 為非字串 ReactNode 時建議傳,否則 SR 讀不出移除哪個 tag。預設取 string children。對齊 FileUpload `removeAriaLabel`(同「移除集合項目」場景 + DS `<動作>AriaLabel` 命名慣例)。 */\n removeAriaLabel?: string\n /** @deprecated 2026-07-17 改名為 `removeAriaLabel`(對齊 FileUpload + onRemove callback 語意;dismiss 保留給 Alert/Notice/Toast「通知被忽略」)。舊名過渡一版後移除。 */\n dismissLabel?: string\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({ onRemove, label, solid, color }: { onRemove: () => 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(); onRemove() }}\n aria-label={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, onRemove, removeAriaLabel, dismissLabel, solid, unbounded = false, children, style, ...props }: TagProps,\n forwardedRef: React.ForwardedRef<HTMLDivElement>,\n) {\n const solidClass = solid ? SOLID_CLASSES[color ?? 'neutral'] : undefined\n // 截斷偵測用 SSOT 引擎 `useTruncated`(patterns/element-anatomy 的 hook)。Tag 走**變異型**:\n // - measure = Canvas measureText(flex 內 scrollWidth 不可靠,line 20):observe root、量測內層\n // `[data-tag-text]` span(trigger≠量測元素);回 undefined = 無法量測時保留前一 state。\n // - timing='layoutEffect'(避免 paint flash)/ deps=[children](children 變重量)/ recheckAfterPaint=false\n // (Tag 原本即無 rAF/timeout 二次量)—— 三處變異全走 options,行為零漂移。\n const { ref: ownRef, isTruncated } = useTruncated<HTMLDivElement>({\n measure: (el) => {\n const textSpan = el.querySelector('[data-tag-text]')\n const ctx = getMeasureCtx()\n if (!textSpan || !ctx) return undefined\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 return needed > (textSpan as HTMLElement).clientWidth + 1\n },\n deps: [children],\n timing: 'layoutEffect',\n recheckAfterPaint: false,\n })\n\n const label = typeof children === 'string' ? children : ''\n\n // ── Dev-mode warning:icon 與 avatar 互斥 ──\n // 兩者同為 prefix slot(tag.spec.md Props 表「與 avatar 互斥」),同時傳會並列渲染破壞 Tag\n // 內部結構;原僅 jsdoc/spec 約束會靜默通過 → 2026-07-05 對齊 Button overlayBadge dev-warn\n // 先例(button.tsx「Dev-mode warning」段)補齊家族一致 runtime 防線。\n if (process.env.NODE_ENV !== 'production' && Icon && avatar) {\n // eslint-disable-next-line no-console\n console.warn(\n '[DS Tag] `icon` 與 `avatar` 互斥(同為 prefix slot),同時傳會並列渲染破壞 Tag 內部結構,請只擇一。SSOT:tag.spec.md Props 表 + tag.principles.stories.tsx IconRule。'\n )\n }\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 // 截斷時條件性可 focus(2026-07-06 user 拍板「確認世界級有這樣做就做」——APG Tooltip\n // pattern 原文:tooltip 在元素「收到鍵盤焦點」或 hover 時顯示,trigger 可 focus 是\n // W3C 官方 pattern 前提;Radix Tooltip focus-open 自動生效 → 鍵盤 Tab 到即見全文。\n // 未截斷不入 tab 序(維持 tag.spec「靜態 Tag 不取得 focus」預設,不汙染 tab order)。\n tabIndex={isTruncated ? 0 : undefined}\n className={cn(\n tagVariants({ color, size }),\n solidClass,\n 'w-fit min-w-0 overflow-hidden',\n isTruncated && 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n className,\n )}\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 {onRemove && <TagDismiss onRemove={onRemove} label={removeAriaLabel ?? dismissLabel ?? 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「尺寸」段——Tag 與 Field 尺寸獨立;段名指法免行號漂移)。\n // height = Tag 自身高度(cva h-5/h-6/h-6 = 20/24/24,lg = md alias)。\n // iconSize 全尺寸統一 16(本檔 CAT icon render 硬寫 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「為何無 StateBehavior」段;段名指法免行號漂移)。\n // 唯一行為 dismiss 屬 Inline Action pattern,非 Tag 自有 state。\n states: ['default'],\n tokens: {\n // 2026-07-04 audit 註記:另有動態色相家族不逐一列 — CAT_SUBTLE 12 色相\n // bg-[var(--color-{hue}-1)] + step-7 深字、CAT_SOLID step-6 底 + --on-emphasis(-dark) 配字、\n // neutral solid bg-[var(--color-neutral-9)] — 全由 categorical-color.ts SSOT 衍生\n //(anatomy TOKEN_MAP 以 stripVar 機械衍生,零漂移)。\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 { useTruncated } from \"@/design-system/hooks/use-truncated\"\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 自動渲染 remove 按鈕並控制尺寸與互動樣式(從集合移除 item) */\n onRemove?: () => void\n /** remove 按鈕的 aria-label 目標名(a11y)。children 為非字串 ReactNode 時建議傳,否則 SR 讀不出移除哪個 tag。預設取 string children。對齊 FileUpload `removeAriaLabel`(同「移除集合項目」場景 + DS `<動作>AriaLabel` 命名慣例)。 */\n removeAriaLabel?: string\n /** @deprecated 2026-07-17 改名為 `removeAriaLabel`(對齊 FileUpload + onRemove callback 語意;dismiss 保留給 Alert/Notice/Toast「通知被忽略」)。舊名過渡一版後移除。 */\n dismissLabel?: string\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({ onRemove, label, solid, color }: { onRemove: () => 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(); onRemove() }}\n aria-label={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, onRemove, removeAriaLabel, dismissLabel, solid, unbounded = false, children, style, ...props }: TagProps,\n forwardedRef: React.ForwardedRef<HTMLDivElement>,\n) {\n const solidClass = solid ? SOLID_CLASSES[color ?? 'neutral'] : undefined\n // 截斷偵測用 SSOT 引擎 `useTruncated`(patterns/element-anatomy 的 hook)。Tag 走**變異型**:\n // - measure = Canvas measureText(flex 內 scrollWidth 不可靠,line 20):observe root、量測內層\n // `[data-tag-text]` span(trigger≠量測元素);回 undefined = 無法量測時保留前一 state。\n // - timing='layoutEffect'(避免 paint flash)/ deps=[children](children 變重量)/ recheckAfterPaint=false\n // (Tag 原本即無 rAF/timeout 二次量)—— 三處變異全走 options,行為零漂移。\n const { ref: ownRef, isTruncated } = useTruncated<HTMLDivElement>({\n measure: (el) => {\n const textSpan = el.querySelector('[data-tag-text]')\n const ctx = getMeasureCtx()\n if (!textSpan || !ctx) return undefined\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 return needed > (textSpan as HTMLElement).clientWidth + 1\n },\n deps: [children],\n timing: 'layoutEffect',\n recheckAfterPaint: false,\n })\n\n const label = typeof children === 'string' ? children : ''\n\n // ── Dev-mode warning:icon 與 avatar 互斥 ──\n // 兩者同為 prefix slot(tag.spec.md Props 表「與 avatar 互斥」),同時傳會並列渲染破壞 Tag\n // 內部結構;原僅 jsdoc/spec 約束會靜默通過 → 2026-07-05 對齊 Button overlayBadge dev-warn\n // 先例(button.tsx「Dev-mode warning」段)補齊家族一致 runtime 防線。\n if (process.env.NODE_ENV !== 'production' && Icon && avatar) {\n // eslint-disable-next-line no-console\n console.warn(\n '[DS Tag] `icon` 與 `avatar` 互斥(同為 prefix slot),同時傳會並列渲染破壞 Tag 內部結構,請只擇一。SSOT:tag.spec.md Props 表 + tag.principles.stories.tsx IconRule。'\n )\n }\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 // 截斷時條件性可 focus(2026-07-06 user 拍板「確認世界級有這樣做就做」——APG Tooltip\n // pattern 原文:tooltip 在元素「收到鍵盤焦點」或 hover 時顯示,trigger 可 focus 是\n // W3C 官方 pattern 前提;Radix Tooltip focus-open 自動生效 → 鍵盤 Tab 到即見全文。\n // 未截斷不入 tab 序(維持 tag.spec「靜態 Tag 不取得 focus」預設,不汙染 tab order)。\n tabIndex={isTruncated ? 0 : undefined}\n className={cn(\n tagVariants({ color, size }),\n solidClass,\n 'w-fit min-w-0 overflow-hidden',\n isTruncated && 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n className,\n )}\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 {onRemove && <TagDismiss onRemove={onRemove} label={removeAriaLabel ?? dismissLabel ?? label} solid={solid} color={color ?? 'neutral'} />}\n </div>\n )\n\n // 永遠 wrap Tooltip、以 open 控制可見(對齊 truncated-text.spec.md:49「永遠 wrap」+ :55「❌ 條件 wrap」canonical\n // + useTruncated 家族;tooltip.principles.stories.tsx:190「沒被截斷就不該顯示 tooltip」由 open={false} 機械保證)。\n // 未截斷 open={false} → tooltip 靜默(TooltipTrigger asChild 只把 handler/data-state 合併到 tag div,零多餘 DOM、\n // 視覺與裸 tag 等價,pixel-identical)。always-wrap 保 tag div 生命週期穩定 → isTruncated false→true 不 remount:\n // 先前條件-wrap(未截斷回裸 tag / 截斷才 wrap)切換 JSX 樹會 remount tag div,但 useTruncated deps=[children] 未變\n // → effect 不重跑重新 observe 新 div → shared RO 卡在舊 detached div → isTruncated 卡 true(未截斷仍顯示 tooltip)。\n return (\n <Tooltip open={isTruncated ? undefined : false}>\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「尺寸」段——Tag 與 Field 尺寸獨立;段名指法免行號漂移)。\n // height = Tag 自身高度(cva h-5/h-6/h-6 = 20/24/24,lg = md alias)。\n // iconSize 全尺寸統一 16(本檔 CAT icon render 硬寫 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「為何無 StateBehavior」段;段名指法免行號漂移)。\n // 唯一行為 dismiss 屬 Inline Action pattern,非 Tag 自有 state。\n states: ['default'],\n tokens: {\n // 2026-07-04 audit 註記:另有動態色相家族不逐一列 — CAT_SUBTLE 12 色相\n // bg-[var(--color-{hue}-1)] + step-7 深字、CAT_SOLID step-6 底 + --on-emphasis(-dark) 配字、\n // neutral solid bg-[var(--color-neutral-9)] — 全由 categorical-color.ts SSOT 衍生\n //(anatomy TOKEN_MAP 以 stripVar 機械衍生,零漂移)。\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 +1 @@
1
- {"version":3,"file":"use-truncated.d.ts","sourceRoot":"","sources":["../../src/hooks/use-truncated.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA2C9B,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,WAAW,KAAK,OAAO,GAAG,SAAS,CAAA;IAC1D,8EAA8E;IAC9E,IAAI,CAAC,EAAE,KAAK,CAAC,cAAc,CAAA;IAC3B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,gHAAgH;IAChH,MAAM,CAAC,EAAE,QAAQ,GAAG,cAAc,CAAA;CACnC;AAID;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAC9D,OAAO,GAAE,mBAAwB,GAChC;IAAE,GAAG,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAwCjE"}
1
+ {"version":3,"file":"use-truncated.d.ts","sourceRoot":"","sources":["../../src/hooks/use-truncated.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA4C9B,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,WAAW,KAAK,OAAO,GAAG,SAAS,CAAA;IAC1D,8EAA8E;IAC9E,IAAI,CAAC,EAAE,KAAK,CAAC,cAAc,CAAA;IAC3B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,gHAAgH;IAChH,MAAM,CAAC,EAAE,QAAQ,GAAG,cAAc,CAAA;CACnC;AAID;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAC9D,OAAO,GAAE,mBAAwB,GAChC;IAAE,GAAG,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAwCjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-truncated.js","sources":["../../src/hooks/use-truncated.ts"],"sourcesContent":["import * as React from 'react'\n\n// ── useTruncated — 單行文字截斷偵測引擎(SSOT)─────────────────────────────────\n// 收斂 Breadcrumb TruncatedLabel / DataTable TruncateCell / Tag 三處**逐字重複**的\n// module-level shared ResizeObserver 引擎 + isTruncated 狀態機為唯一 owner(M17 SSOT)。\n//\n// 世界級對照(M8):MUI/MUI-X 官方無第一方 primitive,社群共識 = 「hook 量測 scrollWidth>clientWidth\n// + ref + 以 Tooltip `open` prop 條件控制」(issue #37211);此 hook 即該低階量測層,\n// 上層 `<TruncatedText>`(patterns/element-anatomy)對應 Ant `Typography.Text ellipsis` 高階元件。\n//\n// 三處變異點皆以 options 注入,**行為零漂移**:\n// - Breadcrumb / DataTable:預設 scrollWidth>clientWidth。\n// - Tag:trigger≠量測元素(observe root、量測內層 span)+ Canvas measureText(flex 內 scrollWidth\n// 不可靠)+ useLayoutEffect(避免 paint flash)+ deps=[children] → 全走 options。\n\ntype RoCallback = (entry: ResizeObserverEntry) => void\n\nlet sharedRO: ResizeObserver | null = null\nconst sharedROCallbacks = new WeakMap<Element, RoCallback>()\n\n// 全 DS 共用單一 ResizeObserver,dispatch 到 per-element callback(2026-04-22 DataTable D3 perf audit\n// 成果:10 col × 100 row = 1 RO 而非 1000)。element 卸載時 cleanup,singleton RO 本身不 disconnect。\nfunction getSharedRO(): ResizeObserver {\n if (sharedRO) return sharedRO\n sharedRO = new ResizeObserver((entries) => {\n entries.forEach((e) => {\n const cb = sharedROCallbacks.get(e.target)\n if (cb) cb(e)\n })\n })\n return sharedRO\n}\n\nfunction observeShared(el: Element, cb: RoCallback): () => void {\n const obs = getSharedRO()\n sharedROCallbacks.set(el, cb)\n obs.observe(el)\n return () => {\n sharedROCallbacks.delete(el)\n obs.unobserve(el)\n }\n}\n\nexport interface UseTruncatedOptions {\n /**\n * 偵測策略:回傳 `true` = 截斷、`false` = 未截斷、`undefined` = 本次無法量測(保留前一狀態)。\n * 預設 = `el.scrollWidth > el.clientWidth`(觀察元素自身)。\n * Tag 傳自訂 fn:observe root、內部 `querySelector('[data-tag-text]')` + Canvas measureText。\n */\n measure?: (observedEl: HTMLElement) => boolean | undefined\n /** deps 變更時 cleanup + 重新量測 / 重新訂閱(Tag 傳 `[children]`)。預設 `[]`(mount-once)。 */\n deps?: React.DependencyList\n /**\n * 是否在首次 paint 後以 `requestAnimationFrame` + `setTimeout(100)` 再量一次,捕獲首幀 layout\n * 未完成 / 字型 async load 的假陰性(Breadcrumb 已採此 robust 版)。預設 `true`。\n */\n recheckAfterPaint?: boolean\n /** `'layoutEffect'` = `useLayoutEffect`(量測在 paint 前,避免 flash;Tag 用)/ `'effect'` = `useEffect`。預設 `'effect'`。 */\n timing?: 'effect' | 'layoutEffect'\n}\n\nconst defaultMeasure = (el: HTMLElement): boolean => el.scrollWidth > el.clientWidth\n\n/**\n * 偵測 ref 所指元素的文字是否被截斷,回傳 `{ ref, isTruncated }`。\n * `isTruncated` 用來驅動 Tooltip「僅截斷時顯示」(對齊 `tooltip.principles.stories.tsx`「沒被截斷就不該顯示 tooltip」)。\n */\nexport function useTruncated<E extends HTMLElement = HTMLElement>(\n options: UseTruncatedOptions = {},\n): { ref: React.MutableRefObject<E | null>; isTruncated: boolean } {\n const { measure = defaultMeasure, deps = [], recheckAfterPaint = true, timing = 'effect' } = options\n // MutableRefObject(consumer 可經 callback ref 寫入 .current 以與 forwardedRef 合併,如 Tag)。\n const ref = React.useRef<E | null>(null)\n const [isTruncated, setIsTruncated] = React.useState(false)\n\n // measure 每 render 可能是新 closure(consumer inline);收進 ref 讓 effect 不因它重訂閱,\n // 但 RO 回呼仍讀到最新版。\n const measureRef = React.useRef(measure)\n measureRef.current = measure\n\n // timing 為 call-site 固定字面值(某 consumer 永遠同一值)→ 每個 component instance 每 render\n // 呼叫同一個 effect hook,符合 rules-of-hooks。\n const useIsoEffect = timing === 'layoutEffect' ? React.useLayoutEffect : React.useEffect\n\n useIsoEffect(() => {\n const el = ref.current\n if (!el) return\n const check = () => {\n const r = measureRef.current(el)\n if (r !== undefined) setIsTruncated(r)\n }\n check()\n let raf = 0\n let t: ReturnType<typeof setTimeout> | undefined\n if (recheckAfterPaint) {\n raf = requestAnimationFrame(check)\n t = setTimeout(check, 100)\n }\n const cleanup = observeShared(el, check)\n return () => {\n if (raf) cancelAnimationFrame(raf)\n if (t) clearTimeout(t)\n cleanup()\n }\n // deps 由 consumer 決定(Tag: [children];其餘: []);measure 走 ref 不需進 deps。\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps)\n\n return { ref, isTruncated }\n}\n"],"names":[],"mappings":";AAiBA,IAAI,WAAkC;AACtC,MAAM,wCAAwB,QAAA;AAI9B,SAAS,cAA8B;AACrC,MAAI,SAAU,QAAO;AACrB,aAAW,IAAI,eAAe,CAAC,YAAY;AACzC,YAAQ,QAAQ,CAAC,MAAM;AACrB,YAAM,KAAK,kBAAkB,IAAI,EAAE,MAAM;AACzC,UAAI,OAAO,CAAC;AAAA,IACd,CAAC;AAAA,EACH,CAAC;AACD,SAAO;AACT;AAEA,SAAS,cAAc,IAAa,IAA4B;AAC9D,QAAM,MAAM,YAAA;AACZ,oBAAkB,IAAI,IAAI,EAAE;AAC5B,MAAI,QAAQ,EAAE;AACd,SAAO,MAAM;AACX,sBAAkB,OAAO,EAAE;AAC3B,QAAI,UAAU,EAAE;AAAA,EAClB;AACF;AAoBA,MAAM,iBAAiB,CAAC,OAA6B,GAAG,cAAc,GAAG;AAMlE,SAAS,aACd,UAA+B,IACkC;AACjE,QAAM,EAAE,UAAU,gBAAgB,OAAO,CAAA,GAAI,oBAAoB,MAAM,SAAS,SAAA,IAAa;AAE7F,QAAM,MAAM,MAAM,OAAiB,IAAI;AACvC,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,KAAK;AAI1D,QAAM,aAAa,MAAM,OAAO,OAAO;AACvC,aAAW,UAAU;AAIrB,QAAM,eAAe,WAAW,iBAAiB,MAAM,kBAAkB,MAAM;AAE/E,eAAa,MAAM;AACjB,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AACT,UAAM,QAAQ,MAAM;AAClB,YAAM,IAAI,WAAW,QAAQ,EAAE;AAC/B,UAAI,MAAM,OAAW,gBAAe,CAAC;AAAA,IACvC;AACA,UAAA;AACA,QAAI,MAAM;AACV,QAAI;AACJ,QAAI,mBAAmB;AACrB,YAAM,sBAAsB,KAAK;AACjC,UAAI,WAAW,OAAO,GAAG;AAAA,IAC3B;AACA,UAAM,UAAU,cAAc,IAAI,KAAK;AACvC,WAAO,MAAM;AACX,UAAI,0BAA0B,GAAG;AACjC,UAAI,gBAAgB,CAAC;AACrB,cAAA;AAAA,IACF;AAAA,EAGF,GAAG,IAAI;AAEP,SAAO,EAAE,KAAK,YAAA;AAChB;"}
1
+ {"version":3,"file":"use-truncated.js","sources":["../../src/hooks/use-truncated.ts"],"sourcesContent":["import * as React from 'react'\n\n// ── useTruncated — 單行文字截斷偵測引擎(SSOT)─────────────────────────────────\n// 收斂 Breadcrumb TruncatedLabel / DataTable TruncateCell(兩處 module-level shared ResizeObserver 引擎\n// **逐字重複**,僅變數名不同)+ Tag(原 per-instance `new ResizeObserver`)三處的截斷偵測 + isTruncated\n// 狀態機為唯一 owner(M17 SSOT);Tag 亦收斂進共用 shared RO,行為等價。\n//\n// 世界級對照(M8):MUI/MUI-X 官方無第一方 primitive,社群共識 = 「hook 量測 scrollWidth>clientWidth\n// + ref + 以 Tooltip `open` prop 條件控制」(issue #37211);此 hook 即該低階量測層,\n// 上層 `<TruncatedText>`(patterns/element-anatomy)對應 Ant `Typography.Text ellipsis` 高階元件。\n//\n// 三處變異點皆以 options 注入,**行為零漂移**:\n// - Breadcrumb / DataTable:預設 scrollWidth>clientWidth。\n// - Tag:trigger≠量測元素(observe root、量測內層 span)+ Canvas measureText(flex 內 scrollWidth\n// 不可靠)+ useLayoutEffect(避免 paint flash)+ deps=[children] → 全走 options。\n\ntype RoCallback = (entry: ResizeObserverEntry) => void\n\nlet sharedRO: ResizeObserver | null = null\nconst sharedROCallbacks = new WeakMap<Element, RoCallback>()\n\n// 全 DS 共用單一 ResizeObserver,dispatch 到 per-element callback(2026-04-22 DataTable D3 perf audit\n// 成果:10 col × 100 row = 1 RO 而非 1000)。element 卸載時 cleanup,singleton RO 本身不 disconnect。\nfunction getSharedRO(): ResizeObserver {\n if (sharedRO) return sharedRO\n sharedRO = new ResizeObserver((entries) => {\n entries.forEach((e) => {\n const cb = sharedROCallbacks.get(e.target)\n if (cb) cb(e)\n })\n })\n return sharedRO\n}\n\nfunction observeShared(el: Element, cb: RoCallback): () => void {\n const obs = getSharedRO()\n sharedROCallbacks.set(el, cb)\n obs.observe(el)\n return () => {\n sharedROCallbacks.delete(el)\n obs.unobserve(el)\n }\n}\n\nexport interface UseTruncatedOptions {\n /**\n * 偵測策略:回傳 `true` = 截斷、`false` = 未截斷、`undefined` = 本次無法量測(保留前一狀態)。\n * 預設 = `el.scrollWidth > el.clientWidth`(觀察元素自身)。\n * Tag 傳自訂 fn:observe root、內部 `querySelector('[data-tag-text]')` + Canvas measureText。\n */\n measure?: (observedEl: HTMLElement) => boolean | undefined\n /** deps 變更時 cleanup + 重新量測 / 重新訂閱(Tag 傳 `[children]`)。預設 `[]`(mount-once)。 */\n deps?: React.DependencyList\n /**\n * 是否在首次 paint 後以 `requestAnimationFrame` + `setTimeout(100)` 再量一次,捕獲首幀 layout\n * 未完成 / 字型 async load 的假陰性(Breadcrumb 已採此 robust 版)。預設 `true`。\n */\n recheckAfterPaint?: boolean\n /** `'layoutEffect'` = `useLayoutEffect`(量測在 paint 前,避免 flash;Tag 用)/ `'effect'` = `useEffect`。預設 `'effect'`。 */\n timing?: 'effect' | 'layoutEffect'\n}\n\nconst defaultMeasure = (el: HTMLElement): boolean => el.scrollWidth > el.clientWidth\n\n/**\n * 偵測 ref 所指元素的文字是否被截斷,回傳 `{ ref, isTruncated }`。\n * `isTruncated` 用來驅動 Tooltip「僅截斷時顯示」(對齊 `tooltip.principles.stories.tsx`「沒被截斷就不該顯示 tooltip」)。\n */\nexport function useTruncated<E extends HTMLElement = HTMLElement>(\n options: UseTruncatedOptions = {},\n): { ref: React.MutableRefObject<E | null>; isTruncated: boolean } {\n const { measure = defaultMeasure, deps = [], recheckAfterPaint = true, timing = 'effect' } = options\n // MutableRefObject(consumer 可經 callback ref 寫入 .current 以與 forwardedRef 合併,如 Tag)。\n const ref = React.useRef<E | null>(null)\n const [isTruncated, setIsTruncated] = React.useState(false)\n\n // measure 每 render 可能是新 closure(consumer inline);收進 ref 讓 effect 不因它重訂閱,\n // 但 RO 回呼仍讀到最新版。\n const measureRef = React.useRef(measure)\n measureRef.current = measure\n\n // timing 為 call-site 固定字面值(某 consumer 永遠同一值)→ 每個 component instance 每 render\n // 呼叫同一個 effect hook,符合 rules-of-hooks。\n const useIsoEffect = timing === 'layoutEffect' ? React.useLayoutEffect : React.useEffect\n\n useIsoEffect(() => {\n const el = ref.current\n if (!el) return\n const check = () => {\n const r = measureRef.current(el)\n if (r !== undefined) setIsTruncated(r)\n }\n check()\n let raf = 0\n let t: ReturnType<typeof setTimeout> | undefined\n if (recheckAfterPaint) {\n raf = requestAnimationFrame(check)\n t = setTimeout(check, 100)\n }\n const cleanup = observeShared(el, check)\n return () => {\n if (raf) cancelAnimationFrame(raf)\n if (t) clearTimeout(t)\n cleanup()\n }\n // deps 由 consumer 決定(Tag: [children];其餘: []);measure 走 ref 不需進 deps。\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps)\n\n return { ref, isTruncated }\n}\n"],"names":[],"mappings":";AAkBA,IAAI,WAAkC;AACtC,MAAM,wCAAwB,QAAA;AAI9B,SAAS,cAA8B;AACrC,MAAI,SAAU,QAAO;AACrB,aAAW,IAAI,eAAe,CAAC,YAAY;AACzC,YAAQ,QAAQ,CAAC,MAAM;AACrB,YAAM,KAAK,kBAAkB,IAAI,EAAE,MAAM;AACzC,UAAI,OAAO,CAAC;AAAA,IACd,CAAC;AAAA,EACH,CAAC;AACD,SAAO;AACT;AAEA,SAAS,cAAc,IAAa,IAA4B;AAC9D,QAAM,MAAM,YAAA;AACZ,oBAAkB,IAAI,IAAI,EAAE;AAC5B,MAAI,QAAQ,EAAE;AACd,SAAO,MAAM;AACX,sBAAkB,OAAO,EAAE;AAC3B,QAAI,UAAU,EAAE;AAAA,EAClB;AACF;AAoBA,MAAM,iBAAiB,CAAC,OAA6B,GAAG,cAAc,GAAG;AAMlE,SAAS,aACd,UAA+B,IACkC;AACjE,QAAM,EAAE,UAAU,gBAAgB,OAAO,CAAA,GAAI,oBAAoB,MAAM,SAAS,SAAA,IAAa;AAE7F,QAAM,MAAM,MAAM,OAAiB,IAAI;AACvC,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,KAAK;AAI1D,QAAM,aAAa,MAAM,OAAO,OAAO;AACvC,aAAW,UAAU;AAIrB,QAAM,eAAe,WAAW,iBAAiB,MAAM,kBAAkB,MAAM;AAE/E,eAAa,MAAM;AACjB,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AACT,UAAM,QAAQ,MAAM;AAClB,YAAM,IAAI,WAAW,QAAQ,EAAE;AAC/B,UAAI,MAAM,OAAW,gBAAe,CAAC;AAAA,IACvC;AACA,UAAA;AACA,QAAI,MAAM;AACV,QAAI;AACJ,QAAI,mBAAmB;AACrB,YAAM,sBAAsB,KAAK;AACjC,UAAI,WAAW,OAAO,GAAG;AAAA,IAC3B;AACA,UAAM,UAAU,cAAc,IAAI,KAAK;AACvC,WAAO,MAAM;AACX,UAAI,0BAA0B,GAAG;AACjC,UAAI,gBAAgB,CAAC;AACrB,cAAA;AAAA,IACF;AAAA,EAGF,GAAG,IAAI;AAEP,SAAO,EAAE,KAAK,YAAA;AAChB;"}
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cva } from "class-variance-authority";
4
- import { lineClampClass, cn } from "../../lib/utils.js";
4
+ import { cn, lineClampClass } from "../../lib/utils.js";
5
5
  import { Avatar } from "../../components/Avatar/avatar.js";
6
6
  import { Tooltip, TooltipTrigger, TooltipContent } from "../../components/Tooltip/tooltip.js";
7
7
  const ICON_SIZE = {
package/llms-full.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # @qijenchen/design-system — 完整設計參考(llms-full)
2
2
 
3
- > 全 component / pattern 的 variants / sizes / 禁止事項。build-time 從 spec.md frontmatter 生成,禁手改。v0.1.0-beta.89
3
+ > 全 component / pattern 的 variants / sizes / 禁止事項。build-time 從 spec.md frontmatter 生成,禁手改。v0.1.0-beta.90
4
4
 
5
5
  # Components
6
6
 
package/llms.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # @qijenchen/design-system
2
2
 
3
3
  > World-class React design system(Radix/shadcn + Tailwind v4 + 自訂 design token)。
4
- > 57 components + 4 public patterns + design tokens。v0.1.0-beta.89
4
+ > 57 components + 4 public patterns + design tokens。v0.1.0-beta.90
5
5
 
6
6
  本檔由 source(spec.md frontmatter + Storybook index)build-time 自動生成,**禁手改**(CI --check drift gate 守)。
7
7
  每元件 / pattern 的完整 variants / sizes / 禁止事項 全文見 [llms-full.txt](./llms-full.txt)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qijenchen/design-system",
3
- "version": "0.1.0-beta.89",
3
+ "version": "0.1.0-beta.90",
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",
@@ -13,7 +13,7 @@ import {
13
13
  DropdownMenuItem,
14
14
  } from '@/design-system/components/DropdownMenu/dropdown-menu'
15
15
 
16
- // ── TruncatedLabel ── 2026-07-19:truncate+tooltip 引擎 + presentation 已抽成 SSOT primitive
16
+ // ── TruncatedText ── 2026-07-19:truncate+tooltip 引擎 + presentation 已抽成 SSOT primitive
17
17
  // `patterns/element-anatomy/truncated-text`(`<TruncatedText>` 消費 `useTruncated` hook)。本檔改為
18
18
  // 消費 `<TruncatedText display="block" tooltip={fullText}>`,原 file-local shared RO + TruncatedLabel 已移除。
19
19
 
@@ -146,7 +146,7 @@ const BreadcrumbList = React.forwardRef<HTMLOListElement, BreadcrumbListProps>(
146
146
  const declarativeContent = React.useMemo(() => {
147
147
  if (!items) return null
148
148
  // 2026-07-04 修:非字串 label 的 key 原用 Math.random() → 每次 render 新 key 造成
149
- // BreadcrumbItem unmount/remount(重置 TruncatedLabel ResizeObserver 與 state)→ 改 stable idx key
149
+ // BreadcrumbItem unmount/remount(重置 TruncatedText ResizeObserver 與 state)→ 改 stable idx key
150
150
  const renderItem = (spec: BreadcrumbItemSpec, role: 'root' | 'middle' | 'current', idx: number) => (
151
151
  <BreadcrumbItem key={`${role}-${typeof spec.label === 'string' ? spec.label : idx}`} role={role}>
152
152
  {role === 'current'
@@ -342,7 +342,7 @@ BreadcrumbList.displayName = 'BreadcrumbList'
342
342
  * 設計回應 user 兩 challenges:
343
343
  * (a) Root 也 truncate(shrink:3,不是 shrink-0)
344
344
  * (b) 不用 fixed max-width — flex-shrink hierarchy 容器寬時自然展開不浪費空間,
345
- * 窄時按優先級縮 + TruncatedLabel 內部 CSS truncate + tooltip。
345
+ * 窄時按優先級縮 + TruncatedText 內部 CSS truncate + tooltip。
346
346
  */
347
347
  interface BreadcrumbItemProps extends React.ComponentPropsWithoutRef<'li'> {
348
348
  role?: 'root' | 'middle' | 'current' | 'ellipsis'
@@ -396,7 +396,7 @@ const BreadcrumbLink = React.forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(
396
396
  ({ asChild, className, children, startIcon: StartIcon, ...props }, ref) => {
397
397
  const { size } = React.useContext(BreadcrumbContext)
398
398
  // 2026-05-12 fix(user 抓 image 2 Deep story 麵包屑沒符合 single-line + truncate canonical):
399
- // 純文字 children → auto-wrap TruncatedLabel(canonical「single-line + ellipsis + tooltip
399
+ // 純文字 children → auto-wrap TruncatedText(canonical「single-line + ellipsis + tooltip
400
400
  // on truncate」per spec.md / Polaris breadcrumb)。Non-string children(consumer 自訂 icon+text
401
401
  // 結構)→ pass-through 不 force-wrap(consumer own truncate)。
402
402
  const wrappedChildren = typeof children === 'string'
@@ -446,7 +446,7 @@ interface BreadcrumbPageProps extends React.ComponentPropsWithoutRef<'span'> {
446
446
  const BreadcrumbPage = React.forwardRef<HTMLSpanElement, BreadcrumbPageProps>(
447
447
  ({ className, children, startIcon: StartIcon, ...props }, ref) => {
448
448
  const { size } = React.useContext(BreadcrumbContext)
449
- // 2026-05-12 fix(同 BreadcrumbLink):純文字 children → auto-wrap TruncatedLabel
449
+ // 2026-05-12 fix(同 BreadcrumbLink):純文字 children → auto-wrap TruncatedText
450
450
  const wrappedChildren = typeof children === 'string'
451
451
  ? <TruncatedText display="block" tooltip={children}>{children}</TruncatedText>
452
452
  : children
@@ -404,7 +404,7 @@ function columnSizeStyle(
404
404
  const SYSTEM_COL_IDS = new Set([SELECT_COL_ID, '__drag__', '__actions__'])
405
405
  const isSystemColumn = (colId: string) => SYSTEM_COL_IDS.has(colId)
406
406
 
407
- // ── TruncateCell ── 2026-07-19:truncate+tooltip 引擎 + presentation 已抽成 SSOT primitive
407
+ // ── TruncatedText ── 2026-07-19:truncate+tooltip 引擎 + presentation 已抽成 SSOT primitive
408
408
  // `patterns/element-anatomy/truncated-text`(`<TruncatedText>` 消費 `useTruncated` hook)。原 file-local
409
409
  // shared RO(2026-04-22 D3 perf audit 的「全 DS 共用單一 RO」特性由 hook 承接)+ TruncateCell 已移除,
410
410
  // 改用 `<TruncatedText tooltip={children} className={...}>`(display 預設 inline,保 cell baseline)。
@@ -1473,10 +1473,10 @@ function DataTableInner<TData>(
1473
1473
  const meta = cell.column.columnDef.meta
1474
1474
  const colType = meta?.type as ColumnType | undefined
1475
1475
  const wrap = autoRowHeight && meta?.wrap === true
1476
- // 已知 compound 欄位(Tag / PersonDisplay / LinkInput 等自帶 layout)直接 bypass TruncateCell,
1476
+ // 已知 compound 欄位(Tag / PersonDisplay / LinkInput 等自帶 layout)直接 bypass TruncatedText,
1477
1477
  // 因為 `truncate` 的 inline baseline context 會破壞自訂 layout 的垂直對齊。
1478
1478
  // 2026-05-09 D-path:date / time 加入(showDisplayEndIcon → Field naked-view 需 full width 才能
1479
- // 右對齊 ItemSuffix。TruncateCell 的 `<span truncate min-w-0>` block-display 會 collapse Field
1479
+ // 右對齊 ItemSuffix。TruncatedText 的 `<span truncate min-w-0>` block-display 會 collapse Field
1480
1480
  // to content size,讓 Calendar / Clock icon 緊貼 value text 而非右邊緣)。
1481
1481
  const isKnownCompound = colType === 'select' || colType === 'multiSelect' || colType === 'person' || colType === 'multiPerson' || colType === 'url' || colType === 'date' || colType === 'time'
1482
1482
  const rowId = cell.row.id
@@ -1517,8 +1517,8 @@ function DataTableInner<TData>(
1517
1517
  content = flexRender(cell.column.columnDef.cell, cell.getContext())
1518
1518
  }
1519
1519
  // Consumer 自訂 cell(無 colType)若回傳 React element,視為 compound — consumer 自己處理
1520
- // 對齊與截斷。回傳 primitive(string / number)才走 TruncateCell
1521
- // 理由:TruncateCell 的 `span.truncate` 強制 white-space:nowrap + inline baseline,
1520
+ // 對齊與截斷。回傳 primitive(string / number)才走 TruncatedText
1521
+ // 理由:TruncatedText 的 `span.truncate` 強制 white-space:nowrap + inline baseline,
1522
1522
  // 對 inline-flex / icon+text 自訂結構會拉歪(見 circular-progress sync table 案例)。
1523
1523
  // **edit mode bypass**(2026-05-05 v9 Bug 2 修):editing cell 內部是 Field 控件
1524
1524
  // (Input/Textarea/Select etc.)自管 layout + 替代元素(textarea)不該被包進 inline span
@@ -1752,7 +1752,7 @@ function DataTableInner<TData>(
1752
1752
  // - **沒有** cell 自己 box-shadow ring — focus / hover / open ring 由 Field naked 自帶
1753
1753
  // state machine 提供(對齊 user「狀態樣式取決於原輸入框」reminder)
1754
1754
  // 字級隨 size:sm/md text-body / lg text-body-lg(fieldDisplayTextClass),對齊 Field family
1755
- // size→font SSOT。此為非-Field content(consumer 自訂 cell / TruncateCell 純文字)的 fallback 字級;
1755
+ // size→font SSOT。此為非-Field content(consumer 自訂 cell / TruncatedText 純文字)的 fallback 字級;
1756
1756
  // typed cell 各自的 Field 控件已自帶 size→font(cell-registry 傳 size)。
1757
1757
  'group/cell flex text-foreground font-normal shrink-0 relative self-stretch',
1758
1758
  fieldDisplayTextClass(size),
@@ -208,10 +208,14 @@ function TagInner(
208
208
  </div>
209
209
  )
210
210
 
211
- if (!isTruncated) return tag
212
-
211
+ // 永遠 wrap Tooltip、以 open 控制可見(對齊 truncated-text.spec.md:49「永遠 wrap」+ :55「❌ 條件 wrap」canonical
212
+ // + useTruncated 家族;tooltip.principles.stories.tsx:190「沒被截斷就不該顯示 tooltip」由 open={false} 機械保證)。
213
+ // 未截斷 open={false} → tooltip 靜默(TooltipTrigger asChild 只把 handler/data-state 合併到 tag div,零多餘 DOM、
214
+ // 視覺與裸 tag 等價,pixel-identical)。always-wrap 保 tag div 生命週期穩定 → isTruncated false→true 不 remount:
215
+ // 先前條件-wrap(未截斷回裸 tag / 截斷才 wrap)切換 JSX 樹會 remount tag div,但 useTruncated deps=[children] 未變
216
+ // → effect 不重跑重新 observe 新 div → shared RO 卡在舊 detached div → isTruncated 卡 true(未截斷仍顯示 tooltip)。
213
217
  return (
214
- <Tooltip>
218
+ <Tooltip open={isTruncated ? undefined : false}>
215
219
  <TooltipTrigger asChild>{tag}</TooltipTrigger>
216
220
  <TooltipContent>{children}</TooltipContent>
217
221
  </Tooltip>
@@ -1,8 +1,9 @@
1
1
  import * as React from 'react'
2
2
 
3
3
  // ── useTruncated — 單行文字截斷偵測引擎(SSOT)─────────────────────────────────
4
- // 收斂 Breadcrumb TruncatedLabel / DataTable TruncateCell / Tag 三處**逐字重複**的
5
- // module-level shared ResizeObserver 引擎 + isTruncated 狀態機為唯一 owner(M17 SSOT)。
4
+ // 收斂 Breadcrumb TruncatedLabel / DataTable TruncateCell(兩處 module-level shared ResizeObserver 引擎
5
+ // **逐字重複**,僅變數名不同)+ Tag(原 per-instance `new ResizeObserver`)三處的截斷偵測 + isTruncated
6
+ // 狀態機為唯一 owner(M17 SSOT);Tag 亦收斂進共用 shared RO,行為等價。
6
7
  //
7
8
  // 世界級對照(M8):MUI/MUI-X 官方無第一方 primitive,社群共識 = 「hook 量測 scrollWidth>clientWidth
8
9
  // + ref + 以 Tooltip `open` prop 條件控制」(issue #37211);此 hook 即該低階量測層,