@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,371 @@
|
|
|
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 useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react'
|
|
4
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
import { Button } from '@/design-system/components/Button/button'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Carousel — 圖片 / 內容水平(或垂直)輪播
|
|
10
|
+
*
|
|
11
|
+
* 實作基礎:shadcn `Carousel` 結構 + `embla-carousel-react` v8 engine + 本 DS token。
|
|
12
|
+
*
|
|
13
|
+
* ── 世界級對照 ──
|
|
14
|
+
* shadcn Carousel(本元件主要參考)/ Ant Carousel / Polaris 無 /
|
|
15
|
+
* Swiper(獨立 lib,功能更多但不在 DS 範疇)
|
|
16
|
+
*
|
|
17
|
+
* ── 視覺慣例(user 指示) ──
|
|
18
|
+
* 預設「hover 上去」左右兩邊才出現 prev/next 按鈕,不打擾主視覺;
|
|
19
|
+
* 指示器(dots)在底部中央,clickable。
|
|
20
|
+
*
|
|
21
|
+
* ── API(shadcn parity) ──
|
|
22
|
+
* <Carousel opts plugins orientation>
|
|
23
|
+
* <CarouselContent>
|
|
24
|
+
* <CarouselItem>...</CarouselItem>
|
|
25
|
+
* <CarouselItem>...</CarouselItem>
|
|
26
|
+
* </CarouselContent>
|
|
27
|
+
* <CarouselPrevious /> ← 左箭頭
|
|
28
|
+
* <CarouselNext /> ← 右箭頭
|
|
29
|
+
* <CarouselDots /> ← 本 DS 擴充(shadcn 無,Ant/Swiper 慣例)
|
|
30
|
+
* </Carousel>
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
type CarouselApi = UseEmblaCarouselType[1]
|
|
34
|
+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
|
35
|
+
type CarouselOptions = UseCarouselParameters[0]
|
|
36
|
+
type CarouselPlugin = UseCarouselParameters[1]
|
|
37
|
+
|
|
38
|
+
interface CarouselProps {
|
|
39
|
+
opts?: CarouselOptions
|
|
40
|
+
plugins?: CarouselPlugin
|
|
41
|
+
orientation?: 'horizontal' | 'vertical'
|
|
42
|
+
setApi?: (api: CarouselApi) => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface CarouselContextProps extends CarouselProps {
|
|
46
|
+
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
|
47
|
+
api: ReturnType<typeof useEmblaCarousel>[1]
|
|
48
|
+
scrollPrev: () => void
|
|
49
|
+
scrollNext: () => void
|
|
50
|
+
scrollTo: (i: number) => void
|
|
51
|
+
canScrollPrev: boolean
|
|
52
|
+
canScrollNext: boolean
|
|
53
|
+
selectedIndex: number
|
|
54
|
+
scrollSnaps: number[]
|
|
55
|
+
orientation: 'horizontal' | 'vertical'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
|
|
59
|
+
|
|
60
|
+
function useCarousel() {
|
|
61
|
+
const ctx = React.useContext(CarouselContext)
|
|
62
|
+
if (!ctx) throw new Error('useCarousel 必須在 <Carousel> 內使用')
|
|
63
|
+
return ctx
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Root ────────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
const Carousel = React.forwardRef<
|
|
69
|
+
HTMLDivElement,
|
|
70
|
+
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
|
71
|
+
>(
|
|
72
|
+
(
|
|
73
|
+
{
|
|
74
|
+
orientation = 'horizontal',
|
|
75
|
+
opts,
|
|
76
|
+
setApi,
|
|
77
|
+
plugins,
|
|
78
|
+
className,
|
|
79
|
+
children,
|
|
80
|
+
...props
|
|
81
|
+
},
|
|
82
|
+
ref,
|
|
83
|
+
) => {
|
|
84
|
+
const [carouselRef, api] = useEmblaCarousel(
|
|
85
|
+
{ ...opts, axis: orientation === 'horizontal' ? 'x' : 'y' },
|
|
86
|
+
plugins,
|
|
87
|
+
)
|
|
88
|
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
|
89
|
+
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
|
90
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0)
|
|
91
|
+
const [scrollSnaps, setScrollSnaps] = React.useState<number[]>([])
|
|
92
|
+
|
|
93
|
+
const onSelect = React.useCallback((a: CarouselApi) => {
|
|
94
|
+
if (!a) return
|
|
95
|
+
setCanScrollPrev(a.canScrollPrev())
|
|
96
|
+
setCanScrollNext(a.canScrollNext())
|
|
97
|
+
setSelectedIndex(a.selectedScrollSnap())
|
|
98
|
+
}, [])
|
|
99
|
+
|
|
100
|
+
const scrollPrev = React.useCallback(() => api?.scrollPrev(), [api])
|
|
101
|
+
const scrollNext = React.useCallback(() => api?.scrollNext(), [api])
|
|
102
|
+
const scrollTo = React.useCallback((i: number) => api?.scrollTo(i), [api])
|
|
103
|
+
|
|
104
|
+
React.useEffect(() => {
|
|
105
|
+
if (!api || !setApi) return
|
|
106
|
+
setApi(api)
|
|
107
|
+
}, [api, setApi])
|
|
108
|
+
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
if (!api) return
|
|
111
|
+
setScrollSnaps(api.scrollSnapList())
|
|
112
|
+
onSelect(api)
|
|
113
|
+
api.on('reInit', onSelect)
|
|
114
|
+
api.on('select', onSelect)
|
|
115
|
+
return () => {
|
|
116
|
+
api?.off('select', onSelect)
|
|
117
|
+
api?.off('reInit', onSelect) // D3 fix: previously leaked — stale closure on remount
|
|
118
|
+
}
|
|
119
|
+
}, [api, onSelect])
|
|
120
|
+
|
|
121
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
122
|
+
if (e.key === 'ArrowLeft') { e.preventDefault(); scrollPrev() }
|
|
123
|
+
else if (e.key === 'ArrowRight') { e.preventDefault(); scrollNext() }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const contextValue = React.useMemo(
|
|
127
|
+
() => ({
|
|
128
|
+
carouselRef,
|
|
129
|
+
api,
|
|
130
|
+
opts,
|
|
131
|
+
orientation,
|
|
132
|
+
scrollPrev,
|
|
133
|
+
scrollNext,
|
|
134
|
+
scrollTo,
|
|
135
|
+
canScrollPrev,
|
|
136
|
+
canScrollNext,
|
|
137
|
+
selectedIndex,
|
|
138
|
+
scrollSnaps,
|
|
139
|
+
}),
|
|
140
|
+
[
|
|
141
|
+
carouselRef,
|
|
142
|
+
api,
|
|
143
|
+
opts,
|
|
144
|
+
orientation,
|
|
145
|
+
scrollPrev,
|
|
146
|
+
scrollNext,
|
|
147
|
+
scrollTo,
|
|
148
|
+
canScrollPrev,
|
|
149
|
+
canScrollNext,
|
|
150
|
+
selectedIndex,
|
|
151
|
+
scrollSnaps,
|
|
152
|
+
]
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<CarouselContext.Provider value={contextValue}>
|
|
157
|
+
<div
|
|
158
|
+
ref={ref}
|
|
159
|
+
onKeyDownCapture={handleKeyDown}
|
|
160
|
+
className={cn('group/carousel relative', className)}
|
|
161
|
+
role="region"
|
|
162
|
+
aria-roledescription="carousel"
|
|
163
|
+
{...props}
|
|
164
|
+
>
|
|
165
|
+
{children}
|
|
166
|
+
</div>
|
|
167
|
+
</CarouselContext.Provider>
|
|
168
|
+
)
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
Carousel.displayName = 'Carousel'
|
|
172
|
+
|
|
173
|
+
// ── Content / Item ──────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
const CarouselContent = React.forwardRef<
|
|
176
|
+
HTMLDivElement,
|
|
177
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
178
|
+
>(({ className, ...props }, ref) => {
|
|
179
|
+
const { carouselRef, orientation } = useCarousel()
|
|
180
|
+
return (
|
|
181
|
+
<div ref={carouselRef} className="overflow-hidden">
|
|
182
|
+
<div
|
|
183
|
+
ref={ref}
|
|
184
|
+
className={cn(
|
|
185
|
+
'flex',
|
|
186
|
+
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
|
|
187
|
+
className,
|
|
188
|
+
)}
|
|
189
|
+
{...props}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
193
|
+
})
|
|
194
|
+
CarouselContent.displayName = 'CarouselContent'
|
|
195
|
+
|
|
196
|
+
const CarouselItem = React.forwardRef<
|
|
197
|
+
HTMLDivElement,
|
|
198
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
199
|
+
>(({ className, ...props }, ref) => {
|
|
200
|
+
const { orientation } = useCarousel()
|
|
201
|
+
return (
|
|
202
|
+
<div
|
|
203
|
+
ref={ref}
|
|
204
|
+
role="group"
|
|
205
|
+
aria-roledescription="slide"
|
|
206
|
+
className={cn(
|
|
207
|
+
'min-w-0 shrink-0 grow-0 basis-full',
|
|
208
|
+
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
|
|
209
|
+
className,
|
|
210
|
+
)}
|
|
211
|
+
{...props}
|
|
212
|
+
/>
|
|
213
|
+
)
|
|
214
|
+
})
|
|
215
|
+
CarouselItem.displayName = 'CarouselItem'
|
|
216
|
+
|
|
217
|
+
// ── Arrow buttons(hover 才顯示)────────────────────────────────────────────
|
|
218
|
+
// 使用 DS Button (secondary + iconOnly size md);hover-only 顯示由 wrapper 的
|
|
219
|
+
// opacity transition 控制(Button 本身不負責)。此 wrapper 存在僅為絕對定位 +
|
|
220
|
+
// hover/focus 可見性,不再覆寫 Button 的視覺 token。
|
|
221
|
+
|
|
222
|
+
type ArrowProps = {
|
|
223
|
+
className?: string
|
|
224
|
+
/** ARIA label. Override for i18n. Prev default: 「上一張」;Next default: 「下一張」 */
|
|
225
|
+
'aria-label'?: string
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const arrowWrapperClass = cn(
|
|
229
|
+
'absolute z-10',
|
|
230
|
+
'transition-opacity duration-200',
|
|
231
|
+
'opacity-0 group-hover/carousel:opacity-100',
|
|
232
|
+
'focus-within:opacity-100',
|
|
233
|
+
'[&:has(button:disabled)]:opacity-0 [&:has(button:disabled)]:pointer-events-none',
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
// code-quality-allow: long-function — foundational composite main body — 拆 sub-fn 會複雜化 local state / ref / context binding
|
|
237
|
+
const CarouselPrevious = React.forwardRef<HTMLButtonElement, ArrowProps>(
|
|
238
|
+
({ className, 'aria-label': ariaLabel = '上一張' /* i18n-allow: DS default; consumer override via aria-label prop */ }, ref) => {
|
|
239
|
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
|
|
240
|
+
return (
|
|
241
|
+
<div
|
|
242
|
+
className={cn(
|
|
243
|
+
arrowWrapperClass,
|
|
244
|
+
orientation === 'horizontal'
|
|
245
|
+
? 'left-3 top-1/2 -translate-y-1/2'
|
|
246
|
+
: 'top-3 left-1/2 -translate-x-1/2 rotate-90',
|
|
247
|
+
className,
|
|
248
|
+
)}
|
|
249
|
+
>
|
|
250
|
+
<Button
|
|
251
|
+
ref={ref}
|
|
252
|
+
variant="tertiary"
|
|
253
|
+
size="md"
|
|
254
|
+
iconOnly
|
|
255
|
+
startIcon={ChevronLeft}
|
|
256
|
+
aria-label={ariaLabel}
|
|
257
|
+
disabled={!canScrollPrev}
|
|
258
|
+
onClick={scrollPrev}
|
|
259
|
+
// documented exception:視覺取向的 media carousel 箭頭用 rounded-full 圓形,
|
|
260
|
+
// 優於 DS default rounded-md。對齊 Instagram / Airbnb / Notion / Apple Photos
|
|
261
|
+
// 世界級慣例 — media carousel 箭頭圓形減少視覺方塊感壓迫內容。spec「箭頭視覺規格」有明示。
|
|
262
|
+
className="rounded-full"
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
)
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
CarouselPrevious.displayName = 'CarouselPrevious'
|
|
269
|
+
|
|
270
|
+
const CarouselNext = React.forwardRef<HTMLButtonElement, ArrowProps>(
|
|
271
|
+
({ className, 'aria-label': ariaLabel = '下一張' /* i18n-allow: DS default; consumer override via aria-label prop */ }, ref) => {
|
|
272
|
+
const { orientation, scrollNext, canScrollNext } = useCarousel()
|
|
273
|
+
return (
|
|
274
|
+
<div
|
|
275
|
+
className={cn(
|
|
276
|
+
arrowWrapperClass,
|
|
277
|
+
orientation === 'horizontal'
|
|
278
|
+
? 'right-3 top-1/2 -translate-y-1/2'
|
|
279
|
+
: 'bottom-3 left-1/2 -translate-x-1/2 rotate-90',
|
|
280
|
+
className,
|
|
281
|
+
)}
|
|
282
|
+
>
|
|
283
|
+
<Button
|
|
284
|
+
ref={ref}
|
|
285
|
+
variant="tertiary"
|
|
286
|
+
size="md"
|
|
287
|
+
iconOnly
|
|
288
|
+
startIcon={ChevronRight}
|
|
289
|
+
aria-label={ariaLabel}
|
|
290
|
+
disabled={!canScrollNext}
|
|
291
|
+
onClick={scrollNext}
|
|
292
|
+
// documented exception:同 Previous,媒體導向 carousel 箭頭圓形
|
|
293
|
+
className="rounded-full"
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
)
|
|
297
|
+
},
|
|
298
|
+
)
|
|
299
|
+
CarouselNext.displayName = 'CarouselNext'
|
|
300
|
+
|
|
301
|
+
// ── Dots indicator(底部中央)───────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
const CarouselDots = React.forwardRef<
|
|
304
|
+
HTMLDivElement,
|
|
305
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
306
|
+
>(({ className, ...props }, ref) => {
|
|
307
|
+
const { scrollSnaps, selectedIndex, scrollTo } = useCarousel()
|
|
308
|
+
if (scrollSnaps.length <= 1) return null
|
|
309
|
+
return (
|
|
310
|
+
<div
|
|
311
|
+
ref={ref}
|
|
312
|
+
className={cn(
|
|
313
|
+
'absolute bottom-3 left-1/2 -translate-x-1/2 z-10',
|
|
314
|
+
'flex items-center gap-1.5',
|
|
315
|
+
className,
|
|
316
|
+
)}
|
|
317
|
+
role="tablist"
|
|
318
|
+
aria-label="carousel pagination"
|
|
319
|
+
{...props}
|
|
320
|
+
>
|
|
321
|
+
{scrollSnaps.map((_, i) => (
|
|
322
|
+
<button
|
|
323
|
+
key={i}
|
|
324
|
+
type="button"
|
|
325
|
+
role="tab"
|
|
326
|
+
aria-selected={i === selectedIndex}
|
|
327
|
+
aria-label={`跳至第 ${i + 1} 張`}
|
|
328
|
+
onClick={() => scrollTo(i)}
|
|
329
|
+
className={cn(
|
|
330
|
+
'h-1.5 rounded-full transition-all',
|
|
331
|
+
// Dots 疊在 media(image/video)之上,不是 token color 底——用 --on-emphasis 保持語義
|
|
332
|
+
// 跟其他「於飽和色底上的淺色前景」一致
|
|
333
|
+
'bg-on-emphasis/60 hover:bg-on-emphasis/80',
|
|
334
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
335
|
+
i === selectedIndex ? 'w-6 bg-on-emphasis' : 'w-1.5',
|
|
336
|
+
)}
|
|
337
|
+
/>
|
|
338
|
+
))}
|
|
339
|
+
</div>
|
|
340
|
+
)
|
|
341
|
+
})
|
|
342
|
+
CarouselDots.displayName = 'CarouselDots'
|
|
343
|
+
|
|
344
|
+
// Story auto-compile metadata — Phase 1 mechanical migration(2026-04-24)
|
|
345
|
+
// Phase 2 fill needed: purpose descriptions + when rationale + world-class refs
|
|
346
|
+
export const carouselMeta = {
|
|
347
|
+
component: 'Carousel',
|
|
348
|
+
family: null, // non-family composite / overlay / layout
|
|
349
|
+
variants: {
|
|
350
|
+
|
|
351
|
+
},
|
|
352
|
+
sizes: {
|
|
353
|
+
|
|
354
|
+
},
|
|
355
|
+
states: ['default', 'hover', 'active', 'focus-visible', 'disabled'],
|
|
356
|
+
tokens: {
|
|
357
|
+
bg: [],
|
|
358
|
+
fg: [],
|
|
359
|
+
ring: ['ring-ring'],
|
|
360
|
+
},
|
|
361
|
+
} as const
|
|
362
|
+
|
|
363
|
+
export {
|
|
364
|
+
Carousel,
|
|
365
|
+
CarouselContent,
|
|
366
|
+
CarouselItem,
|
|
367
|
+
CarouselPrevious,
|
|
368
|
+
CarouselNext,
|
|
369
|
+
CarouselDots,
|
|
370
|
+
type CarouselApi,
|
|
371
|
+
}
|