@qijenchen/design-system 0.1.0-beta.3

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 (119) hide show
  1. package/package.json +93 -0
  2. package/src/README.md +32 -0
  3. package/src/components/Accordion/accordion.tsx +104 -0
  4. package/src/components/Alert/alert.tsx +188 -0
  5. package/src/components/AppShell/_demo-helpers.tsx +198 -0
  6. package/src/components/AppShell/app-shell.tsx +364 -0
  7. package/src/components/AspectRatio/aspect-ratio.tsx +58 -0
  8. package/src/components/Avatar/avatar.tsx +368 -0
  9. package/src/components/Badge/badge.tsx +104 -0
  10. package/src/components/Breadcrumb/breadcrumb.tsx +609 -0
  11. package/src/components/BulkActionBar/bulk-action-bar.tsx +156 -0
  12. package/src/components/Button/button-group.tsx +96 -0
  13. package/src/components/Button/button.tsx +539 -0
  14. package/src/components/Calendar/calendar.tsx +411 -0
  15. package/src/components/Carousel/carousel.tsx +371 -0
  16. package/src/components/Chart/chart.tsx +376 -0
  17. package/src/components/Checkbox/checkbox-group.tsx +94 -0
  18. package/src/components/Checkbox/checkbox.tsx +237 -0
  19. package/src/components/Chip/chip.tsx +359 -0
  20. package/src/components/CircularProgress/circular-progress.tsx +204 -0
  21. package/src/components/Coachmark/coachmark.tsx +255 -0
  22. package/src/components/Combobox/combobox.tsx +826 -0
  23. package/src/components/Command/command.tsx +187 -0
  24. package/src/components/DataTable/active-editor-controller.ts +72 -0
  25. package/src/components/DataTable/cell-registry.tsx +520 -0
  26. package/src/components/DataTable/column-types.ts +180 -0
  27. package/src/components/DataTable/data-table-column-visibility-panel.tsx +261 -0
  28. package/src/components/DataTable/data-table-filter-panel.tsx +813 -0
  29. package/src/components/DataTable/data-table-interaction-layer.tsx +483 -0
  30. package/src/components/DataTable/data-table-sort-manager.tsx +210 -0
  31. package/src/components/DataTable/data-table.css +165 -0
  32. package/src/components/DataTable/data-table.tsx +2924 -0
  33. package/src/components/DataTable/filter-operators.ts +225 -0
  34. package/src/components/DataTable/filter-tree.ts +313 -0
  35. package/src/components/DataTable/lib/column-meta.ts +79 -0
  36. package/src/components/DateGrid/date-grid.tsx +209 -0
  37. package/src/components/DatePicker/date-picker.tsx +1114 -0
  38. package/src/components/DescriptionList/description-list.tsx +141 -0
  39. package/src/components/Dialog/dialog.tsx +267 -0
  40. package/src/components/DropdownMenu/dropdown-menu.tsx +475 -0
  41. package/src/components/Empty/empty.tsx +108 -0
  42. package/src/components/Field/field-context.ts +136 -0
  43. package/src/components/Field/field-types.ts +52 -0
  44. package/src/components/Field/field-wrapper.tsx +348 -0
  45. package/src/components/Field/field.tsx +535 -0
  46. package/src/components/FieldControlGroup/field-control-group.tsx +136 -0
  47. package/src/components/FileItem/file-item.tsx +322 -0
  48. package/src/components/FileUpload/file-upload.tsx +326 -0
  49. package/src/components/FileViewer/file-viewer-types.ts +76 -0
  50. package/src/components/FileViewer/file-viewer.tsx +1065 -0
  51. package/src/components/FileViewer/image-renderer.tsx +256 -0
  52. package/src/components/HoverCard/hover-card.tsx +79 -0
  53. package/src/components/Input/input.tsx +233 -0
  54. package/src/components/LinkInput/link-input.tsx +304 -0
  55. package/src/components/Menu/menu-item.tsx +334 -0
  56. package/src/components/NameCard/name-card.tsx +319 -0
  57. package/src/components/Notice/notice.tsx +196 -0
  58. package/src/components/NumberInput/number-input.tsx +203 -0
  59. package/src/components/OverflowIndicator/overflow-indicator.tsx +156 -0
  60. package/src/components/PeoplePicker/avatar-stack-overflow.ts +100 -0
  61. package/src/components/PeoplePicker/people-picker-helpers.ts +76 -0
  62. package/src/components/PeoplePicker/people-picker.tsx +455 -0
  63. package/src/components/PeoplePicker/person-display.tsx +358 -0
  64. package/src/components/Popover/popover.tsx +183 -0
  65. package/src/components/ProgressBar/progress-bar.tsx +157 -0
  66. package/src/components/README.md +58 -0
  67. package/src/components/RadioGroup/radio-group.tsx +261 -0
  68. package/src/components/Rating/rating.tsx +295 -0
  69. package/src/components/ScrollArea/scroll-area.tsx +110 -0
  70. package/src/components/SegmentedControl/segmented-control.tsx +304 -0
  71. package/src/components/Select/select.tsx +658 -0
  72. package/src/components/SelectMenu/select-menu.tsx +430 -0
  73. package/src/components/SelectionControl/selection-item.tsx +261 -0
  74. package/src/components/Separator/separator.tsx +48 -0
  75. package/src/components/Sheet/sheet.tsx +240 -0
  76. package/src/components/Sidebar/sidebar.tsx +1280 -0
  77. package/src/components/Skeleton/skeleton.tsx +35 -0
  78. package/src/components/Slider/slider.tsx +158 -0
  79. package/src/components/Steps/steps.tsx +850 -0
  80. package/src/components/Switch/switch.tsx +285 -0
  81. package/src/components/Tabs/tabs.tsx +515 -0
  82. package/src/components/Tag/tag.tsx +246 -0
  83. package/src/components/Textarea/textarea.tsx +280 -0
  84. package/src/components/TimePicker/time-columns.tsx +260 -0
  85. package/src/components/TimePicker/time-picker.tsx +419 -0
  86. package/src/components/Toast/toast.tsx +129 -0
  87. package/src/components/Tooltip/tooltip.tsx +68 -0
  88. package/src/components/TreeView/tree-view.tsx +1031 -0
  89. package/src/hooks/use-controllable.ts +40 -0
  90. package/src/hooks/use-is-narrow-viewport.ts +19 -0
  91. package/src/hooks/use-is-touch-device.ts +21 -0
  92. package/src/hooks/use-overflow-items.ts +256 -0
  93. package/src/index.ts +85 -0
  94. package/src/lib/README.md +82 -0
  95. package/src/lib/drag-visual.ts +272 -0
  96. package/src/lib/i18n/README.md +60 -0
  97. package/src/lib/i18n/i18n-context.tsx +129 -0
  98. package/src/lib/multi-select-ordering.ts +61 -0
  99. package/src/lib/utils.ts +93 -0
  100. package/src/patterns/README.md +67 -0
  101. package/src/patterns/element-anatomy/item-anatomy.tsx +744 -0
  102. package/src/patterns/header-canonical/chrome-header.tsx +175 -0
  103. package/src/patterns/header-canonical/header-canonical.css +27 -0
  104. package/src/patterns/horizontal-overflow/horizontal-overflow.tsx +217 -0
  105. package/src/patterns/overlay-surface/overlay-surface.tsx +191 -0
  106. package/src/patterns/resize-handle/resize-handle.tsx +188 -0
  107. package/src/stories-helpers/anatomy/anatomy-utils.tsx +64 -0
  108. package/src/tokens/README.md +53 -0
  109. package/src/tokens/color/primitives.css +429 -0
  110. package/src/tokens/color/semantic.css +539 -0
  111. package/src/tokens/elevation/overlay-geometry.ts +13 -0
  112. package/src/tokens/layoutSpace/layoutSpace.css +36 -0
  113. package/src/tokens/motion/motion.css +30 -0
  114. package/src/tokens/motion/motion.ts +17 -0
  115. package/src/tokens/opacity/opacity.css +23 -0
  116. package/src/tokens/radius/radius.css +19 -0
  117. package/src/tokens/typography/typography.css +118 -0
  118. package/src/tokens/uiSize/icon-size.ts +52 -0
  119. package/src/tokens/uiSize/uiSize.css +125 -0
@@ -0,0 +1,246 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { X } from "lucide-react"
4
+ import type { LucideIcon } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { Tooltip, TooltipTrigger, TooltipContent } from "@/design-system/components/Tooltip/tooltip"
8
+ import { ItemInlineActionButton } from "@/design-system/patterns/element-anatomy/item-anatomy"
9
+
10
+ // ── Tag(inline label)─────────────────────────────────────────────────────
11
+ // 用於分類標籤、狀態標記、多選已選值。
12
+ //
13
+ // 三種尺寸(子元件補齊原則——消費端直接透傳 size,不做 mapping):
14
+ // sm — 20px 高, 12px 字, 4px tag-px, font-medium(配 field sm)
15
+ // md — 24px 高, 14px 字, 4px tag-px, font-normal(配 field md)— 預設
16
+ // lg — 24px = md alias(配 field lg,子元件補齊原則)
17
+ //
18
+ // 截斷:max-w-40(160px),超出時文字 truncate + 自動 tooltip。
19
+ // 用 Canvas measureText 偵測截斷(scrollWidth 在 flex 內不可靠)。
20
+
21
+ let _measureCtx: CanvasRenderingContext2D | null = null
22
+ function getMeasureCtx() {
23
+ if (!_measureCtx) _measureCtx = document.createElement('canvas').getContext('2d')
24
+ return _measureCtx
25
+ }
26
+
27
+ const tagVariants = cva(
28
+ "inline-flex items-center rounded-md border border-transparent transition-colors cursor-text",
29
+ {
30
+ variants: {
31
+ color: {
32
+ // 直接引用 primitive(bg=step-1, text=step-7),不經過語義層
33
+ // step-1 在 dark mode 用 alpha 公式,step-7 用 lighter 公式——兩個 mode 都正確
34
+ neutral: "bg-secondary text-foreground",
35
+ blue: "bg-[var(--color-blue-1)] text-[var(--color-blue-7)]",
36
+ red: "bg-[var(--color-deep-orange-1)] text-[var(--color-deep-orange-7)]",
37
+ green: "bg-[var(--color-green-1)] text-[var(--color-green-7)]",
38
+ yellow: "bg-[var(--color-yellow-1)] text-[var(--color-yellow-7)]",
39
+ turquoise: "bg-[var(--color-turquoise-1)] text-[var(--color-turquoise-7)]",
40
+ purple: "bg-[var(--color-purple-1)] text-[var(--color-purple-7)]",
41
+ magenta: "bg-[var(--color-magenta-1)] text-[var(--color-magenta-7)]",
42
+ indigo: "bg-[var(--color-indigo-1)] text-[var(--color-indigo-7)]",
43
+ },
44
+ size: {
45
+ sm: "h-5 px-1 text-caption font-medium",
46
+ md: "h-6 px-1 text-body font-normal",
47
+ lg: "h-6 px-1 text-body font-normal",
48
+ },
49
+ },
50
+ defaultVariants: {
51
+ color: "neutral",
52
+ size: "md",
53
+ },
54
+ }
55
+ )
56
+
57
+ // ── Solid variant 色彩(step-6 底 + 白字,warning 用 --warning-foreground)──
58
+ // 直接引用 primitive step-6,不經過語義層
59
+ // neutral 用 neutral-9 + --inverse-fg(light=白字, dark=深字,自動反轉)
60
+ const SOLID_CLASSES: Record<string, string> = {
61
+ neutral: 'bg-[var(--color-neutral-9)] text-inverse-fg',
62
+ blue: 'bg-[var(--color-blue-6)] text-on-emphasis',
63
+ red: 'bg-[var(--color-deep-orange-6)] text-on-emphasis',
64
+ green: 'bg-[var(--color-green-6)] text-on-emphasis',
65
+ yellow: 'bg-[var(--color-yellow-6)] text-[var(--warning-foreground)]',
66
+ turquoise: 'bg-[var(--color-turquoise-6)] text-on-emphasis',
67
+ purple: 'bg-[var(--color-purple-6)] text-on-emphasis',
68
+ magenta: 'bg-[var(--color-magenta-6)] text-on-emphasis',
69
+ indigo: 'bg-[var(--color-indigo-6)] text-on-emphasis',
70
+ }
71
+
72
+ export interface TagProps
73
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, 'prefix' | 'color'>,
74
+ VariantProps<typeof tagVariants> {
75
+ /** 左側 icon(LucideIcon),由 Tag 統一 16px。與 avatar 互斥。 */
76
+ icon?: LucideIcon
77
+ /** 左側 avatar(ReactNode),與 icon 互斥。 */
78
+ avatar?: React.ReactNode
79
+ /** 可移除——Tag 自動渲染 dismiss 按鈕並控制尺寸與互動樣式 */
80
+ onDismiss?: () => void
81
+ /** 深底白字模式(step-6 背景 + 白色前景,warning 例外) */
82
+ solid?: boolean
83
+ /**
84
+ * 2026-05-15 Q3 真 SSOT fix(per user verbatim「同空間兩判斷點」+「不要冰山一角」):
85
+ * Tag 寬度由 parent constrain,不套預設 max-w-40(160px)。用於 cell-as-input narrow cell
86
+ * (< 160px)時 Tag fit cell 寬度 + truncate ellipsis,而非 160px 後被 cell `overflow-hidden`
87
+ * 硬切。對齊「同 cell width → 同 overflow 判斷」SSOT。Default false 保 backward compat
88
+ * (wrap layout / pill rail 等仍受 160px 保護)。
89
+ */
90
+ unbounded?: boolean
91
+ }
92
+
93
+ // ── Solid dismiss hover/active bg ──
94
+ // 用 semantic --{hue}-hover/active token,跟 --primary-hover/active 同模式:
95
+ // solid 色彩 shade change(hover 較亮 step、active 較暗 step),跟 Button 等
96
+ // 互動元件視覺一致。在 semantic 層做 dark mode swap 確保方向跨 mode 一致。
97
+ // neutral 特例:bg 是 neutral-9 隨 mode 反轉,用 --inverse-neutral-* 鏡射
98
+ const SOLID_DISMISS_HOVER: Record<string, { hover: string; active: string }> = {
99
+ neutral: { hover: 'var(--inverse-neutral-hover)', active: 'var(--inverse-neutral-active)' },
100
+ blue: { hover: 'var(--blue-hover)', active: 'var(--blue-active)' },
101
+ red: { hover: 'var(--red-hover)', active: 'var(--red-active)' },
102
+ green: { hover: 'var(--green-hover)', active: 'var(--green-active)' },
103
+ yellow: { hover: 'var(--yellow-hover)', active: 'var(--yellow-active)' },
104
+ turquoise: { hover: 'var(--turquoise-hover)', active: 'var(--turquoise-active)' },
105
+ purple: { hover: 'var(--purple-hover)', active: 'var(--purple-active)' },
106
+ magenta: { hover: 'var(--magenta-hover)', active: 'var(--magenta-active)' },
107
+ indigo: { hover: 'var(--indigo-hover)', active: 'var(--indigo-active)' },
108
+ }
109
+
110
+ // ── Dismiss(internal)────────────────────────────────────────────────────
111
+ // 走 `ItemInlineActionButton`(item-anatomy SSOT)+ `hoverBgClassName` override prop
112
+ // (2026-05-01 整合,消除原 Tag 自刻 `<button>` 繞 DS infra 的 tech debt)。
113
+ //
114
+ // 視覺對齊:`size="md"` → icon 16 / hover-bg 18,跟 Tag 既有手刻幾何完全相等。
115
+ // Solid variant(blue/green/red 等)透過 `hoverBgClassName` 套色相 override token;
116
+ // Subtle variant 落用 ItemInlineActionButton 預設 neutral-hover。
117
+ // 圖標色繼承 Tag 文字色 → `text-current` 三態覆寫。
118
+
119
+ function TagDismiss({ onDismiss, label, solid, color }: { onDismiss: () => void; label: string; solid?: boolean; color?: string }) {
120
+ const solidColors = solid && color ? SOLID_DISMISS_HOVER[color] : undefined
121
+
122
+ return (
123
+ <ItemInlineActionButton
124
+ icon={X}
125
+ size="md"
126
+ onClick={(e) => { e.stopPropagation(); onDismiss() }}
127
+ aria-label={`移除 ${label}`}
128
+ style={solidColors ? ({ '--dismiss-hover': solidColors.hover, '--dismiss-active': solidColors.active } as React.CSSProperties) : undefined}
129
+ hoverBgClassName={
130
+ solidColors
131
+ ? 'group-hover/action:bg-[var(--dismiss-hover)] group-active/action:bg-[var(--dismiss-active)]'
132
+ : undefined
133
+ }
134
+ // Override default fg-muted → 繼承 Tag 文字色(label 同色)
135
+ className="text-current hover:text-current active:text-current"
136
+ />
137
+ )
138
+ }
139
+
140
+ function TagInner(
141
+ { className, color, size, icon: Icon, avatar, onDismiss, solid, unbounded = false, children, style, ...props }: TagProps,
142
+ forwardedRef: React.ForwardedRef<HTMLDivElement>,
143
+ ) {
144
+ const solidClass = solid ? SOLID_CLASSES[color ?? 'neutral'] : undefined
145
+ const ownRef = React.useRef<HTMLDivElement | null>(null)
146
+ const [isTruncated, setIsTruncated] = React.useState(false)
147
+
148
+ React.useLayoutEffect(() => {
149
+ const el = ownRef.current
150
+ if (!el) return
151
+ const ctx = getMeasureCtx()
152
+ const check = () => {
153
+ const textSpan = el.querySelector('[data-tag-text]')
154
+ if (!textSpan || !ctx) return
155
+ const text = textSpan.textContent || ''
156
+ const cs = getComputedStyle(textSpan)
157
+ ctx.font = `${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`
158
+ const textWidth = ctx.measureText(text).width
159
+ const padL = parseFloat(cs.paddingLeft) || 0
160
+ const padR = parseFloat(cs.paddingRight) || 0
161
+ const needed = textWidth + padL + padR
162
+ setIsTruncated(needed > (textSpan as HTMLElement).clientWidth + 1)
163
+ }
164
+ check()
165
+ const obs = new ResizeObserver(check)
166
+ obs.observe(el)
167
+ return () => obs.disconnect()
168
+ }, [children])
169
+
170
+ const label = typeof children === 'string' ? children : ''
171
+
172
+ const tag = (
173
+ <div
174
+ ref={(el) => {
175
+ ownRef.current = el
176
+ if (typeof forwardedRef === 'function') forwardedRef(el)
177
+ else if (forwardedRef) (forwardedRef as React.MutableRefObject<HTMLDivElement | null>).current = el
178
+ }}
179
+ data-tag-root=""
180
+ className={cn(tagVariants({ color, size }), solidClass, 'w-fit min-w-0 overflow-hidden', className)}
181
+ // 2026-05-18 Round 5 fix(per Codex M31 Round 5 verdict + user 拍板「那就開始做」):
182
+ // 用 CSS var `--combobox-tag-area-inline-size`(由 Combobox useOverflowCount JS-injected)取代
183
+ // `min(100%, 10rem)` cyclic percentage。CSS Sizing 3 §5.2.1:percentage 在 indefinite containing
184
+ // block 退化為 initial value → Round 4 的 100% 沒 enforce。改 explicit px 值(JS measured)避此 trap。
185
+ // unbounded=true:cap = inject 寬(回 cell-as-input narrow cell 原 behavior)
186
+ // default:cap = min(inject 寬, 160px)— 兩 cap 取小。fallback(無 var,Form context 等)= 100% / 10rem。
187
+ style={{
188
+ maxWidth: unbounded
189
+ ? 'var(--combobox-tag-area-inline-size, 100%)'
190
+ : 'min(var(--combobox-tag-area-inline-size, 10rem), 10rem)',
191
+ ...style,
192
+ }}
193
+ {...props}
194
+ >
195
+ {Icon && <Icon size={16} aria-hidden />}
196
+ {avatar && <span className="shrink-0 w-4 h-4 rounded-full overflow-hidden inline-grid place-content-center [&>*]:w-full [&>*]:h-full">{avatar}</span>}
197
+ <span data-tag-text="" className="px-1 truncate min-w-0">{children}</span>
198
+ {onDismiss && <TagDismiss onDismiss={onDismiss} label={label} solid={solid} color={color ?? 'neutral'} />}
199
+ </div>
200
+ )
201
+
202
+ if (!isTruncated) return tag
203
+
204
+ return (
205
+ <Tooltip>
206
+ <TooltipTrigger asChild>{tag}</TooltipTrigger>
207
+ <TooltipContent>{children}</TooltipContent>
208
+ </Tooltip>
209
+ )
210
+ }
211
+
212
+ const Tag = React.forwardRef<HTMLDivElement, TagProps>(TagInner)
213
+ Tag.displayName = 'Tag'
214
+
215
+ // Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)
216
+ // Phase 2 fill needed: purpose descriptions + when rationale + world-class refs
217
+ export const tagMeta = {
218
+ component: 'Tag',
219
+ family: 3,
220
+ variants: {
221
+ neutral: { purpose: '通用分類、草稿、無特定語義' },
222
+ blue: { purpose: '進行中、資訊提示、active 狀態(對應 --info)' },
223
+ red: { purpose: '錯誤、已封鎖、危險(對應 --error)' },
224
+ green: { purpose: '成功、已完成、已核准(對應 --success)' },
225
+ yellow: { purpose: '警告、待審核、注意(對應 --warning)' },
226
+ turquoise: { purpose: '分類色(無固定語義)' },
227
+ purple: { purpose: '分類色(無固定語義)' },
228
+ magenta: { purpose: '分類色(無固定語義)' },
229
+ indigo: { purpose: '分類色(無固定語義)' },
230
+ },
231
+ sizes: {
232
+ sm: { fieldHeight: 28, iconSize: 16, typography: 'body' },
233
+ md: { fieldHeight: 32, iconSize: 16, typography: 'body' },
234
+ lg: { fieldHeight: 40, iconSize: 20, typography: 'body' },
235
+ },
236
+ states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],
237
+ tokens: {
238
+ bg: ['bg-neutral-active', 'bg-neutral-hover', 'bg-secondary', 'bg-transparent'],
239
+ fg: ['text-foreground', 'text-inverse-fg'],
240
+ ring: [],
241
+ },
242
+ defaultVariant: 'neutral',
243
+ defaultSize: 'md',
244
+ } as const
245
+
246
+ export { Tag, tagVariants }
@@ -0,0 +1,280 @@
1
+ // @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.
2
+ import * as React from 'react'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+ import { cn } from '@/lib/utils'
5
+ import type { FieldMode, FieldVariant } from '@/design-system/components/Field/field-types'
6
+ import { useFieldContext } from '@/design-system/components/Field/field-context'
7
+ import { EMPTY_DISPLAY } from '@/design-system/components/Field/field-wrapper'
8
+
9
+ /**
10
+ * Textarea — 多行文字輸入
11
+ *
12
+ * ── 定位 ────────────────────────────────────────────────────────────────
13
+ * 多行版本的 Input,edit / display / readonly / disabled 四態與 Input 邏輯一致(Phase B1 2026-05-05)。
14
+ * 不同於 Input:
15
+ * - 沒有固定 field-height(高度由 rows 或 min-h 決定)
16
+ * - 沒有 startIcon / endAction(textarea 慣例不放 icon)
17
+ * - readonly 呈現保留邊框與 padding,只改底色,讓多行文字有合理閱讀區
18
+ * - display 渲染 <div> + white-space:pre-wrap 保留多行文本
19
+ *
20
+ * ── Padding 規則 ───────────────────────────────────────────────────────
21
+ * 多行內容必須有上下內距才能閱讀舒適。不沿用 Input 的 items-center,
22
+ * 改用 py-2(8px)固定上下內距 + px-3 左右內距(與 Input 一致)。
23
+ *
24
+ * ── Size ────────────────────────────────────────────────────────────────
25
+ * sm / md → text-body(14px)
26
+ * lg → text-body-lg(16px)
27
+ *
28
+ * ── rows / min-h ───────────────────────────────────────────────────────
29
+ * 預設 rows={3}。消費者可透過 rows prop 調整,或透過 min-h-* className 覆寫。
30
+ */
31
+
32
+ // Phase B1(2026-05-05):新增 chrome variant(default / bare),mode×chrome 的 chrome 規則由
33
+ // compoundVariants 決定,鏡射 fieldWrapperStyles 對齊 canonical(Phase D 將整併進 fieldWrapperStyles)。
34
+ const textareaVariants = cva(
35
+ [
36
+ 'w-full rounded-md',
37
+ 'text-foreground font-normal',
38
+ 'outline-none resize-y',
39
+ 'placeholder:text-fg-muted',
40
+ // K10 fix(2026-05-04):disabled 時 placeholder + text 切 fg-disabled(parallel 到 bareInputStyles)
41
+ // Textarea 自身 `<textarea disabled>` 帶 disabled HTML attribute,用 `disabled:` variant 直接命中
42
+ 'disabled:placeholder:text-fg-disabled disabled:text-fg-disabled',
43
+ 'px-3 py-2',
44
+ 'transition-colors duration-150',
45
+ ],
46
+ {
47
+ variants: {
48
+ mode: {
49
+ edit: '',
50
+ display: '',
51
+ readonly: '',
52
+ disabled: '',
53
+ },
54
+ // chrome 對齊 fieldWrapperStyles.variant(default / bare / naked)。
55
+ variant: {
56
+ default: '',
57
+ bare: '',
58
+ naked: '',
59
+ },
60
+ size: {
61
+ sm: 'text-body',
62
+ md: 'text-body',
63
+ lg: 'text-body-lg',
64
+ },
65
+ },
66
+ compoundVariants: [
67
+ // default chrome × mode
68
+ {
69
+ mode: 'edit',
70
+ variant: 'default',
71
+ className: [
72
+ 'bg-surface border border-border',
73
+ 'hover:border-border-hover',
74
+ 'focus-visible:!border-primary focus-visible:hover:!border-primary',
75
+ ],
76
+ },
77
+ {
78
+ mode: 'display',
79
+ variant: 'default',
80
+ // 2026-05-13 Q3 Path Ⅰ:Textarea default display zero chrome,!px-0 !py-0 override base `px-3 py-2`
81
+ // (跟 Input 同 SSOT,per field-controls.spec.md (d))
82
+ className: 'bg-transparent border border-transparent !px-0 !py-0',
83
+ },
84
+ {
85
+ mode: 'readonly',
86
+ variant: 'default',
87
+ className: 'bg-disabled border border-transparent',
88
+ },
89
+ {
90
+ mode: 'disabled',
91
+ variant: 'default',
92
+ className: 'bg-disabled border border-transparent cursor-not-allowed text-fg-disabled',
93
+ },
94
+ // bare chrome × mode(對齊 fieldWrapperStyles bare 規則)
95
+ {
96
+ mode: 'edit',
97
+ variant: 'bare',
98
+ className: [
99
+ 'bg-transparent border border-transparent',
100
+ 'hover:border-border',
101
+ 'focus-visible:!border-primary focus-visible:hover:!border-primary',
102
+ ],
103
+ },
104
+ {
105
+ mode: 'display',
106
+ variant: 'bare',
107
+ className: 'bg-transparent border border-transparent',
108
+ },
109
+ {
110
+ mode: 'readonly',
111
+ variant: 'bare',
112
+ className: 'bg-transparent border border-transparent',
113
+ },
114
+ {
115
+ mode: 'disabled',
116
+ variant: 'bare',
117
+ className: 'bg-transparent border border-transparent cursor-not-allowed opacity-disabled text-fg-disabled',
118
+ },
119
+ // naked chrome × mode — cell-as-input substrate(2026-05-06 v14 revert v12)。
120
+ // v12 `!absolute -inset-px` autoRowHeight 不相容 → revert v9 baseline + 保留 v13.3
121
+ // focus !important。focus-visible 用 textarea 自身 selector(focusable element)。
122
+ {
123
+ mode: 'edit',
124
+ variant: 'naked',
125
+ className: [
126
+ 'bg-transparent !rounded-none !resize-none !h-full',
127
+ '!px-[var(--table-cell-px)] !py-[var(--table-cell-py)]',
128
+ 'border border-border',
129
+ 'hover:border-border-hover',
130
+ 'focus-visible:!border-primary focus-visible:hover:!border-primary',
131
+ // textarea UA stylesheet 預設 line-height: normal(1.2-1.5 不定),會跟 display
132
+ // `<div>` text-body line-height: 1.5(21px @ 14px)不一致 → cell 進 edit 後 height
133
+ // shift。顯式 leading 對齊 div 行為。
134
+ '!leading-[1.5]',
135
+ ],
136
+ },
137
+ // 2026-05-13 Q1 R4 verify(per codex Q1 verdict 補 Textarea nuance):
138
+ // Textarea naked display/readonly/disabled 用 `!h-full`,**不**對齊 Field wrapper 的 `!h-auto`。
139
+ // Why divergence:textarea 是 native form element 帶 intrinsic rows-based height,且 cell 內
140
+ // multi-line text 需要撐滿 cell 而非依 line-height intrinsic。`!h-full` 讓 textarea 填滿 cell
141
+ // 高度,文字 anchored to cell.top + cell padding(同視覺結果 Field wrapper autoRow !h-auto)。
142
+ // 此 divergence intentional + documented;非 SSOT violation。
143
+ { mode: 'display', variant: 'naked', className: 'bg-transparent !rounded-none !h-full !resize-none !px-0 !py-0 border border-transparent !leading-[1.5]' },
144
+ { mode: 'readonly', variant: 'naked', className: 'bg-transparent !rounded-none !h-full !resize-none !px-0 !py-0 border border-transparent !leading-[1.5]' },
145
+ // 2026-05-13 codex V2 fix:移除 `opacity-disabled` blanket(對齊 field-wrapper.tsx naked R3 fix +
146
+ // color.spec.md:729 逃生艙 rule)。Textarea 已用具體 `text-fg-disabled` token,不需要 wrapper opacity。
147
+ { mode: 'disabled', variant: 'naked', className: 'bg-transparent !rounded-none cursor-not-allowed text-fg-disabled !h-full !resize-none !px-0 !py-0 border border-transparent !leading-[1.5]' },
148
+ ],
149
+ defaultVariants: {
150
+ mode: 'edit',
151
+ variant: 'default',
152
+ size: 'md',
153
+ },
154
+ }
155
+ )
156
+
157
+ export interface TextareaProps
158
+ extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'size'>,
159
+ Omit<VariantProps<typeof textareaVariants>, 'mode' | 'variant'> {
160
+ /** Field display mode */
161
+ mode?: FieldMode
162
+ /**
163
+ * Visual chrome(正交於 mode);Phase B1(2026-05-05)新增。透傳自 FieldContext.variant,per-prop override。
164
+ * - `'default'` — 完整 chrome(form 場景)
165
+ * - `'bare'` — 透明 variant,hover/focus reveal(toolbar / cell-as-input)
166
+ */
167
+ variant?: FieldVariant
168
+ /** Error 狀態(正交於 mode)。border-error + aria-invalid。 */
169
+ error?: boolean
170
+ }
171
+
172
+ // code-quality-allow: long-function — Textarea forwardRef body 含 mode×size×variant×error 4 軸 prop + autoFocus + aria 完整覆蓋,拆 sub-fn 會把 useFieldContext / fieldWrapperStyles 跨檔 drilling
173
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
174
+ (
175
+ {
176
+ mode: modeProp,
177
+ variant: variantProp,
178
+ error: errorProp = false,
179
+ size: sizeProp,
180
+ className,
181
+ disabled,
182
+ readOnly,
183
+ rows = 3,
184
+ value,
185
+ id: idProp,
186
+ 'aria-describedby': ariaDescribedByProp,
187
+ 'aria-errormessage': ariaErrorMessageProp,
188
+ ...props
189
+ },
190
+ ref
191
+ ) => {
192
+ // Field context 整合:disabled / mode / chrome / invalid / size / id 都能從 context 繼承
193
+ const fieldCtx = useFieldContext()
194
+ // chrome 透傳:per-prop override context
195
+ const variant: FieldVariant = variantProp ?? fieldCtx?.variant ?? 'default'
196
+ const error = errorProp || (fieldCtx?.invalid ?? false)
197
+ const size = sizeProp ?? fieldCtx?.size ?? 'md'
198
+ // mode resolve order(Phase B1 2026-05-05):prop > fieldCtx > readOnly/disabled fallback > 'edit'
199
+ const resolvedMode: FieldMode = modeProp
200
+ ?? fieldCtx?.mode
201
+ ?? (readOnly ? 'readonly' : (disabled ?? fieldCtx?.disabled) ? 'disabled' : 'edit')
202
+ const isEditable = resolvedMode === 'edit'
203
+ const isDisplay = resolvedMode === 'display'
204
+ const inputId = idProp ?? fieldCtx?.id
205
+ const ariaDescribedBy = ariaDescribedByProp ?? fieldCtx?.descriptionId
206
+ const ariaErrorMessage = ariaErrorMessageProp ?? (error ? fieldCtx?.errorId : undefined)
207
+
208
+ // ── display mode:純展示,渲染 <div> 取代 <textarea>(white-space:pre-wrap 保留多行) ──
209
+ // 對齊 Carbon read-only / Cloudscape display-mode
210
+ if (isDisplay) {
211
+ const displayValue = value != null && value !== '' ? String(value) : null
212
+ return (
213
+ <div
214
+ id={inputId}
215
+ data-field-mode="display"
216
+ aria-describedby={ariaDescribedBy}
217
+ className={cn(
218
+ textareaVariants({ mode: 'display', variant: variant, size }),
219
+ 'whitespace-pre-wrap break-words',
220
+ displayValue == null && 'text-fg-muted',
221
+ className,
222
+ )}
223
+ >
224
+ {displayValue ?? EMPTY_DISPLAY}
225
+ </div>
226
+ )
227
+ }
228
+
229
+ return (
230
+ <textarea
231
+ ref={ref}
232
+ id={inputId}
233
+ rows={rows}
234
+ value={value as string | number | readonly string[] | undefined}
235
+ disabled={resolvedMode === 'disabled'}
236
+ readOnly={resolvedMode === 'readonly'}
237
+ aria-invalid={error || undefined}
238
+ aria-required={fieldCtx?.required || undefined}
239
+ aria-describedby={ariaDescribedBy}
240
+ aria-errormessage={ariaErrorMessage}
241
+ data-field-mode={resolvedMode}
242
+ data-error={isEditable && error ? '' : undefined}
243
+ className={cn(
244
+ textareaVariants({ mode: resolvedMode, variant: variant, size }),
245
+ isEditable && error && [
246
+ 'border-error hover:border-error-hover',
247
+ 'focus-visible:border-error focus-visible:hover:border-error',
248
+ ],
249
+ className
250
+ )}
251
+ {...props}
252
+ />
253
+ )
254
+ }
255
+ )
256
+ Textarea.displayName = 'Textarea'
257
+
258
+ // Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)
259
+ // Phase 2 fill needed: purpose descriptions + when rationale + world-class refs
260
+ export const textareaMeta = {
261
+ component: 'Textarea',
262
+ family: 4,
263
+ variants: {
264
+
265
+ },
266
+ sizes: {
267
+ sm: { fieldHeight: 28, iconSize: 16, typography: 'body' },
268
+ md: { fieldHeight: 32, iconSize: 16, typography: 'body' },
269
+ lg: { fieldHeight: 40, iconSize: 20, typography: 'body' },
270
+ },
271
+ states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],
272
+ tokens: {
273
+ bg: ['bg-disabled', 'bg-surface'],
274
+ fg: ['text-fg-disabled', 'text-fg-muted', 'text-foreground'],
275
+ ring: [],
276
+ },
277
+ defaultSize: 'md',
278
+ } as const
279
+
280
+ export { Textarea, textareaVariants }