@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,850 @@
|
|
|
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
|
+
// code-quality-allow: file-size — foundational composite(Steps + StepItem + orientation/state/connector 邏輯緊密耦合,拆檔會讓 props drilling 複雜化超過可讀 gain)
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Check, ChevronDown, X } from 'lucide-react'
|
|
5
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
6
|
+
import { cn } from '@/lib/utils'
|
|
7
|
+
|
|
8
|
+
// ── Types ─────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export type StepsSize = 'sm' | 'md' | 'lg'
|
|
11
|
+
// code-quality-allow: dead-export — public API surface — consumer-exposed for future use
|
|
12
|
+
export type StepsOrientation = 'vertical' | 'horizontal'
|
|
13
|
+
export type StepsExpansion = 'follow-active' | 'multiple'
|
|
14
|
+
export type StepContentState = 'upcoming' | 'reachable' | 'current' | 'completed' | 'error'
|
|
15
|
+
|
|
16
|
+
// ── Constants ─────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const INDICATOR_SIZE: Record<StepsSize, number> = {
|
|
19
|
+
sm: 8,
|
|
20
|
+
md: 24,
|
|
21
|
+
lg: 32,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const INDICATOR_ICON_SIZE: Record<StepsSize, number> = {
|
|
25
|
+
sm: 0,
|
|
26
|
+
md: 16,
|
|
27
|
+
lg: 20,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SM_HIT_AREA = 24
|
|
31
|
+
|
|
32
|
+
const INDICATOR_BOX_WIDTH: Record<StepsSize, number> = {
|
|
33
|
+
sm: SM_HIT_AREA,
|
|
34
|
+
md: INDICATOR_SIZE.md,
|
|
35
|
+
lg: INDICATOR_SIZE.lg,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Outer ring (box-shadow, zero layout impact) ───────────────────────────
|
|
39
|
+
|
|
40
|
+
const RING_GAP_PX = 2
|
|
41
|
+
const RING_WIDTH_PX = 2
|
|
42
|
+
|
|
43
|
+
function getOuterRingShadow(ringColor: string): string {
|
|
44
|
+
return `0 0 0 ${RING_GAP_PX}px var(--surface), 0 0 0 ${RING_GAP_PX + RING_WIDTH_PX}px ${ringColor}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveRingColor(state: StepContentState, linear: boolean): string {
|
|
48
|
+
if (state === 'error') return 'var(--error-hover)'
|
|
49
|
+
if (state === 'current' && !linear) return 'var(--border-hover)'
|
|
50
|
+
return 'var(--primary-hover)'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Contexts ──────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
interface StepsContextValue {
|
|
56
|
+
value: string | undefined
|
|
57
|
+
completedValues: Set<string>
|
|
58
|
+
errorValues: Set<string>
|
|
59
|
+
reachableValues: Set<string>
|
|
60
|
+
linear: boolean
|
|
61
|
+
size: StepsSize
|
|
62
|
+
orientation: StepsOrientation
|
|
63
|
+
expansion: StepsExpansion
|
|
64
|
+
expandedSet: Set<string>
|
|
65
|
+
setValue: (value: string) => void
|
|
66
|
+
toggleExpanded: (value: string) => void
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const StepsContext = React.createContext<StepsContextValue | null>(null)
|
|
70
|
+
|
|
71
|
+
function useStepsContext(): StepsContextValue {
|
|
72
|
+
const ctx = React.useContext(StepsContext)
|
|
73
|
+
if (!ctx) throw new Error('Steps compound components must be rendered inside <Steps>')
|
|
74
|
+
return ctx
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface StepItemContextValue {
|
|
78
|
+
value: string
|
|
79
|
+
state: StepContentState
|
|
80
|
+
focused: boolean
|
|
81
|
+
disabled: boolean
|
|
82
|
+
clickable: boolean
|
|
83
|
+
expanded: boolean
|
|
84
|
+
isLast: boolean
|
|
85
|
+
activate: () => void
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const StepItemContext = React.createContext<StepItemContextValue | null>(null)
|
|
89
|
+
|
|
90
|
+
function useStepItemContext(): StepItemContextValue {
|
|
91
|
+
const ctx = React.useContext(StepItemContext)
|
|
92
|
+
if (!ctx) throw new Error('StepLabel / StepDescription / StepContent must be inside <StepItem>')
|
|
93
|
+
return ctx
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const StepIndexContext = React.createContext<number>(0)
|
|
97
|
+
|
|
98
|
+
// ── Pure helpers ──────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
function computeState(
|
|
101
|
+
itemValue: string,
|
|
102
|
+
value: string | undefined,
|
|
103
|
+
completedValues: Set<string>,
|
|
104
|
+
errorValues: Set<string>,
|
|
105
|
+
reachableValues: Set<string>,
|
|
106
|
+
linear: boolean,
|
|
107
|
+
override: StepContentState | undefined,
|
|
108
|
+
): StepContentState {
|
|
109
|
+
if (override) return override
|
|
110
|
+
if (errorValues.has(itemValue)) return 'error'
|
|
111
|
+
if (completedValues.has(itemValue)) return 'completed'
|
|
112
|
+
if (itemValue === value) return 'current'
|
|
113
|
+
if (linear && reachableValues.has(itemValue)) return 'reachable'
|
|
114
|
+
return 'upcoming'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isClickable(
|
|
118
|
+
state: StepContentState,
|
|
119
|
+
linear: boolean,
|
|
120
|
+
disabled: boolean,
|
|
121
|
+
): boolean {
|
|
122
|
+
if (disabled) return false
|
|
123
|
+
if (!linear) return true
|
|
124
|
+
return state !== 'upcoming'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeExpanded(
|
|
128
|
+
defaultExpanded: 'all' | 'none' | string[] | undefined,
|
|
129
|
+
allValues: string[],
|
|
130
|
+
): Set<string> {
|
|
131
|
+
if (defaultExpanded === 'all') return new Set(allValues)
|
|
132
|
+
if (!defaultExpanded || defaultExpanded === 'none') return new Set()
|
|
133
|
+
return new Set(defaultExpanded)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function computeReachableValues(
|
|
137
|
+
childValues: string[],
|
|
138
|
+
completedValues: string[],
|
|
139
|
+
): Set<string> {
|
|
140
|
+
const completed = new Set(completedValues)
|
|
141
|
+
const reachable = new Set(completed)
|
|
142
|
+
for (const v of childValues) {
|
|
143
|
+
if (!completed.has(v)) {
|
|
144
|
+
reachable.add(v)
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return reachable
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Steps root ────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
const stepsRootVariants = cva('list-none p-0 m-0', {
|
|
154
|
+
variants: {
|
|
155
|
+
orientation: {
|
|
156
|
+
vertical: 'flex flex-col',
|
|
157
|
+
horizontal: 'flex flex-row items-start gap-3',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
defaultVariants: { orientation: 'vertical' },
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
export interface StepsProps
|
|
164
|
+
extends Omit<React.HTMLAttributes<HTMLOListElement>, 'onChange' | 'defaultValue'>,
|
|
165
|
+
VariantProps<typeof stepsRootVariants> {
|
|
166
|
+
value?: string
|
|
167
|
+
defaultValue?: string
|
|
168
|
+
onValueChange?: (value: string) => void
|
|
169
|
+
completedValues?: string[]
|
|
170
|
+
errorValues?: string[]
|
|
171
|
+
linear?: boolean
|
|
172
|
+
size?: StepsSize
|
|
173
|
+
orientation?: StepsOrientation
|
|
174
|
+
expansion?: StepsExpansion
|
|
175
|
+
defaultExpanded?: 'all' | 'none' | string[]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding
|
|
179
|
+
const Steps = React.forwardRef<HTMLOListElement, StepsProps>(
|
|
180
|
+
(
|
|
181
|
+
{
|
|
182
|
+
value: valueProp,
|
|
183
|
+
defaultValue,
|
|
184
|
+
onValueChange,
|
|
185
|
+
completedValues = [],
|
|
186
|
+
errorValues = [],
|
|
187
|
+
linear = true,
|
|
188
|
+
size = 'md',
|
|
189
|
+
orientation = 'vertical',
|
|
190
|
+
expansion = 'follow-active',
|
|
191
|
+
defaultExpanded,
|
|
192
|
+
className,
|
|
193
|
+
children,
|
|
194
|
+
...props
|
|
195
|
+
},
|
|
196
|
+
ref,
|
|
197
|
+
) => {
|
|
198
|
+
const [internalValue, setInternalValue] = React.useState<string | undefined>(defaultValue)
|
|
199
|
+
const isControlled = valueProp !== undefined
|
|
200
|
+
const value = isControlled ? valueProp : internalValue
|
|
201
|
+
|
|
202
|
+
const setValue = React.useCallback(
|
|
203
|
+
(next: string) => {
|
|
204
|
+
if (!isControlled) setInternalValue(next)
|
|
205
|
+
onValueChange?.(next)
|
|
206
|
+
},
|
|
207
|
+
[isControlled, onValueChange],
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
const childValues = React.useMemo(() => {
|
|
211
|
+
const vals: string[] = []
|
|
212
|
+
React.Children.forEach(children, child => {
|
|
213
|
+
if (
|
|
214
|
+
React.isValidElement(child) &&
|
|
215
|
+
typeof child.props === 'object' &&
|
|
216
|
+
child.props &&
|
|
217
|
+
'value' in child.props
|
|
218
|
+
) {
|
|
219
|
+
vals.push(String((child.props as { value: string }).value))
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
return vals
|
|
223
|
+
}, [children])
|
|
224
|
+
|
|
225
|
+
const reachableValues = React.useMemo(
|
|
226
|
+
() => computeReachableValues(childValues, completedValues),
|
|
227
|
+
[childValues, completedValues],
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
const [expandedSet, setExpandedSet] = React.useState<Set<string>>(() =>
|
|
231
|
+
normalizeExpanded(defaultExpanded, childValues),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const toggleExpanded = React.useCallback((itemValue: string) => {
|
|
235
|
+
setExpandedSet(prev => {
|
|
236
|
+
const next = new Set(prev)
|
|
237
|
+
if (next.has(itemValue)) next.delete(itemValue)
|
|
238
|
+
else next.add(itemValue)
|
|
239
|
+
return next
|
|
240
|
+
})
|
|
241
|
+
}, [])
|
|
242
|
+
|
|
243
|
+
const ctxValue = React.useMemo<StepsContextValue>(
|
|
244
|
+
() => ({
|
|
245
|
+
value,
|
|
246
|
+
completedValues: new Set(completedValues),
|
|
247
|
+
errorValues: new Set(errorValues),
|
|
248
|
+
reachableValues,
|
|
249
|
+
linear,
|
|
250
|
+
size,
|
|
251
|
+
orientation,
|
|
252
|
+
expansion,
|
|
253
|
+
expandedSet,
|
|
254
|
+
setValue,
|
|
255
|
+
toggleExpanded,
|
|
256
|
+
}),
|
|
257
|
+
[value, completedValues, errorValues, reachableValues, linear, size, orientation, expansion, expandedSet, setValue, toggleExpanded],
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
// Interleave horizontal connectors between items
|
|
261
|
+
const count = React.Children.count(children)
|
|
262
|
+
const itemsWithIndex: React.ReactNode[] = []
|
|
263
|
+
|
|
264
|
+
React.Children.forEach(children, (child, index) => {
|
|
265
|
+
if (!React.isValidElement(child)) {
|
|
266
|
+
itemsWithIndex.push(child)
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
const isLast = index === count - 1
|
|
270
|
+
const cloned = React.cloneElement(
|
|
271
|
+
child as React.ReactElement<StepItemInjectedProps>,
|
|
272
|
+
{ __isLast: isLast },
|
|
273
|
+
)
|
|
274
|
+
itemsWithIndex.push(
|
|
275
|
+
<StepIndexContext.Provider key={`item-${index}`} value={index + 1}>
|
|
276
|
+
{cloned}
|
|
277
|
+
</StepIndexContext.Provider>,
|
|
278
|
+
)
|
|
279
|
+
// Horizontal connectors are now INSIDE each StepItem (Ant Design pattern),
|
|
280
|
+
// not between items. No interleaving needed.
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<StepsContext.Provider value={ctxValue}>
|
|
285
|
+
<ol
|
|
286
|
+
ref={ref}
|
|
287
|
+
data-orientation={orientation}
|
|
288
|
+
data-size={size}
|
|
289
|
+
className={cn(stepsRootVariants({ orientation }), className)}
|
|
290
|
+
{...props}
|
|
291
|
+
>
|
|
292
|
+
{itemsWithIndex}
|
|
293
|
+
</ol>
|
|
294
|
+
</StepsContext.Provider>
|
|
295
|
+
)
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
Steps.displayName = 'Steps'
|
|
299
|
+
|
|
300
|
+
// ── StepItem ──────────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
interface StepItemInjectedProps {
|
|
303
|
+
__isLast?: boolean
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export interface StepItemProps
|
|
307
|
+
extends Omit<React.HTMLAttributes<HTMLLIElement>, 'value'>,
|
|
308
|
+
StepItemInjectedProps {
|
|
309
|
+
value: string
|
|
310
|
+
state?: 'error'
|
|
311
|
+
disabled?: boolean
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const stepItemVariants = cva('group/step-item outline-none', {
|
|
315
|
+
variants: {
|
|
316
|
+
orientation: {
|
|
317
|
+
// pb-6 on li provides spacing for next item; connector is absolute within li
|
|
318
|
+
vertical: 'relative flex flex-col',
|
|
319
|
+
// Ant Design pattern:flex-1 等寬(最後一步用 last: 覆蓋成自然寬度)。
|
|
320
|
+
// Connector 在 item 內部(不是 items 之間的獨立元素)。
|
|
321
|
+
horizontal: 'flex-1 min-w-0 last:flex-none last:shrink-0',
|
|
322
|
+
},
|
|
323
|
+
size: {
|
|
324
|
+
sm: 'text-body',
|
|
325
|
+
md: 'text-body',
|
|
326
|
+
lg: 'text-body-lg',
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
defaultVariants: { orientation: 'vertical', size: 'md' },
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding
|
|
333
|
+
const StepItem = React.forwardRef<HTMLLIElement, StepItemProps>(
|
|
334
|
+
({ value, state: stateOverride, disabled = false, children, className, __isLast = false, ...props }, ref) => {
|
|
335
|
+
const steps = useStepsContext()
|
|
336
|
+
const state = computeState(
|
|
337
|
+
value,
|
|
338
|
+
steps.value,
|
|
339
|
+
steps.completedValues,
|
|
340
|
+
steps.errorValues,
|
|
341
|
+
steps.reachableValues,
|
|
342
|
+
steps.linear,
|
|
343
|
+
stateOverride,
|
|
344
|
+
)
|
|
345
|
+
const focused = value === steps.value
|
|
346
|
+
const clickable = isClickable(state, steps.linear, disabled)
|
|
347
|
+
const expanded =
|
|
348
|
+
steps.expansion === 'follow-active' ? focused : steps.expandedSet.has(value)
|
|
349
|
+
|
|
350
|
+
const activate = React.useCallback(() => {
|
|
351
|
+
if (!clickable) return
|
|
352
|
+
// 永遠更新 focus(value),multiple 模式額外 toggle 展開
|
|
353
|
+
steps.setValue(value)
|
|
354
|
+
if (steps.expansion === 'multiple') {
|
|
355
|
+
steps.toggleExpanded(value)
|
|
356
|
+
}
|
|
357
|
+
}, [clickable, steps, value])
|
|
358
|
+
|
|
359
|
+
const itemCtx = React.useMemo<StepItemContextValue>(() => ({
|
|
360
|
+
value,
|
|
361
|
+
state,
|
|
362
|
+
focused,
|
|
363
|
+
disabled,
|
|
364
|
+
clickable,
|
|
365
|
+
expanded,
|
|
366
|
+
isLast: __isLast,
|
|
367
|
+
activate,
|
|
368
|
+
}), [value, state, focused, disabled, clickable, expanded, __isLast, activate])
|
|
369
|
+
|
|
370
|
+
const isVertical = steps.orientation === 'vertical'
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<StepItemContext.Provider value={itemCtx}>
|
|
374
|
+
<li
|
|
375
|
+
ref={ref}
|
|
376
|
+
data-state={state}
|
|
377
|
+
data-focused={focused || undefined}
|
|
378
|
+
data-disabled={disabled || undefined}
|
|
379
|
+
data-clickable={clickable || undefined}
|
|
380
|
+
aria-current={focused ? 'step' : undefined}
|
|
381
|
+
aria-disabled={disabled || undefined}
|
|
382
|
+
className={cn(
|
|
383
|
+
stepItemVariants({ orientation: steps.orientation, size: steps.size }),
|
|
384
|
+
isVertical && !__isLast && 'pb-6',
|
|
385
|
+
!clickable && 'cursor-not-allowed',
|
|
386
|
+
className,
|
|
387
|
+
)}
|
|
388
|
+
{...props}
|
|
389
|
+
>
|
|
390
|
+
<StepItemLayout>{children}</StepItemLayout>
|
|
391
|
+
</li>
|
|
392
|
+
</StepItemContext.Provider>
|
|
393
|
+
)
|
|
394
|
+
},
|
|
395
|
+
)
|
|
396
|
+
StepItem.displayName = 'StepItem'
|
|
397
|
+
|
|
398
|
+
// ── StepItem internal layout ─────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
function StepItemLayout({ children }: { children: React.ReactNode }) {
|
|
401
|
+
const steps = useStepsContext()
|
|
402
|
+
const item = useStepItemContext()
|
|
403
|
+
|
|
404
|
+
let labelNode: React.ReactNode = null
|
|
405
|
+
let descNode: React.ReactNode = null
|
|
406
|
+
let contentNode: React.ReactNode = null
|
|
407
|
+
React.Children.forEach(children, child => {
|
|
408
|
+
if (!React.isValidElement(child)) return
|
|
409
|
+
if (child.type === StepLabel) labelNode = child
|
|
410
|
+
else if (child.type === StepDescription) descNode = child
|
|
411
|
+
else if (child.type === StepContent) contentNode = child
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
if (steps.orientation === 'horizontal') {
|
|
415
|
+
return <HorizontalLayout label={labelNode} description={descNode} />
|
|
416
|
+
}
|
|
417
|
+
return (
|
|
418
|
+
<VerticalLayout label={labelNode} description={descNode} content={contentNode} isLast={item.isLast} />
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ── Clickable header ─────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
function StepItemHeader({ children, className, style }: { children: React.ReactNode; className?: string; style?: React.CSSProperties }) {
|
|
425
|
+
const item = useStepItemContext()
|
|
426
|
+
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
427
|
+
if (!item.clickable) return
|
|
428
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
429
|
+
e.preventDefault()
|
|
430
|
+
item.activate()
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return (
|
|
434
|
+
<div
|
|
435
|
+
role={item.clickable ? 'button' : undefined}
|
|
436
|
+
tabIndex={item.clickable ? 0 : undefined}
|
|
437
|
+
onClick={item.clickable ? item.activate : undefined}
|
|
438
|
+
onKeyDown={item.clickable ? onKeyDown : undefined}
|
|
439
|
+
aria-disabled={item.disabled || undefined}
|
|
440
|
+
className={cn(
|
|
441
|
+
'outline-none',
|
|
442
|
+
item.clickable
|
|
443
|
+
? 'cursor-pointer rounded-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
|
|
444
|
+
: 'cursor-not-allowed',
|
|
445
|
+
className,
|
|
446
|
+
)}
|
|
447
|
+
style={style}
|
|
448
|
+
>
|
|
449
|
+
{children}
|
|
450
|
+
</div>
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── Vertical layout ──────────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
function VerticalLayout({
|
|
457
|
+
label,
|
|
458
|
+
description,
|
|
459
|
+
content,
|
|
460
|
+
isLast,
|
|
461
|
+
}: {
|
|
462
|
+
label: React.ReactNode
|
|
463
|
+
description: React.ReactNode
|
|
464
|
+
content: React.ReactNode
|
|
465
|
+
isLast: boolean
|
|
466
|
+
}) {
|
|
467
|
+
const steps = useStepsContext()
|
|
468
|
+
const item = useStepItemContext()
|
|
469
|
+
const showContent = !!content && item.expanded
|
|
470
|
+
const indicatorBox = INDICATOR_BOX_WIDTH[steps.size]
|
|
471
|
+
|
|
472
|
+
return (
|
|
473
|
+
<>
|
|
474
|
+
<StepItemHeader className="flex items-start gap-3">
|
|
475
|
+
<div className="shrink-0" style={{ width: indicatorBox }}>
|
|
476
|
+
<div className="h-[1lh] flex items-center justify-center">
|
|
477
|
+
<StepIndicator />
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
<div className="flex-1 min-w-0 flex items-start">
|
|
481
|
+
<div className="flex-1 min-w-0 flex flex-col">
|
|
482
|
+
{label}
|
|
483
|
+
{description}
|
|
484
|
+
</div>
|
|
485
|
+
{steps.expansion === 'multiple' && !!content && (
|
|
486
|
+
<span aria-hidden className="h-[1lh] flex items-center shrink-0 ml-2">
|
|
487
|
+
<ChevronDown
|
|
488
|
+
size={16}
|
|
489
|
+
className={cn(
|
|
490
|
+
'text-fg-muted transition-transform duration-150',
|
|
491
|
+
item.expanded && 'rotate-180',
|
|
492
|
+
)}
|
|
493
|
+
/>
|
|
494
|
+
</span>
|
|
495
|
+
)}
|
|
496
|
+
</div>
|
|
497
|
+
</StepItemHeader>
|
|
498
|
+
{showContent && (
|
|
499
|
+
<div className="flex items-start gap-3 mt-3">
|
|
500
|
+
<div className="shrink-0" style={{ width: indicatorBox }} />
|
|
501
|
+
<div className="flex-1 min-w-0">{content}</div>
|
|
502
|
+
</div>
|
|
503
|
+
)}
|
|
504
|
+
{!isLast && <VerticalConnectorLine />}
|
|
505
|
+
</>
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// ── Vertical connector ───────────────────────────────────────────────────
|
|
510
|
+
|
|
511
|
+
function VerticalConnectorLine() {
|
|
512
|
+
const steps = useStepsContext()
|
|
513
|
+
const item = useStepItemContext()
|
|
514
|
+
const isBlue = item.state === 'completed'
|
|
515
|
+
const radius = INDICATOR_SIZE[steps.size] / 2
|
|
516
|
+
const gap = 8
|
|
517
|
+
|
|
518
|
+
return (
|
|
519
|
+
<div
|
|
520
|
+
aria-hidden
|
|
521
|
+
className={cn(
|
|
522
|
+
'absolute w-px',
|
|
523
|
+
isBlue ? 'bg-primary' : 'bg-border',
|
|
524
|
+
)}
|
|
525
|
+
style={{
|
|
526
|
+
left: INDICATOR_BOX_WIDTH[steps.size] / 2,
|
|
527
|
+
top: `calc(0.5lh + ${radius}px + ${gap}px)`,
|
|
528
|
+
bottom: `calc(${radius}px - 0.5lh + ${gap}px)`,
|
|
529
|
+
}}
|
|
530
|
+
/>
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── Horizontal layout (Ant Design pattern) ──────────────────────────────
|
|
535
|
+
//
|
|
536
|
+
// Connector 在 **item 內部**(不是 items 之間的獨立元素):
|
|
537
|
+
// Step (flex-1): [indicator][gap][label][gap][──connector──]
|
|
538
|
+
// Last step: [indicator][gap][label] (無 connector)
|
|
539
|
+
//
|
|
540
|
+
// Root: flex-row gap-3 → gap 只在 step items 之間
|
|
541
|
+
// Step items: flex-1 等寬(最後一步 flex-none 自然寬度)
|
|
542
|
+
//
|
|
543
|
+
// 等距保證:
|
|
544
|
+
// label→connector gap = item 內 flex gap-3 = 12px
|
|
545
|
+
// connector→next circle = root gap-3 = 12px
|
|
546
|
+
// 兩邊都是 12px ✓
|
|
547
|
+
//
|
|
548
|
+
// Description 在 step item 內(connector 下方),wrap 到 item 寬度 = 最長到連結線尾段 ✓
|
|
549
|
+
|
|
550
|
+
function HorizontalLayout({
|
|
551
|
+
label,
|
|
552
|
+
description,
|
|
553
|
+
}: {
|
|
554
|
+
label: React.ReactNode
|
|
555
|
+
description: React.ReactNode
|
|
556
|
+
}) {
|
|
557
|
+
const item = useStepItemContext()
|
|
558
|
+
const steps = useStepsContext()
|
|
559
|
+
const isBlue = item.state === 'completed'
|
|
560
|
+
const indicatorBox = INDICATOR_BOX_WIDTH[steps.size]
|
|
561
|
+
|
|
562
|
+
return (
|
|
563
|
+
<>
|
|
564
|
+
{/* Row 1: indicator + label + connector(在同一個 flex row) */}
|
|
565
|
+
<StepItemHeader className="flex items-start gap-3">
|
|
566
|
+
<div className="h-[1lh] flex items-center shrink-0">
|
|
567
|
+
<StepIndicator />
|
|
568
|
+
</div>
|
|
569
|
+
<div className="shrink-0 min-w-0">{label}</div>
|
|
570
|
+
{/* Connector 在 item 內部,flex-1 填滿剩餘寬度 */}
|
|
571
|
+
{!item.isLast && (
|
|
572
|
+
<div className="h-[1lh] flex-1 flex items-center min-w-4" aria-hidden>
|
|
573
|
+
<div className={cn('h-px w-full', isBlue ? 'bg-primary' : 'bg-border')} />
|
|
574
|
+
</div>
|
|
575
|
+
)}
|
|
576
|
+
</StepItemHeader>
|
|
577
|
+
{/* Row 2: description — 在 item 寬度內 wrap(含 connector 佔的空間) */}
|
|
578
|
+
{description && (
|
|
579
|
+
<div className="min-w-0" style={{ paddingLeft: indicatorBox + 12 }}>
|
|
580
|
+
{description}
|
|
581
|
+
</div>
|
|
582
|
+
)}
|
|
583
|
+
</>
|
|
584
|
+
)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ── StepIndicator ────────────────────────────────────────────────────────
|
|
588
|
+
|
|
589
|
+
function StepIndicator() {
|
|
590
|
+
const steps = useStepsContext()
|
|
591
|
+
const item = useStepItemContext()
|
|
592
|
+
const { size, linear } = steps
|
|
593
|
+
const { state, focused, disabled } = item
|
|
594
|
+
|
|
595
|
+
if (size === 'sm') return <SmIndicator state={state} focused={focused} disabled={disabled} linear={linear} />
|
|
596
|
+
return <MdLgIndicator size={size} state={state} focused={focused} disabled={disabled} linear={linear} />
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ── sm indicator: 8px dot in 24px hit area ───────────────────────────────
|
|
600
|
+
|
|
601
|
+
function SmIndicator({
|
|
602
|
+
state,
|
|
603
|
+
focused,
|
|
604
|
+
disabled,
|
|
605
|
+
linear,
|
|
606
|
+
}: {
|
|
607
|
+
state: StepContentState
|
|
608
|
+
focused: boolean
|
|
609
|
+
disabled: boolean
|
|
610
|
+
linear: boolean
|
|
611
|
+
}) {
|
|
612
|
+
// sm current (linear) and reachable: hollow ring
|
|
613
|
+
const isHollow = (state === 'current' && linear) || state === 'reachable'
|
|
614
|
+
|
|
615
|
+
let dotStyle: React.CSSProperties
|
|
616
|
+
if (isHollow) {
|
|
617
|
+
dotStyle = {
|
|
618
|
+
width: INDICATOR_SIZE.sm,
|
|
619
|
+
height: INDICATOR_SIZE.sm,
|
|
620
|
+
background: 'transparent',
|
|
621
|
+
border: '2px solid var(--primary-hover)',
|
|
622
|
+
boxShadow: focused ? getOuterRingShadow(resolveRingColor(state, linear)) : undefined,
|
|
623
|
+
}
|
|
624
|
+
} else {
|
|
625
|
+
const dotBg =
|
|
626
|
+
state === 'completed' ? 'var(--primary)'
|
|
627
|
+
: state === 'error' ? 'var(--error)'
|
|
628
|
+
: state === 'current' && !linear ? 'var(--fg-disabled)'
|
|
629
|
+
: 'var(--fg-disabled)' // upcoming + non-linear fallback
|
|
630
|
+
|
|
631
|
+
dotStyle = {
|
|
632
|
+
width: INDICATOR_SIZE.sm,
|
|
633
|
+
height: INDICATOR_SIZE.sm,
|
|
634
|
+
background: dotBg,
|
|
635
|
+
boxShadow: focused ? getOuterRingShadow(resolveRingColor(state, linear)) : undefined,
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return (
|
|
640
|
+
<span
|
|
641
|
+
aria-hidden
|
|
642
|
+
className="relative inline-flex items-center justify-center shrink-0"
|
|
643
|
+
style={{ width: SM_HIT_AREA, height: SM_HIT_AREA }}
|
|
644
|
+
>
|
|
645
|
+
<span
|
|
646
|
+
className={cn('block rounded-full', disabled && 'opacity-disabled')}
|
|
647
|
+
style={dotStyle}
|
|
648
|
+
/>
|
|
649
|
+
</span>
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ── md/lg indicator: filled circle with number/icon ──────────────────────
|
|
654
|
+
|
|
655
|
+
function MdLgIndicator({
|
|
656
|
+
size,
|
|
657
|
+
state,
|
|
658
|
+
focused,
|
|
659
|
+
disabled,
|
|
660
|
+
linear,
|
|
661
|
+
}: {
|
|
662
|
+
size: StepsSize
|
|
663
|
+
state: StepContentState
|
|
664
|
+
focused: boolean
|
|
665
|
+
disabled: boolean
|
|
666
|
+
linear: boolean
|
|
667
|
+
}) {
|
|
668
|
+
const diameter = INDICATOR_SIZE[size]
|
|
669
|
+
const iconPx = INDICATOR_ICON_SIZE[size]
|
|
670
|
+
|
|
671
|
+
let fillBg: string
|
|
672
|
+
let contentColor: string
|
|
673
|
+
|
|
674
|
+
switch (state) {
|
|
675
|
+
case 'error':
|
|
676
|
+
fillBg = 'var(--error)'
|
|
677
|
+
contentColor = 'var(--on-emphasis)'
|
|
678
|
+
break
|
|
679
|
+
case 'completed':
|
|
680
|
+
fillBg = 'var(--primary)'
|
|
681
|
+
contentColor = 'var(--on-emphasis)'
|
|
682
|
+
break
|
|
683
|
+
case 'current':
|
|
684
|
+
if (linear) {
|
|
685
|
+
fillBg = 'var(--primary)'
|
|
686
|
+
contentColor = 'var(--on-emphasis)'
|
|
687
|
+
} else {
|
|
688
|
+
fillBg = 'var(--secondary)'
|
|
689
|
+
contentColor = 'var(--foreground)'
|
|
690
|
+
}
|
|
691
|
+
break
|
|
692
|
+
case 'reachable':
|
|
693
|
+
fillBg = 'var(--primary)'
|
|
694
|
+
contentColor = 'var(--on-emphasis)'
|
|
695
|
+
break
|
|
696
|
+
default: // upcoming
|
|
697
|
+
if (linear) {
|
|
698
|
+
fillBg = 'var(--muted)'
|
|
699
|
+
contentColor = 'var(--fg-disabled)'
|
|
700
|
+
} else {
|
|
701
|
+
fillBg = 'var(--secondary)'
|
|
702
|
+
contentColor = 'var(--foreground)'
|
|
703
|
+
}
|
|
704
|
+
break
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return (
|
|
708
|
+
<span
|
|
709
|
+
aria-hidden
|
|
710
|
+
className={cn(
|
|
711
|
+
'relative inline-flex items-center justify-center shrink-0 rounded-full',
|
|
712
|
+
'font-medium leading-none transition-colors',
|
|
713
|
+
disabled && 'opacity-disabled',
|
|
714
|
+
)}
|
|
715
|
+
style={{
|
|
716
|
+
width: diameter,
|
|
717
|
+
height: diameter,
|
|
718
|
+
background: fillBg,
|
|
719
|
+
color: contentColor,
|
|
720
|
+
fontSize: size === 'lg' ? 'var(--font-body-size)' : 'var(--font-caption-size)',
|
|
721
|
+
boxShadow: focused ? getOuterRingShadow(resolveRingColor(state, linear)) : undefined,
|
|
722
|
+
}}
|
|
723
|
+
>
|
|
724
|
+
<IndicatorContent state={state} iconPx={iconPx} />
|
|
725
|
+
</span>
|
|
726
|
+
)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function IndicatorContent({ state, iconPx }: { state: StepContentState; iconPx: number }) {
|
|
730
|
+
if (state === 'completed') return <Check size={iconPx} strokeWidth={2.5} />
|
|
731
|
+
if (state === 'error') return <X size={iconPx} strokeWidth={2.5} />
|
|
732
|
+
return <StepNumber />
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function StepNumber() {
|
|
736
|
+
const index = React.useContext(StepIndexContext)
|
|
737
|
+
return <span>{index}</span>
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// ── StepLabel ────────────────────────────────────────────────────────────
|
|
741
|
+
|
|
742
|
+
export interface StepLabelProps extends React.HTMLAttributes<HTMLSpanElement> {}
|
|
743
|
+
|
|
744
|
+
// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding
|
|
745
|
+
const StepLabel = React.forwardRef<HTMLSpanElement, StepLabelProps>(
|
|
746
|
+
({ className, children, ...props }, ref) => {
|
|
747
|
+
const { size } = useStepsContext()
|
|
748
|
+
const { state, focused, disabled } = useStepItemContext()
|
|
749
|
+
|
|
750
|
+
return (
|
|
751
|
+
<span
|
|
752
|
+
ref={ref}
|
|
753
|
+
className={cn(
|
|
754
|
+
'font-medium break-words',
|
|
755
|
+
size === 'lg' ? 'text-body-lg' : 'text-body',
|
|
756
|
+
disabled
|
|
757
|
+
? 'text-fg-disabled'
|
|
758
|
+
: state === 'error'
|
|
759
|
+
? 'text-error-text'
|
|
760
|
+
: focused
|
|
761
|
+
? 'text-foreground'
|
|
762
|
+
: 'text-fg-secondary',
|
|
763
|
+
className,
|
|
764
|
+
)}
|
|
765
|
+
{...props}
|
|
766
|
+
>
|
|
767
|
+
{children}
|
|
768
|
+
</span>
|
|
769
|
+
)
|
|
770
|
+
},
|
|
771
|
+
)
|
|
772
|
+
StepLabel.displayName = 'StepLabel'
|
|
773
|
+
|
|
774
|
+
// ── StepDescription ──────────────────────────────────────────────────────
|
|
775
|
+
|
|
776
|
+
export interface StepDescriptionProps extends React.HTMLAttributes<HTMLSpanElement> {}
|
|
777
|
+
|
|
778
|
+
const StepDescription = React.forwardRef<HTMLSpanElement, StepDescriptionProps>(
|
|
779
|
+
({ className, children, style, ...props }, ref) => {
|
|
780
|
+
const { size } = useStepsContext()
|
|
781
|
+
const { disabled } = useStepItemContext()
|
|
782
|
+
|
|
783
|
+
return (
|
|
784
|
+
<span
|
|
785
|
+
ref={ref}
|
|
786
|
+
className={cn(
|
|
787
|
+
// Steps 跟 MenuItem 同 scanning-family:sm/md = scanning(body+caption),lg = scanning-lg(body-lg+body-compact)
|
|
788
|
+
size === 'lg'
|
|
789
|
+
? 'mt-[var(--item-gap-label-desc-scanning-lg)]'
|
|
790
|
+
: 'mt-[var(--item-gap-label-desc-scanning)]',
|
|
791
|
+
'leading-compact break-words',
|
|
792
|
+
disabled ? 'text-fg-disabled' : 'text-fg-secondary',
|
|
793
|
+
className,
|
|
794
|
+
)}
|
|
795
|
+
style={{
|
|
796
|
+
fontSize: size === 'lg' ? 'var(--font-body-size)' : 'var(--font-caption-size)',
|
|
797
|
+
...style,
|
|
798
|
+
}}
|
|
799
|
+
{...props}
|
|
800
|
+
>
|
|
801
|
+
{children}
|
|
802
|
+
</span>
|
|
803
|
+
)
|
|
804
|
+
},
|
|
805
|
+
)
|
|
806
|
+
StepDescription.displayName = 'StepDescription'
|
|
807
|
+
|
|
808
|
+
// ── StepContent ──────────────────────────────────────────────────────────
|
|
809
|
+
|
|
810
|
+
export interface StepContentProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
811
|
+
|
|
812
|
+
const StepContent = React.forwardRef<HTMLDivElement, StepContentProps>(
|
|
813
|
+
({ className, children, ...props }, ref) => {
|
|
814
|
+
const { orientation } = useStepsContext()
|
|
815
|
+
if (orientation === 'horizontal') return null
|
|
816
|
+
return (
|
|
817
|
+
<div
|
|
818
|
+
ref={ref}
|
|
819
|
+
className={cn('text-body text-foreground min-w-0', className)}
|
|
820
|
+
{...props}
|
|
821
|
+
>
|
|
822
|
+
{children}
|
|
823
|
+
</div>
|
|
824
|
+
)
|
|
825
|
+
},
|
|
826
|
+
)
|
|
827
|
+
StepContent.displayName = 'StepContent'
|
|
828
|
+
|
|
829
|
+
// ── Exports ──────────────────────────────────────────────────────────────
|
|
830
|
+
|
|
831
|
+
// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)
|
|
832
|
+
// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs
|
|
833
|
+
export const stepsMeta = {
|
|
834
|
+
component: 'Steps',
|
|
835
|
+
family: 2,
|
|
836
|
+
variants: {},
|
|
837
|
+
sizes: {
|
|
838
|
+
sm: { px: 8, when: 'Sidebar / 緊湊 onboarding;indicator 8px dot' },
|
|
839
|
+
md: { px: 24, when: '預設 — wizard / checkout / 註冊主流程;indicator 24px circle' },
|
|
840
|
+
lg: { px: 32, when: 'Marketing 流程展示 / 重要 onboarding;indicator 32px circle' },
|
|
841
|
+
},
|
|
842
|
+
states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],
|
|
843
|
+
tokens: {
|
|
844
|
+
bg: ['bg-primary'],
|
|
845
|
+
fg: ['--fg-disabled', '--foreground', '--on-emphasis', 'text-error-text', 'text-fg-disabled', 'text-fg-muted', 'text-fg-secondary', 'text-foreground'],
|
|
846
|
+
ring: [],
|
|
847
|
+
},
|
|
848
|
+
} as const
|
|
849
|
+
|
|
850
|
+
export { Steps, StepItem, StepLabel, StepDescription, StepContent, stepsRootVariants, stepItemVariants }
|