@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,304 @@
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 * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'
4
+ import { cva } from 'class-variance-authority'
5
+ import type { LucideIcon } from 'lucide-react'
6
+ import { cn } from '@/lib/utils'
7
+ import { useFieldContext } from '@/design-system/components/Field/field-context'
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/design-system/components/Tooltip/tooltip'
9
+ import { ICON_SIZE } from '@/design-system/tokens/uiSize/icon-size'
10
+
11
+ /**
12
+ * SegmentedControl — 互斥多選一的 value 選擇器
13
+ *
14
+ * 基於 Radix ToggleGroup(寫死 type="single"),橋接設計系統 token。
15
+ * 內部 item 結構鏡射 Button,但兩者型別獨立。
16
+ *
17
+ * ── 定位 ──
18
+ * 切 value(不是切 view)。塞得進 Field,Tabs 不行。
19
+ * 切 view → Tabs;單一 on/off → Button pressed;>5 個選項 → Select。
20
+ *
21
+ * ── Size(對齊 field-height / Button 系列)──
22
+ * xs h-field-xs(24 固定)
23
+ * sm h-field-sm(28/32)
24
+ * md h-field-md(32/36)★ 預設(跟 Button / Input / 所有 field-height 系列一致)
25
+ * lg h-field-lg(36/40)
26
+ *
27
+ * ── fullWidth ──
28
+ * false ★ 預設,item 寬度由內容決定(hug content)
29
+ * true SegmentedControl 撐滿父容器寬度,所有 item 等分
30
+ *
31
+ * ── Item 結構 ──
32
+ * [startIcon?] [<span px-1>label</span>] [<span gap-1>suffix?</span>]
33
+ * suffix 目前只支援 badge;endIcon 為保留 slot、未開放
34
+ *
35
+ * ── iconOnly(group-level)──
36
+ * 整組同時 icon-only 或全帶 label,不可混搭。
37
+ */
38
+
39
+ type SegmentedControlSize = 'xs' | 'sm' | 'md' | 'lg'
40
+
41
+ interface SegmentedControlContextValue {
42
+ size: SegmentedControlSize
43
+ fullWidth: boolean
44
+ iconOnly: boolean
45
+ disabled: boolean
46
+ }
47
+
48
+ const SegmentedControlContext = React.createContext<SegmentedControlContextValue>({
49
+ size: 'md',
50
+ fullWidth: false,
51
+ iconOnly: false,
52
+ disabled: false,
53
+ })
54
+
55
+ // ── Root ──
56
+ const segmentedControlVariants = cva(
57
+ 'flex items-stretch rounded-md',
58
+ {
59
+ variants: {
60
+ fullWidth: {
61
+ true: 'w-full',
62
+ false: 'w-fit',
63
+ },
64
+ },
65
+ defaultVariants: {
66
+ fullWidth: false,
67
+ },
68
+ }
69
+ )
70
+
71
+ export interface SegmentedControlProps
72
+ extends Omit<
73
+ React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>,
74
+ 'type' | 'defaultValue' | 'value' | 'onValueChange'
75
+ > {
76
+ size?: SegmentedControlSize
77
+ /** 撐滿父容器寬度,所有 item 等分。預設 false(hug content) */
78
+ fullWidth?: boolean
79
+ iconOnly?: boolean
80
+ value?: string
81
+ defaultValue?: string
82
+ onValueChange?: (value: string) => void
83
+ }
84
+
85
+ const SegmentedControl = React.forwardRef<
86
+ React.ElementRef<typeof ToggleGroupPrimitive.Root>,
87
+ SegmentedControlProps
88
+ >(
89
+ (
90
+ {
91
+ className,
92
+ size: sizeProp,
93
+ fullWidth = false,
94
+ iconOnly = false,
95
+ disabled = false,
96
+ children,
97
+ value,
98
+ defaultValue,
99
+ onValueChange,
100
+ ...props
101
+ },
102
+ ref
103
+ ) => {
104
+ // Field 內自動讀 size,跟 Button 同機制
105
+ const fieldCtx = useFieldContext?.()
106
+ const resolvedSize: SegmentedControlSize =
107
+ sizeProp ?? (fieldCtx?.size as SegmentedControlSize) ?? 'md'
108
+ const resolvedDisabled = disabled || !!fieldCtx?.disabled
109
+
110
+ // Memoize provider value(2026-04-22 D3 perf audit):避免每次父 render 重建 4-field object
111
+ // 讓 children SegmentedControlItem 不必要 re-render
112
+ const ctxValue = React.useMemo(
113
+ () => ({ size: resolvedSize, fullWidth, iconOnly, disabled: resolvedDisabled }),
114
+ [resolvedSize, fullWidth, iconOnly, resolvedDisabled],
115
+ )
116
+
117
+ return (
118
+ <SegmentedControlContext.Provider value={ctxValue}>
119
+ <ToggleGroupPrimitive.Root
120
+ ref={ref}
121
+ type="single"
122
+ value={value}
123
+ defaultValue={defaultValue}
124
+ onValueChange={(v) => {
125
+ // Radix 的 single ToggleGroup 允許 deselect(返回空字串)
126
+ // SegmentedControl 是 radio 語意,不允許空值——忽略 deselect
127
+ if (v) onValueChange?.(v)
128
+ }}
129
+ disabled={resolvedDisabled}
130
+ className={cn(segmentedControlVariants({ fullWidth }), className)}
131
+ {...props}
132
+ >
133
+ {children}
134
+ </ToggleGroupPrimitive.Root>
135
+ </SegmentedControlContext.Provider>
136
+ )
137
+ }
138
+ )
139
+ SegmentedControl.displayName = 'SegmentedControl'
140
+
141
+ // ── Item ──
142
+
143
+ /**
144
+ * Icon-only base — Polaris/Atlassian idiom:`aspect-square + p-0 + flex-center`。
145
+ * 0 magic-number、0 公式、0 border-deduction;flex centering(已在 itemVariants base)
146
+ * 自動將 SVG 視覺置中,任何 size / icon 都自然正方形。
147
+ *
148
+ * 對齊 `Button.tsx` 的 `ICON_ONLY_BASE`(2026-04-25 從 padding-formula 派改 padding-free)。
149
+ * Rule-of-3:目前 2 處 consumer(Button + SegmentedControl),尚未抽 utility/token;
150
+ * 第 3 個 host 加入時抽到 `packages/design-system/src/utils/`。
151
+ *
152
+ * 舊公式 `(field-height - icon)/2` 沒扣 border 2px,造成 SegmentedControl item 從
153
+ * 設計 spec「自然正方形」漂移為 34×32 長方形(2026-04-25 audit 發現)。
154
+ */
155
+ const ICON_ONLY_BASE = 'aspect-square p-0 min-w-0 gap-0'
156
+
157
+ const itemVariants = cva(
158
+ [
159
+ 'relative inline-flex items-center justify-center',
160
+ 'whitespace-nowrap font-medium',
161
+ 'border border-border bg-surface text-fg-secondary',
162
+ 'transition-colors duration-150',
163
+ 'cursor-pointer select-none',
164
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:z-20',
165
+ // disabled:cursor-not-allowed + 鎖住 hover 色(不用 pointer-events-none,否則 cursor 無法變)
166
+ // button[disabled] 本身擋 click,不需靠 pointer-events: none
167
+ 'disabled:cursor-not-allowed disabled:text-fg-disabled',
168
+ 'disabled:hover:text-fg-disabled disabled:hover:border-border',
169
+ // hover(未選):border 加深一階 + 文字轉深,對齊 Input / Chip hover
170
+ 'hover:text-foreground hover:border-border-hover hover:z-[5]',
171
+ // selected: 文字 + 邊框都用 primary-hover,底色維持 bg-surface 不變 (跟 Chip 一致)
172
+ // ── 這是 pill 風格元件 (Chip / SegmentedControl) 的 canonical 選中規則:
173
+ // primary-hover 同時染文字和線條;底色不改 (不用 primary-subtle)。
174
+ // Tabs 是 underline 風格,規則不同 (文字 foreground + 底線 primary-hover)。
175
+ // z-10 讓 border 浮在相鄰 item 之上
176
+ 'data-[state=on]:text-primary-hover data-[state=on]:border-primary-hover data-[state=on]:z-10',
177
+ // item 連體:除第一個外,-ml-px 讓相鄰 border 重疊
178
+ 'first:rounded-l-md last:rounded-r-md',
179
+ '[&:not(:first-child)]:-ml-px',
180
+ ],
181
+ {
182
+ variants: {
183
+ size: {
184
+ xs: 'h-field-xs px-2 text-caption leading-compact gap-0',
185
+ sm: 'h-field-sm px-3 text-body leading-compact gap-1',
186
+ md: 'h-field-md px-3 text-body leading-compact gap-1',
187
+ lg: 'h-field-lg px-3 text-body-lg leading-compact gap-1',
188
+ },
189
+ fullWidth: {
190
+ true: 'flex-1 min-w-0',
191
+ false: '',
192
+ },
193
+ },
194
+ defaultVariants: {
195
+ size: 'md',
196
+ fullWidth: false,
197
+ },
198
+ }
199
+ )
200
+
201
+ // iconOnly 是 group-level 決策(由 <SegmentedControl iconOnly> 控制),
202
+ // 不是 per-item 決策,所以 Item 型別不用 discriminated union 強制 iconOnly。
203
+ // 但當 group iconOnly = true 時,每個 item 必須提供 startIcon + aria-label——
204
+ // 這是語意契約,在 render 階段檢查並在 dev mode 發出警告(TS 層做不到這層檢查)。
205
+ export interface SegmentedControlItemProps
206
+ extends React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> {
207
+ /** 左側 icon(LucideIcon)。group iconOnly = true 時必填。 */
208
+ startIcon?: LucideIcon
209
+ /** 右側 suffix badge(通常是計數指示器)。group iconOnly = true 時會被忽略。 */
210
+ badge?: React.ReactNode
211
+ /**
212
+ * 無障礙名稱。帶 label 時可選(label 已傳達語意),
213
+ * 但 group iconOnly = true 時必填——同時驅動 screen reader 和自動 tooltip。
214
+ */
215
+ 'aria-label'?: string
216
+ children?: React.ReactNode
217
+ }
218
+
219
+ const SegmentedControlItem = React.forwardRef<
220
+ React.ElementRef<typeof ToggleGroupPrimitive.Item>,
221
+ SegmentedControlItemProps
222
+ >(({ className, startIcon: StartIcon, badge, children, 'aria-label': ariaLabel, ...restProps }, ref) => {
223
+ const { size, fullWidth, iconOnly: groupIconOnly } = React.useContext(SegmentedControlContext)
224
+ // 2026-05-18 改 per user 拍板「沒機械化導致偏移」+「做完」approval(對齊 Button xs / uiSize Icon Tier):
225
+ // 原 xs=14 跟 Button xs=16 不一致,xs/sm/md 統一 16 對齊 uiSize.spec.md(xs/sm/md→16, lg→20)。
226
+ // 2026-05-18 改 import ICON_SIZE SSOT(per user『做完』approval,消除 M17 違反 7+ 重複 ternary)
227
+ const iconSize = ICON_SIZE[size as 'sm' | 'md' | 'lg']
228
+ const hasSuffix = badge != null
229
+
230
+ // Dev-mode 語意契約檢查:group iconOnly = true 時,item 必須有 startIcon + aria-label
231
+ if (import.meta.env?.DEV && groupIconOnly) {
232
+ if (!StartIcon) {
233
+ console.warn(
234
+ '[SegmentedControl] iconOnly 群組內的 item 必須提供 startIcon。'
235
+ )
236
+ }
237
+ if (!ariaLabel) {
238
+ console.warn(
239
+ '[SegmentedControl] iconOnly 群組內的 item 必須提供 aria-label(作為無障礙名稱與自動 tooltip 來源)。'
240
+ )
241
+ }
242
+ }
243
+
244
+ const effectiveIconOnly = groupIconOnly
245
+
246
+ const itemEl = (
247
+ <ToggleGroupPrimitive.Item
248
+ ref={ref}
249
+ className={cn(
250
+ itemVariants({ size, fullWidth }),
251
+ effectiveIconOnly && ICON_ONLY_BASE,
252
+ className,
253
+ )}
254
+ aria-label={ariaLabel}
255
+ {...restProps}
256
+ >
257
+ {StartIcon && <StartIcon size={iconSize} aria-hidden />}
258
+ {!effectiveIconOnly && children != null && <span className="px-1">{children}</span>}
259
+ {/* suffix wrapper:即使目前只有 badge,wrapper 從第一天就存在,
260
+ 未來若開放 endIcon,直接加入此 span 內 gap-1 自動生效 */}
261
+ {hasSuffix && !effectiveIconOnly && (
262
+ <span className="inline-flex items-center gap-1">{badge}</span>
263
+ )}
264
+ </ToggleGroupPrimitive.Item>
265
+ )
266
+
267
+ // icon-only 自動包 Tooltip(與 Button icon-only 一致)
268
+ if (effectiveIconOnly && typeof ariaLabel === 'string') {
269
+ return (
270
+ <Tooltip>
271
+ <TooltipTrigger asChild>{itemEl}</TooltipTrigger>
272
+ <TooltipContent>{ariaLabel}</TooltipContent>
273
+ </Tooltip>
274
+ )
275
+ }
276
+
277
+ return itemEl
278
+ })
279
+ SegmentedControlItem.displayName = 'SegmentedControlItem'
280
+
281
+ // Story auto-compile metadata — Phase 2 fill(2026-05-15)
282
+ // Sizes 真實 cva keys = xs/sm/md/lg(itemVariants.size),映射 field-height-* token tier
283
+ // SegmentedControl 無語意 variant(選中 vs 未選 是 state 不是 variant)— 與 Button.variant 刻意不同(spec「與 Button 的血緣」段)
284
+ export const segmentedControlMeta = {
285
+ component: 'SegmentedControl',
286
+ family: null, // non-family composite / overlay / layout
287
+ variants: {},
288
+ sizes: {
289
+ xs: { fieldHeight: 24, typography: 'caption', iconSize: 14, purpose: 'row-embedded inline action / DataTable inline filter' },
290
+ sm: { fieldHeight: 28, typography: 'body', iconSize: 16, purpose: 'compact chrome bar / dense toolbar' },
291
+ md: { fieldHeight: 32, typography: 'body', iconSize: 16, purpose: '預設 — form field 對齊' },
292
+ lg: { fieldHeight: 36, typography: 'body-lg', iconSize: 20, purpose: 'touch / prominent CTA' },
293
+ },
294
+ states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],
295
+ tokens: {
296
+ bg: ['bg-surface'],
297
+ fg: ['text-fg-disabled', 'text-fg-secondary', 'text-foreground'],
298
+ ring: ['ring-ring'],
299
+ },
300
+ defaultSize: 'md',
301
+ } as const
302
+
303
+ export { SegmentedControl, SegmentedControlItem, segmentedControlVariants, itemVariants as segmentedControlItemVariants }
304
+ export type { SegmentedControlSize }