@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.
- package/package.json +93 -0
- package/src/README.md +32 -0
- package/src/components/Accordion/accordion.tsx +104 -0
- package/src/components/Alert/alert.tsx +188 -0
- package/src/components/AppShell/_demo-helpers.tsx +198 -0
- package/src/components/AppShell/app-shell.tsx +364 -0
- package/src/components/AspectRatio/aspect-ratio.tsx +58 -0
- package/src/components/Avatar/avatar.tsx +368 -0
- package/src/components/Badge/badge.tsx +104 -0
- package/src/components/Breadcrumb/breadcrumb.tsx +609 -0
- package/src/components/BulkActionBar/bulk-action-bar.tsx +156 -0
- package/src/components/Button/button-group.tsx +96 -0
- package/src/components/Button/button.tsx +539 -0
- package/src/components/Calendar/calendar.tsx +411 -0
- package/src/components/Carousel/carousel.tsx +371 -0
- package/src/components/Chart/chart.tsx +376 -0
- package/src/components/Checkbox/checkbox-group.tsx +94 -0
- package/src/components/Checkbox/checkbox.tsx +237 -0
- package/src/components/Chip/chip.tsx +359 -0
- package/src/components/CircularProgress/circular-progress.tsx +204 -0
- package/src/components/Coachmark/coachmark.tsx +255 -0
- package/src/components/Combobox/combobox.tsx +826 -0
- package/src/components/Command/command.tsx +187 -0
- package/src/components/DataTable/active-editor-controller.ts +72 -0
- package/src/components/DataTable/cell-registry.tsx +520 -0
- package/src/components/DataTable/column-types.ts +180 -0
- package/src/components/DataTable/data-table-column-visibility-panel.tsx +261 -0
- package/src/components/DataTable/data-table-filter-panel.tsx +813 -0
- package/src/components/DataTable/data-table-interaction-layer.tsx +483 -0
- package/src/components/DataTable/data-table-sort-manager.tsx +210 -0
- package/src/components/DataTable/data-table.css +165 -0
- package/src/components/DataTable/data-table.tsx +2924 -0
- package/src/components/DataTable/filter-operators.ts +225 -0
- package/src/components/DataTable/filter-tree.ts +313 -0
- package/src/components/DataTable/lib/column-meta.ts +79 -0
- package/src/components/DateGrid/date-grid.tsx +209 -0
- package/src/components/DatePicker/date-picker.tsx +1114 -0
- package/src/components/DescriptionList/description-list.tsx +141 -0
- package/src/components/Dialog/dialog.tsx +267 -0
- package/src/components/DropdownMenu/dropdown-menu.tsx +475 -0
- package/src/components/Empty/empty.tsx +108 -0
- package/src/components/Field/field-context.ts +136 -0
- package/src/components/Field/field-types.ts +52 -0
- package/src/components/Field/field-wrapper.tsx +348 -0
- package/src/components/Field/field.tsx +535 -0
- package/src/components/FieldControlGroup/field-control-group.tsx +136 -0
- package/src/components/FileItem/file-item.tsx +322 -0
- package/src/components/FileUpload/file-upload.tsx +326 -0
- package/src/components/FileViewer/file-viewer-types.ts +76 -0
- package/src/components/FileViewer/file-viewer.tsx +1065 -0
- package/src/components/FileViewer/image-renderer.tsx +256 -0
- package/src/components/HoverCard/hover-card.tsx +79 -0
- package/src/components/Input/input.tsx +233 -0
- package/src/components/LinkInput/link-input.tsx +304 -0
- package/src/components/Menu/menu-item.tsx +334 -0
- package/src/components/NameCard/name-card.tsx +319 -0
- package/src/components/Notice/notice.tsx +196 -0
- package/src/components/NumberInput/number-input.tsx +203 -0
- package/src/components/OverflowIndicator/overflow-indicator.tsx +156 -0
- package/src/components/PeoplePicker/avatar-stack-overflow.ts +100 -0
- package/src/components/PeoplePicker/people-picker-helpers.ts +76 -0
- package/src/components/PeoplePicker/people-picker.tsx +455 -0
- package/src/components/PeoplePicker/person-display.tsx +358 -0
- package/src/components/Popover/popover.tsx +183 -0
- package/src/components/ProgressBar/progress-bar.tsx +157 -0
- package/src/components/README.md +58 -0
- package/src/components/RadioGroup/radio-group.tsx +261 -0
- package/src/components/Rating/rating.tsx +295 -0
- package/src/components/ScrollArea/scroll-area.tsx +110 -0
- package/src/components/SegmentedControl/segmented-control.tsx +304 -0
- package/src/components/Select/select.tsx +658 -0
- package/src/components/SelectMenu/select-menu.tsx +430 -0
- package/src/components/SelectionControl/selection-item.tsx +261 -0
- package/src/components/Separator/separator.tsx +48 -0
- package/src/components/Sheet/sheet.tsx +240 -0
- package/src/components/Sidebar/sidebar.tsx +1280 -0
- package/src/components/Skeleton/skeleton.tsx +35 -0
- package/src/components/Slider/slider.tsx +158 -0
- package/src/components/Steps/steps.tsx +850 -0
- package/src/components/Switch/switch.tsx +285 -0
- package/src/components/Tabs/tabs.tsx +515 -0
- package/src/components/Tag/tag.tsx +246 -0
- package/src/components/Textarea/textarea.tsx +280 -0
- package/src/components/TimePicker/time-columns.tsx +260 -0
- package/src/components/TimePicker/time-picker.tsx +419 -0
- package/src/components/Toast/toast.tsx +129 -0
- package/src/components/Tooltip/tooltip.tsx +68 -0
- package/src/components/TreeView/tree-view.tsx +1031 -0
- package/src/hooks/use-controllable.ts +40 -0
- package/src/hooks/use-is-narrow-viewport.ts +19 -0
- package/src/hooks/use-is-touch-device.ts +21 -0
- package/src/hooks/use-overflow-items.ts +256 -0
- package/src/index.ts +85 -0
- package/src/lib/README.md +82 -0
- package/src/lib/drag-visual.ts +272 -0
- package/src/lib/i18n/README.md +60 -0
- package/src/lib/i18n/i18n-context.tsx +129 -0
- package/src/lib/multi-select-ordering.ts +61 -0
- package/src/lib/utils.ts +93 -0
- package/src/patterns/README.md +67 -0
- package/src/patterns/element-anatomy/item-anatomy.tsx +744 -0
- package/src/patterns/header-canonical/chrome-header.tsx +175 -0
- package/src/patterns/header-canonical/header-canonical.css +27 -0
- package/src/patterns/horizontal-overflow/horizontal-overflow.tsx +217 -0
- package/src/patterns/overlay-surface/overlay-surface.tsx +191 -0
- package/src/patterns/resize-handle/resize-handle.tsx +188 -0
- package/src/stories-helpers/anatomy/anatomy-utils.tsx +64 -0
- package/src/tokens/README.md +53 -0
- package/src/tokens/color/primitives.css +429 -0
- package/src/tokens/color/semantic.css +539 -0
- package/src/tokens/elevation/overlay-geometry.ts +13 -0
- package/src/tokens/layoutSpace/layoutSpace.css +36 -0
- package/src/tokens/motion/motion.css +30 -0
- package/src/tokens/motion/motion.ts +17 -0
- package/src/tokens/opacity/opacity.css +23 -0
- package/src/tokens/radius/radius.css +19 -0
- package/src/tokens/typography/typography.css +118 -0
- package/src/tokens/uiSize/icon-size.ts +52 -0
- 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 }
|