@makolabs/ripple 2.5.9 → 3.0.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/README.md +403 -497
- package/dist/adapters/storage/S3Adapter.d.ts +49 -1
- package/dist/adapters/storage/S3Adapter.js +38 -1
- package/dist/adapters/storage/types.d.ts +20 -0
- package/dist/ai/AIChatInterface.svelte +2 -1
- package/dist/ai/AIChatInterface.svelte.d.ts +2 -1
- package/dist/ai/CodeRenderer.svelte +7 -2
- package/dist/ai/CodeRenderer.svelte.d.ts +2 -1
- package/dist/ai/ComposeDropdown.svelte +1 -1
- package/dist/ai/MessageBox.svelte +3 -3
- package/dist/ai/MessageBox.svelte.d.ts +3 -2
- package/dist/ai/ThinkingDisplay.svelte +4 -3
- package/dist/ai/ThinkingDisplay.svelte.d.ts +2 -1
- package/dist/ai/ai-types.d.ts +55 -1
- package/dist/button/Button.svelte +5 -5
- package/dist/button/button-types.d.ts +49 -4
- package/dist/button/button.d.ts +9 -9
- package/dist/button/button.js +6 -6
- package/dist/charts/Chart.svelte +8 -16
- package/dist/charts/chart-types.d.ts +78 -1
- package/dist/drawer/Drawer.svelte +6 -26
- package/dist/drawer/drawer-types.d.ts +33 -12
- package/dist/drawer/drawer.d.ts +3 -3
- package/dist/drawer/drawer.js +1 -1
- package/dist/elements/accordion/Accordion.svelte +6 -17
- package/dist/elements/accordion/accordion-types.d.ts +53 -6
- package/dist/elements/alert/Alert.svelte +3 -0
- package/dist/elements/badge/Badge.svelte +1 -1
- package/dist/elements/badge/badge-types.d.ts +22 -0
- package/dist/elements/badge/badge.d.ts +3 -3
- package/dist/elements/badge/badge.js +1 -1
- package/dist/elements/combobox/ComboBox.svelte +247 -0
- package/dist/elements/combobox/ComboBox.svelte.d.ts +4 -0
- package/dist/elements/combobox/combobox-types.d.ts +41 -0
- package/dist/elements/combobox/combobox-types.js +1 -0
- package/dist/elements/context-menu/ContextMenu.svelte +137 -0
- package/dist/elements/context-menu/ContextMenu.svelte.d.ts +4 -0
- package/dist/elements/context-menu/context-menu-types.d.ts +40 -0
- package/dist/elements/context-menu/context-menu-types.js +1 -0
- package/dist/elements/dropdown/Dropdown.svelte +1 -1
- package/dist/elements/dropdown/Select.svelte +4 -1
- package/dist/elements/dropdown/dropdown-types.d.ts +114 -0
- package/dist/elements/dropdown/dropdown.d.ts +3 -3
- package/dist/elements/dropdown/dropdown.js +2 -2
- package/dist/elements/dropdown/select.d.ts +3 -3
- package/dist/elements/dropdown/select.js +2 -2
- package/dist/elements/empty-state/EmptyState.svelte +1 -1
- package/dist/elements/empty-state/empty-state-types.d.ts +32 -1
- package/dist/elements/empty-state/empty-state.d.ts +3 -3
- package/dist/elements/empty-state/empty-state.js +2 -2
- package/dist/elements/file-upload/FileUpload.svelte +5 -0
- package/dist/elements/file-upload/file-upload-types.d.ts +59 -0
- package/dist/elements/pagination/Pagination.svelte +53 -21
- package/dist/elements/pagination/Pagination.svelte.d.ts +33 -5
- package/dist/elements/popover/Popover.svelte +234 -0
- package/dist/elements/popover/Popover.svelte.d.ts +4 -0
- package/dist/elements/popover/index.d.ts +2 -0
- package/dist/elements/popover/index.js +1 -0
- package/dist/elements/popover/popover-types.d.ts +60 -0
- package/dist/elements/popover/popover-types.js +1 -0
- package/dist/elements/progress/Progress.svelte +32 -7
- package/dist/elements/progress/progress-types.d.ts +48 -1
- package/dist/elements/skeleton/Skeleton.svelte +56 -0
- package/dist/elements/skeleton/Skeleton.svelte.d.ts +4 -0
- package/dist/elements/skeleton/index.d.ts +2 -0
- package/dist/elements/skeleton/index.js +1 -0
- package/dist/elements/skeleton/skeleton-types.d.ts +50 -0
- package/dist/elements/skeleton/skeleton-types.js +1 -0
- package/dist/elements/spinner/Spinner.svelte +1 -1
- package/dist/elements/spinner/spinner-types.d.ts +20 -0
- package/dist/elements/spinner/spinner.d.ts +3 -3
- package/dist/elements/spinner/spinner.js +2 -2
- package/dist/elements/tooltip/Tooltip.svelte +108 -11
- package/dist/elements/tooltip/tooltip-types.d.ts +49 -1
- package/dist/file-browser/FileBrowser.svelte +21 -12
- package/dist/filters/CompactFilters.svelte +221 -33
- package/dist/filters/CompactFilters.svelte.d.ts +1 -1
- package/dist/filters/FilterBar.svelte +184 -0
- package/dist/filters/FilterBar.svelte.d.ts +4 -0
- package/dist/filters/FilterPopover.svelte +346 -0
- package/dist/filters/FilterPopover.svelte.d.ts +4 -0
- package/dist/filters/date-presets.d.ts +15 -0
- package/dist/filters/date-presets.js +107 -0
- package/dist/filters/filter-types.d.ts +69 -3
- package/dist/filters/index.d.ts +5 -0
- package/dist/filters/index.js +4 -0
- package/dist/filters/sync-filters-to-url.svelte.d.ts +37 -0
- package/dist/filters/sync-filters-to-url.svelte.js +114 -0
- package/dist/forms/DateRange.svelte +4 -2
- package/dist/forms/Input.svelte +2 -2
- package/dist/forms/MarketSelector.svelte +8 -3
- package/dist/forms/NumberInput.svelte +4 -4
- package/dist/forms/RadioGroup.svelte +123 -0
- package/dist/forms/RadioGroup.svelte.d.ts +4 -0
- package/dist/forms/SegmentedControl.svelte +11 -4
- package/dist/forms/Slider.svelte +72 -3
- package/dist/forms/Tags.svelte +14 -5
- package/dist/forms/Textarea.svelte +126 -0
- package/dist/forms/Textarea.svelte.d.ts +4 -0
- package/dist/forms/Toggle.svelte +8 -8
- package/dist/forms/calendar/Calendar.svelte +218 -0
- package/dist/forms/calendar/Calendar.svelte.d.ts +4 -0
- package/dist/forms/calendar/calendar-types.d.ts +46 -0
- package/dist/forms/calendar/calendar-types.js +1 -0
- package/dist/forms/calendar/index.d.ts +2 -0
- package/dist/forms/calendar/index.js +1 -0
- package/dist/forms/date-picker/DatePicker.svelte +144 -0
- package/dist/forms/date-picker/DatePicker.svelte.d.ts +4 -0
- package/dist/forms/date-picker/date-picker-types.d.ts +29 -0
- package/dist/forms/date-picker/date-picker-types.js +1 -0
- package/dist/forms/form-types.d.ts +425 -6
- package/dist/forms/market/market-selector-types.d.ts +52 -1
- package/dist/forms/segmented-control.d.ts +5 -2
- package/dist/forms/segmented-control.js +16 -5
- package/dist/forms/slider.d.ts +3 -3
- package/dist/forms/slider.js +2 -2
- package/dist/funcs/user-management.remote.js +1 -1
- package/dist/header/Breadcrumbs.svelte +4 -20
- package/dist/header/PageHeader.svelte +6 -14
- package/dist/header/breadcrumbs.d.ts +3 -11
- package/dist/header/breadcrumbs.js +10 -5
- package/dist/header/header-types.d.ts +62 -11
- package/dist/index.d.ts +35 -9
- package/dist/index.js +24 -4
- package/dist/layout/activity-list/ActivityList.svelte +13 -7
- package/dist/layout/activity-list/activity-list-types.d.ts +46 -7
- package/dist/layout/card/Card.svelte +12 -15
- package/dist/layout/card/MetricCard.svelte +50 -32
- package/dist/layout/card/card-types.d.ts +114 -4
- package/dist/layout/navbar/navbar-types.d.ts +48 -0
- package/dist/layout/navbar/navbar.d.ts +3 -3
- package/dist/layout/navbar/navbar.js +2 -2
- package/dist/layout/sidebar/Sidebar.svelte +87 -11
- package/dist/layout/sidebar/sidebar-types.d.ts +60 -1
- package/dist/layout/stepper/Stepper.svelte +288 -0
- package/dist/layout/stepper/Stepper.svelte.d.ts +4 -0
- package/dist/layout/stepper/stepper-types.d.ts +80 -0
- package/dist/layout/stepper/stepper-types.js +1 -0
- package/dist/layout/table/Table.svelte +91 -85
- package/dist/layout/table/table-types.d.ts +148 -24
- package/dist/layout/table/table.d.ts +3 -3
- package/dist/layout/table/table.js +2 -2
- package/dist/layout/tabs/Tab.svelte +6 -2
- package/dist/layout/tabs/Tab.svelte.d.ts +4 -1
- package/dist/layout/tabs/TabGroup.svelte +9 -2
- package/dist/layout/tabs/tabs-types.d.ts +63 -0
- package/dist/layout/tabs/tabs.d.ts +3 -3
- package/dist/layout/tabs/tabs.js +12 -6
- package/dist/modal/ConfirmDialog.svelte +65 -0
- package/dist/modal/ConfirmDialog.svelte.d.ts +4 -0
- package/dist/modal/Modal.svelte +6 -26
- package/dist/modal/confirm-dialog-types.d.ts +39 -0
- package/dist/modal/confirm-dialog-types.js +1 -0
- package/dist/modal/modal-types.d.ts +51 -12
- package/dist/modal/modal.d.ts +3 -3
- package/dist/modal/modal.js +3 -3
- package/dist/pipeline/Pipeline.svelte +8 -3
- package/dist/pipeline/pipeline-types.d.ts +55 -3
- package/dist/pipeline/pipeline.d.ts +18 -3
- package/dist/pipeline/pipeline.js +7 -2
- package/dist/server/s3.d.ts +35 -3
- package/dist/sonner/Toaster.svelte +29 -0
- package/dist/sonner/Toaster.svelte.d.ts +4 -0
- package/dist/sonner/index.d.ts +21 -0
- package/dist/sonner/index.js +20 -0
- package/dist/user-management/UserManagement.svelte +22 -16
- package/dist/user-management/UserModal.svelte +10 -7
- package/dist/user-management/UserTable.svelte +16 -17
- package/dist/user-management/UserViewModal.svelte +11 -11
- package/dist/user-management/user-management-types.d.ts +118 -31
- package/dist/variants.d.ts +1 -1
- package/dist/variants.js +1 -1
- package/package.json +7 -4
- package/dist/config/ai.d.ts +0 -13
- package/dist/config/ai.js +0 -44
- package/dist/elements/empty-state/EmptyStateTestWrapper.svelte +0 -25
- package/dist/elements/empty-state/EmptyStateTestWrapper.svelte.d.ts +0 -8
- package/dist/elements/tooltip/TooltipTestWrapper.svelte +0 -14
- package/dist/elements/tooltip/TooltipTestWrapper.svelte.d.ts +0 -7
- package/dist/helper/deprecation.d.ts +0 -14
- package/dist/helper/deprecation.js +0 -24
- package/dist/modal/ModalFooterTestWrapper.svelte +0 -17
- package/dist/modal/ModalFooterTestWrapper.svelte.d.ts +0 -8
|
@@ -12,15 +12,46 @@
|
|
|
12
12
|
});
|
|
13
13
|
setContext('menubar', menubar);
|
|
14
14
|
|
|
15
|
-
// Track screen size for responsive behavior
|
|
16
|
-
|
|
15
|
+
// Track screen size for responsive behavior.
|
|
16
|
+
// `isSmallScreen`: below xl (1280px) → auto-collapse to icon rail.
|
|
17
|
+
// `isMobile`: below md (768px) → sidebar slides in as a fixed overlay
|
|
18
|
+
// instead of taking a permanent column.
|
|
19
|
+
//
|
|
20
|
+
// Initialised via `matchMedia` inside the `$state` expression so the
|
|
21
|
+
// client-side first render already has the correct value — avoids the
|
|
22
|
+
// flash where mobile viewports would briefly show the desktop layout
|
|
23
|
+
// before onMount runs. On SSR both resolve to `false`; the CSS below
|
|
24
|
+
// uses `md:` / `max-md:` modifiers to hide the drawer regardless so
|
|
25
|
+
// the SSR output doesn't paint a w-64 column on narrow viewports.
|
|
26
|
+
function matchMediaOrFalse(query: string): boolean {
|
|
27
|
+
return typeof window !== 'undefined' && window.matchMedia(query).matches;
|
|
28
|
+
}
|
|
29
|
+
let isSmallScreen = $state(matchMediaOrFalse('(max-width: 1279.98px)'));
|
|
30
|
+
let isMobile = $state(matchMediaOrFalse('(max-width: 767.98px)'));
|
|
31
|
+
|
|
32
|
+
// Track previous viewport class so we only force-close the drawer
|
|
33
|
+
// when transitioning INTO mobile from a larger viewport. Without this,
|
|
34
|
+
// a minor resize on mobile (soft keyboard, orientation change) would
|
|
35
|
+
// unexpectedly close an already-open drawer.
|
|
36
|
+
let wasMobile = isMobile;
|
|
17
37
|
|
|
18
38
|
function updateCollapseState() {
|
|
19
39
|
if (typeof window !== 'undefined') {
|
|
20
|
-
isSmallScreen = window.innerWidth < 1280;
|
|
21
|
-
|
|
40
|
+
isSmallScreen = window.innerWidth < 1280;
|
|
41
|
+
const nextIsMobile = window.innerWidth < 768;
|
|
42
|
+
// Keep legacy auto-collapse on md-xl only — below md we fully
|
|
43
|
+
// hide via translate, so don't force the icon-rail state.
|
|
44
|
+
if (isSmallScreen && !nextIsMobile && !menubar.collapsed) {
|
|
22
45
|
menubar.collapsed = true;
|
|
23
46
|
}
|
|
47
|
+
if (nextIsMobile && !wasMobile) {
|
|
48
|
+
// Transitioning into mobile: close the drawer so it doesn't
|
|
49
|
+
// cover the app. Subsequent resizes on mobile won't re-close
|
|
50
|
+
// an open drawer.
|
|
51
|
+
menubar.collapsed = true;
|
|
52
|
+
}
|
|
53
|
+
isMobile = nextIsMobile;
|
|
54
|
+
wasMobile = nextIsMobile;
|
|
24
55
|
}
|
|
25
56
|
}
|
|
26
57
|
|
|
@@ -34,6 +65,10 @@
|
|
|
34
65
|
menubar.collapsed = !menubar.collapsed;
|
|
35
66
|
}
|
|
36
67
|
|
|
68
|
+
function openMobile() {
|
|
69
|
+
menubar.collapsed = false;
|
|
70
|
+
}
|
|
71
|
+
|
|
37
72
|
/**
|
|
38
73
|
* Process a navigation item to determine if it should be active
|
|
39
74
|
*/
|
|
@@ -73,11 +108,19 @@
|
|
|
73
108
|
|
|
74
109
|
const sidebarClasses = $derived(
|
|
75
110
|
clsx(
|
|
76
|
-
`
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
111
|
+
`flex flex-col bg-gradient-to-b from-default-900 to-default-900 overflow-hidden`,
|
|
112
|
+
'transition-[width,transform] duration-300 ease-out',
|
|
113
|
+
// Mobile drawer mode: fixed overlay that slides in from the left.
|
|
114
|
+
// Full-width drawer (w-64), translates off-screen when collapsed.
|
|
115
|
+
// `isMobile` is initialised synchronously via matchMedia above so
|
|
116
|
+
// the client-side first render already picks the right branch,
|
|
117
|
+
// minimising the hydration flash.
|
|
118
|
+
isMobile && 'fixed inset-y-0 left-0 z-50 w-64 shadow-2xl',
|
|
119
|
+
isMobile && menubar.collapsed && '-translate-x-full',
|
|
120
|
+
// Desktop / tablet: normal flow, collapses to an icon rail.
|
|
121
|
+
!isMobile && 'min-h-screen h-full shrink-0',
|
|
122
|
+
!isMobile && menubar.collapsed && 'w-16',
|
|
123
|
+
!isMobile && !menubar.collapsed && 'w-64'
|
|
81
124
|
)
|
|
82
125
|
);
|
|
83
126
|
|
|
@@ -92,14 +135,47 @@
|
|
|
92
135
|
);
|
|
93
136
|
</script>
|
|
94
137
|
|
|
138
|
+
{#if menubar.collapsed}
|
|
139
|
+
<!-- Floating hamburger trigger: only needed on mobile when the drawer
|
|
140
|
+
is closed. `md:hidden` keeps it mobile-only without needing a JS
|
|
141
|
+
viewport check, which avoids a first-paint flash. -->
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onclick={openMobile}
|
|
145
|
+
class="bg-default-900 fixed top-4 left-4 z-40 flex size-10 cursor-pointer items-center justify-center rounded-lg text-white shadow-lg md:hidden"
|
|
146
|
+
aria-label="Open navigation"
|
|
147
|
+
data-testid={buildTestId('sidebar', 'mobile-trigger', testId)}
|
|
148
|
+
>
|
|
149
|
+
{@render ToggleIcon('size-5')}
|
|
150
|
+
</button>
|
|
151
|
+
{:else}
|
|
152
|
+
<!-- Backdrop: tap-to-close. Also `md:hidden` so desktop doesn't get
|
|
153
|
+
covered by a full-viewport click-catcher. -->
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
onclick={toggle}
|
|
157
|
+
aria-label="Close navigation"
|
|
158
|
+
class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm md:hidden"
|
|
159
|
+
></button>
|
|
160
|
+
{/if}
|
|
161
|
+
|
|
95
162
|
<div class={sidebarClasses} data-testid={buildTestId('sidebar', undefined, testId)}>
|
|
96
163
|
<div class={logoWrapperClasses}>
|
|
97
164
|
<div class="flex items-center gap-x-1">
|
|
98
165
|
{#if logo.src && !menubar.collapsed}
|
|
99
166
|
<img src={logo.src} alt={logo.title} class="size-8 shrink-0" />
|
|
100
167
|
{/if}
|
|
101
|
-
{#if logo.title
|
|
102
|
-
<h1
|
|
168
|
+
{#if logo.title}
|
|
169
|
+
<h1
|
|
170
|
+
class="overflow-hidden text-xl font-bold whitespace-nowrap text-white transition-all duration-200 ease-out"
|
|
171
|
+
class:max-w-0={menubar.collapsed}
|
|
172
|
+
class:opacity-0={menubar.collapsed}
|
|
173
|
+
class:max-w-xs={!menubar.collapsed}
|
|
174
|
+
class:opacity-100={!menubar.collapsed}
|
|
175
|
+
aria-hidden={menubar.collapsed}
|
|
176
|
+
>
|
|
177
|
+
{logo.title}
|
|
178
|
+
</h1>
|
|
103
179
|
{/if}
|
|
104
180
|
</div>
|
|
105
181
|
<button
|
|
@@ -1,54 +1,113 @@
|
|
|
1
1
|
import type { ClassValue } from 'tailwind-variants';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { Component } from 'svelte';
|
|
4
|
+
/** Collapse state passed to sidebar snippets. */
|
|
4
5
|
export type MenuBar = {
|
|
5
6
|
collapsed: boolean;
|
|
6
7
|
};
|
|
8
|
+
/** Shared fields for any named navigation entry. */
|
|
7
9
|
export interface BaseNavigationItem {
|
|
8
10
|
label: string;
|
|
9
11
|
}
|
|
12
|
+
/** Mixin for entries that can render a leading icon. */
|
|
10
13
|
export interface WithIcon {
|
|
11
14
|
Icon?: Component;
|
|
12
15
|
}
|
|
16
|
+
/** Mixin for entries that can show an "active" (current route) state. */
|
|
13
17
|
export interface Activatable {
|
|
14
18
|
active?: boolean;
|
|
15
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Standard clickable link in the sidebar. Most sidebars are built from
|
|
22
|
+
* a mix of these plus `ParentItem`s (groups) and `DividerItem`s (rules).
|
|
23
|
+
*/
|
|
16
24
|
export interface LinkItem extends BaseNavigationItem, WithIcon, Activatable {
|
|
17
25
|
href: string;
|
|
26
|
+
/** Short text rendered to the right of the label (e.g. count, shortcut). */
|
|
18
27
|
meta?: string;
|
|
19
28
|
/**
|
|
20
29
|
* When true, the link will be active if the current route starts with this href.
|
|
21
|
-
* Example: href="/tushar" will be active for "/tushar/overview" and "/tushar/profile"
|
|
30
|
+
* Example: `href="/tushar"` will be active for `"/tushar/overview"` and `"/tushar/profile"`
|
|
22
31
|
* @default false
|
|
23
32
|
*/
|
|
24
33
|
matchPartial?: boolean;
|
|
25
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Collapsible group with child links. Active when any descendant link
|
|
37
|
+
* matches the current route.
|
|
38
|
+
*/
|
|
26
39
|
export interface ParentItem extends BaseNavigationItem, Activatable {
|
|
27
40
|
children: LinkItem[];
|
|
28
41
|
}
|
|
42
|
+
/** Horizontal separator between sections. */
|
|
29
43
|
export interface DividerItem {
|
|
30
44
|
type: 'horizontal-divider';
|
|
31
45
|
}
|
|
46
|
+
/** Union of everything that can appear in `SidebarProps.items`. */
|
|
32
47
|
export type NavigationItem = LinkItem | ParentItem | DividerItem;
|
|
48
|
+
/** Logo configuration rendered at the top of the sidebar. */
|
|
33
49
|
export type LogoType = {
|
|
50
|
+
/** Image path. Omit for a text-only wordmark. */
|
|
34
51
|
src?: string;
|
|
52
|
+
/** Wordmark shown next to (or instead of) the image. */
|
|
35
53
|
title: string;
|
|
36
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* Props for `<NavGroup>` — a collapsible group of nav items. Usually
|
|
57
|
+
* consumed via `<Sidebar>`'s `items` prop, but exposed for custom
|
|
58
|
+
* compositions.
|
|
59
|
+
*/
|
|
37
60
|
export interface NavGroupProps {
|
|
61
|
+
/** Header snippet — receives `(label, meta)` from the parent. */
|
|
38
62
|
labelArea: Snippet<[string, string]>;
|
|
63
|
+
/** Whether the group is currently "open" (expanded). */
|
|
39
64
|
active?: boolean;
|
|
40
65
|
children?: Snippet;
|
|
41
66
|
class?: ClassValue;
|
|
42
67
|
testId?: string;
|
|
43
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Props for `<NavItem>` — a single navigation link. Usually consumed via
|
|
71
|
+
* `<Sidebar>`, but exposed for custom navigation compositions.
|
|
72
|
+
*/
|
|
44
73
|
export interface NavItemProps {
|
|
45
74
|
href: string;
|
|
46
75
|
active?: boolean;
|
|
76
|
+
/** Item content — receives a class value so custom children can style consistently. */
|
|
47
77
|
children: Snippet<[ClassValue]>;
|
|
48
78
|
class?: ClassValue;
|
|
49
79
|
testId?: string;
|
|
50
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Props for `<Sidebar>` — a left-rail navigation. Supports collapse
|
|
83
|
+
* (icon-only), nested groups, dividers, and custom below-logo / footer
|
|
84
|
+
* snippets (for market selectors, user menus, etc.).
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```svelte
|
|
88
|
+
* <Sidebar
|
|
89
|
+
* logo={{ src: '/logo.svg', title: 'Clark' }}
|
|
90
|
+
* items={[
|
|
91
|
+
* { label: 'Dashboard', href: '/', Icon: HomeIcon, matchPartial: false },
|
|
92
|
+
* { label: 'Reports', href: '/reports', Icon: ReportIcon, matchPartial: true },
|
|
93
|
+
* { type: 'horizontal-divider' },
|
|
94
|
+
* {
|
|
95
|
+
* label: 'Admin',
|
|
96
|
+
* children: [
|
|
97
|
+
* { label: 'Users', href: '/admin/users', Icon: UsersIcon },
|
|
98
|
+
* { label: 'Billing', href: '/admin/billing', Icon: BillIcon }
|
|
99
|
+
* ]
|
|
100
|
+
* }
|
|
101
|
+
* ]}
|
|
102
|
+
* >
|
|
103
|
+
* {#snippet footer({ collapsed })}
|
|
104
|
+
* <UserMenu compact={collapsed} />
|
|
105
|
+
* {/snippet}
|
|
106
|
+
* </Sidebar>
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
51
109
|
export interface SidebarProps {
|
|
110
|
+
/** Navigation items in render order. */
|
|
52
111
|
items?: NavigationItem[];
|
|
53
112
|
logo: LogoType;
|
|
54
113
|
/** Optional snippet between the logo row and the navigation (receives collapse state) */
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../helper/cls.js';
|
|
3
|
+
import { buildTestId } from '../../helper/testid.js';
|
|
4
|
+
import Button from '../../button/Button.svelte';
|
|
5
|
+
import { Color } from '../../variants.js';
|
|
6
|
+
import type { StepperProps, StepState, StepperStep } from './stepper-types.js';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
steps,
|
|
10
|
+
currentStep = $bindable(0),
|
|
11
|
+
orientation = 'horizontal',
|
|
12
|
+
color = Color.PRIMARY,
|
|
13
|
+
clickableCompleted = true,
|
|
14
|
+
showNav = true,
|
|
15
|
+
backLabel = 'Back',
|
|
16
|
+
nextLabel = 'Next',
|
|
17
|
+
finishLabel = 'Finish',
|
|
18
|
+
content,
|
|
19
|
+
onfinish,
|
|
20
|
+
onchange,
|
|
21
|
+
responsive = true,
|
|
22
|
+
class: className = '',
|
|
23
|
+
testId
|
|
24
|
+
}: StepperProps = $props();
|
|
25
|
+
|
|
26
|
+
function stateFor(index: number, step: StepperStep): StepState {
|
|
27
|
+
if (step.state) return step.state;
|
|
28
|
+
if (index < safeStep) return 'complete';
|
|
29
|
+
if (index === safeStep) return 'active';
|
|
30
|
+
return 'upcoming';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Clamp `currentStep` to valid bounds so an out-of-range bound value
|
|
34
|
+
// (or a parent that shrinks `steps`) can't leave us with an undefined
|
|
35
|
+
// `activeStep` while `canAdvance` reports true. Empty `steps` resolves
|
|
36
|
+
// to 0, which `steps[0]` tolerates (returns undefined handled below).
|
|
37
|
+
const safeStep = $derived(
|
|
38
|
+
steps.length === 0 ? 0 : Math.min(Math.max(currentStep, 0), steps.length - 1)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const activeStep = $derived(steps[safeStep]);
|
|
42
|
+
const isLast = $derived(safeStep === steps.length - 1);
|
|
43
|
+
const canAdvance = $derived(!!activeStep && !(activeStep.disabled ?? false));
|
|
44
|
+
const canGoBack = $derived(safeStep > 0);
|
|
45
|
+
|
|
46
|
+
// Fire `onchange` whenever the effective step index changes from any
|
|
47
|
+
// source — parent updates, `safeStep` clamping after `steps` shrinks,
|
|
48
|
+
// or our own `goTo()`. Docs promise "from any source", so handling it
|
|
49
|
+
// once here (instead of only inside `goTo`) keeps that contract.
|
|
50
|
+
let lastReportedStep: number | undefined = undefined;
|
|
51
|
+
$effect(() => {
|
|
52
|
+
if (lastReportedStep === undefined) {
|
|
53
|
+
lastReportedStep = safeStep;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (safeStep !== lastReportedStep) {
|
|
57
|
+
lastReportedStep = safeStep;
|
|
58
|
+
onchange?.(safeStep);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function goTo(index: number) {
|
|
63
|
+
if (index < 0 || index >= steps.length) return;
|
|
64
|
+
if (index === currentStep) return;
|
|
65
|
+
currentStep = index;
|
|
66
|
+
// `onchange` fires via the effect above.
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleMarkerClick(index: number, state: StepState) {
|
|
70
|
+
// Require both `clickableCompleted` and `index < safeStep` so a
|
|
71
|
+
// consumer-supplied `state: 'complete'` on a future step can't
|
|
72
|
+
// unlock forward navigation — only backward jumps are allowed.
|
|
73
|
+
if (index < safeStep && state === 'complete' && clickableCompleted) goTo(index);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleNext() {
|
|
77
|
+
if (!canAdvance) return;
|
|
78
|
+
if (isLast) {
|
|
79
|
+
onfinish?.();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
goTo(safeStep + 1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleBack() {
|
|
86
|
+
if (!canGoBack) return;
|
|
87
|
+
goTo(safeStep - 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const colorClasses = $derived(
|
|
91
|
+
{
|
|
92
|
+
[Color.DEFAULT]: { bg: 'bg-default-600', ring: 'ring-default-600', text: 'text-default-600' },
|
|
93
|
+
[Color.PRIMARY]: { bg: 'bg-primary-500', ring: 'ring-primary-500', text: 'text-primary-600' },
|
|
94
|
+
[Color.SECONDARY]: {
|
|
95
|
+
bg: 'bg-secondary-500',
|
|
96
|
+
ring: 'ring-secondary-500',
|
|
97
|
+
text: 'text-secondary-600'
|
|
98
|
+
},
|
|
99
|
+
[Color.INFO]: { bg: 'bg-info-500', ring: 'ring-info-500', text: 'text-info-600' },
|
|
100
|
+
[Color.SUCCESS]: { bg: 'bg-success-500', ring: 'ring-success-500', text: 'text-success-600' },
|
|
101
|
+
[Color.WARNING]: { bg: 'bg-warning-500', ring: 'ring-warning-500', text: 'text-warning-600' },
|
|
102
|
+
[Color.DANGER]: { bg: 'bg-danger-500', ring: 'ring-danger-500', text: 'text-danger-600' }
|
|
103
|
+
}[color]
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
function markerClass(state: StepState): string {
|
|
107
|
+
// `[&_svg]:size-4` constrains consumer-provided icon components so
|
|
108
|
+
// they match the built-in checkmark (size-4) inside the size-8 circle.
|
|
109
|
+
const base =
|
|
110
|
+
'flex size-8 items-center justify-center rounded-full border text-sm font-semibold transition-colors [&_svg]:size-4';
|
|
111
|
+
if (state === 'complete') return `${base} ${colorClasses.bg} border-transparent text-white`;
|
|
112
|
+
if (state === 'active')
|
|
113
|
+
return `${base} ${colorClasses.text} bg-white ring-2 ring-offset-2 ${colorClasses.ring} border-transparent`;
|
|
114
|
+
if (state === 'error') return `${base} bg-danger-500 border-transparent text-white`;
|
|
115
|
+
return `${base} border-default-300 bg-white text-default-400`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function connectorBg(index: number): string {
|
|
119
|
+
return index < safeStep ? colorClasses.bg : 'bg-default-200';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// When responsive, the full list hides below `sm` and the compact
|
|
123
|
+
// indicator takes its place. Vertical doesn't need this — it already
|
|
124
|
+
// fits narrow viewports.
|
|
125
|
+
const horizontalListClass = $derived(
|
|
126
|
+
cn('flex flex-row items-start', responsive && 'hidden sm:flex')
|
|
127
|
+
);
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
<div
|
|
131
|
+
class={cn('flex flex-col gap-4', className)}
|
|
132
|
+
data-testid={buildTestId('stepper', undefined, testId)}
|
|
133
|
+
>
|
|
134
|
+
{#if orientation === 'horizontal'}
|
|
135
|
+
{#if responsive}
|
|
136
|
+
<!-- Compact indicator: dots + "Step X of Y · Label", shown only below sm. -->
|
|
137
|
+
<div
|
|
138
|
+
class="flex items-center gap-3 sm:hidden"
|
|
139
|
+
data-testid={buildTestId('stepper', 'compact', testId)}
|
|
140
|
+
>
|
|
141
|
+
<div class="flex items-center gap-1.5" aria-hidden="true">
|
|
142
|
+
{#each steps as _s, i (i)}
|
|
143
|
+
<span
|
|
144
|
+
class={cn(
|
|
145
|
+
'size-2 rounded-full transition-colors',
|
|
146
|
+
i < safeStep
|
|
147
|
+
? colorClasses.bg
|
|
148
|
+
: i === safeStep
|
|
149
|
+
? cn(colorClasses.bg, 'ring-2 ring-offset-1', colorClasses.ring)
|
|
150
|
+
: 'bg-default-200'
|
|
151
|
+
)}
|
|
152
|
+
></span>
|
|
153
|
+
{/each}
|
|
154
|
+
</div>
|
|
155
|
+
{#if activeStep}
|
|
156
|
+
<span class="text-default-800 text-sm font-medium">
|
|
157
|
+
Step {safeStep + 1} of {steps.length}: {activeStep.label}
|
|
158
|
+
</span>
|
|
159
|
+
{/if}
|
|
160
|
+
</div>
|
|
161
|
+
{/if}
|
|
162
|
+
|
|
163
|
+
<ol class={horizontalListClass} aria-label="Progress">
|
|
164
|
+
{#each steps as step, i (step.label + i)}
|
|
165
|
+
{@const state = stateFor(i, step)}
|
|
166
|
+
{@const canClick = i < safeStep && state === 'complete' && clickableCompleted}
|
|
167
|
+
{@const notLast = i < steps.length - 1}
|
|
168
|
+
<li class="flex flex-1 items-start last:flex-none">
|
|
169
|
+
<div class="flex flex-col items-center gap-2">
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
class={cn(markerClass(state), canClick && 'cursor-pointer hover:brightness-95')}
|
|
173
|
+
disabled={!canClick}
|
|
174
|
+
aria-current={state === 'active' ? 'step' : undefined}
|
|
175
|
+
aria-label={step.label}
|
|
176
|
+
onclick={() => handleMarkerClick(i, state)}
|
|
177
|
+
>
|
|
178
|
+
{#if state === 'complete'}
|
|
179
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
180
|
+
<path
|
|
181
|
+
fill-rule="evenodd"
|
|
182
|
+
d="M16.704 5.295a1 1 0 0 1 .001 1.414l-7.5 7.516a1 1 0 0 1-1.414 0l-3.5-3.5a1 1 0 1 1 1.414-1.414l2.793 2.793 6.793-6.809a1 1 0 0 1 1.414 0z"
|
|
183
|
+
clip-rule="evenodd"
|
|
184
|
+
/>
|
|
185
|
+
</svg>
|
|
186
|
+
{:else if step.icon}
|
|
187
|
+
{@const Icon = step.icon}
|
|
188
|
+
<Icon />
|
|
189
|
+
{:else}
|
|
190
|
+
{i + 1}
|
|
191
|
+
{/if}
|
|
192
|
+
</button>
|
|
193
|
+
<div class="flex flex-col text-center">
|
|
194
|
+
<span
|
|
195
|
+
class={cn(
|
|
196
|
+
'text-sm font-medium',
|
|
197
|
+
state === 'upcoming' ? 'text-default-500' : 'text-default-800'
|
|
198
|
+
)}
|
|
199
|
+
>
|
|
200
|
+
{step.label}
|
|
201
|
+
</span>
|
|
202
|
+
{#if step.description}
|
|
203
|
+
<span class="text-default-500 text-xs">{step.description}</span>
|
|
204
|
+
{/if}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
{#if notLast}
|
|
208
|
+
<!-- mt-4 = half the size-8 marker so the line centres on the circle. -->
|
|
209
|
+
<div
|
|
210
|
+
class={cn('mx-2 mt-4 h-0.5 flex-1 transition-colors', connectorBg(i))}
|
|
211
|
+
aria-hidden="true"
|
|
212
|
+
></div>
|
|
213
|
+
{/if}
|
|
214
|
+
</li>
|
|
215
|
+
{/each}
|
|
216
|
+
</ol>
|
|
217
|
+
{:else}
|
|
218
|
+
<!-- Vertical: marker column owns the connector so flex-1 stretches it to the next marker. -->
|
|
219
|
+
<ol class="flex flex-col" aria-label="Progress">
|
|
220
|
+
{#each steps as step, i (step.label + i)}
|
|
221
|
+
{@const state = stateFor(i, step)}
|
|
222
|
+
{@const canClick = i < safeStep && state === 'complete' && clickableCompleted}
|
|
223
|
+
{@const notLast = i < steps.length - 1}
|
|
224
|
+
<li class="flex items-stretch gap-3">
|
|
225
|
+
<div class="flex flex-col items-center">
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
class={cn(markerClass(state), canClick && 'cursor-pointer hover:brightness-95')}
|
|
229
|
+
disabled={!canClick}
|
|
230
|
+
aria-current={state === 'active' ? 'step' : undefined}
|
|
231
|
+
aria-label={step.label}
|
|
232
|
+
onclick={() => handleMarkerClick(i, state)}
|
|
233
|
+
>
|
|
234
|
+
{#if state === 'complete'}
|
|
235
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
236
|
+
<path
|
|
237
|
+
fill-rule="evenodd"
|
|
238
|
+
d="M16.704 5.295a1 1 0 0 1 .001 1.414l-7.5 7.516a1 1 0 0 1-1.414 0l-3.5-3.5a1 1 0 1 1 1.414-1.414l2.793 2.793 6.793-6.809a1 1 0 0 1 1.414 0z"
|
|
239
|
+
clip-rule="evenodd"
|
|
240
|
+
/>
|
|
241
|
+
</svg>
|
|
242
|
+
{:else if step.icon}
|
|
243
|
+
{@const Icon = step.icon}
|
|
244
|
+
<Icon />
|
|
245
|
+
{:else}
|
|
246
|
+
{i + 1}
|
|
247
|
+
{/if}
|
|
248
|
+
</button>
|
|
249
|
+
{#if notLast}
|
|
250
|
+
<div
|
|
251
|
+
class={cn('my-2 w-0.5 flex-1 transition-colors', connectorBg(i))}
|
|
252
|
+
aria-hidden="true"
|
|
253
|
+
></div>
|
|
254
|
+
{/if}
|
|
255
|
+
</div>
|
|
256
|
+
<div class={cn('flex flex-col pt-0.5', notLast && 'pb-6')}>
|
|
257
|
+
<span
|
|
258
|
+
class={cn(
|
|
259
|
+
'text-sm font-medium',
|
|
260
|
+
state === 'upcoming' ? 'text-default-500' : 'text-default-800'
|
|
261
|
+
)}
|
|
262
|
+
>
|
|
263
|
+
{step.label}
|
|
264
|
+
</span>
|
|
265
|
+
{#if step.description}
|
|
266
|
+
<span class="text-default-500 text-xs">{step.description}</span>
|
|
267
|
+
{/if}
|
|
268
|
+
</div>
|
|
269
|
+
</li>
|
|
270
|
+
{/each}
|
|
271
|
+
</ol>
|
|
272
|
+
{/if}
|
|
273
|
+
|
|
274
|
+
{#if content && activeStep}
|
|
275
|
+
<div data-testid={buildTestId('stepper', 'content', testId)}>
|
|
276
|
+
{@render content(safeStep, activeStep)}
|
|
277
|
+
</div>
|
|
278
|
+
{/if}
|
|
279
|
+
|
|
280
|
+
{#if showNav}
|
|
281
|
+
<div class="flex items-center justify-between">
|
|
282
|
+
<Button variant="outline" onclick={handleBack} disabled={!canGoBack}>{backLabel}</Button>
|
|
283
|
+
<Button {color} onclick={handleNext} disabled={!canAdvance}>
|
|
284
|
+
{isLast ? finishLabel : nextLabel}
|
|
285
|
+
</Button>
|
|
286
|
+
</div>
|
|
287
|
+
{/if}
|
|
288
|
+
</div>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ClassValue } from 'tailwind-variants';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { Component } from 'svelte';
|
|
4
|
+
import type { VariantColors } from '../../index.js';
|
|
5
|
+
export type StepperStep = {
|
|
6
|
+
/** Short title shown under the step marker. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Optional sub-label (e.g. "3 min"). */
|
|
9
|
+
description?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Optional icon rendered inside the marker instead of a number. Falls
|
|
12
|
+
* back to the index when omitted.
|
|
13
|
+
*/
|
|
14
|
+
icon?: Component;
|
|
15
|
+
/**
|
|
16
|
+
* Explicit state override. When omitted, state is derived from
|
|
17
|
+
* `currentStep`:
|
|
18
|
+
* - index < currentStep → `'complete'`
|
|
19
|
+
* - index === currentStep → `'active'`
|
|
20
|
+
* - index > currentStep → `'upcoming'`
|
|
21
|
+
*/
|
|
22
|
+
state?: StepState;
|
|
23
|
+
/**
|
|
24
|
+
* When true, the user cannot advance past this step (next button is
|
|
25
|
+
* disabled). Use to gate progression on validation.
|
|
26
|
+
*/
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
};
|
|
29
|
+
export type StepState = 'complete' | 'active' | 'upcoming' | 'error';
|
|
30
|
+
export type StepperOrientation = 'horizontal' | 'vertical';
|
|
31
|
+
export type StepperProps = {
|
|
32
|
+
/** Ordered list of steps. */
|
|
33
|
+
steps: StepperStep[];
|
|
34
|
+
/**
|
|
35
|
+
* Zero-based index of the currently active step. Bindable. @default 0
|
|
36
|
+
*/
|
|
37
|
+
currentStep?: number;
|
|
38
|
+
orientation?: StepperOrientation;
|
|
39
|
+
/** Color accent for the active/complete states. @default 'primary' */
|
|
40
|
+
color?: VariantColors;
|
|
41
|
+
/**
|
|
42
|
+
* Allow clicking completed step markers to jump back. Upcoming steps
|
|
43
|
+
* are never clickable. @default true
|
|
44
|
+
*/
|
|
45
|
+
clickableCompleted?: boolean;
|
|
46
|
+
/** Show the built-in Back/Next navigation below the step list. @default true */
|
|
47
|
+
showNav?: boolean;
|
|
48
|
+
/** Back button label. @default 'Back' */
|
|
49
|
+
backLabel?: string;
|
|
50
|
+
/** Next button label. @default 'Next' */
|
|
51
|
+
nextLabel?: string;
|
|
52
|
+
/** Label for the last step's advance button. @default 'Finish' */
|
|
53
|
+
finishLabel?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Content for the current step. Receives the current index and the
|
|
56
|
+
* step object so you can render per-step panels.
|
|
57
|
+
*
|
|
58
|
+
* ```svelte
|
|
59
|
+
* {#snippet content(index, step)}
|
|
60
|
+
* {#if index === 0}<StepOneForm />{/if}
|
|
61
|
+
* {#if index === 1}<StepTwoForm />{/if}
|
|
62
|
+
* {/snippet}
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
content?: Snippet<[number, StepperStep]>;
|
|
66
|
+
/**
|
|
67
|
+
* Fires when the user advances past the last step. No-op if unset.
|
|
68
|
+
*/
|
|
69
|
+
onfinish?: () => void;
|
|
70
|
+
/** Fires whenever `currentStep` changes (from any source). */
|
|
71
|
+
onchange?: (index: number) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Collapse to a compact "Step X of Y · Label" indicator below the
|
|
74
|
+
* `sm` (640px) breakpoint. Only affects `orientation: 'horizontal'`
|
|
75
|
+
* — vertical stepping already fits on narrow viewports. @default true
|
|
76
|
+
*/
|
|
77
|
+
responsive?: boolean;
|
|
78
|
+
class?: ClassValue;
|
|
79
|
+
testId?: string;
|
|
80
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|