solid-tom-ui 1.0.10 → 1.0.14

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 (120) hide show
  1. package/README.md +246 -246
  2. package/dist/README.md +246 -246
  3. package/dist/components/avatar/avatar.js.map +1 -1
  4. package/dist/components/badge/badge.js.map +1 -1
  5. package/dist/components/breadcrumb/breadcrumb.js.map +1 -1
  6. package/dist/components/button/button.js.map +1 -1
  7. package/dist/components/carousel/carousel.js.map +1 -1
  8. package/dist/components/chat-bubble/chatBubble.js.map +1 -1
  9. package/dist/components/checkbox/checkbox.js.map +1 -1
  10. package/dist/components/collapse/collapse.js.map +1 -1
  11. package/dist/components/context-menu/context-menu.js.map +1 -1
  12. package/dist/components/context-menu/context-menu.store.js.map +1 -1
  13. package/dist/components/divider/divider.js.map +1 -1
  14. package/dist/components/dropdown/dropdown.js.map +1 -1
  15. package/dist/components/dropdown/dropdown.store.js.map +1 -1
  16. package/dist/components/float-button/float-button.js.map +1 -1
  17. package/dist/components/hover-3d-image/hover-3d-image.js.map +1 -1
  18. package/dist/components/image-preview/image-preview.js.map +1 -1
  19. package/dist/components/input/input.js.map +1 -1
  20. package/dist/components/input/input.utils.js.map +1 -1
  21. package/dist/components/input/variants/input-color.js.map +1 -1
  22. package/dist/components/input/variants/input-date.js.map +1 -1
  23. package/dist/components/input/variants/input-number.d.ts.map +1 -1
  24. package/dist/components/input/variants/input-number.js +1 -1
  25. package/dist/components/input/variants/input-number.js.map +1 -1
  26. package/dist/components/input/variants/input-otp.js.map +1 -1
  27. package/dist/components/input/variants/input-password.js.map +1 -1
  28. package/dist/components/input/variants/input-radio.js.map +1 -1
  29. package/dist/components/input/variants/input-range.js.map +1 -1
  30. package/dist/components/input/variants/input-text.d.ts.map +1 -1
  31. package/dist/components/input/variants/input-text.js +1 -1
  32. package/dist/components/input/variants/input-text.js.map +1 -1
  33. package/dist/components/input/variants/input-textarea.js.map +1 -1
  34. package/dist/components/loading/loading.js.map +1 -1
  35. package/dist/components/mansory/mansory.js.map +1 -1
  36. package/dist/components/menu/menu.js.map +1 -1
  37. package/dist/components/menu/menu.types.d.ts +2 -3
  38. package/dist/components/menu/menu.types.d.ts.map +1 -1
  39. package/dist/components/modal/modal.js.map +1 -1
  40. package/dist/components/modal/modalContext.js.map +1 -1
  41. package/dist/components/pagination/pagination.js.map +1 -1
  42. package/dist/components/progress-bar/progress-bar.js.map +1 -1
  43. package/dist/components/qr-code/qr-code.js.map +1 -1
  44. package/dist/components/select/select.js.map +1 -1
  45. package/dist/components/select-zone/select-zone.js.map +1 -1
  46. package/dist/components/skeleton/skeleton.js.map +1 -1
  47. package/dist/components/slider/slider.js.map +1 -1
  48. package/dist/components/splitter/splitter.js.map +1 -1
  49. package/dist/components/steps/steps.js.map +1 -1
  50. package/dist/components/swap/swap.js.map +1 -1
  51. package/dist/components/switch/switch.js.map +1 -1
  52. package/dist/components/tab/tab.js.map +1 -1
  53. package/dist/components/table/table.js.map +1 -1
  54. package/dist/components/timeline/timeline.js.map +1 -1
  55. package/dist/components/toast/icons/ErrorIcon.js.map +1 -1
  56. package/dist/components/toast/icons/IconCircle.js.map +1 -1
  57. package/dist/components/toast/icons/InfoIcon.js.map +1 -1
  58. package/dist/components/toast/icons/LoaderIcon.js.map +1 -1
  59. package/dist/components/toast/icons/SuccessIcon.js.map +1 -1
  60. package/dist/components/toast/icons/WarningIcon.js.map +1 -1
  61. package/dist/components/toast/toast.js.map +1 -1
  62. package/dist/components/toast/toast.store.js.map +1 -1
  63. package/dist/components/tooltip/tooltip.js.map +1 -1
  64. package/dist/components/tour/tour.js.map +1 -1
  65. package/dist/components/upload/upload.js.map +1 -1
  66. package/dist/components/z-index/z-index.context.js.map +1 -1
  67. package/dist/components/z-index/z-index.js.map +1 -1
  68. package/dist/components/z-index/z-index.store.js.map +1 -1
  69. package/dist/components/z-index/z-index.types.js.map +1 -1
  70. package/dist/package.json +1 -1
  71. package/dist/skill/avatar.skill.md.txt +255 -255
  72. package/dist/skill/badge.skill.md.txt +223 -223
  73. package/dist/skill/breadcrumb.skill.md.txt +177 -177
  74. package/dist/skill/button.skill.md.txt +198 -198
  75. package/dist/skill/carousel.skill.md.txt +406 -406
  76. package/dist/skill/chat-bubble.skill.md.txt +342 -342
  77. package/dist/skill/checkbox.skill.md.txt +326 -326
  78. package/dist/skill/code-preview.skill.md.txt +240 -240
  79. package/dist/skill/collapse.skill.md.txt +329 -329
  80. package/dist/skill/context-menu.skill.md.txt +233 -233
  81. package/dist/skill/diff.skill.md.txt +244 -244
  82. package/dist/skill/divider.skill.md.txt +151 -151
  83. package/dist/skill/doc.skill.md.txt +191 -191
  84. package/dist/skill/drawer.skill.md.txt +157 -157
  85. package/dist/skill/dropdown.skill.md.txt +198 -198
  86. package/dist/skill/float-button.skill.md.txt +315 -315
  87. package/dist/skill/hover-3d-image.skill.md.txt +120 -120
  88. package/dist/skill/iframe.skill.md.txt +114 -114
  89. package/dist/skill/image-preview.skill.md.txt +162 -162
  90. package/dist/skill/indicator.skill.md.txt +60 -60
  91. package/dist/skill/input.skill.md.txt +489 -489
  92. package/dist/skill/loading.skill.md.txt +127 -127
  93. package/dist/skill/menu.skill.md.txt +476 -476
  94. package/dist/skill/modal.skill.md.txt +359 -359
  95. package/dist/skill/pagination.skill.md.txt +405 -405
  96. package/dist/skill/progress-bar.skill.md.txt +207 -207
  97. package/dist/skill/qr-code.skill.md.txt +136 -136
  98. package/dist/skill/rating.skill.md.txt +167 -167
  99. package/dist/skill/select-zone.skill.md.txt +93 -93
  100. package/dist/skill/select.skill.md.txt +663 -663
  101. package/dist/skill/skeleton.skill.md.txt +192 -192
  102. package/dist/skill/slider.skill.md.txt +404 -404
  103. package/dist/skill/splitter.skill.md.txt +411 -411
  104. package/dist/skill/steps.skill.md.txt +264 -264
  105. package/dist/skill/swap.skill.md.txt +139 -139
  106. package/dist/skill/switch.skill.md.txt +191 -191
  107. package/dist/skill/tab.skill.md.txt +484 -484
  108. package/dist/skill/table.example.header.md.txt +666 -666
  109. package/dist/skill/table.skill.md.txt +1407 -1407
  110. package/dist/skill/text-rotate.skill.md.txt +186 -186
  111. package/dist/skill/timeline.skill.md.txt +247 -247
  112. package/dist/skill/toast.skill.md.txt +531 -531
  113. package/dist/skill/tooltip.skill.md.txt +222 -222
  114. package/dist/skill/tour.skill.md.txt +156 -156
  115. package/dist/skill/upload.skill.md.txt +358 -358
  116. package/dist/utils/cn.js.map +1 -1
  117. package/dist/utils/element-tracker.js.map +1 -1
  118. package/dist/utils/helper.js.map +1 -1
  119. package/dist/utils/hoc.js.map +1 -1
  120. package/package.json +132 -133
@@ -1,406 +1,406 @@
1
- ## COMPONENT IDENTITY
2
- - **Import**: `import { Carousel } from 'solid-tom-ui';`
3
- - **Export**: `Carousel` (named export), `CarouselFunction`, `CarouselProps` (type exports)
4
- - **Framework**: SolidJS
5
- - **Purpose**: Image-only carousel component; NOT a `ParentComponent` — images passed via props
6
- - **CSS custom properties**: `--carousel-transition-duration`, `--carousel-autoplay-speed`
7
-
8
- ---
9
-
10
- ## TYPE SIGNATURE
11
-
12
- ```ts
13
- // Image item shape (from @/utils/helper)
14
- type Image = {
15
- src: string;
16
- alt?: string;
17
- class?: string; // extra class on <img> element
18
- // any other standard <img> HTML attributes
19
- };
20
-
21
- // Discriminated union on `direction`
22
- type CarouselProps = HorizontalCarouselProps | VerticalCarouselProps;
23
-
24
- type CarouselBaseProps = {
25
- images: Image[]; // REQUIRED — array of image objects
26
- class?: {
27
- root?: string; // classes on root .sui-carousel div
28
- item?: string; // classes on each .sui-carousel-item div
29
- };
30
- arrows?: boolean; // default: true
31
- autoplay?: boolean | { dotDuration?: boolean }; // default: true
32
- autoplaySpeed?: number; // ms between slides; default: 3000
33
- dots?: boolean; // default: true
34
- infinite?: boolean; // default: true
35
- effect?: 'scrollx' | 'fade'; // default: 'scrollx'
36
- afterChange?: (current: number) => void; // fires after slide changes; index is 0-based
37
- beforeChange?: (current: number, next: number) => void; // fires before transition
38
- onClickSlide?: (index: number) => void; // fires only when clicking the ACTIVE slide
39
- setCarouselFunction?: Setter<CarouselFunction | undefined>; // imperative API hook
40
- };
41
-
42
- type HorizontalCarouselProps = CarouselBaseProps & {
43
- direction?: 'horizontal'; // default when omitted
44
- dotPlacement?: 'top' | 'bottom'; // default: 'bottom'
45
- };
46
-
47
- type VerticalCarouselProps = CarouselBaseProps & {
48
- direction: 'vertical'; // must be explicit
49
- dotPlacement?: 'start' | 'end'; // default: 'end'
50
- };
51
-
52
- // Imperative API object exposed via setCarouselFunction
53
- type CarouselFunction = {
54
- next: () => void;
55
- prev: () => void;
56
- moveTo: (index: number) => void; // 0-based index
57
- pause: () => {};
58
- play: () => {};
59
- };
60
- ```
61
-
62
- ---
63
-
64
- ## DEFAULT VALUES (via `mergeProps`)
65
-
66
- | Prop | Default |
67
- |-----------------|--------------|
68
- | `arrows` | `true` |
69
- | `autoplay` | `true` |
70
- | `autoplaySpeed` | `3000` (ms) |
71
- | `dots` | `true` |
72
- | `infinite` | `true` |
73
- | `effect` | `'scrollx'` |
74
-
75
- ---
76
-
77
- ## PROP REFERENCE
78
-
79
- | Prop | Type | Required | Description |
80
- |-----------------------|---------------------------------------------|----------|------------------------------------------------------------------------------------------------|
81
- | `images` | `Image[]` | ✅ YES | Array of image objects with `src`, optional `alt`, `class` |
82
- | `direction` | `'horizontal' \| 'vertical'` | ❌ NO | Slide direction. Omit for horizontal (default). Must be `'vertical'` for vertical mode |
83
- | `effect` | `'scrollx' \| 'fade'` | ❌ NO | Transition animation between slides |
84
- | `arrows` | `boolean` | ❌ NO | Show prev/next arrow buttons |
85
- | `dots` | `boolean` | ❌ NO | Show dot indicators. Hidden when `images.length <= 1` |
86
- | `dotPlacement` | `'top'\|'bottom'` (h) / `'start'\|'end'` (v) | ❌ NO | Position of dot indicators — valid values differ by `direction` |
87
- | `autoplay` | `boolean \| { dotDuration?: boolean }` | ❌ NO | `true` = auto-advance; `{ dotDuration: true }` = also show progress bar in active dot |
88
- | `autoplaySpeed` | `number` | ❌ NO | Milliseconds per slide during autoplay |
89
- | `infinite` | `boolean` | ❌ NO | Loop from last→first and first→last. When `false`, arrows hide at boundaries |
90
- | `afterChange` | `(current: number) => void` | ❌ NO | Called after slide transition completes; receives new 0-based index |
91
- | `beforeChange` | `(current: number, next: number) => void` | ❌ NO | Called before transition; receives current and next 0-based indexes |
92
- | `onClickSlide` | `(index: number) => void` | ❌ NO | Fires only when clicking the **currently active** slide (not inactive slides) |
93
- | `setCarouselFunction` | `Setter<CarouselFunction \| undefined>` | ❌ NO | SolidJS setter to receive the imperative API object |
94
- | `class.root` | `string` | ❌ NO | Extra classes on root `<div class="sui-carousel">` — use for size, border-radius, etc. |
95
- | `class.item` | `string` | ❌ NO | Extra classes on each `.sui-carousel-item` slide wrapper div |
96
-
97
- ---
98
-
99
- ## DIRECTION × DOT PLACEMENT CONSTRAINT
100
-
101
- `dotPlacement` valid values are **constrained by `direction`** — mixing them has no effect (falls through to default):
102
-
103
- | `direction` | Valid `dotPlacement` values | Default | Visual position |
104
- |----------------|-----------------------------|--------------|------------------------|
105
- | `'horizontal'` (or omitted) | `'top'`, `'bottom'` | `'bottom'` | Above or below slides |
106
- | `'vertical'` | `'start'`, `'end'` | `'end'` | Left or right of slides |
107
-
108
- > ⚠️ Passing `dotPlacement="start"` with `direction="horizontal"` is silently ignored — defaults to `'bottom'`.
109
- > Passing `dotPlacement="top"` with `direction="vertical"` is silently ignored — defaults to `'end'`.
110
-
111
- ---
112
-
113
- ## AUTOPLAY MODES
114
-
115
- ### `autoplay={true}` — basic autoplay
116
- - Slides advance every `autoplaySpeed` ms
117
- - No progress indicator on dots
118
-
119
- ### `autoplay={false}` — no autoplay
120
- - Manual navigation only via arrows, dots, or imperative API
121
- - `play()` via imperative API has no effect
122
-
123
- ### `autoplay={{ dotDuration: true }}` — autoplay with progress bar
124
- - Same as `true` but adds animated progress fill inside the active dot
125
- - Progress bar animates from 0→100% over `autoplaySpeed` ms
126
- - Progress resets when slide changes (including manual navigation)
127
- - Progress direction: horizontal for `dots-bottom`/`dots-top`, vertical for `dots-start`/`dots-end`
128
-
129
- ---
130
-
131
- ## IMPERATIVE API — `setCarouselFunction`
132
-
133
- To control the carousel programmatically, pass a SolidJS signal setter:
134
-
135
- ```tsx
136
- import { createSignal } from 'solid-js';
137
- import { Carousel, CarouselFunction } from 'solid-tom-ui';
138
-
139
- const [carouselApi, setCarouselApi] = createSignal<CarouselFunction>();
140
-
141
- <Carousel
142
- images={images}
143
- setCarouselFunction={setCarouselApi}
144
- afterChange={current => console.log('Now on slide:', current)}
145
- />
146
-
147
- // Use the API:
148
- carouselApi()?.next();
149
- carouselApi()?.prev();
150
- carouselApi()?.moveTo(2); // 0-based index
151
- carouselApi()?.pause();
152
- carouselApi()?.play();
153
- ```
154
-
155
- ### API method details
156
- | Method | Description |
157
- |---------------|------------------------------------------------------------------------------------|
158
- | `next()` | Go to next slide; wraps to first if `infinite=true`, clamps if `infinite=false` |
159
- | `prev()` | Go to previous slide; wraps to last if `infinite=true`, clamps if `infinite=false`|
160
- | `moveTo(n)` | Jump to 0-based index `n` directly; respects `infinite` boundary logic |
161
- | `pause()` | Stops autoplay timer; sets `isPlaying=false` |
162
- | `play()` | Resumes autoplay only if `autoplay` prop is truthy; no-op if `autoplay={false}` |
163
-
164
- > ℹ️ The API object is re-created and set via `createEffect` on every render — always call through the signal accessor `carouselApi()?.method()` rather than caching the object.
165
-
166
- ---
167
-
168
- ## `onClickSlide` BEHAVIOR
169
-
170
- `onClickSlide` fires **only when clicking the currently active/visible slide**:
171
-
172
- ```tsx
173
- // Inside component:
174
- const handleSlideClick = (index: number) => {
175
- if (index === currentIndex()) {
176
- p.onClickSlide?.(index); // only fires for the active slide
177
- }
178
- };
179
- ```
180
-
181
- Clicking an inactive slide (one that is positioned off-screen) triggers no callback. This is intentional — only the visible slide is interactive.
182
-
183
- ---
184
-
185
- ## `infinite` — ARROW VISIBILITY BEHAVIOR
186
-
187
- When `infinite={false}`:
188
- - `showPrevArrow` → `currentIndex() > 0` — hides prev arrow on first slide
189
- - `showNextArrow` → `currentIndex() < totalSlides() - 1` — hides next arrow on last slide
190
-
191
- When `infinite={true}`:
192
- - Both arrows always shown (when `arrows={true}`)
193
-
194
- ---
195
-
196
- ## ANIMATION — INTERNAL MECHANISM
197
-
198
- ### `effect="scrollx"` (default)
199
- - Uses pixel-based `translateX` / `translateY` (vertical) CSS transforms
200
- - 1px overlap on slide positioning to eliminate sub-pixel gaps
201
- - Detects wrap-around (first↔last) and reverses direction for natural wrap animation
202
- - Transition duration: hardcoded `400ms` via `--carousel-transition-duration` CSS var
203
-
204
- ### `effect="fade"`
205
- - Uses `opacity` CSS transitions between slides
206
- - All slides stacked absolutely; `z-index` swaps between from/to slides
207
- - Same 400ms duration
208
-
209
- ### Animation guard (`isAnimating`)
210
- - While a transition is running, `goToSlide` calls are ignored (except `skipAnimation=true`)
211
- - Autoplay skips ticks while animating
212
-
213
- ---
214
-
215
- ## INTERNAL DOM STRUCTURE
216
-
217
- ```
218
- <div class="sui-carousel [vertical] [dots-bottom|dots-top|dots-start|dots-end] [class.root]"
219
- style="--carousel-transition-duration:400ms; --carousel-autoplay-speed:{autoplaySpeed}ms">
220
-
221
- ├── <div class="sui-carousel-slides">
222
- │ <For each={images}>
223
- │ <div class="sui-carousel-item [class.item]" onClick={handleSlideClick}>
224
- │ <img src={...} alt={...} class="sui-carousel-image [image.class]" draggable={false} />
225
- │ </div>
226
- │ </For>
227
- │ </div>
228
-
229
- ├── <Show when={arrows}>
230
- │ <Show when={showPrevArrow()}>
231
- │ <button class="sui-carousel-arrow arrow-prev" aria-label="Previous slide">
232
- │ <DynamicIcon name="chevron-left" />
233
- │ </button>
234
- │ </Show>
235
- │ <Show when={showNextArrow()}>
236
- │ <button class="sui-carousel-arrow arrow-next" aria-label="Next slide">
237
- │ <DynamicIcon name="chevron-right" />
238
- │ </button>
239
- │ </Show>
240
- │ </Show>
241
-
242
- └── <Show when={dots && totalSlides() > 1}>
243
- <div class="sui-carousel-dots">
244
- <For each={images}>
245
- <button class="sui-carousel-dot [active] [has-progress]"
246
- aria-label="Go to slide {n}">
247
- <Show when={hasDotDuration()}>
248
- <div class="dot-progress" />
249
- </Show>
250
- </button>
251
- </For>
252
- </div>
253
- </Show>
254
- ```
255
-
256
- ---
257
-
258
- ## SIZING — ALWAYS REQUIRED
259
-
260
- The component has `width: 100%` and fills its container's height. **Always set explicit height** on `class.root`:
261
-
262
- ```tsx
263
- // ✅ Correct — explicit height
264
- <Carousel class={{ root: 'h-[400px] rounded-xl' }} images={images} />
265
-
266
- // ✅ Fixed width + height
267
- <Carousel class={{ root: 'h-[150px] w-[200px] rounded-xl' }} images={images} />
268
-
269
- // ❌ Wrong — no height = carousel collapses to 0px
270
- <Carousel images={images} />
271
- ```
272
-
273
- ---
274
-
275
- ## BEHAVIOR NOTES FOR AGENTS
276
-
277
- 1. **`images` items only render `<img>` tags** — the component does not support arbitrary JSX slides. Only image-based slides via the `Image` type are supported.
278
-
279
- 2. **`direction` changes arrow orientation** — in `'vertical'` mode, arrows move to top/bottom center (rotated 90°) instead of left/right. This is CSS-driven automatically.
280
-
281
- 3. **`autoplaySpeed` is used for both the timer interval AND the CSS progress animation** — it is set as `--carousel-autoplay-speed` CSS custom property. Changing it reactively will update both simultaneously.
282
-
283
- 4. **Dots are hidden when `images.length <= 1`** — `<Show when={p.dots && totalSlides() > 1}>` — single-image carousels never show dots regardless of `dots` prop.
284
-
285
- 5. **`setCarouselFunction` setter is called inside `createEffect`** — the API object is always fresh. Do not cache the returned object; always call via `carouselApi()?.method()`.
286
-
287
- 6. **`onClickSlide` only fires on the active slide** — clicking a slide that is not `currentIndex()` does nothing, even if partially visible during transition.
288
-
289
- 7. **Transition is blocked during animation** — `isAnimating()` guard prevents overlapping transitions. Rapid clicking or autoplay ticks during a 400ms transition are silently ignored.
290
-
291
- 8. **Autoplay resets on manual navigation** — calling `next()`, `prev()`, `moveTo()`, or clicking arrows/dots clears and restarts the autoplay timer, preventing near-simultaneous auto+manual transitions.
292
-
293
- 9. **`play()` is a no-op when `autoplay={false}`** — the prop check `if (!p.autoplay) return {}` exits immediately. `pause()`/`play()` only work when `autoplay` was originally truthy.
294
-
295
- ---
296
-
297
- ## COMMON MISTAKES TO AVOID
298
-
299
- | Mistake | Correct approach |
300
- |---|---|
301
- | Omitting height on `class.root` | Always set `h-[Npx]` or similar — carousel collapses without explicit height |
302
- | Using `dotPlacement="start"` with horizontal direction | `start`/`end` are only for `direction="vertical"` — use `top`/`bottom` for horizontal |
303
- | Using `dotPlacement="top"` with `direction="vertical"` | `top`/`bottom` are only for horizontal — use `start`/`end` for vertical |
304
- | Calling `carouselApi()?.play()` when `autoplay={false}` | `play()` is a no-op when `autoplay` prop is false |
305
- | Caching `carouselApi()` object directly | Always call via signal accessor — the API object is recreated each render |
306
- | Expecting `onClickSlide` for any slide click | Only fires for the currently active/visible slide |
307
- | Passing JSX as image slides | `images` accepts `Image[]` only — `src`, `alt`, `class` — no arbitrary JSX |
308
- | Expecting dots with single image | Dots are hidden automatically when `images.length <= 1` |
309
-
310
- ---
311
-
312
- ## FULL EXAMPLE — Common configurations
313
-
314
- ```tsx
315
- import { Carousel, CarouselFunction } from 'solid-tom-ui';
316
- import { createSignal } from 'solid-js';
317
-
318
- const images = [
319
- { src: '/slides/1.webp', alt: 'Slide 1' },
320
- { src: '/slides/2.webp', alt: 'Slide 2' },
321
- { src: '/slides/3.webp', alt: 'Slide 3' },
322
- { src: '/slides/4.webp', alt: 'Slide 4' },
323
- ];
324
-
325
- // 1. Basic with autoplay + progress dots
326
- <Carousel
327
- class={{ root: 'h-[400px] rounded-xl' }}
328
- images={images}
329
- autoplay={{ dotDuration: true }}
330
- autoplaySpeed={4000}
331
- />
332
-
333
- // 2. Fade effect, no autoplay
334
- <Carousel
335
- class={{ root: 'h-[400px] rounded-xl' }}
336
- images={images}
337
- effect="fade"
338
- autoplay={false}
339
- dots={true}
340
- arrows={true}
341
- />
342
-
343
- // 3. Vertical direction, dots on end (right)
344
- <Carousel
345
- class={{ root: 'h-[350px] rounded-xl' }}
346
- images={images}
347
- direction="vertical"
348
- autoplay={{ dotDuration: true }}
349
- dotPlacement="end"
350
- />
351
-
352
- // 4. Non-infinite (arrows hide at boundaries)
353
- <Carousel
354
- class={{ root: 'h-[400px] rounded-xl' }}
355
- images={images}
356
- autoplay={false}
357
- infinite={false}
358
- />
359
-
360
- // 5. Imperative API control
361
- const [api, setApi] = createSignal<CarouselFunction>();
362
- const [slide, setSlide] = createSignal(0);
363
-
364
- <div class="flex gap-2 mb-2">
365
- <button onClick={() => api()?.prev()}>Prev</button>
366
- <button onClick={() => api()?.next()}>Next</button>
367
- <button onClick={() => api()?.moveTo(0)}>First</button>
368
- <button onClick={() => api()?.pause()}>Pause</button>
369
- <button onClick={() => api()?.play()}>Play</button>
370
- </div>
371
- <p>Slide: {slide() + 1} / {images.length}</p>
372
- <Carousel
373
- class={{ root: 'h-[400px] rounded-xl' }}
374
- images={images}
375
- autoplay={{ dotDuration: true }}
376
- setCarouselFunction={setApi}
377
- afterChange={i => setSlide(i)}
378
- beforeChange={(cur, next) => console.log(cur, '->', next)}
379
- onClickSlide={i => console.log('clicked slide', i)}
380
- />
381
-
382
- // 6. Dots only, no arrows
383
- <Carousel
384
- class={{ root: 'h-[300px] rounded-xl' }}
385
- images={images}
386
- arrows={false}
387
- dots={true}
388
- autoplay={{ dotDuration: true }}
389
- />
390
-
391
- // 7. Arrows only, no dots
392
- <Carousel
393
- class={{ root: 'h-[300px] rounded-xl' }}
394
- images={images}
395
- arrows={true}
396
- dots={false}
397
- autoplay={true}
398
- />
399
- ```
400
- ---
401
-
402
- ## Component Conventions
403
-
404
- > **CSS encoding**: internal CSS classes use short encoded names (e.g. `cr01`, `cr02`) per project convention.
405
-
406
- > **Unique IDs**: if this component needs to generate HTML `id` attributes, always use `createUniqueId()` from `solid-js` — never `Math.random()` or `Date.now()`.
1
+ ## COMPONENT IDENTITY
2
+ - **Import**: `import { Carousel } from 'solid-tom-ui';`
3
+ - **Export**: `Carousel` (named export), `CarouselFunction`, `CarouselProps` (type exports)
4
+ - **Framework**: SolidJS
5
+ - **Purpose**: Image-only carousel component; NOT a `ParentComponent` — images passed via props
6
+ - **CSS custom properties**: `--carousel-transition-duration`, `--carousel-autoplay-speed`
7
+
8
+ ---
9
+
10
+ ## TYPE SIGNATURE
11
+
12
+ ```ts
13
+ // Image item shape (from @/utils/helper)
14
+ type Image = {
15
+ src: string;
16
+ alt?: string;
17
+ class?: string; // extra class on <img> element
18
+ // any other standard <img> HTML attributes
19
+ };
20
+
21
+ // Discriminated union on `direction`
22
+ type CarouselProps = HorizontalCarouselProps | VerticalCarouselProps;
23
+
24
+ type CarouselBaseProps = {
25
+ images: Image[]; // REQUIRED — array of image objects
26
+ class?: {
27
+ root?: string; // classes on root .sui-carousel div
28
+ item?: string; // classes on each .sui-carousel-item div
29
+ };
30
+ arrows?: boolean; // default: true
31
+ autoplay?: boolean | { dotDuration?: boolean }; // default: true
32
+ autoplaySpeed?: number; // ms between slides; default: 3000
33
+ dots?: boolean; // default: true
34
+ infinite?: boolean; // default: true
35
+ effect?: 'scrollx' | 'fade'; // default: 'scrollx'
36
+ afterChange?: (current: number) => void; // fires after slide changes; index is 0-based
37
+ beforeChange?: (current: number, next: number) => void; // fires before transition
38
+ onClickSlide?: (index: number) => void; // fires only when clicking the ACTIVE slide
39
+ setCarouselFunction?: Setter<CarouselFunction | undefined>; // imperative API hook
40
+ };
41
+
42
+ type HorizontalCarouselProps = CarouselBaseProps & {
43
+ direction?: 'horizontal'; // default when omitted
44
+ dotPlacement?: 'top' | 'bottom'; // default: 'bottom'
45
+ };
46
+
47
+ type VerticalCarouselProps = CarouselBaseProps & {
48
+ direction: 'vertical'; // must be explicit
49
+ dotPlacement?: 'start' | 'end'; // default: 'end'
50
+ };
51
+
52
+ // Imperative API object exposed via setCarouselFunction
53
+ type CarouselFunction = {
54
+ next: () => void;
55
+ prev: () => void;
56
+ moveTo: (index: number) => void; // 0-based index
57
+ pause: () => {};
58
+ play: () => {};
59
+ };
60
+ ```
61
+
62
+ ---
63
+
64
+ ## DEFAULT VALUES (via `mergeProps`)
65
+
66
+ | Prop | Default |
67
+ |-----------------|--------------|
68
+ | `arrows` | `true` |
69
+ | `autoplay` | `true` |
70
+ | `autoplaySpeed` | `3000` (ms) |
71
+ | `dots` | `true` |
72
+ | `infinite` | `true` |
73
+ | `effect` | `'scrollx'` |
74
+
75
+ ---
76
+
77
+ ## PROP REFERENCE
78
+
79
+ | Prop | Type | Required | Description |
80
+ |-----------------------|---------------------------------------------|----------|------------------------------------------------------------------------------------------------|
81
+ | `images` | `Image[]` | ✅ YES | Array of image objects with `src`, optional `alt`, `class` |
82
+ | `direction` | `'horizontal' \| 'vertical'` | ❌ NO | Slide direction. Omit for horizontal (default). Must be `'vertical'` for vertical mode |
83
+ | `effect` | `'scrollx' \| 'fade'` | ❌ NO | Transition animation between slides |
84
+ | `arrows` | `boolean` | ❌ NO | Show prev/next arrow buttons |
85
+ | `dots` | `boolean` | ❌ NO | Show dot indicators. Hidden when `images.length <= 1` |
86
+ | `dotPlacement` | `'top'\|'bottom'` (h) / `'start'\|'end'` (v) | ❌ NO | Position of dot indicators — valid values differ by `direction` |
87
+ | `autoplay` | `boolean \| { dotDuration?: boolean }` | ❌ NO | `true` = auto-advance; `{ dotDuration: true }` = also show progress bar in active dot |
88
+ | `autoplaySpeed` | `number` | ❌ NO | Milliseconds per slide during autoplay |
89
+ | `infinite` | `boolean` | ❌ NO | Loop from last→first and first→last. When `false`, arrows hide at boundaries |
90
+ | `afterChange` | `(current: number) => void` | ❌ NO | Called after slide transition completes; receives new 0-based index |
91
+ | `beforeChange` | `(current: number, next: number) => void` | ❌ NO | Called before transition; receives current and next 0-based indexes |
92
+ | `onClickSlide` | `(index: number) => void` | ❌ NO | Fires only when clicking the **currently active** slide (not inactive slides) |
93
+ | `setCarouselFunction` | `Setter<CarouselFunction \| undefined>` | ❌ NO | SolidJS setter to receive the imperative API object |
94
+ | `class.root` | `string` | ❌ NO | Extra classes on root `<div class="sui-carousel">` — use for size, border-radius, etc. |
95
+ | `class.item` | `string` | ❌ NO | Extra classes on each `.sui-carousel-item` slide wrapper div |
96
+
97
+ ---
98
+
99
+ ## DIRECTION × DOT PLACEMENT CONSTRAINT
100
+
101
+ `dotPlacement` valid values are **constrained by `direction`** — mixing them has no effect (falls through to default):
102
+
103
+ | `direction` | Valid `dotPlacement` values | Default | Visual position |
104
+ |----------------|-----------------------------|--------------|------------------------|
105
+ | `'horizontal'` (or omitted) | `'top'`, `'bottom'` | `'bottom'` | Above or below slides |
106
+ | `'vertical'` | `'start'`, `'end'` | `'end'` | Left or right of slides |
107
+
108
+ > ⚠️ Passing `dotPlacement="start"` with `direction="horizontal"` is silently ignored — defaults to `'bottom'`.
109
+ > Passing `dotPlacement="top"` with `direction="vertical"` is silently ignored — defaults to `'end'`.
110
+
111
+ ---
112
+
113
+ ## AUTOPLAY MODES
114
+
115
+ ### `autoplay={true}` — basic autoplay
116
+ - Slides advance every `autoplaySpeed` ms
117
+ - No progress indicator on dots
118
+
119
+ ### `autoplay={false}` — no autoplay
120
+ - Manual navigation only via arrows, dots, or imperative API
121
+ - `play()` via imperative API has no effect
122
+
123
+ ### `autoplay={{ dotDuration: true }}` — autoplay with progress bar
124
+ - Same as `true` but adds animated progress fill inside the active dot
125
+ - Progress bar animates from 0→100% over `autoplaySpeed` ms
126
+ - Progress resets when slide changes (including manual navigation)
127
+ - Progress direction: horizontal for `dots-bottom`/`dots-top`, vertical for `dots-start`/`dots-end`
128
+
129
+ ---
130
+
131
+ ## IMPERATIVE API — `setCarouselFunction`
132
+
133
+ To control the carousel programmatically, pass a SolidJS signal setter:
134
+
135
+ ```tsx
136
+ import { createSignal } from 'solid-js';
137
+ import { Carousel, CarouselFunction } from 'solid-tom-ui';
138
+
139
+ const [carouselApi, setCarouselApi] = createSignal<CarouselFunction>();
140
+
141
+ <Carousel
142
+ images={images}
143
+ setCarouselFunction={setCarouselApi}
144
+ afterChange={current => console.log('Now on slide:', current)}
145
+ />
146
+
147
+ // Use the API:
148
+ carouselApi()?.next();
149
+ carouselApi()?.prev();
150
+ carouselApi()?.moveTo(2); // 0-based index
151
+ carouselApi()?.pause();
152
+ carouselApi()?.play();
153
+ ```
154
+
155
+ ### API method details
156
+ | Method | Description |
157
+ |---------------|------------------------------------------------------------------------------------|
158
+ | `next()` | Go to next slide; wraps to first if `infinite=true`, clamps if `infinite=false` |
159
+ | `prev()` | Go to previous slide; wraps to last if `infinite=true`, clamps if `infinite=false`|
160
+ | `moveTo(n)` | Jump to 0-based index `n` directly; respects `infinite` boundary logic |
161
+ | `pause()` | Stops autoplay timer; sets `isPlaying=false` |
162
+ | `play()` | Resumes autoplay only if `autoplay` prop is truthy; no-op if `autoplay={false}` |
163
+
164
+ > ℹ️ The API object is re-created and set via `createEffect` on every render — always call through the signal accessor `carouselApi()?.method()` rather than caching the object.
165
+
166
+ ---
167
+
168
+ ## `onClickSlide` BEHAVIOR
169
+
170
+ `onClickSlide` fires **only when clicking the currently active/visible slide**:
171
+
172
+ ```tsx
173
+ // Inside component:
174
+ const handleSlideClick = (index: number) => {
175
+ if (index === currentIndex()) {
176
+ p.onClickSlide?.(index); // only fires for the active slide
177
+ }
178
+ };
179
+ ```
180
+
181
+ Clicking an inactive slide (one that is positioned off-screen) triggers no callback. This is intentional — only the visible slide is interactive.
182
+
183
+ ---
184
+
185
+ ## `infinite` — ARROW VISIBILITY BEHAVIOR
186
+
187
+ When `infinite={false}`:
188
+ - `showPrevArrow` → `currentIndex() > 0` — hides prev arrow on first slide
189
+ - `showNextArrow` → `currentIndex() < totalSlides() - 1` — hides next arrow on last slide
190
+
191
+ When `infinite={true}`:
192
+ - Both arrows always shown (when `arrows={true}`)
193
+
194
+ ---
195
+
196
+ ## ANIMATION — INTERNAL MECHANISM
197
+
198
+ ### `effect="scrollx"` (default)
199
+ - Uses pixel-based `translateX` / `translateY` (vertical) CSS transforms
200
+ - 1px overlap on slide positioning to eliminate sub-pixel gaps
201
+ - Detects wrap-around (first↔last) and reverses direction for natural wrap animation
202
+ - Transition duration: hardcoded `400ms` via `--carousel-transition-duration` CSS var
203
+
204
+ ### `effect="fade"`
205
+ - Uses `opacity` CSS transitions between slides
206
+ - All slides stacked absolutely; `z-index` swaps between from/to slides
207
+ - Same 400ms duration
208
+
209
+ ### Animation guard (`isAnimating`)
210
+ - While a transition is running, `goToSlide` calls are ignored (except `skipAnimation=true`)
211
+ - Autoplay skips ticks while animating
212
+
213
+ ---
214
+
215
+ ## INTERNAL DOM STRUCTURE
216
+
217
+ ```
218
+ <div class="sui-carousel [vertical] [dots-bottom|dots-top|dots-start|dots-end] [class.root]"
219
+ style="--carousel-transition-duration:400ms; --carousel-autoplay-speed:{autoplaySpeed}ms">
220
+
221
+ ├── <div class="sui-carousel-slides">
222
+ │ <For each={images}>
223
+ │ <div class="sui-carousel-item [class.item]" onClick={handleSlideClick}>
224
+ │ <img src={...} alt={...} class="sui-carousel-image [image.class]" draggable={false} />
225
+ │ </div>
226
+ │ </For>
227
+ │ </div>
228
+
229
+ ├── <Show when={arrows}>
230
+ │ <Show when={showPrevArrow()}>
231
+ │ <button class="sui-carousel-arrow arrow-prev" aria-label="Previous slide">
232
+ │ <DynamicIcon name="chevron-left" />
233
+ │ </button>
234
+ │ </Show>
235
+ │ <Show when={showNextArrow()}>
236
+ │ <button class="sui-carousel-arrow arrow-next" aria-label="Next slide">
237
+ │ <DynamicIcon name="chevron-right" />
238
+ │ </button>
239
+ │ </Show>
240
+ │ </Show>
241
+
242
+ └── <Show when={dots && totalSlides() > 1}>
243
+ <div class="sui-carousel-dots">
244
+ <For each={images}>
245
+ <button class="sui-carousel-dot [active] [has-progress]"
246
+ aria-label="Go to slide {n}">
247
+ <Show when={hasDotDuration()}>
248
+ <div class="dot-progress" />
249
+ </Show>
250
+ </button>
251
+ </For>
252
+ </div>
253
+ </Show>
254
+ ```
255
+
256
+ ---
257
+
258
+ ## SIZING — ALWAYS REQUIRED
259
+
260
+ The component has `width: 100%` and fills its container's height. **Always set explicit height** on `class.root`:
261
+
262
+ ```tsx
263
+ // ✅ Correct — explicit height
264
+ <Carousel class={{ root: 'h-[400px] rounded-xl' }} images={images} />
265
+
266
+ // ✅ Fixed width + height
267
+ <Carousel class={{ root: 'h-[150px] w-[200px] rounded-xl' }} images={images} />
268
+
269
+ // ❌ Wrong — no height = carousel collapses to 0px
270
+ <Carousel images={images} />
271
+ ```
272
+
273
+ ---
274
+
275
+ ## BEHAVIOR NOTES FOR AGENTS
276
+
277
+ 1. **`images` items only render `<img>` tags** — the component does not support arbitrary JSX slides. Only image-based slides via the `Image` type are supported.
278
+
279
+ 2. **`direction` changes arrow orientation** — in `'vertical'` mode, arrows move to top/bottom center (rotated 90°) instead of left/right. This is CSS-driven automatically.
280
+
281
+ 3. **`autoplaySpeed` is used for both the timer interval AND the CSS progress animation** — it is set as `--carousel-autoplay-speed` CSS custom property. Changing it reactively will update both simultaneously.
282
+
283
+ 4. **Dots are hidden when `images.length <= 1`** — `<Show when={p.dots && totalSlides() > 1}>` — single-image carousels never show dots regardless of `dots` prop.
284
+
285
+ 5. **`setCarouselFunction` setter is called inside `createEffect`** — the API object is always fresh. Do not cache the returned object; always call via `carouselApi()?.method()`.
286
+
287
+ 6. **`onClickSlide` only fires on the active slide** — clicking a slide that is not `currentIndex()` does nothing, even if partially visible during transition.
288
+
289
+ 7. **Transition is blocked during animation** — `isAnimating()` guard prevents overlapping transitions. Rapid clicking or autoplay ticks during a 400ms transition are silently ignored.
290
+
291
+ 8. **Autoplay resets on manual navigation** — calling `next()`, `prev()`, `moveTo()`, or clicking arrows/dots clears and restarts the autoplay timer, preventing near-simultaneous auto+manual transitions.
292
+
293
+ 9. **`play()` is a no-op when `autoplay={false}`** — the prop check `if (!p.autoplay) return {}` exits immediately. `pause()`/`play()` only work when `autoplay` was originally truthy.
294
+
295
+ ---
296
+
297
+ ## COMMON MISTAKES TO AVOID
298
+
299
+ | Mistake | Correct approach |
300
+ |---|---|
301
+ | Omitting height on `class.root` | Always set `h-[Npx]` or similar — carousel collapses without explicit height |
302
+ | Using `dotPlacement="start"` with horizontal direction | `start`/`end` are only for `direction="vertical"` — use `top`/`bottom` for horizontal |
303
+ | Using `dotPlacement="top"` with `direction="vertical"` | `top`/`bottom` are only for horizontal — use `start`/`end` for vertical |
304
+ | Calling `carouselApi()?.play()` when `autoplay={false}` | `play()` is a no-op when `autoplay` prop is false |
305
+ | Caching `carouselApi()` object directly | Always call via signal accessor — the API object is recreated each render |
306
+ | Expecting `onClickSlide` for any slide click | Only fires for the currently active/visible slide |
307
+ | Passing JSX as image slides | `images` accepts `Image[]` only — `src`, `alt`, `class` — no arbitrary JSX |
308
+ | Expecting dots with single image | Dots are hidden automatically when `images.length <= 1` |
309
+
310
+ ---
311
+
312
+ ## FULL EXAMPLE — Common configurations
313
+
314
+ ```tsx
315
+ import { Carousel, CarouselFunction } from 'solid-tom-ui';
316
+ import { createSignal } from 'solid-js';
317
+
318
+ const images = [
319
+ { src: '/slides/1.webp', alt: 'Slide 1' },
320
+ { src: '/slides/2.webp', alt: 'Slide 2' },
321
+ { src: '/slides/3.webp', alt: 'Slide 3' },
322
+ { src: '/slides/4.webp', alt: 'Slide 4' },
323
+ ];
324
+
325
+ // 1. Basic with autoplay + progress dots
326
+ <Carousel
327
+ class={{ root: 'h-[400px] rounded-xl' }}
328
+ images={images}
329
+ autoplay={{ dotDuration: true }}
330
+ autoplaySpeed={4000}
331
+ />
332
+
333
+ // 2. Fade effect, no autoplay
334
+ <Carousel
335
+ class={{ root: 'h-[400px] rounded-xl' }}
336
+ images={images}
337
+ effect="fade"
338
+ autoplay={false}
339
+ dots={true}
340
+ arrows={true}
341
+ />
342
+
343
+ // 3. Vertical direction, dots on end (right)
344
+ <Carousel
345
+ class={{ root: 'h-[350px] rounded-xl' }}
346
+ images={images}
347
+ direction="vertical"
348
+ autoplay={{ dotDuration: true }}
349
+ dotPlacement="end"
350
+ />
351
+
352
+ // 4. Non-infinite (arrows hide at boundaries)
353
+ <Carousel
354
+ class={{ root: 'h-[400px] rounded-xl' }}
355
+ images={images}
356
+ autoplay={false}
357
+ infinite={false}
358
+ />
359
+
360
+ // 5. Imperative API control
361
+ const [api, setApi] = createSignal<CarouselFunction>();
362
+ const [slide, setSlide] = createSignal(0);
363
+
364
+ <div class="flex gap-2 mb-2">
365
+ <button onClick={() => api()?.prev()}>Prev</button>
366
+ <button onClick={() => api()?.next()}>Next</button>
367
+ <button onClick={() => api()?.moveTo(0)}>First</button>
368
+ <button onClick={() => api()?.pause()}>Pause</button>
369
+ <button onClick={() => api()?.play()}>Play</button>
370
+ </div>
371
+ <p>Slide: {slide() + 1} / {images.length}</p>
372
+ <Carousel
373
+ class={{ root: 'h-[400px] rounded-xl' }}
374
+ images={images}
375
+ autoplay={{ dotDuration: true }}
376
+ setCarouselFunction={setApi}
377
+ afterChange={i => setSlide(i)}
378
+ beforeChange={(cur, next) => console.log(cur, '->', next)}
379
+ onClickSlide={i => console.log('clicked slide', i)}
380
+ />
381
+
382
+ // 6. Dots only, no arrows
383
+ <Carousel
384
+ class={{ root: 'h-[300px] rounded-xl' }}
385
+ images={images}
386
+ arrows={false}
387
+ dots={true}
388
+ autoplay={{ dotDuration: true }}
389
+ />
390
+
391
+ // 7. Arrows only, no dots
392
+ <Carousel
393
+ class={{ root: 'h-[300px] rounded-xl' }}
394
+ images={images}
395
+ arrows={true}
396
+ dots={false}
397
+ autoplay={true}
398
+ />
399
+ ```
400
+ ---
401
+
402
+ ## Component Conventions
403
+
404
+ > **CSS encoding**: internal CSS classes use short encoded names (e.g. `cr01`, `cr02`) per project convention.
405
+
406
+ > **Unique IDs**: if this component needs to generate HTML `id` attributes, always use `createUniqueId()` from `solid-js` — never `Math.random()` or `Date.now()`.