@marianmeres/stuic 3.35.0 → 3.36.0
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/AGENTS.md +1 -1
- package/API.md +222 -5
- package/README.md +1 -1
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/onboarding/OnboardingShell.svelte +71 -0
- package/dist/actions/onboarding/OnboardingShell.svelte.d.ts +19 -0
- package/dist/actions/onboarding/index.css +93 -0
- package/dist/actions/onboarding/onboarding.svelte.d.ts +138 -0
- package/dist/actions/onboarding/onboarding.svelte.js +247 -0
- package/dist/index.css +1 -0
- package/docs/domains/components.md +5 -3
- package/docs/domains/utils.md +10 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
src/lib/
|
|
26
26
|
├── components/ # 45 UI components
|
|
27
27
|
├── actions/ # 14 Svelte actions
|
|
28
|
-
├── utils/ #
|
|
28
|
+
├── utils/ # 43 utility modules
|
|
29
29
|
├── themes/ # 29 theme definitions (.ts) + generated CSS (css/)
|
|
30
30
|
├── icons/ # Icon re-exports
|
|
31
31
|
├── index.css # Centralized CSS imports
|
package/API.md
CHANGED
|
@@ -76,7 +76,18 @@ Overlay container with backdrop. Controlled programmatically.
|
|
|
76
76
|
|
|
77
77
|
#### `ModalDialog`
|
|
78
78
|
|
|
79
|
-
Pre-styled modal dialog with title and action buttons.
|
|
79
|
+
Pre-styled modal dialog with title and action buttons. Uses native `<dialog>` element with focus trap and backdrop.
|
|
80
|
+
|
|
81
|
+
| Prop | Type | Default | Description |
|
|
82
|
+
| ------------------ | ---------- | ----------- | ----------------------------------------------------- |
|
|
83
|
+
| `classDialog` | `string` | `undefined` | CSS class for the dialog element |
|
|
84
|
+
| `noClickOutsideClose` | `boolean` | `false` | Disable close on outside click |
|
|
85
|
+
| `noEscapeClose` | `boolean` | `false` | Disable close on Escape key |
|
|
86
|
+
| `noScrollLock` | `boolean` | `false` | Disable body scroll lock when open |
|
|
87
|
+
| `preEscapeClose` | `() => any` | — | Pre-close hook for Escape. Return false to prevent. |
|
|
88
|
+
| `preClose` | `() => any` | — | Pre-close hook. Return false to prevent. |
|
|
89
|
+
|
|
90
|
+
**Methods:** `open(openerOrEvent?)`, `close()`
|
|
80
91
|
|
|
81
92
|
#### `Drawer`
|
|
82
93
|
|
|
@@ -714,7 +725,46 @@ Keyboard shortcut display.
|
|
|
714
725
|
|
|
715
726
|
#### `Carousel`
|
|
716
727
|
|
|
717
|
-
Image/content slider with navigation.
|
|
728
|
+
Image/content slider with scroll snap, keyboard navigation, wheel scroll, optional arrows, and active item tracking via IntersectionObserver.
|
|
729
|
+
|
|
730
|
+
| Prop | Type | Default | Description |
|
|
731
|
+
| ------------------- | ------------------------------------------------- | ----------- | -------------------------------------------------- |
|
|
732
|
+
| `items` | `CarouselItem[]` | required | Array of carousel items |
|
|
733
|
+
| `itemsPerView` | `number` | `1` | Number of items visible per view |
|
|
734
|
+
| `peekPercent` | `number` | `0` | Percentage of next item to show as peek (0-50) |
|
|
735
|
+
| `gap` | `number \| string` | `undefined` | Gap between items |
|
|
736
|
+
| `trackActive` | `boolean` | `false` | Enable active item tracking |
|
|
737
|
+
| `syncActiveOnScroll`| `boolean` | `false` | Sync active item based on scroll position |
|
|
738
|
+
| `activeIndex` | `number` | `0` | Currently active item index (bindable) |
|
|
739
|
+
| `value` | `string \| number` | `undefined` | Currently active item ID (bindable) |
|
|
740
|
+
| `snap` | `boolean` | `true` | Enable scroll snap behavior |
|
|
741
|
+
| `snapAlign` | `"start" \| "center" \| "end"` | `"start"` | Snap alignment |
|
|
742
|
+
| `keyboard` | `boolean` | `true` | Enable keyboard navigation (arrows, Home, End) |
|
|
743
|
+
| `loop` | `boolean` | `false` | Allow cycling from last to first and vice versa |
|
|
744
|
+
| `scrollBehavior` | `ScrollBehavior` | `"smooth"` | Scroll behavior for programmatic navigation |
|
|
745
|
+
| `scrollbar` | `boolean` | `true` | Show scrollbar on hover |
|
|
746
|
+
| `wheelScroll` | `boolean` | `true` | Enable horizontal scrolling via mouse wheel |
|
|
747
|
+
| `arrows` | `boolean` | `false` | Show prev/next arrow buttons |
|
|
748
|
+
| `minItemWidth` | `number` | `undefined` | Minimum item width (px) for auto-fit |
|
|
749
|
+
| `onActiveChange` | `(item: CarouselItem, index: number) => void` | — | Callback when active item changes |
|
|
750
|
+
| `renderItem` | `Snippet<[{ item, index, active }]>` | — | Custom render snippet for items |
|
|
751
|
+
|
|
752
|
+
**Methods:** `goTo(index)`, `goToId(id)`, `next()`, `previous()`
|
|
753
|
+
|
|
754
|
+
```svelte
|
|
755
|
+
<Carousel
|
|
756
|
+
items={slides}
|
|
757
|
+
itemsPerView={3}
|
|
758
|
+
gap={16}
|
|
759
|
+
arrows
|
|
760
|
+
trackActive
|
|
761
|
+
syncActiveOnScroll
|
|
762
|
+
>
|
|
763
|
+
{#snippet renderItem({ item, index, active })}
|
|
764
|
+
<img src={item.data.src} alt={item.data.alt} />
|
|
765
|
+
{/snippet}
|
|
766
|
+
</Carousel>
|
|
767
|
+
```
|
|
718
768
|
|
|
719
769
|
#### `AnimatedElipsis`
|
|
720
770
|
|
|
@@ -789,7 +839,65 @@ Theme color swatch preview.
|
|
|
789
839
|
|
|
790
840
|
#### `Book`
|
|
791
841
|
|
|
792
|
-
Interactive book/flipbook reader with 3D CSS page flip animation, zoom, pan, swipe, and responsive single-page mode.
|
|
842
|
+
Interactive book/flipbook reader with 3D CSS page flip animation, zoom, pan, swipe, clickable areas, and responsive single-page mode.
|
|
843
|
+
|
|
844
|
+
| Prop | Type | Default | Description |
|
|
845
|
+
| ---------------- | ------------------------------------------------------------- | -------------- | --------------------------------------------- |
|
|
846
|
+
| `pages` | `BookPage[]` | required | Ordered array of book pages |
|
|
847
|
+
| `baseUrl` | `string` | `undefined` | Fallback base URL for relative page src |
|
|
848
|
+
| `activeSpread` | `number` | `0` | Currently active spread index (bindable) |
|
|
849
|
+
| `keyboard` | `boolean` | `true` | Enable keyboard navigation |
|
|
850
|
+
| `swipe` | `boolean` | `true` | Enable swipe gesture navigation |
|
|
851
|
+
| `duration` | `number` | `500` | Flip animation duration in ms |
|
|
852
|
+
| `zoom` | `boolean` | `true` | Enable zoom capability |
|
|
853
|
+
| `zoomLevels` | `readonly number[]` | `[1,1.5,2,3]` | Discrete zoom levels |
|
|
854
|
+
| `clampPan` | `boolean` | `false` | Clamp panning within bounds |
|
|
855
|
+
| `singlePage` | `boolean` | `false` | Force single-page layout |
|
|
856
|
+
| `responsive` | `boolean` | `true` | Auto-switch to single-page when narrow |
|
|
857
|
+
| `onSpreadChange` | `(spread: BookSpread, index: number) => void` | — | Callback when active spread changes |
|
|
858
|
+
| `onPageClick` | `(data: { page: BookPage; x: number; y: number }) => void` | — | Callback on page click (coordinates 0–1) |
|
|
859
|
+
| `onAreaClick` | `(data: { area: BookPageArea; page: BookPage }) => void` | — | Callback when clickable area is clicked |
|
|
860
|
+
| `renderPage` | `Snippet<[{ page, position }]>` | — | Custom page render snippet |
|
|
861
|
+
|
|
862
|
+
**Exported helpers:** `buildSpreads(pages)`, `buildSinglePageSpreads(pages)`, `buildSheets(spreads)`, `computeBookPageSize(pages)`
|
|
863
|
+
|
|
864
|
+
**Types:** `BookPage`, `BookPageArea`, `BookSpread`, `BookSheet`, `BookCollection`
|
|
865
|
+
|
|
866
|
+
```svelte
|
|
867
|
+
<Book
|
|
868
|
+
pages={[
|
|
869
|
+
{ id: 1, src: "/cover.jpg", width: 800, height: 1100 },
|
|
870
|
+
{ id: 2, src: "/page1.jpg", width: 800, height: 1100 },
|
|
871
|
+
{ id: 3, src: "/page2.jpg", width: 800, height: 1100 },
|
|
872
|
+
]}
|
|
873
|
+
bind:activeSpread
|
|
874
|
+
/>
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
#### `BookResponsive`
|
|
878
|
+
|
|
879
|
+
Responsive wrapper around Book that intelligently switches between book mode (dual/single-page) and an inline asset preview mode based on container width. Inherits all Book props except `responsive` and `singlePage` (managed internally).
|
|
880
|
+
|
|
881
|
+
| Prop | Type | Default | Description |
|
|
882
|
+
| ----------------- | --------------------- | ----------- | ------------------------------------------------------- |
|
|
883
|
+
| `minPageWidth` | `number` | `150` | Min page width (px) before switching to single-page |
|
|
884
|
+
| `debounce` | `number` | `150` | Resize debounce delay in ms |
|
|
885
|
+
| `inlineThreshold` | `number` | `480` | Container width (px) below which switches to inline (0 = disabled) |
|
|
886
|
+
| `forceInline` | `boolean` | `false` | Force inline asset preview mode |
|
|
887
|
+
| `noPrevNext` | `boolean` | `false` | Hide prev/next arrow buttons |
|
|
888
|
+
| `classControls` | `string` | `undefined` | Custom class for prev/next buttons |
|
|
889
|
+
| `noModeSwitch` | `boolean` | `false` | Hide the book/inline toggle button |
|
|
890
|
+
| `initialMode` | `"book" \| "inline"` | `undefined` | Override auto-detection on mount |
|
|
891
|
+
|
|
892
|
+
**Exported utility:** `bookPagesToAssets(pages)` — Converts `BookPage[]` to `AssetPreview[]` for inline mode.
|
|
893
|
+
|
|
894
|
+
```svelte
|
|
895
|
+
<BookResponsive
|
|
896
|
+
pages={bookPages}
|
|
897
|
+
inlineThreshold={600}
|
|
898
|
+
onAreaClick={handleAreaClick}
|
|
899
|
+
/>
|
|
900
|
+
```
|
|
793
901
|
|
|
794
902
|
#### `Circle`
|
|
795
903
|
|
|
@@ -831,7 +939,75 @@ Element that expands width on hover with delayed transitions and shadow effects.
|
|
|
831
939
|
|
|
832
940
|
#### `AssetsPreview`
|
|
833
941
|
|
|
834
|
-
Modal-based asset/file preview gallery with zoom, pan, pinch-zoom, and download controls.
|
|
942
|
+
Modal-based asset/file preview gallery with zoom, pan, pinch-zoom, swipe navigation, clickable area overlays, and download controls. Opens in a full-screen ModalDialog.
|
|
943
|
+
|
|
944
|
+
| Prop | Type | Default | Description |
|
|
945
|
+
| ----------------- | ----------------------------------------------------------------- | ----------- | -------------------------------------------- |
|
|
946
|
+
| `assets` | `string[] \| AssetPreview[]` | required | Asset URLs or asset objects |
|
|
947
|
+
| `baseUrl` | `string` | `undefined` | Fallback base URL for relative asset URLs |
|
|
948
|
+
| `modalClassDialog`| `string` | `undefined` | CSS class for the modal dialog |
|
|
949
|
+
| `modalClass` | `string` | `undefined` | CSS class for the modal container |
|
|
950
|
+
| `classControls` | `string` | `undefined` | CSS class for control buttons |
|
|
951
|
+
| `t` | `TranslateFn` | built-in | Translation function |
|
|
952
|
+
| `onDelete` | `(asset: AssetPreview, index: number, { close }) => void` | — | Delete handler (shows delete button) |
|
|
953
|
+
| `onAreaClick` | `(data: { area: AssetArea; asset: AssetPreviewNormalized }) => void` | — | Callback for clickable area on image |
|
|
954
|
+
| `noName` | `boolean` | `false` | Hide file name display |
|
|
955
|
+
| `clampPan` | `boolean` | `true` | Clamp panning within image bounds |
|
|
956
|
+
| `noDownload` | `boolean` | `false` | Hide download button |
|
|
957
|
+
| `noPrevNext` | `boolean` | `false` | Hide prev/next arrows |
|
|
958
|
+
| `noZoom` | `boolean` | `false` | Disable all zooming |
|
|
959
|
+
| `noZoomButtons` | `boolean` | `false` | Hide zoom buttons (gestures still work) |
|
|
960
|
+
| `noDots` | `boolean` | `false` | Never show pagination dots |
|
|
961
|
+
| `noCurrentOfTotal`| `boolean` | `false` | Never show "x / y" counter |
|
|
962
|
+
|
|
963
|
+
**Methods:** `open(index?)`, `close()`
|
|
964
|
+
|
|
965
|
+
**Types:** `AssetPreview`, `AssetPreviewUrlObj`, `AssetArea`
|
|
966
|
+
|
|
967
|
+
**Utility:** `getAssetIcon(ext)` — Returns icon function for file extension.
|
|
968
|
+
|
|
969
|
+
```svelte
|
|
970
|
+
<AssetsPreview
|
|
971
|
+
assets={[
|
|
972
|
+
{ url: { full: "/photo.jpg", thumb: "/photo-thumb.jpg" }, name: "Photo", type: "image/jpeg" },
|
|
973
|
+
"/document.pdf",
|
|
974
|
+
]}
|
|
975
|
+
onDelete={(asset, i, { close }) => { deleteAsset(i); close(); }}
|
|
976
|
+
/>
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
#### `AssetsPreviewInline`
|
|
980
|
+
|
|
981
|
+
Always-visible (non-modal) variant of AssetsPreview with the same zoom, pan, swipe, and area clicking features. For embedding asset galleries directly in layouts.
|
|
982
|
+
|
|
983
|
+
| Prop | Type | Default | Description |
|
|
984
|
+
| ----------------- | ----------------------------------------------------------------- | ----------- | -------------------------------------------- |
|
|
985
|
+
| `assets` | `string[] \| AssetPreview[]` | required | Asset URLs or asset objects |
|
|
986
|
+
| `baseUrl` | `string` | `undefined` | Fallback base URL for relative asset URLs |
|
|
987
|
+
| `initialIndex` | `number` | `0` | Starting asset index |
|
|
988
|
+
| `currentIndex` | `number` | `0` | Current display index (bindable) |
|
|
989
|
+
| `class` | `string` | `undefined` | Container CSS class |
|
|
990
|
+
| `classControls` | `string` | `undefined` | CSS class for control buttons |
|
|
991
|
+
| `t` | `TranslateFn` | built-in | Translation function |
|
|
992
|
+
| `onDelete` | `(asset, index, { close }) => void` | — | Delete handler |
|
|
993
|
+
| `onAreaClick` | `(data: { area: AssetArea; asset: AssetPreviewNormalized }) => void` | — | Callback for clickable area on image |
|
|
994
|
+
| `noName` | `boolean` | `false` | Hide file name display |
|
|
995
|
+
| `clampPan` | `boolean` | `true` | Clamp panning within bounds |
|
|
996
|
+
| `noDownload` | `boolean` | `false` | Hide download button |
|
|
997
|
+
| `noPrevNext` | `boolean` | `false` | Hide prev/next arrows |
|
|
998
|
+
| `noZoom` | `boolean` | `false` | Disable all zooming |
|
|
999
|
+
| `noZoomButtons` | `boolean` | `false` | Hide zoom buttons (gestures still work) |
|
|
1000
|
+
| `noDots` | `boolean` | `false` | Never show pagination dots |
|
|
1001
|
+
| `noCurrentOfTotal`| `boolean` | `false` | Never show "x / y" counter |
|
|
1002
|
+
|
|
1003
|
+
**Methods:** `goTo(index)`, `next()`, `previous()`
|
|
1004
|
+
|
|
1005
|
+
```svelte
|
|
1006
|
+
<AssetsPreviewInline
|
|
1007
|
+
assets={imageUrls}
|
|
1008
|
+
bind:currentIndex
|
|
1009
|
+
/>
|
|
1010
|
+
```
|
|
835
1011
|
|
|
836
1012
|
#### `X`
|
|
837
1013
|
|
|
@@ -1289,6 +1465,36 @@ Generate SVG circle path data.
|
|
|
1289
1465
|
|
|
1290
1466
|
Value oscillation for animations.
|
|
1291
1467
|
|
|
1468
|
+
### URL
|
|
1469
|
+
|
|
1470
|
+
#### `resolveUrl(url, baseUrl?)`
|
|
1471
|
+
|
|
1472
|
+
Resolve a possibly relative URL against an optional base URL. Returns the original URL if no baseUrl or on error.
|
|
1473
|
+
|
|
1474
|
+
**Parameters:**
|
|
1475
|
+
|
|
1476
|
+
- `url` (string) — URL to resolve
|
|
1477
|
+
- `baseUrl` (string, optional) — Base URL to resolve against
|
|
1478
|
+
|
|
1479
|
+
**Returns:** `string`
|
|
1480
|
+
|
|
1481
|
+
```ts
|
|
1482
|
+
import { resolveUrl } from "@marianmeres/stuic";
|
|
1483
|
+
resolveUrl("images/photo.jpg", "https://example.com/books/1/");
|
|
1484
|
+
// => "https://example.com/books/1/images/photo.jpg"
|
|
1485
|
+
```
|
|
1486
|
+
|
|
1487
|
+
#### `resolveSrcset(srcset, baseUrl?)`
|
|
1488
|
+
|
|
1489
|
+
Resolve all URLs within a srcset string against an optional base URL.
|
|
1490
|
+
|
|
1491
|
+
**Parameters:**
|
|
1492
|
+
|
|
1493
|
+
- `srcset` (string) — Srcset string with relative URLs
|
|
1494
|
+
- `baseUrl` (string, optional) — Base URL to resolve against
|
|
1495
|
+
|
|
1496
|
+
**Returns:** `string`
|
|
1497
|
+
|
|
1292
1498
|
### Files
|
|
1293
1499
|
|
|
1294
1500
|
#### `fileFromBlobUrl(blobUrl, filename)`
|
|
@@ -1388,6 +1594,10 @@ Re-exported icon render functions from `@marianmeres/icons-fns`. Import from `@m
|
|
|
1388
1594
|
|
|
1389
1595
|
`iconFile`, `iconFileBinary`, `iconFileCode`, `iconFileImage`, `iconFileMusic`, `iconFilePdf`, `iconFileRichtext`, `iconFileSlides`, `iconFileSpreadsheet`, `iconFileText`, `iconFileWord`, `iconFileZip`
|
|
1390
1596
|
|
|
1597
|
+
### Book Icons
|
|
1598
|
+
|
|
1599
|
+
`iconBookOpen`
|
|
1600
|
+
|
|
1391
1601
|
### Alert Icons
|
|
1392
1602
|
|
|
1393
1603
|
`iconAlertSuccess`, `iconAlertInfo`, `iconAlertError`, `iconAlertWarning`, `iconRefresh`
|
|
@@ -1398,7 +1608,11 @@ Re-exported icon render functions from `@marianmeres/icons-fns`. Import from `@m
|
|
|
1398
1608
|
|
|
1399
1609
|
### UI Control Icons
|
|
1400
1610
|
|
|
1401
|
-
`iconCheck`, `iconChevronDown`, `iconChevronLeft`, `iconChevronRight`, `iconChevronUp`, `iconCircle`, `iconDot`, `iconEllipsisVertical`, `iconLanguages`, `iconMenu`, `iconSearch`, `iconSettings`, `iconSquare`, `iconUser`, `iconX`
|
|
1611
|
+
`iconCheck`, `iconChevronDown`, `iconChevronLeft`, `iconChevronRight`, `iconChevronUp`, `iconCircle`, `iconCircleCheckBig`, `iconDot`, `iconEllipsisVertical`, `iconGrip`, `iconGripHorizontal`, `iconGripVertical`, `iconLanguages`, `iconMenu`, `iconPencil`, `iconSearch`, `iconSettings`, `iconSquare`, `iconUser`, `iconX`
|
|
1612
|
+
|
|
1613
|
+
### Brand Icons
|
|
1614
|
+
|
|
1615
|
+
`iconApple`, `iconFacebook`, `iconGithub`, `iconGoogle`, `iconInstagram`, `iconMicrosoft`, `iconTwitterX`, `iconXbox`, `iconYoutube`
|
|
1402
1616
|
|
|
1403
1617
|
---
|
|
1404
1618
|
|
|
@@ -1419,6 +1633,9 @@ Naming pattern: `{ComponentName}Props`
|
|
|
1419
1633
|
|
|
1420
1634
|
Additional exported types include:
|
|
1421
1635
|
|
|
1636
|
+
- `BookPage`, `BookPageArea`, `BookSpread`, `BookSheet`, `BookCollection` — Book component types
|
|
1637
|
+
- `AssetPreview`, `AssetPreviewUrlObj`, `AssetArea` — AssetPreview types
|
|
1638
|
+
- `CarouselItem` — Carousel item type
|
|
1422
1639
|
- `LoginFormData`, `LoginFormValidationError` — Login form types
|
|
1423
1640
|
- `CartComponentItem`, `CartVariant` — Cart component types
|
|
1424
1641
|
- `CheckoutStep`, `CheckoutAddressData`, `CheckoutCustomerFormData`, `CheckoutLoginFormData`, `CheckoutOrderLineItem`, `CheckoutOrderTotals`, `CheckoutDeliveryOption`, `CheckoutDeliverySnapshot`, `CheckoutOrderData`, `CheckoutValidationError`, `CheckoutFormMode` — Checkout types
|
package/README.md
CHANGED
|
@@ -143,7 +143,7 @@ CommandMenu, DropdownMenu, TabbedMenu, TypeaheadInput, KbdShortcut
|
|
|
143
143
|
|
|
144
144
|
### Display & Utility
|
|
145
145
|
|
|
146
|
-
Avatar, Book, Carousel, Circle, AnimatedElipsis, H, IconSwap, Separator, ThemePreview, ColorScheme, Thc, HoverExpandableWidth, AssetsPreview, DataTable
|
|
146
|
+
Avatar, Book, BookResponsive, Carousel, Circle, AnimatedElipsis, H, IconSwap, Separator, ThemePreview, ColorScheme, Thc, HoverExpandableWidth, AssetsPreview, AssetsPreviewInline, DataTable
|
|
147
147
|
|
|
148
148
|
### E-commerce
|
|
149
149
|
|
package/dist/actions/index.d.ts
CHANGED
package/dist/actions/index.js
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { TourStepDef, TourLabels, TourShellContext } from "./onboarding.svelte.js";
|
|
4
|
+
|
|
5
|
+
export interface Props {
|
|
6
|
+
step: TourStepDef;
|
|
7
|
+
/** 0-based index of this step */
|
|
8
|
+
index: number;
|
|
9
|
+
total: number;
|
|
10
|
+
isFirst: boolean;
|
|
11
|
+
isLast: boolean;
|
|
12
|
+
labels: Required<TourLabels>;
|
|
13
|
+
/** Optional custom shell snippet — replaces the entire default layout */
|
|
14
|
+
shell?: Snippet<[TourShellContext]>;
|
|
15
|
+
next: () => void;
|
|
16
|
+
prev: () => void;
|
|
17
|
+
skip: () => void;
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<script lang="ts">
|
|
22
|
+
import Thc from "../../components/Thc/Thc.svelte";
|
|
23
|
+
|
|
24
|
+
let { step, index, total, isFirst, isLast, labels, shell, next, prev, skip }: Props = $props();
|
|
25
|
+
|
|
26
|
+
const context: TourShellContext = $derived({
|
|
27
|
+
step,
|
|
28
|
+
index,
|
|
29
|
+
total,
|
|
30
|
+
isFirst,
|
|
31
|
+
isLast,
|
|
32
|
+
next,
|
|
33
|
+
prev,
|
|
34
|
+
skip,
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
{#if shell}
|
|
39
|
+
{@render shell(context)}
|
|
40
|
+
{:else}
|
|
41
|
+
<div class="stuic-onboarding-shell">
|
|
42
|
+
{#if step.title}
|
|
43
|
+
<div class="stuic-onboarding-title">{step.title}</div>
|
|
44
|
+
{/if}
|
|
45
|
+
{#if step.content}
|
|
46
|
+
<div class="stuic-onboarding-content">
|
|
47
|
+
<Thc thc={step.content} />
|
|
48
|
+
</div>
|
|
49
|
+
{/if}
|
|
50
|
+
<div class="stuic-onboarding-footer">
|
|
51
|
+
<span class="stuic-onboarding-steps">{index + 1} / {total}</span>
|
|
52
|
+
<div class="stuic-onboarding-actions">
|
|
53
|
+
{#if !isLast}
|
|
54
|
+
<button class="stuic-onboarding-btn-skip" onclick={skip}>
|
|
55
|
+
{step.skipLabel ?? labels.skip}
|
|
56
|
+
</button>
|
|
57
|
+
{/if}
|
|
58
|
+
{#if !isFirst}
|
|
59
|
+
<button class="stuic-onboarding-btn-prev" onclick={prev}>
|
|
60
|
+
{step.prevLabel ?? labels.prev}
|
|
61
|
+
</button>
|
|
62
|
+
{/if}
|
|
63
|
+
<button class="stuic-onboarding-btn-next" onclick={next}>
|
|
64
|
+
{isLast
|
|
65
|
+
? (step.finishLabel ?? labels.finish)
|
|
66
|
+
: (step.nextLabel ?? labels.next)}
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
{/if}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { TourStepDef, TourLabels, TourShellContext } from "./onboarding.svelte.js";
|
|
3
|
+
export interface Props {
|
|
4
|
+
step: TourStepDef;
|
|
5
|
+
/** 0-based index of this step */
|
|
6
|
+
index: number;
|
|
7
|
+
total: number;
|
|
8
|
+
isFirst: boolean;
|
|
9
|
+
isLast: boolean;
|
|
10
|
+
labels: Required<TourLabels>;
|
|
11
|
+
/** Optional custom shell snippet — replaces the entire default layout */
|
|
12
|
+
shell?: Snippet<[TourShellContext]>;
|
|
13
|
+
next: () => void;
|
|
14
|
+
prev: () => void;
|
|
15
|
+
skip: () => void;
|
|
16
|
+
}
|
|
17
|
+
declare const OnboardingShell: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type OnboardingShell = ReturnType<typeof OnboardingShell>;
|
|
19
|
+
export default OnboardingShell;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* Onboarding shell tokens */
|
|
2
|
+
:root {
|
|
3
|
+
--stuic-onboarding-shell-padding: calc(var(--spacing) * 3);
|
|
4
|
+
--stuic-onboarding-shell-gap: calc(var(--spacing) * 2);
|
|
5
|
+
--stuic-onboarding-shell-min-width: 200px;
|
|
6
|
+
--stuic-onboarding-shell-max-width: 320px;
|
|
7
|
+
--stuic-onboarding-title-size: var(--text-base);
|
|
8
|
+
--stuic-onboarding-content-size: var(--text-sm);
|
|
9
|
+
--stuic-onboarding-footer-gap: calc(var(--spacing) * 1.5);
|
|
10
|
+
--stuic-onboarding-steps-size: var(--text-xs);
|
|
11
|
+
--stuic-onboarding-btn-padding-x: calc(var(--spacing) * 2.5);
|
|
12
|
+
--stuic-onboarding-btn-padding-y: calc(var(--spacing) * 1);
|
|
13
|
+
--stuic-onboarding-btn-radius: calc(var(--spacing) * 1);
|
|
14
|
+
--stuic-onboarding-btn-font-size: var(--text-xs);
|
|
15
|
+
--stuic-onboarding-btn-primary-bg: var(--stuic-color-primary, #737373);
|
|
16
|
+
--stuic-onboarding-btn-primary-fg: var(--stuic-color-primary-foreground, #fff);
|
|
17
|
+
--stuic-onboarding-btn-secondary-bg: oklch(from currentColor l c h / 0.08);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.stuic-onboarding-shell {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
gap: var(--stuic-onboarding-shell-gap);
|
|
24
|
+
padding: var(--stuic-onboarding-shell-padding);
|
|
25
|
+
min-width: var(--stuic-onboarding-shell-min-width);
|
|
26
|
+
max-width: var(--stuic-onboarding-shell-max-width);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.stuic-onboarding-title {
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
font-size: var(--stuic-onboarding-title-size);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.stuic-onboarding-content {
|
|
35
|
+
font-size: var(--stuic-onboarding-content-size);
|
|
36
|
+
opacity: 0.85;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.stuic-onboarding-footer {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: space-between;
|
|
43
|
+
gap: calc(var(--spacing) * 2);
|
|
44
|
+
padding-top: calc(var(--spacing) * 2);
|
|
45
|
+
margin-top: calc(var(--spacing) * 1);
|
|
46
|
+
border-top: 1px solid var(--stuic-spotlight-annotation-border, rgba(0, 0, 0, 0.1));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.stuic-onboarding-steps {
|
|
50
|
+
font-size: var(--stuic-onboarding-steps-size);
|
|
51
|
+
opacity: 0.5;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.stuic-onboarding-actions {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: var(--stuic-onboarding-footer-gap);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Shared button base */
|
|
61
|
+
.stuic-onboarding-btn-skip,
|
|
62
|
+
.stuic-onboarding-btn-prev,
|
|
63
|
+
.stuic-onboarding-btn-next {
|
|
64
|
+
font-size: var(--stuic-onboarding-btn-font-size);
|
|
65
|
+
padding: var(--stuic-onboarding-btn-padding-y) var(--stuic-onboarding-btn-padding-x);
|
|
66
|
+
border-radius: var(--stuic-onboarding-btn-radius);
|
|
67
|
+
line-height: 1.5;
|
|
68
|
+
transition:
|
|
69
|
+
opacity 0.15s,
|
|
70
|
+
filter 0.15s;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.stuic-onboarding-btn-skip {
|
|
74
|
+
opacity: 0.45;
|
|
75
|
+
&:hover {
|
|
76
|
+
opacity: 0.85;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.stuic-onboarding-btn-prev {
|
|
81
|
+
background: var(--stuic-onboarding-btn-secondary-bg);
|
|
82
|
+
&:hover {
|
|
83
|
+
filter: brightness(0.9);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.stuic-onboarding-btn-next {
|
|
88
|
+
background: var(--stuic-onboarding-btn-primary-bg);
|
|
89
|
+
color: var(--stuic-onboarding-btn-primary-fg);
|
|
90
|
+
&:hover {
|
|
91
|
+
filter: brightness(0.9);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { SpotlightPosition } from "../spotlight/spotlight.svelte.js";
|
|
3
|
+
import type { THC } from "../../components/Thc/Thc.svelte";
|
|
4
|
+
/**
|
|
5
|
+
* Definition of a single step in an onboarding tour.
|
|
6
|
+
*/
|
|
7
|
+
export interface TourStepDef {
|
|
8
|
+
/** Unique identifier — must match the `id` passed to `use:tourStep` */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Short title shown in the default shell header */
|
|
11
|
+
title?: string;
|
|
12
|
+
/** Rich step description (any THC value: string, html, component, snippet) */
|
|
13
|
+
content?: THC;
|
|
14
|
+
/** Spotlight annotation position relative to the target element */
|
|
15
|
+
position?: SpotlightPosition;
|
|
16
|
+
/** Spotlight cutout padding in px */
|
|
17
|
+
padding?: number;
|
|
18
|
+
/** Spotlight cutout border radius in px */
|
|
19
|
+
borderRadius?: number;
|
|
20
|
+
/** Override "Next" button label for this step */
|
|
21
|
+
nextLabel?: string;
|
|
22
|
+
/** Override "Back" button label for this step */
|
|
23
|
+
prevLabel?: string;
|
|
24
|
+
/** Override "Skip" button label for this step */
|
|
25
|
+
skipLabel?: string;
|
|
26
|
+
/** Override "Finish" button label for the last step */
|
|
27
|
+
finishLabel?: string;
|
|
28
|
+
/** Called when tour enters this step */
|
|
29
|
+
onEnter?: () => void;
|
|
30
|
+
/** Called when tour leaves this step */
|
|
31
|
+
onLeave?: () => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Default label overrides for the navigation shell buttons.
|
|
35
|
+
*/
|
|
36
|
+
export interface TourLabels {
|
|
37
|
+
next?: string;
|
|
38
|
+
prev?: string;
|
|
39
|
+
skip?: string;
|
|
40
|
+
finish?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Context passed to a custom `shell` snippet.
|
|
44
|
+
*/
|
|
45
|
+
export interface TourShellContext {
|
|
46
|
+
step: TourStepDef;
|
|
47
|
+
/** 0-based index of the current step */
|
|
48
|
+
index: number;
|
|
49
|
+
total: number;
|
|
50
|
+
isFirst: boolean;
|
|
51
|
+
isLast: boolean;
|
|
52
|
+
next: () => void;
|
|
53
|
+
prev: () => void;
|
|
54
|
+
skip: () => void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Options for `createTour`.
|
|
58
|
+
*/
|
|
59
|
+
export interface TourOptions {
|
|
60
|
+
steps: TourStepDef[];
|
|
61
|
+
/** How long to wait (ms) for a step's element to appear before skipping it. Default: 500 */
|
|
62
|
+
waitForElement?: number;
|
|
63
|
+
/** Default button labels (per-step overrides take precedence) */
|
|
64
|
+
labels?: TourLabels;
|
|
65
|
+
/** Replace the entire default shell with a custom Svelte snippet */
|
|
66
|
+
shell?: Snippet<[TourShellContext]>;
|
|
67
|
+
/** Press Escape to skip the tour. Default: true */
|
|
68
|
+
closeOnEscape?: boolean;
|
|
69
|
+
/** Called when tour starts */
|
|
70
|
+
onStart?: () => void;
|
|
71
|
+
/** Called when tour reaches the end naturally (after last step) */
|
|
72
|
+
onEnd?: () => void;
|
|
73
|
+
/** Called when tour is skipped by the user */
|
|
74
|
+
onSkip?: () => void;
|
|
75
|
+
/** Called on every step change */
|
|
76
|
+
onStepChange?: (step: TourStepDef, index: number) => void;
|
|
77
|
+
/**
|
|
78
|
+
* If set, the tour result ('completed' or 'skipped') is persisted under this key.
|
|
79
|
+
* On subsequent `start()` calls, the tour will silently skip if the key exists.
|
|
80
|
+
* Use `tour.reset()` to clear the persisted state and allow the tour to run again.
|
|
81
|
+
*/
|
|
82
|
+
storageKey?: string;
|
|
83
|
+
/** Storage backend for persistence. Default: 'local' */
|
|
84
|
+
storage?: "local" | "session";
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Creates a multi-step onboarding tour on top of the spotlight primitive.
|
|
88
|
+
*
|
|
89
|
+
* Define all steps centrally here, then attach `use:tourStep={[tour, stepId]}`
|
|
90
|
+
* to each target element in your markup.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```svelte
|
|
94
|
+
* <script>
|
|
95
|
+
* import { createTour, tourStep } from '../..';
|
|
96
|
+
*
|
|
97
|
+
* const tour = createTour({
|
|
98
|
+
* steps: [
|
|
99
|
+
* { id: 'header', title: 'Welcome', content: 'This is the top of the page.' },
|
|
100
|
+
* { id: 'save-btn', title: 'Save your work', content: 'Click here to save.' },
|
|
101
|
+
* ],
|
|
102
|
+
* onEnd: () => console.log('Tour complete!'),
|
|
103
|
+
* });
|
|
104
|
+
* </script>
|
|
105
|
+
*
|
|
106
|
+
* <header use:tourStep={[tour, 'header']}>...</header>
|
|
107
|
+
* <button use:tourStep={[tour, 'save-btn']}>Save</button>
|
|
108
|
+
* <button onclick={tour.start}>Start Tour</button>
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export declare function createTour(options: TourOptions): {
|
|
112
|
+
readonly active: boolean;
|
|
113
|
+
readonly currentStep: TourStepDef;
|
|
114
|
+
readonly currentIndex: number;
|
|
115
|
+
readonly seen: boolean;
|
|
116
|
+
start: () => void;
|
|
117
|
+
next: () => void;
|
|
118
|
+
prev: () => void;
|
|
119
|
+
skip: () => void;
|
|
120
|
+
reset: () => void;
|
|
121
|
+
_register: (id: string, el: HTMLElement) => void;
|
|
122
|
+
_unregister: (id: string) => void;
|
|
123
|
+
_isCurrentStep: (id: string) => boolean;
|
|
124
|
+
_getShellContent: (id: string) => THC;
|
|
125
|
+
};
|
|
126
|
+
export type TourInstance = ReturnType<typeof createTour>;
|
|
127
|
+
/**
|
|
128
|
+
* Svelte action that registers a DOM element as the target for a specific tour step.
|
|
129
|
+
*
|
|
130
|
+
* @param el - The target element
|
|
131
|
+
* @param args - Tuple of `[TourInstance, stepId]`
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```svelte
|
|
135
|
+
* <button use:tourStep={[tour, 'save-btn']}>Save</button>
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export declare function tourStep(el: HTMLElement, args: [TourInstance, string]): void;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { spotlight } from "../spotlight/spotlight.svelte.js";
|
|
2
|
+
import OnboardingShell from "./OnboardingShell.svelte";
|
|
3
|
+
import { StorageAbstraction } from "../../utils/storage-abstraction.js";
|
|
4
|
+
/**
|
|
5
|
+
* Creates a multi-step onboarding tour on top of the spotlight primitive.
|
|
6
|
+
*
|
|
7
|
+
* Define all steps centrally here, then attach `use:tourStep={[tour, stepId]}`
|
|
8
|
+
* to each target element in your markup.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <script>
|
|
13
|
+
* import { createTour, tourStep } from '../..';
|
|
14
|
+
*
|
|
15
|
+
* const tour = createTour({
|
|
16
|
+
* steps: [
|
|
17
|
+
* { id: 'header', title: 'Welcome', content: 'This is the top of the page.' },
|
|
18
|
+
* { id: 'save-btn', title: 'Save your work', content: 'Click here to save.' },
|
|
19
|
+
* ],
|
|
20
|
+
* onEnd: () => console.log('Tour complete!'),
|
|
21
|
+
* });
|
|
22
|
+
* </script>
|
|
23
|
+
*
|
|
24
|
+
* <header use:tourStep={[tour, 'header']}>...</header>
|
|
25
|
+
* <button use:tourStep={[tour, 'save-btn']}>Save</button>
|
|
26
|
+
* <button onclick={tour.start}>Start Tour</button>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function createTour(options) {
|
|
30
|
+
let currentIndex = $state(-1);
|
|
31
|
+
const active = $derived(currentIndex >= 0);
|
|
32
|
+
const currentStep = $derived(options.steps[currentIndex] ?? null);
|
|
33
|
+
// Optional persistence store
|
|
34
|
+
const store = options.storageKey
|
|
35
|
+
? new StorageAbstraction(options.storage ?? "local")
|
|
36
|
+
: null;
|
|
37
|
+
// Element registry: stepId -> HTMLElement
|
|
38
|
+
const registry = new Map();
|
|
39
|
+
// Wait-for-element mechanism (one pending wait at a time)
|
|
40
|
+
let pendingStepId = null;
|
|
41
|
+
let pendingResolve = null;
|
|
42
|
+
// Guard against concurrent navigation calls
|
|
43
|
+
let advancing = false;
|
|
44
|
+
const resolvedLabels = {
|
|
45
|
+
next: "Next",
|
|
46
|
+
prev: "Back",
|
|
47
|
+
skip: "Skip",
|
|
48
|
+
finish: "Finish",
|
|
49
|
+
...options.labels,
|
|
50
|
+
};
|
|
51
|
+
// Escape key listener — active only while tour is running
|
|
52
|
+
$effect(() => {
|
|
53
|
+
if (active && options.closeOnEscape !== false) {
|
|
54
|
+
const handler = (e) => {
|
|
55
|
+
if (e.key === "Escape") {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
e.stopPropagation();
|
|
58
|
+
e.stopImmediatePropagation();
|
|
59
|
+
skip();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
document.addEventListener("keydown", handler);
|
|
63
|
+
return () => document.removeEventListener("keydown", handler);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
// -- Internal API (used by tourStep action) -----------------------------------------
|
|
67
|
+
function _register(id, el) {
|
|
68
|
+
registry.set(id, el);
|
|
69
|
+
// If we were waiting for this element, resolve immediately
|
|
70
|
+
if (pendingStepId === id && pendingResolve) {
|
|
71
|
+
pendingResolve(true);
|
|
72
|
+
pendingStepId = null;
|
|
73
|
+
pendingResolve = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function _unregister(id) {
|
|
77
|
+
registry.delete(id);
|
|
78
|
+
// If the active step's element unmounts mid-tour, end gracefully
|
|
79
|
+
if (active && currentStep?.id === id) {
|
|
80
|
+
console.warn(`[createTour] Active step "${id}" element unmounted — ending tour`);
|
|
81
|
+
_end();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function _isCurrentStep(id) {
|
|
85
|
+
return active && currentStep?.id === id;
|
|
86
|
+
}
|
|
87
|
+
function _getShellContent(id) {
|
|
88
|
+
const step = options.steps.find((s) => s.id === id);
|
|
89
|
+
const index = options.steps.indexOf(step);
|
|
90
|
+
const total = options.steps.length;
|
|
91
|
+
return {
|
|
92
|
+
component: OnboardingShell,
|
|
93
|
+
props: {
|
|
94
|
+
step,
|
|
95
|
+
index,
|
|
96
|
+
total,
|
|
97
|
+
isFirst: index === 0,
|
|
98
|
+
isLast: index === total - 1,
|
|
99
|
+
labels: resolvedLabels,
|
|
100
|
+
shell: options.shell,
|
|
101
|
+
next,
|
|
102
|
+
prev,
|
|
103
|
+
skip,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// -- Navigation ---------------------------------------------------------------------
|
|
108
|
+
function waitForElement(id) {
|
|
109
|
+
// Cancel any previous pending wait
|
|
110
|
+
if (pendingResolve) {
|
|
111
|
+
pendingResolve(false);
|
|
112
|
+
pendingResolve = null;
|
|
113
|
+
pendingStepId = null;
|
|
114
|
+
}
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
pendingStepId = id;
|
|
117
|
+
pendingResolve = resolve;
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
if (pendingStepId === id) {
|
|
120
|
+
console.warn(`[createTour] Step "${id}" element not found after ${options.waitForElement ?? 500}ms — skipping`);
|
|
121
|
+
pendingStepId = null;
|
|
122
|
+
pendingResolve = null;
|
|
123
|
+
resolve(false);
|
|
124
|
+
}
|
|
125
|
+
}, options.waitForElement ?? 500);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async function advanceTo(targetIndex) {
|
|
129
|
+
if (advancing)
|
|
130
|
+
return;
|
|
131
|
+
advancing = true;
|
|
132
|
+
try {
|
|
133
|
+
// Call onLeave for the step we're leaving
|
|
134
|
+
currentStep?.onLeave?.();
|
|
135
|
+
const direction = targetIndex >= currentIndex ? 1 : -1;
|
|
136
|
+
let index = targetIndex;
|
|
137
|
+
// Find the nearest available step in the direction of travel
|
|
138
|
+
while (index >= 0 && index < options.steps.length) {
|
|
139
|
+
const step = options.steps[index];
|
|
140
|
+
if (!registry.has(step.id)) {
|
|
141
|
+
const found = await waitForElement(step.id);
|
|
142
|
+
if (!found) {
|
|
143
|
+
index += direction;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Step element is available — navigate here
|
|
148
|
+
currentIndex = index;
|
|
149
|
+
step.onEnter?.();
|
|
150
|
+
options.onStepChange?.(step, index);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Exhausted all steps in the direction of travel
|
|
154
|
+
_end();
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
advancing = false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function _end() {
|
|
161
|
+
currentIndex = -1;
|
|
162
|
+
store?.set(options.storageKey, "completed");
|
|
163
|
+
options.onEnd?.();
|
|
164
|
+
}
|
|
165
|
+
// -- Public API ---------------------------------------------------------------------
|
|
166
|
+
function start() {
|
|
167
|
+
if (store && store.has(options.storageKey))
|
|
168
|
+
return;
|
|
169
|
+
options.onStart?.();
|
|
170
|
+
advanceTo(0);
|
|
171
|
+
}
|
|
172
|
+
function next() {
|
|
173
|
+
advanceTo(currentIndex + 1);
|
|
174
|
+
}
|
|
175
|
+
function prev() {
|
|
176
|
+
if (currentIndex > 0)
|
|
177
|
+
advanceTo(currentIndex - 1);
|
|
178
|
+
}
|
|
179
|
+
function skip() {
|
|
180
|
+
currentIndex = -1;
|
|
181
|
+
store?.set(options.storageKey, "skipped");
|
|
182
|
+
options.onSkip?.();
|
|
183
|
+
}
|
|
184
|
+
function reset() {
|
|
185
|
+
store?.remove(options.storageKey);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
get active() {
|
|
189
|
+
return active;
|
|
190
|
+
},
|
|
191
|
+
get currentStep() {
|
|
192
|
+
return currentStep;
|
|
193
|
+
},
|
|
194
|
+
get currentIndex() {
|
|
195
|
+
return currentIndex;
|
|
196
|
+
},
|
|
197
|
+
get seen() {
|
|
198
|
+
return store ? store.has(options.storageKey) : false;
|
|
199
|
+
},
|
|
200
|
+
start,
|
|
201
|
+
next,
|
|
202
|
+
prev,
|
|
203
|
+
skip,
|
|
204
|
+
reset,
|
|
205
|
+
// Internal — used by tourStep action
|
|
206
|
+
_register,
|
|
207
|
+
_unregister,
|
|
208
|
+
_isCurrentStep,
|
|
209
|
+
_getShellContent,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Svelte action that registers a DOM element as the target for a specific tour step.
|
|
214
|
+
*
|
|
215
|
+
* @param el - The target element
|
|
216
|
+
* @param args - Tuple of `[TourInstance, stepId]`
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```svelte
|
|
220
|
+
* <button use:tourStep={[tour, 'save-btn']}>Save</button>
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
export function tourStep(el, args) {
|
|
224
|
+
const [tour, id] = args;
|
|
225
|
+
tour._register(id, el);
|
|
226
|
+
spotlight(el, () => {
|
|
227
|
+
const isActive = tour._isCurrentStep(id);
|
|
228
|
+
if (!isActive) {
|
|
229
|
+
return { open: false };
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
open: true,
|
|
233
|
+
content: tour._getShellContent(id),
|
|
234
|
+
position: tour.currentStep?.position ?? "bottom",
|
|
235
|
+
padding: tour.currentStep?.padding,
|
|
236
|
+
borderRadius: tour.currentStep?.borderRadius,
|
|
237
|
+
closeOnEscape: false,
|
|
238
|
+
closeOnBackdropClick: false,
|
|
239
|
+
scrollIntoView: true,
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
$effect(() => {
|
|
243
|
+
return () => {
|
|
244
|
+
tour._unregister(id);
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
}
|
package/dist/index.css
CHANGED
|
@@ -63,14 +63,16 @@
|
|
|
63
63
|
| --------------- | ------------------------------------------------------------------- |
|
|
64
64
|
| Avatar | User avatars with fallback |
|
|
65
65
|
| KbdShortcut | Keyboard shortcut hints |
|
|
66
|
-
| Carousel | Image/content slider
|
|
66
|
+
| Carousel | Image/content slider with snap, keyboard nav, wheel scroll, arrows |
|
|
67
67
|
| ListItemButton | List item with actions |
|
|
68
68
|
| AnimatedElipsis | Loading dots animation |
|
|
69
69
|
| IconSwap | N-state visibility swap with opacity transitions (e.g. hamburger/X) |
|
|
70
70
|
| DataTable | Responsive data table with paging, selection, batch actions |
|
|
71
71
|
| ThemePreview | Theme color swatches |
|
|
72
|
-
| AssetsPreview
|
|
73
|
-
|
|
|
72
|
+
| AssetsPreview | Modal-based asset/file preview with zoom, pan, swipe, area clicking |
|
|
73
|
+
| AssetsPreviewInline | Always-visible (non-modal) asset preview with same feature set |
|
|
74
|
+
| Book | Interactive book/flipbook with 3D page-flip, zoom, pan, areas |
|
|
75
|
+
| BookResponsive | Responsive Book wrapper: auto single/dual-page + inline mode |
|
|
74
76
|
| Circle | SVG circular progress indicator |
|
|
75
77
|
| H | Semantic heading (h1-h6) with separate visual/semantic levels |
|
|
76
78
|
| Separator | Horizontal/vertical separator line |
|
package/docs/domains/utils.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
43 utility modules for common tasks. Organized by category.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -124,6 +124,15 @@ twMerge("px-4 py-2", "px-6"); // => "py-2 px-6"
|
|
|
124
124
|
|
|
125
125
|
---
|
|
126
126
|
|
|
127
|
+
## URL
|
|
128
|
+
|
|
129
|
+
| Util | Purpose |
|
|
130
|
+
| --------------- | -------------------------------------------- |
|
|
131
|
+
| `resolveUrl` | Resolve relative URL against base URL |
|
|
132
|
+
| `resolveSrcset` | Resolve all URLs within a srcset string |
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
127
136
|
## Files
|
|
128
137
|
|
|
129
138
|
| Util | Purpose |
|