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

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.
Files changed (62) hide show
  1. package/CLAUDE.md +1 -1
  2. package/dist/components/Checkbox/checkbox.d.ts.map +1 -1
  3. package/dist/components/Checkbox/checkbox.js +28 -3
  4. package/dist/components/Checkbox/checkbox.js.map +1 -1
  5. package/dist/components/PeoplePicker/person-display.d.ts.map +1 -1
  6. package/dist/components/PeoplePicker/person-display.js +1 -1
  7. package/dist/components/PeoplePicker/person-display.js.map +1 -1
  8. package/dist/components/RadioGroup/radio-group.d.ts +1 -1
  9. package/dist/components/RadioGroup/radio-group.d.ts.map +1 -1
  10. package/dist/components/RadioGroup/radio-group.js +46 -14
  11. package/dist/components/RadioGroup/radio-group.js.map +1 -1
  12. package/dist/components/Rating/rating.d.ts.map +1 -1
  13. package/dist/components/Rating/rating.js +5 -3
  14. package/dist/components/Rating/rating.js.map +1 -1
  15. package/dist/components/Slider/slider.d.ts +1 -1
  16. package/dist/components/Slider/slider.d.ts.map +1 -1
  17. package/dist/components/Slider/slider.js +11 -6
  18. package/dist/components/Slider/slider.js.map +1 -1
  19. package/dist/components/Switch/switch.d.ts +9 -7
  20. package/dist/components/Switch/switch.d.ts.map +1 -1
  21. package/dist/components/Switch/switch.js +30 -5
  22. package/dist/components/Switch/switch.js.map +1 -1
  23. package/dist/components/Tabs/tabs.d.ts.map +1 -1
  24. package/dist/components/Tabs/tabs.js +9 -3
  25. package/dist/components/Tabs/tabs.js.map +1 -1
  26. package/ds-canonical/hooks/check_consumer_app_invariants.sh +9 -0
  27. package/ds-canonical/references/story-baseline-registry.json +18 -2
  28. package/ds-canonical/references/ui-dev-rules.md +21 -0
  29. package/llms-full.txt +1 -1
  30. package/llms.txt +1 -1
  31. package/package.json +1 -1
  32. package/src/components/Accordion/accordion.spec.md +1 -1
  33. package/src/components/AppShell/app-shell.stories.tsx +3 -3
  34. package/src/components/Carousel/carousel.principles.stories.tsx +3 -3
  35. package/src/components/Checkbox/checkbox.spec.md +9 -1
  36. package/src/components/Checkbox/checkbox.tsx +45 -3
  37. package/src/components/Field/field-controls.spec.md +3 -1
  38. package/src/components/Field/field.anatomy.stories.tsx +3 -1
  39. package/src/components/Field/field.stories.tsx +14 -1
  40. package/src/components/PeoplePicker/person-display.tsx +4 -3
  41. package/src/components/ProgressBar/progress-bar.anatomy.stories.tsx +2 -2
  42. package/src/components/RadioGroup/radio-group.anatomy.stories.tsx +1 -1
  43. package/src/components/RadioGroup/radio-group.spec.md +2 -0
  44. package/src/components/RadioGroup/radio-group.tsx +59 -15
  45. package/src/components/Rating/rating.tsx +7 -3
  46. package/src/components/Sidebar/sidebar.spec.md +2 -0
  47. package/src/components/Slider/slider.anatomy.stories.tsx +2 -1
  48. package/src/components/Slider/slider.spec.md +8 -7
  49. package/src/components/Slider/slider.tsx +24 -11
  50. package/src/components/Switch/switch.anatomy.stories.tsx +4 -4
  51. package/src/components/Switch/switch.principles.stories.tsx +3 -3
  52. package/src/components/Switch/switch.spec.md +10 -6
  53. package/src/components/Switch/switch.tsx +45 -12
  54. package/src/components/Tabs/tabs.anatomy.stories.tsx +3 -3
  55. package/src/components/Tabs/tabs.principles.stories.tsx +1 -1
  56. package/src/components/Tabs/tabs.spec.md +4 -0
  57. package/src/components/Tabs/tabs.stories.tsx +4 -4
  58. package/src/components/Tabs/tabs.tsx +9 -3
  59. package/src/patterns/header-canonical/header-canonical.spec.md +1 -1
  60. package/src/styles/base.css +9 -2
  61. package/src/tokens/color/color.spec.md +7 -5
  62. package/src/tokens/color/semantic.css +5 -1
@@ -1 +1 @@
1
- {"version":3,"file":"rating.js","sources":["../../../src/components/Rating/rating.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 { Star, type LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { useFieldContext, useResolvedFieldSize, useResolvedFieldDisabled } from '@/design-system/components/Field/field-context'\n\n/**\n * Rating — 星星評分元件\n *\n * 世界級對照:Ant Design `<Rate>`、Material MUI `<Rating>`。\n * shadcn 核心沒有 Rating,本元件自建。\n *\n * ── 使用情境 ──\n * - review / feedback:商品評分 / 服務評分(可編輯 + 唯讀兩種)\n * - display:已提交評分的唯讀呈現(商品清單星等)\n *\n * ── 視覺 ──\n * 填色用 `var(--warning)`(yellow-6,世界級黃星 convention;與 warning 語意共用色相\n * 但語境不同,評分 = UX convention color 非 status)。\n * 空色用 `var(--color-neutral-4)`(灰色;與 disabled/empty 同級)。\n *\n * ── 互動 ──\n * interactive(預設):hover 預覽、click 設值、keyboard Left/Right 改值\n * readOnly:純顯示,不響應 hover / click\n *\n * ── 精度 ──\n * precision=\"full\"(預設) — 整星(1, 2, 3, 4, 5)\n * precision=\"half\" — 半星(0.5, 1, 1.5, 2, 2.5, ..., 5)\n */\n\n// ── Icon size canonical(2026-04-21 AR48 修正)──\n//\n// Rating 的「一顆星」視覺重量接近 **Avatar / identity icon**,不是純 inline icon。\n// 理由:\n// - 星星是 filled shape(解析整個 icon 是重量感的一部分),不像純 outline icon 靠 stroke\n// - Field 內 Rating 跟 Avatar / Tag 並排時視覺份量要對齊,否則 row height 一致但 icon 看起來比重量不對\n// - 世界級對照:Ant Rate in Form = 20px、Material MUI Rating fontSize=inherit 預設約 24、Airbnb 評分星 24px\n//\n// 因此 Field 內 Rating icon size 對齊 **item-anatomy inline Avatar sizes**:sm=20 / md=24 / lg=24。\n// 非 icon tier(16/16/20)——star 不是次要 affordance icon,它是主要資料視覺。\n//\n// Container 高度仍對齊 `--field-height-*`(sm=28 / md=32 / lg=36),讓 Rating 可與其他\n// field-height family 元件(Input / Select)並排時 row height 對齊。\n//\n// ── 使用情境 ──\n// - **Standalone**(獨立展示評分,如商品卡 / 評論)→ 預設 `xs`(container 24,icon 20,\n// 對齊 Avatar sm 20px;iOS HIG / Airbnb 商品卡星星 20-24px)\n// - **Field 內**(表單評分欄位)→ 跟 Field 尺寸對齊(sm=20 / md=24 / lg=24,default md)\nconst SIZE_PX = { xs: 20, sm: 20, md: 24, lg: 24 } as const\nconst CONTAINER_HEIGHT: Record<'xs' | 'sm' | 'md' | 'lg', string> = {\n xs: 'h-field-xs',\n sm: 'h-field-sm',\n md: 'h-field-md',\n lg: 'h-field-lg',\n}\n\nexport interface RatingProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {\n /** 當前評分(0 ~ max) */\n value?: number\n /** 預設值(uncontrolled) */\n defaultValue?: number\n /** 評分改變 callback */\n onChange?: (value: number) => void\n /** 滿分(預設 5) */\n max?: number\n /** 尺寸。standalone 建議 xs(24px);Field 內跟隨 Field size 傳 sm/md/lg */\n size?: 'xs' | 'sm' | 'md' | 'lg'\n /** 精度:full = 整星,half = 半星 */\n precision?: 'full' | 'half'\n /** 唯讀(無 hover / click 響應) */\n readOnly?: boolean\n /** 完全停用 */\n disabled?: boolean\n /**\n * Loading 狀態 — 正在取得既有評分 / 正在儲存。\n * 視覺同 disabled(composite 整塊 opacity-disabled)但 semantic 不同:\n * loading = 暫時性等待(aria-busy),disabled = 永久業務規則(aria-disabled)。\n * 詳 rating.spec.md「Interactive vs ReadOnly」+「Loading canonical」\n */\n loading?: boolean\n /** 自訂 icon(預設 Star);傳 LucideIcon */\n icon?: LucideIcon\n /**\n * a11y label。readOnly(role=img)時必填。\n * interactive(role=slider)時:在 Field 內免填(自動 aria-labelledby 指向 FieldLabel);\n * standalone(無 Field)時必填——role=slider 依 WAI-ARIA APG 必有 accessible name。\n */\n 'aria-label'?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst Rating = React.forwardRef<HTMLDivElement, RatingProps>(\n (\n {\n value,\n defaultValue = 0,\n onChange,\n max = 5,\n size: sizeProp,\n precision = 'full',\n readOnly = false,\n disabled: disabledProp,\n loading = false,\n icon: Icon = Star,\n className,\n ...props\n },\n ref,\n ) => {\n // Context-aware default size(AR31 canonical):\n // - Field 內(有 FieldContext.size) → 跟 Field size 對齊(sm / md / lg)\n // - Standalone(無 Field context) → default `xs`(24px,對齊 Avatar / Tag sm / iOS HIG standalone)\n // consumer 可傳 size 顯式 override。世界級對照:Material Rating standalone 24dp、\n // Ant Rate in Form 跟 Form.itemSize,standalone 24px。\n const fieldCtx = useFieldContext() // 保留:aria-labelledby 用 fieldCtx.labelId\n // 2026-06-08 SSOT:<Field disabled> cascade(原 isInteractive 只看 local disabled prop)\n const disabled = useResolvedFieldDisabled(disabledProp)\n const size = useResolvedFieldSize<'xs' | 'sm' | 'md' | 'lg'>(sizeProp, 'xs') // SSOT:統一 size resolution(Rating default 'xs')\n const [internalValue, setInternalValue] = React.useState(defaultValue)\n const [hoverValue, setHoverValue] = React.useState<number | null>(null)\n const isControlled = value !== undefined\n const currentValue = isControlled ? value : internalValue\n const displayValue = hoverValue ?? currentValue\n const iconPx = SIZE_PX[size]\n const isInteractive = !readOnly && !disabled && !loading\n\n const setValue = (v: number) => {\n if (!isControlled) setInternalValue(v)\n onChange?.(v)\n }\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (!isInteractive) return\n const step = precision === 'half' ? 0.5 : 1\n // Full ARIA slider pattern(WAI-ARIA):Arrow / Home / End 支援 — D4 UX audit 2026-04-22\n if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {\n e.preventDefault()\n setValue(Math.min(max, currentValue + step))\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {\n e.preventDefault()\n setValue(Math.max(0, currentValue - step))\n } else if (e.key === 'Home') {\n e.preventDefault()\n setValue(0)\n } else if (e.key === 'End') {\n e.preventDefault()\n setValue(max)\n }\n }\n\n return (\n <div\n ref={ref}\n role={isInteractive ? 'slider' : 'img'}\n // a11y(#30):role=slider 必有 accessible name(WAI-ARIA APG slider pattern)。\n // Field 內 → 自動 aria-labelledby 指向 FieldLabel 的 id(fieldCtx.labelId,免填);\n // Standalone → 仍需 consumer 傳 aria-label。對齊 TimePicker / DatePicker 同 canonical\n // (time-picker.tsx:313 / date-picker.tsx:514:aria-labelledby={fieldCtx?.labelId})。\n // 置於 {...props} 前,consumer 顯式傳的 aria-labelledby 仍可覆寫。\n aria-labelledby={isInteractive ? fieldCtx?.labelId : undefined}\n aria-valuenow={isInteractive ? currentValue : undefined}\n aria-valuemin={isInteractive ? 0 : undefined}\n aria-valuemax={isInteractive ? max : undefined}\n aria-valuetext={isInteractive ? `${currentValue} of ${max} stars` : undefined}\n aria-disabled={disabled || undefined}\n // a11y: 刻意不設 aria-readonly — readOnly 時 role=img(axe aria-allowed-attr 禁 img 用 aria-readonly,2026-04-25);\n // interactive 時 role=slider 但必非 readOnly(isInteractive = !readOnly)。兩 state 皆不該有此屬性,故省略。\n aria-busy={loading || undefined}\n tabIndex={isInteractive ? 0 : undefined}\n onKeyDown={handleKeyDown}\n onMouseLeave={() => setHoverValue(null)}\n className={cn(\n 'inline-flex items-center gap-1',\n // Container 對齊 field-height family,讓 Rating 可與 Input/Select/Button 並排 row-align\n CONTAINER_HEIGHT[size],\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md',\n // disabled 跟 loading 視覺相同(composite uniform dim),semantic 由 aria-disabled / aria-busy 區分\n (disabled || loading) && 'opacity-disabled pointer-events-none',\n className,\n )}\n {...props}\n >\n {Array.from({ length: max }, (_, i) => {\n const starValue = i + 1\n const fillRatio = Math.max(0, Math.min(1, displayValue - i)) // 0..1\n const isHalf = precision === 'half' && fillRatio > 0 && fillRatio < 1\n\n return (\n <StarIcon\n key={i}\n Icon={Icon}\n sizePx={iconPx}\n fillRatio={fillRatio}\n isHalf={isHalf}\n interactive={isInteractive}\n onHover={(halfFirst) => {\n if (!isInteractive) return\n const v = precision === 'half' && halfFirst ? starValue - 0.5 : starValue\n setHoverValue(v)\n }}\n onClick={(halfFirst) => {\n if (!isInteractive) return\n const v = precision === 'half' && halfFirst ? starValue - 0.5 : starValue\n setValue(v)\n }}\n />\n )\n })}\n </div>\n )\n },\n)\nRating.displayName = 'Rating'\n\n// ── StarIcon: 單顆星 + half-precision overlay ─────────────────────────────\n\ninterface StarIconProps {\n Icon: LucideIcon\n sizePx: number\n fillRatio: number // 0..1\n isHalf: boolean\n interactive: boolean\n onHover: (halfFirst: boolean) => void\n onClick: (halfFirst: boolean) => void\n}\n\nconst FILL_FILLED = 'var(--warning)' // yellow-6 — 黃星 convention\nconst FILL_EMPTY = 'var(--divider)' // 灰色空星(neutral-4 借 divider semantic alias,user 2026-05-09 拍板;對齊 Material rgba(0,0,0,0.26) muted-fill canonical)\n\nfunction StarIcon({ Icon, sizePx, fillRatio, isHalf, interactive, onHover, onClick }: StarIconProps) {\n // a11y(2026-04-25 axe nested-interactive fix):inner 點擊目標改 <span>(非 interactive\n // element),不會跟外層 role='slider' 形成 nested-interactive 違規。鍵盤控制統一在外層\n // slider 的 arrow keys,inner 只處理 mouse click 定位。Ant Rate / Material MUI 同模式。\n if (!isHalf) {\n // Full: 一整顆 fill(filled 或 empty)\n const fill = fillRatio >= 1 ? FILL_FILLED : FILL_EMPTY\n return (\n <span\n role=\"presentation\"\n onMouseEnter={interactive ? () => onHover(false) : undefined}\n onClick={interactive ? () => onClick(false) : undefined}\n className={cn(\n 'inline-flex',\n interactive ? 'cursor-pointer' : 'cursor-default',\n )}\n style={{ color: fill }}\n aria-hidden\n >\n {/* stroke=\"none\" 移除 Lucide Star 預設的 outline stroke(lucide defaultAttributes\n strokeWidth=2 + stroke=currentColor 會畫輪廓),讓星星是純 fill-only 的 shape——\n fill 與 outline 同色視覺上仍有亮度差。\n 世界級對照:Ant Rate / Material MUI Rating 皆純 fill,無 outline stroke。*/}\n <Icon size={sizePx} fill={fill} stroke=\"none\" className=\"shrink-0\" />\n </span>\n )\n }\n\n // Half: 兩個重疊 icon,左半 filled / 右半 empty + 兩個 hover zone 切半星\n return (\n <span className=\"relative inline-flex\" style={{ width: sizePx, height: sizePx }}>\n <Icon size={sizePx} fill={FILL_EMPTY} stroke=\"none\" className=\"absolute inset-0\" style={{ color: FILL_EMPTY }} />\n <span className=\"absolute inset-0 overflow-hidden\" style={{ width: sizePx * fillRatio }}>\n <Icon size={sizePx} fill={FILL_FILLED} stroke=\"none\" style={{ color: FILL_FILLED }} />\n </span>\n {interactive && (\n <>\n <span\n role=\"presentation\"\n onMouseEnter={() => onHover(true)}\n onClick={() => onClick(true)}\n className=\"absolute inset-y-0 left-0 w-1/2 cursor-pointer\"\n aria-hidden\n />\n <span\n role=\"presentation\"\n onMouseEnter={() => onHover(false)}\n onClick={() => onClick(false)}\n className=\"absolute inset-y-0 right-0 w-1/2 cursor-pointer\"\n aria-hidden\n />\n </>\n )}\n </span>\n )\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 ratingMeta = {\n component: 'Rating',\n family: null, // self-contained primitive(對齊 spec frontmatter self-contained + body L24;非 Family 4)\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-transparent'],\n fg: [],\n ring: ['ring-ring'],\n },\n} as const\n\nexport { Rating }\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"rating.js","sources":["../../../src/components/Rating/rating.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 { Star, type LucideIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { useFieldContext, useResolvedFieldSize, useResolvedFieldDisabled, useResolvedFieldMode } from '@/design-system/components/Field/field-context'\n\n/**\n * Rating — 星星評分元件\n *\n * 世界級對照:Ant Design `<Rate>`、Material MUI `<Rating>`。\n * shadcn 核心沒有 Rating,本元件自建。\n *\n * ── 使用情境 ──\n * - review / feedback:商品評分 / 服務評分(可編輯 + 唯讀兩種)\n * - display:已提交評分的唯讀呈現(商品清單星等)\n *\n * ── 視覺 ──\n * 填色用 `var(--warning)`(yellow-6,世界級黃星 convention;與 warning 語意共用色相\n * 但語境不同,評分 = UX convention color 非 status)。\n * 空色用 `var(--color-neutral-4)`(灰色;與 disabled/empty 同級)。\n *\n * ── 互動 ──\n * interactive(預設):hover 預覽、click 設值、keyboard Left/Right 改值\n * readOnly:純顯示,不響應 hover / click\n *\n * ── 精度 ──\n * precision=\"full\"(預設) — 整星(1, 2, 3, 4, 5)\n * precision=\"half\" — 半星(0.5, 1, 1.5, 2, 2.5, ..., 5)\n */\n\n// ── Icon size canonical(2026-04-21 AR48 修正)──\n//\n// Rating 的「一顆星」視覺重量接近 **Avatar / identity icon**,不是純 inline icon。\n// 理由:\n// - 星星是 filled shape(解析整個 icon 是重量感的一部分),不像純 outline icon 靠 stroke\n// - Field 內 Rating 跟 Avatar / Tag 並排時視覺份量要對齊,否則 row height 一致但 icon 看起來比重量不對\n// - 世界級對照:Ant Rate in Form = 20px、Material MUI Rating fontSize=inherit 預設約 24、Airbnb 評分星 24px\n//\n// 因此 Field 內 Rating icon size 對齊 **item-anatomy inline Avatar sizes**:sm=20 / md=24 / lg=24。\n// 非 icon tier(16/16/20)——star 不是次要 affordance icon,它是主要資料視覺。\n//\n// Container 高度仍對齊 `--field-height-*`(sm=28 / md=32 / lg=36),讓 Rating 可與其他\n// field-height family 元件(Input / Select)並排時 row height 對齊。\n//\n// ── 使用情境 ──\n// - **Standalone**(獨立展示評分,如商品卡 / 評論)→ 預設 `xs`(container 24,icon 20,\n// 對齊 Avatar sm 20px;iOS HIG / Airbnb 商品卡星星 20-24px)\n// - **Field 內**(表單評分欄位)→ 跟 Field 尺寸對齊(sm=20 / md=24 / lg=24,default md)\nconst SIZE_PX = { xs: 20, sm: 20, md: 24, lg: 24 } as const\nconst CONTAINER_HEIGHT: Record<'xs' | 'sm' | 'md' | 'lg', string> = {\n xs: 'h-field-xs',\n sm: 'h-field-sm',\n md: 'h-field-md',\n lg: 'h-field-lg',\n}\n\nexport interface RatingProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {\n /** 當前評分(0 ~ max) */\n value?: number\n /** 預設值(uncontrolled) */\n defaultValue?: number\n /** 評分改變 callback */\n onChange?: (value: number) => void\n /** 滿分(預設 5) */\n max?: number\n /** 尺寸。standalone 建議 xs(24px);Field 內跟隨 Field size 傳 sm/md/lg */\n size?: 'xs' | 'sm' | 'md' | 'lg'\n /** 精度:full = 整星,half = 半星 */\n precision?: 'full' | 'half'\n /** 唯讀(無 hover / click 響應) */\n readOnly?: boolean\n /** 完全停用 */\n disabled?: boolean\n /**\n * Loading 狀態 — 正在取得既有評分 / 正在儲存。\n * 視覺同 disabled(composite 整塊 opacity-disabled)但 semantic 不同:\n * loading = 暫時性等待(aria-busy),disabled = 永久業務規則(aria-disabled)。\n * 詳 rating.spec.md「Interactive vs ReadOnly」+「Loading canonical」\n */\n loading?: boolean\n /** 自訂 icon(預設 Star);傳 LucideIcon */\n icon?: LucideIcon\n /**\n * a11y label。readOnly(role=img)時必填。\n * interactive(role=slider)時:在 Field 內免填(自動 aria-labelledby 指向 FieldLabel);\n * standalone(無 Field)時必填——role=slider 依 WAI-ARIA APG 必有 accessible name。\n */\n 'aria-label'?: string\n}\n\n// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding\nconst Rating = React.forwardRef<HTMLDivElement, RatingProps>(\n (\n {\n value,\n defaultValue = 0,\n onChange,\n max = 5,\n size: sizeProp,\n precision = 'full',\n readOnly: readOnlyProp = false,\n disabled: disabledProp,\n loading = false,\n icon: Icon = Star,\n className,\n ...props\n },\n ref,\n ) => {\n // Context-aware default size(AR31 canonical):\n // - Field 內(有 FieldContext.size) → 跟 Field size 對齊(sm / md / lg)\n // - Standalone(無 Field context) → default `xs`(24px,對齊 Avatar / Tag sm / iOS HIG standalone)\n // consumer 可傳 size 顯式 override。世界級對照:Material Rating standalone 24dp、\n // Ant Rate in Form 跟 Form.itemSize,standalone 24px。\n const fieldCtx = useFieldContext() // 保留:aria-labelledby 用 fieldCtx.labelId\n // 2026-06-08 SSOT:<Field disabled> cascade(原 isInteractive 只看 local disabled prop)\n const disabled = useResolvedFieldDisabled(disabledProp)\n // <Field mode=\"readonly\"> cascade(2026-06-12 補):Rating 的 readonly 呈現 = 星星本身\n // (星星即值語言,role=img,全業界 review-stars canonical)——不包灰框,只鎖互動。\n const resolvedMode = useResolvedFieldMode({ mode: undefined, disabled, readOnly: readOnlyProp })\n const readOnly = readOnlyProp || resolvedMode === 'readonly'\n const size = useResolvedFieldSize<'xs' | 'sm' | 'md' | 'lg'>(sizeProp, 'xs') // SSOT:統一 size resolution(Rating default 'xs')\n const [internalValue, setInternalValue] = React.useState(defaultValue)\n const [hoverValue, setHoverValue] = React.useState<number | null>(null)\n const isControlled = value !== undefined\n const currentValue = isControlled ? value : internalValue\n const displayValue = hoverValue ?? currentValue\n const iconPx = SIZE_PX[size]\n const isInteractive = !readOnly && !disabled && !loading\n\n const setValue = (v: number) => {\n if (!isControlled) setInternalValue(v)\n onChange?.(v)\n }\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (!isInteractive) return\n const step = precision === 'half' ? 0.5 : 1\n // Full ARIA slider pattern(WAI-ARIA):Arrow / Home / End 支援 — D4 UX audit 2026-04-22\n if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {\n e.preventDefault()\n setValue(Math.min(max, currentValue + step))\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {\n e.preventDefault()\n setValue(Math.max(0, currentValue - step))\n } else if (e.key === 'Home') {\n e.preventDefault()\n setValue(0)\n } else if (e.key === 'End') {\n e.preventDefault()\n setValue(max)\n }\n }\n\n return (\n <div\n ref={ref}\n role={isInteractive ? 'slider' : 'img'}\n // a11y(#30):role=slider 必有 accessible name(WAI-ARIA APG slider pattern)。\n // Field 內 → 自動 aria-labelledby 指向 FieldLabel 的 id(fieldCtx.labelId,免填);\n // Standalone → 仍需 consumer 傳 aria-label。對齊 TimePicker / DatePicker 同 canonical\n // (time-picker.tsx:313 / date-picker.tsx:514:aria-labelledby={fieldCtx?.labelId})。\n // 置於 {...props} 前,consumer 顯式傳的 aria-labelledby 仍可覆寫。\n aria-labelledby={fieldCtx?.labelId} // 2026-06-12 修:readonly/disabled(role=img)也需 accessible name,labelledby 對 img 合法\n aria-valuenow={isInteractive ? currentValue : undefined}\n aria-valuemin={isInteractive ? 0 : undefined}\n aria-valuemax={isInteractive ? max : undefined}\n aria-valuetext={isInteractive ? `${currentValue} of ${max} stars` : undefined}\n aria-disabled={disabled || undefined}\n // a11y: 刻意不設 aria-readonly — readOnly 時 role=img(axe aria-allowed-attr 禁 img 用 aria-readonly,2026-04-25);\n // interactive 時 role=slider 但必非 readOnly(isInteractive = !readOnly)。兩 state 皆不該有此屬性,故省略。\n aria-busy={loading || undefined}\n tabIndex={isInteractive ? 0 : undefined}\n onKeyDown={handleKeyDown}\n onMouseLeave={() => setHoverValue(null)}\n className={cn(\n 'inline-flex items-center gap-1',\n // Container 對齊 field-height family,讓 Rating 可與 Input/Select/Button 並排 row-align\n CONTAINER_HEIGHT[size],\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md',\n // disabled 跟 loading 視覺相同(composite uniform dim),semantic 由 aria-disabled / aria-busy 區分\n (disabled || loading) && 'opacity-disabled pointer-events-none',\n className,\n )}\n {...props}\n >\n {Array.from({ length: max }, (_, i) => {\n const starValue = i + 1\n const fillRatio = Math.max(0, Math.min(1, displayValue - i)) // 0..1\n const isHalf = precision === 'half' && fillRatio > 0 && fillRatio < 1\n\n return (\n <StarIcon\n key={i}\n Icon={Icon}\n sizePx={iconPx}\n fillRatio={fillRatio}\n isHalf={isHalf}\n interactive={isInteractive}\n onHover={(halfFirst) => {\n if (!isInteractive) return\n const v = precision === 'half' && halfFirst ? starValue - 0.5 : starValue\n setHoverValue(v)\n }}\n onClick={(halfFirst) => {\n if (!isInteractive) return\n const v = precision === 'half' && halfFirst ? starValue - 0.5 : starValue\n setValue(v)\n }}\n />\n )\n })}\n </div>\n )\n },\n)\nRating.displayName = 'Rating'\n\n// ── StarIcon: 單顆星 + half-precision overlay ─────────────────────────────\n\ninterface StarIconProps {\n Icon: LucideIcon\n sizePx: number\n fillRatio: number // 0..1\n isHalf: boolean\n interactive: boolean\n onHover: (halfFirst: boolean) => void\n onClick: (halfFirst: boolean) => void\n}\n\nconst FILL_FILLED = 'var(--warning)' // yellow-6 — 黃星 convention\nconst FILL_EMPTY = 'var(--divider)' // 灰色空星(neutral-4 借 divider semantic alias,user 2026-05-09 拍板;對齊 Material rgba(0,0,0,0.26) muted-fill canonical)\n\nfunction StarIcon({ Icon, sizePx, fillRatio, isHalf, interactive, onHover, onClick }: StarIconProps) {\n // a11y(2026-04-25 axe nested-interactive fix):inner 點擊目標改 <span>(非 interactive\n // element),不會跟外層 role='slider' 形成 nested-interactive 違規。鍵盤控制統一在外層\n // slider 的 arrow keys,inner 只處理 mouse click 定位。Ant Rate / Material MUI 同模式。\n if (!isHalf) {\n // Full: 一整顆 fill(filled 或 empty)\n const fill = fillRatio >= 1 ? FILL_FILLED : FILL_EMPTY\n return (\n <span\n role=\"presentation\"\n onMouseEnter={interactive ? () => onHover(false) : undefined}\n onClick={interactive ? () => onClick(false) : undefined}\n className={cn(\n 'inline-flex',\n interactive ? 'cursor-pointer' : 'cursor-default',\n )}\n style={{ color: fill }}\n aria-hidden\n >\n {/* stroke=\"none\" 移除 Lucide Star 預設的 outline stroke(lucide defaultAttributes\n strokeWidth=2 + stroke=currentColor 會畫輪廓),讓星星是純 fill-only 的 shape——\n fill 與 outline 同色視覺上仍有亮度差。\n 世界級對照:Ant Rate / Material MUI Rating 皆純 fill,無 outline stroke。*/}\n <Icon size={sizePx} fill={fill} stroke=\"none\" className=\"shrink-0\" />\n </span>\n )\n }\n\n // Half: 兩個重疊 icon,左半 filled / 右半 empty + 兩個 hover zone 切半星\n return (\n <span className=\"relative inline-flex\" style={{ width: sizePx, height: sizePx }}>\n <Icon size={sizePx} fill={FILL_EMPTY} stroke=\"none\" className=\"absolute inset-0\" style={{ color: FILL_EMPTY }} />\n <span className=\"absolute inset-0 overflow-hidden\" style={{ width: sizePx * fillRatio }}>\n <Icon size={sizePx} fill={FILL_FILLED} stroke=\"none\" style={{ color: FILL_FILLED }} />\n </span>\n {interactive && (\n <>\n <span\n role=\"presentation\"\n onMouseEnter={() => onHover(true)}\n onClick={() => onClick(true)}\n className=\"absolute inset-y-0 left-0 w-1/2 cursor-pointer\"\n aria-hidden\n />\n <span\n role=\"presentation\"\n onMouseEnter={() => onHover(false)}\n onClick={() => onClick(false)}\n className=\"absolute inset-y-0 right-0 w-1/2 cursor-pointer\"\n aria-hidden\n />\n </>\n )}\n </span>\n )\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 ratingMeta = {\n component: 'Rating',\n family: null, // self-contained primitive(對齊 spec frontmatter self-contained + body L24;非 Family 4)\n variants: {\n\n },\n sizes: {\n\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-transparent'],\n fg: [],\n ring: ['ring-ring'],\n },\n} as const\n\nexport { Rating }\n"],"names":[],"mappings":"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"}
@@ -39,7 +39,7 @@ export declare const sliderMeta: {
39
39
  };
40
40
  readonly states: readonly ["default", "hover", "active", "focus-visible", "disabled"];
41
41
  readonly tokens: {
42
- readonly bg: readonly ["bg-muted", "bg-primary", "bg-secondary", "bg-surface"];
42
+ readonly bg: readonly ["bg-muted", "bg-primary", "bg-secondary", "bg-on-emphasis", "bg-canvas"];
43
43
  readonly fg: readonly [];
44
44
  readonly ring: readonly [];
45
45
  };
@@ -1 +1 @@
1
- {"version":3,"file":"slider.d.ts","sourceRoot":"","sources":["../../../src/components/Slider/slider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE;;;;;;;;;;;GAWG;AAEH,QAAA,MAAM,kBAAkB;;8EA8BvB,CAAA;AAED,MAAM,WAAW,WACf,SAAQ,IAAI,CAAC,KAAK,CAAC,wBAAwB,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,EACnF,YAAY,CAAC,OAAO,kBAAkB,CAAC;CAAG;AAG9C,QAAA,MAAM,MAAM,qFAqGV,CAAA;AAKF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;CAkBb,CAAA;AAEV,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAA"}
1
+ {"version":3,"file":"slider.d.ts","sourceRoot":"","sources":["../../../src/components/Slider/slider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE;;;;;;;;;;;GAWG;AAEH,QAAA,MAAM,kBAAkB;;8EA8BvB,CAAA;AAED,MAAM,WAAW,WACf,SAAQ,IAAI,CAAC,KAAK,CAAC,wBAAwB,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,EACnF,YAAY,CAAC,OAAO,kBAAkB,CAAC;CAAG;AAG9C,QAAA,MAAM,MAAM,qFAkHV,CAAA;AAKF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;CAkBb,CAAA;AAEV,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAA"}
@@ -3,7 +3,7 @@ import * as React from "react";
3
3
  import * as SliderPrimitive from "@radix-ui/react-slider";
4
4
  import { cva } from "class-variance-authority";
5
5
  import { cn } from "../../lib/utils.js";
6
- import { useResolvedFieldDisabled, useFieldContext } from "../Field/field-context.js";
6
+ import { useResolvedFieldDisabled, useResolvedFieldMode, useFieldContext } from "../Field/field-context.js";
7
7
  const sliderRootVariants = cva(
8
8
  // 容器外層:水平置中 + relative(Radix 會絕對定位內部元素)
9
9
  // flex items-center 讓 track+thumb 在任何 field-height 下都垂直置中
@@ -38,6 +38,8 @@ const sliderRootVariants = cva(
38
38
  const Slider = React.forwardRef(({ className, size, value, defaultValue, "aria-label": ariaLabel, ...props }, ref) => {
39
39
  var _a;
40
40
  const fieldDisabled = useResolvedFieldDisabled();
41
+ const fieldMode = useResolvedFieldMode({ mode: void 0, disabled: void 0, readOnly: void 0 });
42
+ const fieldReadonly = fieldMode === "readonly";
41
43
  const fieldLabelId = (_a = useFieldContext()) == null ? void 0 : _a.labelId;
42
44
  const thumbCount = Array.isArray(value) && value.length || Array.isArray(defaultValue) && defaultValue.length || 1;
43
45
  return /* @__PURE__ */ jsxs(
@@ -46,9 +48,10 @@ const Slider = React.forwardRef(({ className, size, value, defaultValue, "aria-l
46
48
  ref,
47
49
  value,
48
50
  defaultValue,
49
- className: cn(sliderRootVariants({ size }), className),
51
+ className: cn(sliderRootVariants({ size }), fieldReadonly && "pointer-events-none", className),
50
52
  ...props,
51
53
  disabled: props.disabled || fieldDisabled,
54
+ "data-readonly": fieldReadonly || void 0,
52
55
  children: [
53
56
  /* @__PURE__ */ jsx(SliderPrimitive.Track, { className: cn(
54
57
  "relative grow overflow-hidden rounded-full h-1",
@@ -67,17 +70,19 @@ const Slider = React.forwardRef(({ className, size, value, defaultValue, "aria-l
67
70
  Array.from({ length: thumbCount }).map((_, i) => /* @__PURE__ */ jsx(
68
71
  SliderPrimitive.Thumb,
69
72
  {
73
+ tabIndex: fieldReadonly ? -1 : void 0,
74
+ "aria-readonly": fieldReadonly || void 0,
70
75
  className: cn(
71
76
  "block h-4 w-4 shrink-0 rounded-full cursor-grab",
72
- "bg-surface border-2 border-primary",
77
+ "bg-on-emphasis border-2 border-primary",
73
78
  "transition-all duration-150",
74
79
  // Hover:border 加深到 primary-hover + elevation 陰影
75
80
  "hover:border-primary-hover hover:[box-shadow:var(--elevation-100)]",
76
81
  "active:cursor-grabbing active:border-primary-hover active:[box-shadow:var(--elevation-200)]",
77
82
  // Focus:border 加深(跟 hover 同視覺),不加 ring 或 halo
78
83
  "outline-none focus-visible:border-primary-hover",
79
- // Disabled:border 跟 Range 一起退成 border(n-5),bg 保留 bg-surface
80
- "data-[disabled]:cursor-not-allowed data-[disabled]:border-border",
84
+ // Disabled:border 跟 Range 一起退成 border(n-5),bg 沉回 canvas(不透明背景色)
85
+ "data-[disabled]:cursor-not-allowed data-[disabled]:border-border data-[disabled]:bg-canvas",
81
86
  "data-[disabled]:hover:[box-shadow:none]"
82
87
  ),
83
88
  "aria-label": ariaLabel ? thumbCount > 1 ? `${ariaLabel} (${i + 1})` : ariaLabel : void 0,
@@ -105,7 +110,7 @@ const sliderMeta = {
105
110
  },
106
111
  states: ["default", "hover", "active", "focus-visible", "disabled"],
107
112
  tokens: {
108
- bg: ["bg-muted", "bg-primary", "bg-secondary", "bg-surface"],
113
+ bg: ["bg-muted", "bg-primary", "bg-secondary", "bg-on-emphasis", "bg-canvas"],
109
114
  fg: [],
110
115
  ring: []
111
116
  }
@@ -1 +1 @@
1
- {"version":3,"file":"slider.js","sources":["../../../src/components/Slider/slider.tsx"],"sourcesContent":["import * as React from 'react'\nimport * as SliderPrimitive from '@radix-ui/react-slider'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@/lib/utils'\nimport { useFieldContext, useResolvedFieldDisabled } from '@/design-system/components/Field/field-context'\n\n/**\n * Slider — 數值範圍選取器\n *\n * 基於 Radix Slider primitive,橋接設計系統 token。詳細設計原則見 `slider.spec.md`。\n *\n * ── 核心設計 ──\n * 1. **視覺單一**:track 厚度、thumb 直徑、thumb 邊框都是固定值,不隨 `size` 變\n * 2. **`size` 只控容器外高**:對齊 Field family 的 `h-field-*` tier,讓 Slider 能跟\n * Input / NumberInput / Select 在 Field 內並排對齊\n * 3. **Range mode 免費**:Radix 原生支援 `value: number[]`,傳多值自動多 thumb\n * 4. **Hover / active 用 elevation 陰影**:不用色變,避免暗示「這是 button」\n */\n\nconst sliderRootVariants = cva(\n // 容器外層:水平置中 + relative(Radix 會絕對定位內部元素)\n // flex items-center 讓 track+thumb 在任何 field-height 下都垂直置中\n //\n // ── Disabled 策略:灰階 token swap(對齊 Button / Checkbox)──\n // Slider 的藍色 range 是美學視覺,不是 semantic state——使用者從 disabled\n // slider 需要辨識的是 thumb 位置 + range 長度,這兩者不依賴顏色。失去藍色\n // 沒有資訊損失。\n //\n // 跟 Switch 的差別:Switch 的 on/off 是純顏色差異(沒有形狀差異),所以必須\n // 靠 opacity 保留色彩身分。Slider 的位置/長度是形狀差異,不需要保留顏色身分,\n // 跟 Checkbox(checkmark 形狀 = semantic 載體)同類,應該走灰階。\n //\n // 詳細判準見 `slider.spec.md` 的「Disabled 策略」章節。\n [\n 'relative flex items-center w-full min-w-0 touch-none select-none',\n 'data-[disabled]:cursor-not-allowed',\n ],\n {\n variants: {\n size: {\n sm: 'h-field-sm',\n md: 'h-field-md',\n lg: 'h-field-lg',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n },\n)\n\nexport interface SliderProps\n extends Omit<React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>, 'children'>,\n VariantProps<typeof sliderRootVariants> {}\n\n// code-quality-allow: long-function — foundational composite(thumb-count 推導 + Field disabled 整合 + multi-mode render)\nconst Slider = React.forwardRef<\n React.ElementRef<typeof SliderPrimitive.Root>,\n SliderProps\n>(({ className, size, value, defaultValue, 'aria-label': ariaLabel, ...props }, ref) => {\n // Field 家族整合:被 <Field mode=\"disabled\"> 包裹時自動 disabled(per slider.spec.md「Slider 作為 Field\n // 家族整合時繼承其 canonical」)。Slider 已有完整 data-[disabled] 視覺,故只需把 fieldCtx disabled 接上。\n // 2026-06-08 SSOT:讀 useResolvedFieldDisabled()(fieldCtx.disabled)→ <Field disabled> 與 <Field mode=\"disabled\"> 都生效\n const fieldDisabled = useResolvedFieldDisabled()\n // 2026-06-10 a11y:Field 內 Slider thumb(role=slider)無 accessible name(deep-audit axe 抓 aria-input-field-name)\n // → 預設接 FieldLabel(aria-labelledby),consumer ariaLabel 優先。對齊 rating/time-picker labelId 接線。\n const fieldLabelId = useFieldContext()?.labelId\n // 推導要渲染幾個 thumb:controlled 用 value,uncontrolled 用 defaultValue,\n // 都沒有時 fallback 單 thumb(Radix 預設行為)\n const thumbCount =\n (Array.isArray(value) && value.length) ||\n (Array.isArray(defaultValue) && defaultValue.length) ||\n 1\n\n return (\n <SliderPrimitive.Root\n ref={ref}\n value={value}\n defaultValue={defaultValue}\n className={cn(sliderRootVariants({ size }), className)}\n {...props}\n disabled={(props as { disabled?: boolean }).disabled || fieldDisabled}\n >\n {/*\n Track — rest 用 bg-secondary(n-3,「微淡可辨」),disabled 用 bg-muted(n-2,退化)。\n 跟 Tag neutral / Badge low 同家族。\n */}\n <SliderPrimitive.Track className={cn(\n 'relative grow overflow-hidden rounded-full h-1',\n // Rest:bg-secondary(n-3,「微淡可辨」的 subtle fill,跟 Tag neutral / Badge low 同級)\n // Disabled:bg-muted(n-2,「disabled-like 退化」底色)\n 'bg-secondary data-[disabled]:bg-muted',\n )}>\n {/*\n Range — 填滿段。\n\n ── Range 色 = Thumb border 色(語意一致) ──\n Rest 兩者都是 `primary`,disabled 兩者都是 `border`(n-5)。為什麼要一致?\n Range 是「填充視覺」,Thumb border 是「thumb 的輪廓線」——視覺上 thumb\n 的 border 剛好是 range 的延續(thumb 坐落在 range 的端點上,border 跟\n range 在色彩上融為一體,看起來像「range 包住 thumb」而不是「thumb 浮在\n range 上」)。兩個 token 綁在一起,不論什麼 state 都一致。\n */}\n <SliderPrimitive.Range\n className={cn(\n 'absolute h-full bg-primary',\n 'data-[disabled]:bg-border',\n )}\n />\n </SliderPrimitive.Track>\n\n {/*\n Thumb — N 個(由 thumbCount 決定)。\n 白底 + 2px 邊框,邊框色 = Range 色(**一致綁定**):\n - Rest: `border-primary` ↔ Range `bg-primary`\n - Disabled: `border-border` ↔ Range `bg-border`\n 這個一致性讓 thumb border 跟 range 融為一體,看起來像「range 包住 thumb」\n 的連續視覺。thumb 的白底則是「被 range 圍住的空心洞」,讓 thumb 的位置\n 清楚浮出。不論 state,thumb border 跟 range 永遠同色。\n\n **為什麼 thumb bg 不能改**:`bg-surface`(白)必須在 rest / disabled 都維持,\n 否則會融入 track 的 `bg-muted` 裡消失。這是之前踩過的同色融色 bug\n (曾經寫成 `data-[disabled]:bg-muted` 讓 thumb 跟 track 完全融合)。\n */}\n {Array.from({ length: thumbCount }).map((_, i) => (\n <SliderPrimitive.Thumb\n key={i}\n className={cn(\n 'block h-4 w-4 shrink-0 rounded-full cursor-grab',\n 'bg-surface border-2 border-primary',\n 'transition-all duration-150',\n // Hover:border 加深到 primary-hover + elevation 陰影\n 'hover:border-primary-hover hover:[box-shadow:var(--elevation-100)]',\n 'active:cursor-grabbing active:border-primary-hover active:[box-shadow:var(--elevation-200)]',\n // Focus:border 加深(跟 hover 同視覺),不加 ring 或 halo\n 'outline-none focus-visible:border-primary-hover',\n // Disabled:border 跟 Range 一起退成 border(n-5),bg 保留 bg-surface\n 'data-[disabled]:cursor-not-allowed data-[disabled]:border-border',\n 'data-[disabled]:hover:[box-shadow:none]',\n )}\n // aria-label 策略(對齊 WAI-ARIA APG multi-thumb slider + Radix 原生語意標籤):\n // - consumer 傳 ariaLabel:單 thumb → 原樣;多 thumb → `{label} (N)` 區分\n // - 沒傳 ariaLabel:回傳 undefined,讓 Radix getLabel 提供語意標籤\n // (2 thumb → Minimum/Maximum;>2 thumb → Value N of M;單 thumb → 無)\n // 比硬寫 `Thumb N` 更語意化,且讓「繼承 Radix a11y 預設」名實相符\n aria-label={\n ariaLabel\n ? thumbCount > 1\n ? `${ariaLabel} (${i + 1})`\n : ariaLabel\n : undefined\n }\n aria-labelledby={!ariaLabel && fieldLabelId ? fieldLabelId : undefined}\n />\n ))}\n </SliderPrimitive.Root>\n )\n})\nSlider.displayName = 'Slider'\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 sliderMeta = {\n component: 'Slider',\n family: null, // self-contained(對齊 spec;同 rating.tsx 慣例),\n variants: {},\n // size 只控容器外高,對齊 Field family h-field-*(md density:28/32/36px,uiSize.css)。\n // track 厚度 / thumb 直徑固定不隨 size 變,故無 iconSize / typography。\n // fieldHeight key 對齊 compile-stories.mjs 消費 schema(同 Button/Checkbox/Switch)。\n sizes: {\n sm: { fieldHeight: 28, when: 'Toolbar / inline 編輯' },\n md: { fieldHeight: 32, when: '預設 — Form / cell inline edit' },\n lg: { fieldHeight: 36, when: 'Marketing / 高 touch 區' },\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-muted', 'bg-primary', 'bg-secondary', 'bg-surface'],\n fg: [],\n ring: [],\n },\n} as const\n\nexport { Slider, sliderRootVariants }\n"],"names":[],"mappings":";;;;;;AAmBA,MAAM,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczB;AAAA,IACE;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MAAA;AAAA,IACN;AAAA,IAEF,iBAAiB;AAAA,MACf,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ;AAOA,MAAM,SAAS,MAAM,WAGnB,CAAC,EAAE,WAAW,MAAM,OAAO,cAAc,cAAc,WAAW,GAAG,MAAA,GAAS,QAAQ;;AAItF,QAAM,gBAAgB,yBAAA;AAGtB,QAAM,gBAAe,2BAAA,mBAAmB;AAGxC,QAAM,aACH,MAAM,QAAQ,KAAK,KAAK,MAAM,UAC9B,MAAM,QAAQ,YAAY,KAAK,aAAa,UAC7C;AAEF,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,GAAG,mBAAmB,EAAE,KAAA,CAAM,GAAG,SAAS;AAAA,MACpD,GAAG;AAAA,MACJ,UAAW,MAAiC,YAAY;AAAA,MAMxD,UAAA;AAAA,QAAA,oBAAC,gBAAgB,OAAhB,EAAsB,WAAW;AAAA,UAChC;AAAA;AAAA;AAAA,UAGA;AAAA,QAAA,GAYA,UAAA;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,YAAA;AAAA,UACF;AAAA,QAAA,GAEJ;AAAA,QAeC,MAAM,KAAK,EAAE,QAAQ,WAAA,CAAY,EAAE,IAAI,CAAC,GAAG,MAC1C;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YAEC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA;AAAA,cAEA;AAAA,cACA;AAAA;AAAA,cAEA;AAAA;AAAA,cAEA;AAAA,cACA;AAAA,YAAA;AAAA,YAOF,cACE,YACI,aAAa,IACX,GAAG,SAAS,KAAK,IAAI,CAAC,MACtB,YACF;AAAA,YAEN,mBAAiB,CAAC,aAAa,eAAe,eAAe;AAAA,UAAA;AAAA,UA1BxD;AAAA,QAAA,CA4BR;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGP,CAAC;AACD,OAAO,cAAc;AAId,MAAM,aAAa;AAAA,EACxB,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA;AAAA;AAAA;AAAA,EAIV,OAAO;AAAA,IACL,IAAI,EAAE,aAAa,IAAI,MAAM,sBAAA;AAAA,IAC7B,IAAI,EAAE,aAAa,IAAI,MAAM,+BAAA;AAAA,IAC7B,IAAI,EAAE,aAAa,IAAI,MAAM,wBAAA;AAAA,EAAwB;AAAA,EAEvD,QAAQ,CAAC,WAAW,SAAS,UAAU,iBAAiB,UAAU;AAAA,EAClE,QAAQ;AAAA,IACN,IAAI,CAAC,YAAY,cAAc,gBAAgB,YAAY;AAAA,IAC3D,IAAI,CAAA;AAAA,IACJ,MAAM,CAAA;AAAA,EAAC;AAEX;"}
1
+ {"version":3,"file":"slider.js","sources":["../../../src/components/Slider/slider.tsx"],"sourcesContent":["import * as React from 'react'\nimport * as SliderPrimitive from '@radix-ui/react-slider'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@/lib/utils'\nimport { useFieldContext, useResolvedFieldDisabled, useResolvedFieldMode } from '@/design-system/components/Field/field-context'\n\n/**\n * Slider — 數值範圍選取器\n *\n * 基於 Radix Slider primitive,橋接設計系統 token。詳細設計原則見 `slider.spec.md`。\n *\n * ── 核心設計 ──\n * 1. **視覺單一**:track 厚度、thumb 直徑、thumb 邊框都是固定值,不隨 `size` 變\n * 2. **`size` 只控容器外高**:對齊 Field family 的 `h-field-*` tier,讓 Slider 能跟\n * Input / NumberInput / Select 在 Field 內並排對齊\n * 3. **Range mode 免費**:Radix 原生支援 `value: number[]`,傳多值自動多 thumb\n * 4. **Hover / active 用 elevation 陰影**:不用色變,避免暗示「這是 button」\n */\n\nconst sliderRootVariants = cva(\n // 容器外層:水平置中 + relative(Radix 會絕對定位內部元素)\n // flex items-center 讓 track+thumb 在任何 field-height 下都垂直置中\n //\n // ── Disabled 策略:灰階 token swap(對齊 Button / Checkbox)──\n // Slider 的藍色 range 是美學視覺,不是 semantic state——使用者從 disabled\n // slider 需要辨識的是 thumb 位置 + range 長度,這兩者不依賴顏色。失去藍色\n // 沒有資訊損失。\n //\n // 跟 Switch 的差別:Switch 的 on/off 是純顏色差異(沒有形狀差異),所以必須\n // 靠 opacity 保留色彩身分。Slider 的位置/長度是形狀差異,不需要保留顏色身分,\n // 跟 Checkbox(checkmark 形狀 = semantic 載體)同類,應該走灰階。\n //\n // 詳細判準見 `slider.spec.md` 的「Disabled 策略」章節。\n [\n 'relative flex items-center w-full min-w-0 touch-none select-none',\n 'data-[disabled]:cursor-not-allowed',\n ],\n {\n variants: {\n size: {\n sm: 'h-field-sm',\n md: 'h-field-md',\n lg: 'h-field-lg',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n },\n)\n\nexport interface SliderProps\n extends Omit<React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>, 'children'>,\n VariantProps<typeof sliderRootVariants> {}\n\n// code-quality-allow: long-function — foundational composite(thumb-count 推導 + Field disabled 整合 + multi-mode render)\nconst Slider = React.forwardRef<\n React.ElementRef<typeof SliderPrimitive.Root>,\n SliderProps\n>(({ className, size, value, defaultValue, 'aria-label': ariaLabel, ...props }, ref) => {\n // Field 家族整合:被 <Field mode=\"disabled\"> 包裹時自動 disabled(per slider.spec.md「Slider 作為 Field\n // 家族整合時繼承其 canonical」)。Slider 已有完整 data-[disabled] 視覺,故只需把 fieldCtx disabled 接上。\n // 2026-06-08 SSOT:讀 useResolvedFieldDisabled()(fieldCtx.disabled)→ <Field disabled> 與 <Field mode=\"disabled\"> 都生效\n // 2026-06-12 補:<Field mode=\"readonly\"> → 鎖互動保留視覺(readonly ≠ disabled,不降色)\n const fieldDisabled = useResolvedFieldDisabled()\n const fieldMode = useResolvedFieldMode({ mode: undefined, disabled: undefined, readOnly: undefined })\n const fieldReadonly = fieldMode === 'readonly'\n // 2026-06-10 a11y:Field 內 Slider thumb(role=slider)無 accessible name(deep-audit axe 抓 aria-input-field-name)\n // → 預設接 FieldLabel(aria-labelledby),consumer ariaLabel 優先。對齊 rating/time-picker labelId 接線。\n const fieldLabelId = useFieldContext()?.labelId\n // 推導要渲染幾個 thumb:controlled 用 value,uncontrolled 用 defaultValue,\n // 都沒有時 fallback 單 thumb(Radix 預設行為)\n const thumbCount =\n (Array.isArray(value) && value.length) ||\n (Array.isArray(defaultValue) && defaultValue.length) ||\n 1\n\n return (\n <SliderPrimitive.Root\n ref={ref}\n value={value}\n defaultValue={defaultValue}\n className={cn(sliderRootVariants({ size }), fieldReadonly && 'pointer-events-none', className)}\n {...props}\n disabled={(props as { disabled?: boolean }).disabled || fieldDisabled}\n // <Field mode=\"readonly\"> cascade(2026-06-12 補):鎖互動、保留正常視覺(readonly\n // 不降色;值仍可讀)。pointer-events-none 擋滑鼠,thumb tabIndex=-1 擋鍵盤。\n data-readonly={fieldReadonly || undefined}\n >\n {/*\n Track — rest 用 bg-secondary(n-3,「微淡可辨」),disabled 用 bg-muted(n-2,退化)。\n 跟 Tag neutral / Badge low 同家族。\n */}\n <SliderPrimitive.Track className={cn(\n 'relative grow overflow-hidden rounded-full h-1',\n // Rest:bg-secondary(n-3,「微淡可辨」的 subtle fill,跟 Tag neutral / Badge low 同級)\n // Disabled:bg-muted(n-2,「disabled-like 退化」底色)\n 'bg-secondary data-[disabled]:bg-muted',\n )}>\n {/*\n Range — 填滿段。\n\n ── Range 色 = Thumb border 色(語意一致) ──\n Rest 兩者都是 `primary`,disabled 兩者都是 `border`(n-5)。為什麼要一致?\n Range 是「填充視覺」,Thumb border 是「thumb 的輪廓線」——視覺上 thumb\n 的 border 剛好是 range 的延續(thumb 坐落在 range 的端點上,border 跟\n range 在色彩上融為一體,看起來像「range 包住 thumb」而不是「thumb 浮在\n range 上」)。兩個 token 綁在一起,不論什麼 state 都一致。\n */}\n <SliderPrimitive.Range\n className={cn(\n 'absolute h-full bg-primary',\n 'data-[disabled]:bg-border',\n )}\n />\n </SliderPrimitive.Track>\n\n {/*\n Thumb — N 個(由 thumbCount 決定)。\n 白底 + 2px 邊框,邊框色 = Range 色(**一致綁定**):\n - Rest: `border-primary` ↔ Range `bg-primary`\n - Disabled: `border-border` ↔ Range `bg-border`\n 這個一致性讓 thumb border 跟 range 融為一體,看起來像「range 包住 thumb」\n 的連續視覺。不論 state,thumb border 跟 range 永遠同色。\n\n **Thumb bg 兩態(2026-06-12 user 拍板,深色模式破洞修正)**:\n - Rest/hover/active:`bg-on-emphasis`(固定白、深淺主題不反轉)— 對齊自家\n Switch thumb(switch.tsx bg-on-emphasis)+ Radix Themes(thumb 字面 white\n 無 dark override)+ Apple iOS(深色模式旋鈕仍白)。原 `bg-surface` 在深色\n = 8% 白半透明 → thumb 變深色破洞且 track 穿透。\n - Disabled:`bg-canvas`(不透明頁面背景色)— 沉回背景表達「不可動」,\n = Radix disabled thumb 用 gray-1(app background)同款;不透明故 track\n 不穿透。不可用 bg-muted(曾踩 thumb 跟 track 同色融合 bug;canvas 與\n track 的 muted 隔 n-5 邊框 + 不同值,不融)。\n */}\n {Array.from({ length: thumbCount }).map((_, i) => (\n <SliderPrimitive.Thumb\n key={i}\n tabIndex={fieldReadonly ? -1 : undefined}\n aria-readonly={fieldReadonly || undefined}\n className={cn(\n 'block h-4 w-4 shrink-0 rounded-full cursor-grab',\n 'bg-on-emphasis border-2 border-primary',\n 'transition-all duration-150',\n // Hover:border 加深到 primary-hover + elevation 陰影\n 'hover:border-primary-hover hover:[box-shadow:var(--elevation-100)]',\n 'active:cursor-grabbing active:border-primary-hover active:[box-shadow:var(--elevation-200)]',\n // Focus:border 加深(跟 hover 同視覺),不加 ring 或 halo\n 'outline-none focus-visible:border-primary-hover',\n // Disabled:border 跟 Range 一起退成 border(n-5),bg 沉回 canvas(不透明背景色)\n 'data-[disabled]:cursor-not-allowed data-[disabled]:border-border data-[disabled]:bg-canvas',\n 'data-[disabled]:hover:[box-shadow:none]',\n )}\n // aria-label 策略(對齊 WAI-ARIA APG multi-thumb slider + Radix 原生語意標籤):\n // - consumer 傳 ariaLabel:單 thumb → 原樣;多 thumb → `{label} (N)` 區分\n // - 沒傳 ariaLabel:回傳 undefined,讓 Radix getLabel 提供語意標籤\n // (2 thumb → Minimum/Maximum;>2 thumb → Value N of M;單 thumb → 無)\n // 比硬寫 `Thumb N` 更語意化,且讓「繼承 Radix a11y 預設」名實相符\n aria-label={\n ariaLabel\n ? thumbCount > 1\n ? `${ariaLabel} (${i + 1})`\n : ariaLabel\n : undefined\n }\n aria-labelledby={!ariaLabel && fieldLabelId ? fieldLabelId : undefined}\n />\n ))}\n </SliderPrimitive.Root>\n )\n})\nSlider.displayName = 'Slider'\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 sliderMeta = {\n component: 'Slider',\n family: null, // self-contained(對齊 spec;同 rating.tsx 慣例),\n variants: {},\n // size 只控容器外高,對齊 Field family h-field-*(md density:28/32/36px,uiSize.css)。\n // track 厚度 / thumb 直徑固定不隨 size 變,故無 iconSize / typography。\n // fieldHeight key 對齊 compile-stories.mjs 消費 schema(同 Button/Checkbox/Switch)。\n sizes: {\n sm: { fieldHeight: 28, when: 'Toolbar / inline 編輯' },\n md: { fieldHeight: 32, when: '預設 — Form / cell inline edit' },\n lg: { fieldHeight: 36, when: 'Marketing / 高 touch 區' },\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-muted', 'bg-primary', 'bg-secondary', 'bg-on-emphasis', 'bg-canvas'],\n fg: [],\n ring: [],\n },\n} as const\n\nexport { Slider, sliderRootVariants }\n"],"names":[],"mappings":";;;;;;AAmBA,MAAM,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczB;AAAA,IACE;AAAA,IACA;AAAA,EAAA;AAAA,EAEF;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MAAA;AAAA,IACN;AAAA,IAEF,iBAAiB;AAAA,MACf,MAAM;AAAA,IAAA;AAAA,EACR;AAEJ;AAOA,MAAM,SAAS,MAAM,WAGnB,CAAC,EAAE,WAAW,MAAM,OAAO,cAAc,cAAc,WAAW,GAAG,MAAA,GAAS,QAAQ;;AAKtF,QAAM,gBAAgB,yBAAA;AACtB,QAAM,YAAY,qBAAqB,EAAE,MAAM,QAAW,UAAU,QAAW,UAAU,QAAW;AACpG,QAAM,gBAAgB,cAAc;AAGpC,QAAM,gBAAe,2BAAA,mBAAmB;AAGxC,QAAM,aACH,MAAM,QAAQ,KAAK,KAAK,MAAM,UAC9B,MAAM,QAAQ,YAAY,KAAK,aAAa,UAC7C;AAEF,SACE;AAAA,IAAC,gBAAgB;AAAA,IAAhB;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,GAAG,mBAAmB,EAAE,MAAM,GAAG,iBAAiB,uBAAuB,SAAS;AAAA,MAC5F,GAAG;AAAA,MACJ,UAAW,MAAiC,YAAY;AAAA,MAGxD,iBAAe,iBAAiB;AAAA,MAMhC,UAAA;AAAA,QAAA,oBAAC,gBAAgB,OAAhB,EAAsB,WAAW;AAAA,UAChC;AAAA;AAAA;AAAA,UAGA;AAAA,QAAA,GAYA,UAAA;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,YAAA;AAAA,UACF;AAAA,QAAA,GAEJ;AAAA,QAoBC,MAAM,KAAK,EAAE,QAAQ,WAAA,CAAY,EAAE,IAAI,CAAC,GAAG,MAC1C;AAAA,UAAC,gBAAgB;AAAA,UAAhB;AAAA,YAEC,UAAU,gBAAgB,KAAK;AAAA,YAC/B,iBAAe,iBAAiB;AAAA,YAChC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA;AAAA,cAEA;AAAA,cACA;AAAA;AAAA,cAEA;AAAA;AAAA,cAEA;AAAA,cACA;AAAA,YAAA;AAAA,YAOF,cACE,YACI,aAAa,IACX,GAAG,SAAS,KAAK,IAAI,CAAC,MACtB,YACF;AAAA,YAEN,mBAAiB,CAAC,aAAa,eAAe,eAAe;AAAA,UAAA;AAAA,UA5BxD;AAAA,QAAA,CA8BR;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGP,CAAC;AACD,OAAO,cAAc;AAId,MAAM,aAAa;AAAA,EACxB,WAAW;AAAA,EACX,QAAQ;AAAA;AAAA,EACR,UAAU,CAAA;AAAA;AAAA;AAAA;AAAA,EAIV,OAAO;AAAA,IACL,IAAI,EAAE,aAAa,IAAI,MAAM,sBAAA;AAAA,IAC7B,IAAI,EAAE,aAAa,IAAI,MAAM,+BAAA;AAAA,IAC7B,IAAI,EAAE,aAAa,IAAI,MAAM,wBAAA;AAAA,EAAwB;AAAA,EAEvD,QAAQ,CAAC,WAAW,SAAS,UAAU,iBAAiB,UAAU;AAAA,EAClE,QAAQ;AAAA,IACN,IAAI,CAAC,YAAY,cAAc,gBAAgB,kBAAkB,WAAW;AAAA,IAC5E,IAAI,CAAA;AAAA,IACJ,MAAM,CAAA;AAAA,EAAC;AAEX;"}
@@ -17,7 +17,8 @@ import type { FieldMode, FieldVariant } from '../../components/Field/field-types
17
17
  * OFF: track border (neutral-5), thumb 白色 + 2px border-border(neutral-5,與 OFF track 同色)無 check
18
18
  * ON: track primary, thumb 白色 + 2px primary border + primary check icon
19
19
  * disabled: opacity-disabled(整體透明度)
20
- * readOnly: 視覺同一般態,但 pointer-events-none + aria-readonly
20
+ * readOnly(standalone): 視覺同一般態,但 pointer-events-none + aria-readonly
21
+ * readOnly(Field 內): 渲染 readonly 灰框 + ✓/—(2026-06-12 拍板,= Input readonly 同視覺語言)
21
22
  *
22
23
  * ── label / description / readOnly ──
23
24
  * Switch 可以透過 `label` 和 `description` props 在元件內直接渲染緊鄰的文字,
@@ -37,7 +38,8 @@ import type { FieldMode, FieldVariant } from '../../components/Field/field-types
37
38
  *
38
39
  * readOnly 模式:
39
40
  * <Switch readOnly checked={true} label="..." />
40
- * 視覺維持 ON/OFF 正確狀態,但無法互動、不在 tab order 內、寫入 aria-readonly。
41
+ * standalone:視覺維持 ON/OFF 正確狀態,但無法互動、不在 tab order 內、寫入 aria-readonly。
42
+ * Field 內:渲染 readonly 灰框 + ✓/—(不渲染 toggle;見 forwardRef 內 readonly 分支)。
41
43
  * 與 disabled 的差異:readonly 不降色(可讀),disabled 降色(弱化)。
42
44
  */
43
45
  declare const switchVariants: (props?: ({
@@ -57,9 +59,9 @@ export interface SwitchProps extends React.ComponentPropsWithoutRef<typeof Switc
57
59
  */
58
60
  description?: React.ReactNode;
59
61
  /**
60
- * readonly 模式:鎖定互動但維持 ON/OFF 視覺正確。
61
- * 與 disabled 的差異:readonly 不降色(可讀),disabled 降色(弱化)。
62
- * 用於表單 readonly 呈現、DataTable cell 非編輯態。
62
+ * readonly 模式:standalone = 鎖定互動但維持 ON/OFF 視覺;Field 內 = 灰框 + ✓/—。
63
+ * 與 disabled 的差異:readonly 不降色(可讀),disabled 降色(弱化)。
64
+ * DataTable cell 非編輯態用 mode="display"(✓/—),非 readOnly。
63
65
  */
64
66
  readOnly?: boolean;
65
67
  /**
@@ -67,8 +69,8 @@ export interface SwitchProps extends React.ComponentPropsWithoutRef<typeof Switc
67
69
  * edit — 一般可互動 Switch(預設)
68
70
  * display — **純展示**:渲染 ✓ / —(無互動 primitive、無 input chrome);
69
71
  * 對齊 Carbon read-only / DataTable boolean cell。
70
- * `readonly` 保留 toggle 視覺 + 鎖互動;`display` 完全無 toggle 形體 — 兩者語意分離(field-types.ts)。
71
- * readonly — 同 readOnly prop
72
+ * `display` 完全無 toggle 形體;`readonly` 視場景(field-types.ts)。
73
+ * readonly — standalone 同 readOnly prop(保留視覺鎖互動);Field 內 = 灰框 + ✓/—
72
74
  * disabled — 同 disabled prop
73
75
  */
74
76
  mode?: FieldMode;
@@ -1 +1 @@
1
- {"version":3,"file":"switch.d.ts","sourceRoot":"","sources":["../../../src/components/Switch/switch.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,gBAAgB,MAAM,wBAAwB,CAAA;AAE1D,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEjE,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAG3F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,QAAA,MAAM,cAAc;;8EAsBnB,CAAA;AAcD,MAAM,WAAW,WACf,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,gBAAgB,CAAC,IAAI,CAAC,EAClE,YAAY,CAAC,OAAO,cAAc,CAAC;IACrC;;;;OAIG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB;;;;OAIG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB;AAED,QAAA,MAAM,MAAM,uFA6IX,CAAA;AAKD,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkBb,CAAA;AAEV,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"switch.d.ts","sourceRoot":"","sources":["../../../src/components/Switch/switch.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,gBAAgB,MAAM,wBAAwB,CAAA;AAE1D,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAEjE,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAI3F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,QAAA,MAAM,cAAc;;8EAsBnB,CAAA;AAcD,MAAM,WAAW,WACf,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,gBAAgB,CAAC,IAAI,CAAC,EAClE,YAAY,CAAC,OAAO,cAAc,CAAC;IACrC;;;;OAIG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB;;;;OAIG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB;AAED,QAAA,MAAM,MAAM,uFA2KX,CAAA;AAKD,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkBb,CAAA;AAEV,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}
@@ -4,7 +4,8 @@ import * as SwitchPrimitives from "@radix-ui/react-switch";
4
4
  import { Check } from "lucide-react";
5
5
  import { cva } from "class-variance-authority";
6
6
  import { cn } from "../../lib/utils.js";
7
- import { useFieldContext, useResolvedFieldDisabled, useResolvedFieldMode } from "../Field/field-context.js";
7
+ import { useFieldContext, useResolvedFieldDisabled, useResolvedFieldMode, useResolvedFieldSize } from "../Field/field-context.js";
8
+ import { fieldWrapperStyles } from "../Field/field-wrapper.js";
8
9
  const switchVariants = cva(
9
10
  [
10
11
  "group peer inline-flex shrink-0 cursor-pointer items-center rounded-full",
@@ -62,6 +63,8 @@ const Switch = React.forwardRef(
62
63
  const disabled = useResolvedFieldDisabled(disabledProp);
63
64
  const resolvedMode = useResolvedFieldMode({ mode, disabled, readOnly });
64
65
  const effectiveReadOnly = readOnly || resolvedMode === "readonly";
66
+ const effectiveDisabled = disabled || resolvedMode === "disabled";
67
+ const resolvedBoxSize = useResolvedFieldSize(size ?? void 0, "md");
65
68
  const insideField = (fieldCtx == null ? void 0 : fieldCtx.hasFieldWrapper) === true;
66
69
  const effectiveLabel = insideField ? void 0 : label;
67
70
  const effectiveDescription = insideField ? void 0 : description;
@@ -72,13 +75,35 @@ const Switch = React.forwardRef(
72
75
  const isChecked = props.checked === true;
73
76
  return isChecked ? /* @__PURE__ */ jsx("span", { className: "text-foreground", children: "✓" }) : /* @__PURE__ */ jsx("span", { className: "text-fg-muted", children: "—" });
74
77
  }
78
+ if (effectiveReadOnly && insideField) {
79
+ const isChecked = (props.checked ?? props.defaultChecked) === true;
80
+ const boxSize = resolvedBoxSize;
81
+ return /* @__PURE__ */ jsx(
82
+ "div",
83
+ {
84
+ role: "switch",
85
+ "aria-checked": isChecked,
86
+ "aria-readonly": "true",
87
+ "aria-labelledby": fieldCtx == null ? void 0 : fieldCtx.labelId,
88
+ "aria-invalid": (fieldCtx == null ? void 0 : fieldCtx.invalid) || void 0,
89
+ "data-readonly": "true",
90
+ tabIndex: 0,
91
+ className: cn(
92
+ fieldWrapperStyles({ size: boxSize, mode: "readonly", variant: "default" }),
93
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
94
+ className
95
+ ),
96
+ children: isChecked ? /* @__PURE__ */ jsx("span", { className: "text-foreground", children: "✓" }) : /* @__PURE__ */ jsx("span", { className: "text-fg-muted", children: "—" })
97
+ }
98
+ );
99
+ }
75
100
  const rootEl = /* @__PURE__ */ jsx(
76
101
  SwitchPrimitives.Root,
77
102
  {
78
103
  id: inputId,
79
104
  className: cn(switchVariants({ size }), alignRightInField, className),
80
105
  ref,
81
- disabled,
106
+ disabled: effectiveDisabled,
82
107
  "aria-readonly": effectiveReadOnly || void 0,
83
108
  "data-readonly": effectiveReadOnly || void 0,
84
109
  tabIndex: effectiveReadOnly ? -1 : void 0,
@@ -115,7 +140,7 @@ const Switch = React.forwardRef(
115
140
  htmlFor: inputId,
116
141
  className: cn(
117
142
  "inline-flex items-start gap-3 select-none",
118
- disabled ? "cursor-not-allowed" : readOnly ? "cursor-default" : "cursor-pointer"
143
+ effectiveDisabled ? "cursor-not-allowed" : readOnly ? "cursor-default" : "cursor-pointer"
119
144
  ),
120
145
  children: [
121
146
  /* @__PURE__ */ jsxs(
@@ -132,7 +157,7 @@ const Switch = React.forwardRef(
132
157
  className: cn(
133
158
  // Reading mode 字級:lg → text-body-lg (16px),sm/md → text-body (14px)
134
159
  sizeKey === "lg" ? "text-body-lg" : "text-body",
135
- disabled ? "text-fg-disabled" : "text-foreground"
160
+ effectiveDisabled ? "text-fg-disabled" : "text-foreground"
136
161
  ),
137
162
  children: effectiveLabel
138
163
  }
@@ -141,7 +166,7 @@ const Switch = React.forwardRef(
141
166
  "span",
142
167
  {
143
168
  className: cn(
144
- disabled ? "text-fg-disabled" : "text-fg-secondary"
169
+ effectiveDisabled ? "text-fg-disabled" : "text-fg-secondary"
145
170
  ),
146
171
  style: { fontSize: "var(--font-body-size)" },
147
172
  children: effectiveDescription
@@ -1 +1 @@
1
- {"version":3,"file":"switch.js","sources":["../../../src/components/Switch/switch.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 SwitchPrimitives from '@radix-ui/react-switch'\nimport { Check } from 'lucide-react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@/lib/utils'\nimport type { FieldMode, FieldVariant } from '@/design-system/components/Field/field-types'\nimport { useFieldContext, useResolvedFieldDisabled, useResolvedFieldMode } from '@/design-system/components/Field/field-context'\n\n/**\n * Switch — 開關控件\n *\n * ── 結構 ──\n * Track(pill 形容器)→ Thumb(白色圓 + 2px border + check icon)\n * Track 寬 = 2 × 高,thumb 直徑 = track 高度\n *\n * ── 尺寸(sm = md)──\n * sm/md: track 20×40, thumb 20, 白色圓 16, check 12(= checkbox sm/md)\n * lg: track 24×48, thumb 24, 白色圓 20, check 16(= checkbox lg)\n *\n * ── 視覺狀態 ──\n * OFF: track border (neutral-5), thumb 白色 + 2px border-border(neutral-5,與 OFF track 同色)無 check\n * ON: track primary, thumb 白色 + 2px primary border + primary check icon\n * disabled: opacity-disabled(整體透明度)\n * readOnly: 視覺同一般態,但 pointer-events-none + aria-readonly\n *\n * ── label / description / readOnly ──\n * Switch 可以透過 `label` 和 `description` props 在元件內直接渲染緊鄰的文字,\n * 樣式全部 codify 在元件內(text-body、foreground/fg-secondary、disabled 色)。\n *\n * 單獨使用時:\n * <Switch label=\"啟用通知\" description=\"收到新訊息時提醒\" />\n *\n * Form 內使用(在 <Field> context 內):\n * <Field>\n * <FieldLabel>啟用通知</FieldLabel>\n * <Switch /> ← label/description prop 會被自動忽略\n * <FieldDescription>收到新訊息時提醒</FieldDescription>\n * </Field>\n *\n * Field context 透過 useFieldContext() 偵測,避免雙層 label。\n *\n * readOnly 模式:\n * <Switch readOnly checked={true} label=\"...\" />\n * 視覺維持 ON/OFF 正確狀態,但無法互動、不在 tab order 內、寫入 aria-readonly。\n * 與 disabled 的差異:readonly 不降色(可讀),disabled 降色(弱化)。\n */\n\nconst switchVariants = cva(\n [\n 'group peer inline-flex shrink-0 cursor-pointer items-center rounded-full',\n 'transition-colors duration-150',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n 'disabled:cursor-not-allowed disabled:opacity-disabled',\n // readOnly:鎖定互動但視覺正常\n 'data-[readonly=true]:pointer-events-none data-[readonly=true]:cursor-default',\n // OFF → ON 背景色\n 'data-[state=unchecked]:bg-border',\n 'data-[state=checked]:bg-primary',\n ],\n {\n variants: {\n size: {\n sm: 'h-5 w-10', // 20×40\n md: 'h-5 w-10', // 20×40\n lg: 'h-6 w-12', // 24×48\n },\n },\n defaultVariants: { size: 'md' },\n }\n)\n\nconst SPECS: Record<string, { thumb: number; check: number; checkStroke: number; translate: string }> = {\n // checkStroke:16px 以下 icon 視覺不夠顯眼 → 加粗 stroke 補償(跟 Checkbox 共用原則,\n // 見 checkbox.tsx 的 checkStrokeWidth 註解)。12px 用 3.5(render ≈ 1.75px 線寬,比 Lucide\n // 預設的 1px render 明顯更粗,視覺跟 16px 預設 stroke 的 1.33px 有足夠區別),16px 用 2.5\n // (render ≈ 1.67px,比預設 1.33px 稍粗讓 toggle check 夠顯眼)。跨 size 配對值由 checkbox.tsx 共用。\n // 2026-05-18 簡化 per user 視覺證 + Checkbox 同步(3.5 → 3 sm/md):effective render 差\n // 0.08px 視覺看不出,保留 compensation 主旨但不過度差異化。\n sm: { thumb: 20, check: 12, checkStroke: 3, translate: 'translateX(20px)' },\n md: { thumb: 20, check: 12, checkStroke: 3, translate: 'translateX(20px)' },\n lg: { thumb: 24, check: 16, checkStroke: 2.5, translate: 'translateX(24px)' },\n}\n\nexport interface SwitchProps\n extends React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>,\n VariantProps<typeof switchVariants> {\n /**\n * Inline label。提供時 Switch 自動包一個 <label> 並連結 htmlFor,\n * 套用 text-body / text-foreground / disabled 色 的 codified 樣式。\n * 在 <Field> context 內時此 prop 會被忽略(由 FieldLabel 接管)。\n */\n label?: React.ReactNode\n /**\n * Inline description(secondary 文字)。須與 label 搭配使用,\n * 單獨設定 description 無效果。套用 text-body / text-fg-secondary 樣式。\n * 在 <Field> context 內時此 prop 會被忽略(由 FieldDescription 接管)。\n */\n description?: React.ReactNode\n /**\n * readonly 模式:鎖定互動但維持 ON/OFF 視覺正確。\n * 與 disabled 的差異:readonly 不降色(可讀),disabled 降色(弱化)。\n * 用於表單 readonly 呈現、DataTable cell 非編輯態。\n */\n readOnly?: boolean\n /**\n * Field mode(2026-05-05 Phase B3 align):\n * edit — 一般可互動 Switch(預設)\n * display — **純展示**:渲染 ✓ / —(無互動 primitive、無 input chrome);\n * 對齊 Carbon read-only / DataTable boolean cell。\n * `readonly` 保留 toggle 視覺 + 鎖互動;`display` 完全無 toggle 形體 — 兩者語意分離(field-types.ts)。\n * readonly — 同 readOnly prop\n * disabled — 同 disabled prop\n */\n mode?: FieldMode\n /**\n * Visual chrome — Switch 本體無 input wrapper variant,本 prop 對 Switch 主體無視覺影響;\n * 為對齊 Field 4-mode + chrome 透傳契約而保留(M19 一致性)。\n */\n variant?: FieldVariant\n}\n\nconst Switch = React.forwardRef<\n React.ElementRef<typeof SwitchPrimitives.Root>,\n SwitchProps\n>(\n (\n {\n className,\n size,\n label,\n description,\n readOnly = false,\n disabled: disabledProp,\n mode,\n // chrome 對 Switch 主體無視覺影響(無 input wrapper)— 接收純為 prop 一致性;destructure 防 leak 到 DOM。\n variant: _chrome,\n id: idProp,\n ...props\n },\n ref\n ) => {\n const sizeKey = size ?? 'md'\n const spec = SPECS[sizeKey]\n\n // Field context 偵測:在 Field 內時忽略自己的 label/description,避免雙層\n // 2026-05-31 #35:hooks(useFieldContext / useId)必在任何 conditional return 前呼叫(Rules of Hooks)。\n // 原 mode='display' early return 寫在 hooks 之上 → runtime 切 mode 會 hook count 不一致 crash;已下移至 hooks 後。\n const fieldCtx = useFieldContext()\n // 2026-06-08 SSOT:<Field disabled>/<Field mode> cascade(原 disabled/mode 直傳 prop,漏 fieldCtx)\n const disabled = useResolvedFieldDisabled(disabledProp)\n const resolvedMode = useResolvedFieldMode({ mode, disabled, readOnly })\n const effectiveReadOnly = readOnly || resolvedMode === 'readonly'\n const insideField = fieldCtx?.hasFieldWrapper === true\n const effectiveLabel = insideField ? undefined : label\n const effectiveDescription = insideField ? undefined : description\n\n // 在 horizontal Field 內自動齊右(iOS / macOS Settings canonical):\n // horizontal Field 的 layout 是 label(固定寬)| control area(fill),Switch 作為\n // trailing control 應靠右(對齊 horizontal DescriptionItem 的「label 左 / value 右」模式)。\n // 世界級對照:iOS Settings / macOS System Settings / GitHub Settings / Figma prefs\n // 一律 switch 齊右 — 視覺掃描快、對齊一致。\n const alignRightInField =\n insideField && fieldCtx?.orientation === 'horizontal' ? 'ml-auto' : ''\n\n // Id 連結:優先使用 prop,再退到 Field context 的 id,最後用 useId 生成\n const generatedId = React.useId()\n const inputId = idProp ?? fieldCtx?.id ?? generatedId\n\n // ── mode='display'(下移至所有 hooks 之後,per #35 Rules of Hooks)──────────\n // 純展示模式:無互動 toggle、渲染 ✓ / —。與 Checkbox display 對齊(同 boolean primitive)。\n if (resolvedMode === 'display') {\n const isChecked = props.checked === true\n return isChecked\n ? <span className=\"text-foreground\">✓</span>\n : <span className=\"text-fg-muted\">—</span>\n }\n\n const rootEl = (\n <SwitchPrimitives.Root\n id={inputId}\n className={cn(switchVariants({ size }), alignRightInField, className)}\n ref={ref}\n disabled={disabled}\n aria-readonly={effectiveReadOnly || undefined}\n data-readonly={effectiveReadOnly || undefined}\n tabIndex={effectiveReadOnly ? -1 : undefined}\n aria-describedby={fieldCtx?.descriptionId}\n {...props}\n >\n <SwitchPrimitives.Thumb\n className={cn(\n 'pointer-events-none flex items-center justify-center rounded-full bg-on-emphasis border-2',\n 'transition-all duration-150',\n 'data-[state=unchecked]:translate-x-0 data-[state=unchecked]:border-border',\n 'data-[state=checked]:border-primary',\n sizeKey === 'lg' ? 'data-[state=checked]:translate-x-6' : 'data-[state=checked]:translate-x-5',\n )}\n style={{ width: spec.thumb, height: spec.thumb }}\n >\n {/* Check icon — Radix Thumb inherits data-state from Root */}\n <Check\n size={spec.check}\n strokeWidth={spec.checkStroke}\n className=\"text-primary opacity-0 transition-opacity duration-150 group-data-[state=checked]:opacity-100\"\n aria-hidden\n />\n </SwitchPrimitives.Thumb>\n </SwitchPrimitives.Root>\n )\n\n // 無 label → 只渲染 switch 本體\n if (effectiveLabel == null) return rootEl\n\n // 有 label → 包 <label> + codified 樣式\n // Switch 慣例:label 在左、switch 在右(對齊 iOS / Polaris / Material 標準)\n // label 行第一行對齊 switch 中線:容器用 items-start + switch 包 h-[1lh] flex-center\n return (\n <label\n htmlFor={inputId}\n className={cn(\n 'inline-flex items-start gap-3 select-none',\n disabled ? 'cursor-not-allowed' : readOnly ? 'cursor-default' : 'cursor-pointer'\n )}\n >\n {/* Label↔desc gap typography-mode-aware:\n sm/md = reading(body+body 14/1.5),lg = reading-lg(body-lg+body 14/1.5) */}\n <span\n className={cn(\n 'flex-1 min-w-0 flex flex-col',\n sizeKey === 'lg'\n ? 'gap-[var(--item-gap-label-desc-reading-lg)]'\n : 'gap-[var(--item-gap-label-desc-reading)]',\n )}\n >\n <span\n className={cn(\n // Reading mode 字級:lg → text-body-lg (16px),sm/md → text-body (14px)\n sizeKey === 'lg' ? 'text-body-lg' : 'text-body',\n disabled ? 'text-fg-disabled' : 'text-foreground'\n )}\n >\n {effectiveLabel}\n </span>\n {effectiveDescription != null && (\n <span\n className={cn(\n disabled ? 'text-fg-disabled' : 'text-fg-secondary'\n )}\n // Reading mode description:**最小 14px**(spec 14→14px, 16→14px),lh 預設 1.5。\n // 用 inline style 直接繞過 tailwind-merge 對 text-body / text-fg-* 的潛在衝突。\n style={{ fontSize: 'var(--font-body-size)' }}\n >\n {effectiveDescription}\n </span>\n )}\n </span>\n <span className=\"h-[1lh] flex items-center shrink-0\">\n {rootEl}\n </span>\n </label>\n )\n }\n)\nSwitch.displayName = SwitchPrimitives.Root.displayName\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 switchMeta = {\n component: 'Switch',\n family: null, // self-contained primitive(對齊 spec frontmatter self-contained + body L31;非 Family 4)\n variants: {\n\n },\n sizes: {\n sm: { fieldHeight: 28, iconSize: 12, typography: 'body' },\n md: { fieldHeight: 32, iconSize: 12, typography: 'body' },\n lg: { fieldHeight: 36, iconSize: 16, typography: 'body-lg' },\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-primary'],\n fg: ['text-fg-disabled', 'text-fg-secondary', 'text-foreground', 'text-primary'],\n ring: ['ring-ring'],\n },\n defaultSize: 'md',\n} as const\n\nexport { Switch, switchVariants }\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"switch.js","sources":["../../../src/components/Switch/switch.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 SwitchPrimitives from '@radix-ui/react-switch'\nimport { Check } from 'lucide-react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@/lib/utils'\nimport type { FieldMode, FieldVariant } from '@/design-system/components/Field/field-types'\nimport { useFieldContext, useResolvedFieldDisabled, useResolvedFieldMode, useResolvedFieldSize } from '@/design-system/components/Field/field-context'\nimport { fieldWrapperStyles } from '@/design-system/components/Field/field-wrapper'\n\n/**\n * Switch — 開關控件\n *\n * ── 結構 ──\n * Track(pill 形容器)→ Thumb(白色圓 + 2px border + check icon)\n * Track 寬 = 2 × 高,thumb 直徑 = track 高度\n *\n * ── 尺寸(sm = md)──\n * sm/md: track 20×40, thumb 20, 白色圓 16, check 12(= checkbox sm/md)\n * lg: track 24×48, thumb 24, 白色圓 20, check 16(= checkbox lg)\n *\n * ── 視覺狀態 ──\n * OFF: track border (neutral-5), thumb 白色 + 2px border-border(neutral-5,與 OFF track 同色)無 check\n * ON: track primary, thumb 白色 + 2px primary border + primary check icon\n * disabled: opacity-disabled(整體透明度)\n * readOnly(standalone): 視覺同一般態,但 pointer-events-none + aria-readonly\n * readOnly(Field 內): 渲染 readonly 灰框 + ✓/—(2026-06-12 拍板,= Input readonly 同視覺語言)\n *\n * ── label / description / readOnly ──\n * Switch 可以透過 `label` 和 `description` props 在元件內直接渲染緊鄰的文字,\n * 樣式全部 codify 在元件內(text-body、foreground/fg-secondary、disabled 色)。\n *\n * 單獨使用時:\n * <Switch label=\"啟用通知\" description=\"收到新訊息時提醒\" />\n *\n * Form 內使用(在 <Field> context 內):\n * <Field>\n * <FieldLabel>啟用通知</FieldLabel>\n * <Switch /> ← label/description prop 會被自動忽略\n * <FieldDescription>收到新訊息時提醒</FieldDescription>\n * </Field>\n *\n * Field context 透過 useFieldContext() 偵測,避免雙層 label。\n *\n * readOnly 模式:\n * <Switch readOnly checked={true} label=\"...\" />\n * standalone:視覺維持 ON/OFF 正確狀態,但無法互動、不在 tab order 內、寫入 aria-readonly。\n * Field 內:渲染 readonly 灰框 + ✓/—(不渲染 toggle;見 forwardRef 內 readonly 分支)。\n * 與 disabled 的差異:readonly 不降色(可讀),disabled 降色(弱化)。\n */\n\nconst switchVariants = cva(\n [\n 'group peer inline-flex shrink-0 cursor-pointer items-center rounded-full',\n 'transition-colors duration-150',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',\n 'disabled:cursor-not-allowed disabled:opacity-disabled',\n // readOnly:鎖定互動但視覺正常\n 'data-[readonly=true]:pointer-events-none data-[readonly=true]:cursor-default',\n // OFF → ON 背景色\n 'data-[state=unchecked]:bg-border',\n 'data-[state=checked]:bg-primary',\n ],\n {\n variants: {\n size: {\n sm: 'h-5 w-10', // 20×40\n md: 'h-5 w-10', // 20×40\n lg: 'h-6 w-12', // 24×48\n },\n },\n defaultVariants: { size: 'md' },\n }\n)\n\nconst SPECS: Record<string, { thumb: number; check: number; checkStroke: number; translate: string }> = {\n // checkStroke:16px 以下 icon 視覺不夠顯眼 → 加粗 stroke 補償(跟 Checkbox 共用原則,\n // 見 checkbox.tsx 的 checkStrokeWidth 註解)。12px 用 3.5(render ≈ 1.75px 線寬,比 Lucide\n // 預設的 1px render 明顯更粗,視覺跟 16px 預設 stroke 的 1.33px 有足夠區別),16px 用 2.5\n // (render ≈ 1.67px,比預設 1.33px 稍粗讓 toggle check 夠顯眼)。跨 size 配對值由 checkbox.tsx 共用。\n // 2026-05-18 簡化 per user 視覺證 + Checkbox 同步(3.5 → 3 sm/md):effective render 差\n // 0.08px 視覺看不出,保留 compensation 主旨但不過度差異化。\n sm: { thumb: 20, check: 12, checkStroke: 3, translate: 'translateX(20px)' },\n md: { thumb: 20, check: 12, checkStroke: 3, translate: 'translateX(20px)' },\n lg: { thumb: 24, check: 16, checkStroke: 2.5, translate: 'translateX(24px)' },\n}\n\nexport interface SwitchProps\n extends React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>,\n VariantProps<typeof switchVariants> {\n /**\n * Inline label。提供時 Switch 自動包一個 <label> 並連結 htmlFor,\n * 套用 text-body / text-foreground / disabled 色 的 codified 樣式。\n * 在 <Field> context 內時此 prop 會被忽略(由 FieldLabel 接管)。\n */\n label?: React.ReactNode\n /**\n * Inline description(secondary 文字)。須與 label 搭配使用,\n * 單獨設定 description 無效果。套用 text-body / text-fg-secondary 樣式。\n * 在 <Field> context 內時此 prop 會被忽略(由 FieldDescription 接管)。\n */\n description?: React.ReactNode\n /**\n * readonly 模式:standalone = 鎖定互動但維持 ON/OFF 視覺;Field 內 = 灰框 + ✓/—。\n * 與 disabled 的差異:readonly 不降色(可讀),disabled 降色(弱化)。\n * DataTable cell 非編輯態用 mode=\"display\"(✓/—),非 readOnly。\n */\n readOnly?: boolean\n /**\n * Field mode(2026-05-05 Phase B3 align):\n * edit — 一般可互動 Switch(預設)\n * display — **純展示**:渲染 ✓ / —(無互動 primitive、無 input chrome);\n * 對齊 Carbon read-only / DataTable boolean cell。\n * `display` 完全無 toggle 形體;`readonly` 視場景(field-types.ts)。\n * readonly — standalone 同 readOnly prop(保留視覺鎖互動);Field 內 = 灰框 + ✓/—\n * disabled — 同 disabled prop\n */\n mode?: FieldMode\n /**\n * Visual chrome — Switch 本體無 input wrapper variant,本 prop 對 Switch 主體無視覺影響;\n * 為對齊 Field 4-mode + chrome 透傳契約而保留(M19 一致性)。\n */\n variant?: FieldVariant\n}\n\nconst Switch = React.forwardRef<\n React.ElementRef<typeof SwitchPrimitives.Root>,\n SwitchProps\n>(\n (\n {\n className,\n size,\n label,\n description,\n readOnly = false,\n disabled: disabledProp,\n mode,\n // chrome 對 Switch 主體無視覺影響(無 input wrapper)— 接收純為 prop 一致性;destructure 防 leak 到 DOM。\n variant: _chrome,\n id: idProp,\n ...props\n },\n ref\n ) => {\n const sizeKey = size ?? 'md'\n const spec = SPECS[sizeKey]\n\n // Field context 偵測:在 Field 內時忽略自己的 label/description,避免雙層\n // 2026-05-31 #35:hooks(useFieldContext / useId)必在任何 conditional return 前呼叫(Rules of Hooks)。\n // 原 mode='display' early return 寫在 hooks 之上 → runtime 切 mode 會 hook count 不一致 crash;已下移至 hooks 後。\n const fieldCtx = useFieldContext()\n // 2026-06-08 SSOT:<Field disabled>/<Field mode> cascade(原 disabled/mode 直傳 prop,漏 fieldCtx)\n const disabled = useResolvedFieldDisabled(disabledProp)\n const resolvedMode = useResolvedFieldMode({ mode, disabled, readOnly })\n const effectiveReadOnly = readOnly || resolvedMode === 'readonly'\n // mode='disabled' 直傳(無 Field ctx 的 cell 場景)必須落到真 disabled chrome(同 Checkbox 2026-06-12 修)\n const effectiveDisabled = disabled || resolvedMode === 'disabled'\n // readonly 灰框 size:走 SSOT resolver(prop > ctx > 'md',field-context.ts:150-161)\n const resolvedBoxSize = useResolvedFieldSize(size ?? undefined, 'md') as 'sm' | 'md' | 'lg'\n const insideField = fieldCtx?.hasFieldWrapper === true\n const effectiveLabel = insideField ? undefined : label\n const effectiveDescription = insideField ? undefined : description\n\n // 在 horizontal Field 內自動齊右(iOS / macOS Settings canonical):\n // horizontal Field 的 layout 是 label(固定寬)| control area(fill),Switch 作為\n // trailing control 應靠右(對齊 horizontal DescriptionItem 的「label 左 / value 右」模式)。\n // 世界級對照:iOS Settings / macOS System Settings / GitHub Settings / Figma prefs\n // 一律 switch 齊右 — 視覺掃描快、對齊一致。\n const alignRightInField =\n insideField && fieldCtx?.orientation === 'horizontal' ? 'ml-auto' : ''\n\n // Id 連結:優先使用 prop,再退到 Field context 的 id,最後用 useId 生成\n const generatedId = React.useId()\n const inputId = idProp ?? fieldCtx?.id ?? generatedId\n\n // ── mode='display'(下移至所有 hooks 之後,per #35 Rules of Hooks)──────────\n // 純展示模式:無互動 toggle、渲染 ✓ / —。與 Checkbox display 對齊(同 boolean primitive)。\n if (resolvedMode === 'display') {\n const isChecked = props.checked === true\n return isChecked\n ? <span className=\"text-foreground\">✓</span>\n : <span className=\"text-fg-muted\">—</span>\n }\n\n // ── mode='readonly' in Field(2026-06-12 user 拍板「灰框 + ✓/—」,與 Checkbox 同款)──\n // Field 內 readonly boolean = fieldWrapperStyles readonly 灰框(= Input readonly 同源)\n // + ✓/— 值語言;standalone readOnly(settings list)維持原樣鎖互動。詳 checkbox.tsx 同段註解。\n if (effectiveReadOnly && insideField) {\n const isChecked = (props.checked ?? props.defaultChecked) === true\n const boxSize = resolvedBoxSize\n return (\n <div\n role=\"switch\"\n aria-checked={isChecked}\n aria-readonly=\"true\"\n aria-labelledby={fieldCtx?.labelId}\n aria-invalid={fieldCtx?.invalid || undefined}\n data-readonly=\"true\"\n tabIndex={0}\n className={cn(\n fieldWrapperStyles({ size: boxSize, mode: 'readonly', variant: 'default' }),\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n className,\n )}\n >\n {isChecked ? <span className=\"text-foreground\">✓</span> : <span className=\"text-fg-muted\">—</span>}\n </div>\n )\n }\n\n const rootEl = (\n <SwitchPrimitives.Root\n id={inputId}\n className={cn(switchVariants({ size }), alignRightInField, className)}\n ref={ref}\n disabled={effectiveDisabled}\n aria-readonly={effectiveReadOnly || undefined}\n data-readonly={effectiveReadOnly || undefined}\n tabIndex={effectiveReadOnly ? -1 : undefined}\n aria-describedby={fieldCtx?.descriptionId}\n {...props}\n >\n <SwitchPrimitives.Thumb\n className={cn(\n 'pointer-events-none flex items-center justify-center rounded-full bg-on-emphasis border-2',\n 'transition-all duration-150',\n 'data-[state=unchecked]:translate-x-0 data-[state=unchecked]:border-border',\n 'data-[state=checked]:border-primary',\n sizeKey === 'lg' ? 'data-[state=checked]:translate-x-6' : 'data-[state=checked]:translate-x-5',\n )}\n style={{ width: spec.thumb, height: spec.thumb }}\n >\n {/* Check icon — Radix Thumb inherits data-state from Root */}\n <Check\n size={spec.check}\n strokeWidth={spec.checkStroke}\n className=\"text-primary opacity-0 transition-opacity duration-150 group-data-[state=checked]:opacity-100\"\n aria-hidden\n />\n </SwitchPrimitives.Thumb>\n </SwitchPrimitives.Root>\n )\n\n // 無 label → 只渲染 switch 本體\n if (effectiveLabel == null) return rootEl\n\n // 有 label → 包 <label> + codified 樣式\n // Switch 慣例:label 在左、switch 在右(對齊 iOS / Polaris / Material 標準)\n // label 行第一行對齊 switch 中線:容器用 items-start + switch 包 h-[1lh] flex-center\n return (\n <label\n htmlFor={inputId}\n className={cn(\n 'inline-flex items-start gap-3 select-none',\n effectiveDisabled ? 'cursor-not-allowed' : readOnly ? 'cursor-default' : 'cursor-pointer'\n )}\n >\n {/* Label↔desc gap typography-mode-aware:\n sm/md = reading(body+body 14/1.5),lg = reading-lg(body-lg+body 14/1.5) */}\n <span\n className={cn(\n 'flex-1 min-w-0 flex flex-col',\n sizeKey === 'lg'\n ? 'gap-[var(--item-gap-label-desc-reading-lg)]'\n : 'gap-[var(--item-gap-label-desc-reading)]',\n )}\n >\n <span\n className={cn(\n // Reading mode 字級:lg → text-body-lg (16px),sm/md → text-body (14px)\n sizeKey === 'lg' ? 'text-body-lg' : 'text-body',\n effectiveDisabled ? 'text-fg-disabled' : 'text-foreground'\n )}\n >\n {effectiveLabel}\n </span>\n {effectiveDescription != null && (\n <span\n className={cn(\n effectiveDisabled ? 'text-fg-disabled' : 'text-fg-secondary'\n )}\n // Reading mode description:**最小 14px**(spec 14→14px, 16→14px),lh 預設 1.5。\n // 用 inline style 直接繞過 tailwind-merge 對 text-body / text-fg-* 的潛在衝突。\n style={{ fontSize: 'var(--font-body-size)' }}\n >\n {effectiveDescription}\n </span>\n )}\n </span>\n <span className=\"h-[1lh] flex items-center shrink-0\">\n {rootEl}\n </span>\n </label>\n )\n }\n)\nSwitch.displayName = SwitchPrimitives.Root.displayName\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 switchMeta = {\n component: 'Switch',\n family: null, // self-contained primitive(對齊 spec frontmatter self-contained + body L31;非 Family 4)\n variants: {\n\n },\n sizes: {\n sm: { fieldHeight: 28, iconSize: 12, typography: 'body' },\n md: { fieldHeight: 32, iconSize: 12, typography: 'body' },\n lg: { fieldHeight: 36, iconSize: 16, typography: 'body-lg' },\n },\n states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],\n tokens: {\n bg: ['bg-primary'],\n fg: ['text-fg-disabled', 'text-fg-secondary', 'text-foreground', 'text-primary'],\n ring: ['ring-ring'],\n },\n defaultSize: 'md',\n} as const\n\nexport { Switch, switchVariants }\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/tabs.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,aAAa,MAAM,sBAAsB,CAAA;AAErD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAkB9C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,KAAK,QAAQ,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAClC,KAAK,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAU9C,UAAU,SAAU,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,aAAa,CAAC,IAAI,CAAC;CAAG;AAOxF,QAAA,MAAM,IAAI,kFAkCR,CAAA;AAiBF,UAAU,aACR,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,aAAa,CAAC,IAAI,CAAC;IACjE,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAA;CACxB;AAED,QAAA,MAAM,QAAQ,sFAmCZ,CAAA;AAgMF,QAAA,MAAM,mBAAmB;;8EA0CxB,CAAA;AAED,UAAU,gBACR,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,aAAa,CAAC,OAAO,CAAC;IACpE,0BAA0B;IAC1B,SAAS,CAAC,EAAE,UAAU,CAAA;IACtB,yBAAyB;IACzB,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB;;;;OAIG;IACH,OAAO,CAAC,EAAE,UAAU,CAAA;IACpB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC/B;AAED,QAAA,MAAM,WAAW,4FA6Cf,CAAA;AAIF,QAAA,MAAM,WAAW,0JAYf,CAAA;AAKF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmBX,CAAA;AAEV,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,CAAA;AACxE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,gBAAgB,EAAE,CAAA"}
1
+ {"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/tabs.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,aAAa,MAAM,sBAAsB,CAAA;AAErD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAkB9C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,KAAK,QAAQ,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAClC,KAAK,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAU9C,UAAU,SAAU,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,aAAa,CAAC,IAAI,CAAC;CAAG;AAOxF,QAAA,MAAM,IAAI,kFAkCR,CAAA;AAiBF,UAAU,aACR,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,aAAa,CAAC,IAAI,CAAC;IACjE,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAA;CACxB;AAED,QAAA,MAAM,QAAQ,sFAmCZ,CAAA;AAgMF,QAAA,MAAM,mBAAmB;;8EA0CxB,CAAA;AAED,UAAU,gBACR,SAAQ,KAAK,CAAC,wBAAwB,CAAC,OAAO,aAAa,CAAC,OAAO,CAAC;IACpE,0BAA0B;IAC1B,SAAS,CAAC,EAAE,UAAU,CAAA;IACtB,yBAAyB;IACzB,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACvB;;;;OAIG;IACH,OAAO,CAAC,EAAE,UAAU,CAAA;IACpB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC/B;AAED,QAAA,MAAM,WAAW,4FA6Cf,CAAA;AAIF,QAAA,MAAM,WAAW,0JAkBf,CAAA;AAKF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmBX,CAAA;AAEV,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,CAAA;AACxE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,gBAAgB,EAAE,CAAA"}
@@ -53,7 +53,7 @@ const TabsList = React.forwardRef(({ className, size = "sm", overflow = "none",
53
53
  TabsPrimitive.List,
54
54
  {
55
55
  ref,
56
- className: cn(TABS_LIST_BASE, "w-fit", className),
56
+ className: cn(TABS_LIST_BASE, "w-full", className),
57
57
  ...props,
58
58
  children
59
59
  }
@@ -89,7 +89,7 @@ const ScrollTabsList = React.forwardRef(({ className, children, ...props }, ref)
89
89
  TabsPrimitive.List,
90
90
  {
91
91
  ref,
92
- className: cn(TABS_LIST_BASE, "w-fit", className),
92
+ className: cn(TABS_LIST_BASE, "min-w-full", className),
93
93
  ...props,
94
94
  children
95
95
  }
@@ -156,7 +156,7 @@ const MenuTabsList = React.forwardRef(({ className, children, ...props }, ref) =
156
156
  TabsPrimitive.List,
157
157
  {
158
158
  ref,
159
- className: cn(TABS_LIST_BASE, "w-fit", className),
159
+ className: cn(TABS_LIST_BASE, "min-w-full", className),
160
160
  ...props,
161
161
  children: enhancedChildren
162
162
  }
@@ -280,6 +280,12 @@ const TabsContent = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
280
280
  {
281
281
  ref,
282
282
  className: cn(
283
+ // 與 TabsList 的間距 canonical(2026-06-12 user 拍板):--layout-space-tight(md 12px,
284
+ // density 連動)。依 layoutSpace.spec「親疏 3 級」:Tabs↔Content 同 bundle(第一級,
285
+ // 元件 spec own),值取規則 3「直接功能依賴 = tight」精神(heading → labeled content 同類)。
286
+ // 收斂原 DS-wide 四種土法(無間距 / mt-4 / p-4 / pt-4 — M17 假 SSOT)。
287
+ // full-height 佈局(AppShell pane)用 className="mt-0" 覆寫(tailwind-merge)。
288
+ "mt-[var(--layout-space-tight)]",
283
289
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
284
290
  className
285
291
  ),