sv5ui 1.7.0 → 1.8.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/dist/Banner/Banner.svelte +162 -0
- package/dist/Banner/Banner.svelte.d.ts +5 -0
- package/dist/Banner/banner.types.d.ts +148 -0
- package/dist/Banner/banner.types.js +1 -0
- package/dist/Banner/banner.variants.d.ts +293 -0
- package/dist/Banner/banner.variants.js +86 -0
- package/dist/Banner/index.d.ts +2 -0
- package/dist/Banner/index.js +1 -0
- package/dist/Editor/Editor.svelte +738 -0
- package/dist/Editor/Editor.svelte.d.ts +6 -0
- package/dist/Editor/EditorUrlPrompt.svelte +111 -0
- package/dist/Editor/EditorUrlPrompt.svelte.d.ts +15 -0
- package/dist/Editor/SlashPopup.svelte +67 -0
- package/dist/Editor/SlashPopup.svelte.d.ts +9 -0
- package/dist/Editor/editor.extensions.d.ts +23 -0
- package/dist/Editor/editor.extensions.js +123 -0
- package/dist/Editor/editor.schemas.d.ts +4 -0
- package/dist/Editor/editor.schemas.js +3 -0
- package/dist/Editor/editor.slash.svelte.d.ts +34 -0
- package/dist/Editor/editor.slash.svelte.js +273 -0
- package/dist/Editor/editor.suggestion.d.ts +7 -0
- package/dist/Editor/editor.suggestion.js +142 -0
- package/dist/Editor/editor.toolbar.d.ts +11 -0
- package/dist/Editor/editor.toolbar.js +212 -0
- package/dist/Editor/editor.types.d.ts +347 -0
- package/dist/Editor/editor.types.js +1 -0
- package/dist/Editor/editor.variants.d.ts +308 -0
- package/dist/Editor/editor.variants.js +150 -0
- package/dist/Editor/index.d.ts +53 -0
- package/dist/Editor/index.js +52 -0
- package/dist/Stepper/Stepper.svelte +292 -0
- package/dist/Stepper/Stepper.svelte.d.ts +5 -0
- package/dist/Stepper/index.d.ts +2 -0
- package/dist/Stepper/index.js +1 -0
- package/dist/Stepper/stepper.types.d.ts +223 -0
- package/dist/Stepper/stepper.types.js +1 -0
- package/dist/Stepper/stepper.variants.d.ts +428 -0
- package/dist/Stepper/stepper.variants.js +204 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +97 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { tv } from 'tailwind-variants';
|
|
2
|
+
export const editorVariants = tv({
|
|
3
|
+
slots: {
|
|
4
|
+
root: [
|
|
5
|
+
'relative w-full rounded-lg border border-outline-variant bg-surface',
|
|
6
|
+
'transition-shadow',
|
|
7
|
+
'focus-within:ring-2 focus-within:ring-offset-0',
|
|
8
|
+
'data-[disabled=true]:opacity-60 data-[disabled=true]:pointer-events-none'
|
|
9
|
+
],
|
|
10
|
+
toolbar: [
|
|
11
|
+
'flex flex-wrap items-center gap-0.5',
|
|
12
|
+
'border-b border-outline-variant',
|
|
13
|
+
'bg-surface-container-low',
|
|
14
|
+
'rounded-t-lg p-1.5'
|
|
15
|
+
],
|
|
16
|
+
toolbarGroup: 'flex items-center gap-0.5',
|
|
17
|
+
toolbarButton: [
|
|
18
|
+
'inline-flex items-center justify-center rounded text-on-surface-variant',
|
|
19
|
+
'hover:bg-surface-container-high hover:text-on-surface',
|
|
20
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
|
21
|
+
'data-[active=true]:bg-primary-container data-[active=true]:text-on-primary-container',
|
|
22
|
+
'disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent',
|
|
23
|
+
'transition-colors'
|
|
24
|
+
],
|
|
25
|
+
toolbarSeparator: 'mx-1 h-5 w-px shrink-0 bg-outline-variant',
|
|
26
|
+
content: [
|
|
27
|
+
'text-on-surface',
|
|
28
|
+
// ----- Inner ProseMirror element (the actual contenteditable) -----
|
|
29
|
+
'[&_.ProseMirror]:outline-none',
|
|
30
|
+
'[&_.ProseMirror]:focus:outline-none',
|
|
31
|
+
'[&_.ProseMirror]:focus-visible:outline-none',
|
|
32
|
+
'[&_.ProseMirror]:p-4',
|
|
33
|
+
'[&_.ProseMirror]:min-h-32',
|
|
34
|
+
'[&_.ProseMirror]:whitespace-pre-wrap',
|
|
35
|
+
'[&_.ProseMirror]:break-words',
|
|
36
|
+
// ----- Placeholder (when editor is empty) -----
|
|
37
|
+
'[&_.is-editor-empty]:before:content-[attr(data-placeholder)]',
|
|
38
|
+
'[&_.is-editor-empty]:before:text-on-surface-variant/60',
|
|
39
|
+
'[&_.is-editor-empty]:before:pointer-events-none',
|
|
40
|
+
'[&_.is-editor-empty]:before:float-left',
|
|
41
|
+
'[&_.is-editor-empty]:before:h-0',
|
|
42
|
+
// ----- Content typography (lightweight in-house prose) -----
|
|
43
|
+
'[&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0',
|
|
44
|
+
'[&_h1]:text-2xl [&_h1]:font-bold [&_h1]:my-3',
|
|
45
|
+
'[&_h2]:text-xl [&_h2]:font-bold [&_h2]:my-3',
|
|
46
|
+
'[&_h3]:text-lg [&_h3]:font-semibold [&_h3]:my-2',
|
|
47
|
+
'[&_ul]:list-disc [&_ul]:ps-6 [&_ul]:my-2',
|
|
48
|
+
'[&_ol]:list-decimal [&_ol]:ps-6 [&_ol]:my-2',
|
|
49
|
+
'[&_li>p]:my-0',
|
|
50
|
+
'[&_blockquote]:border-l-4 [&_blockquote]:border-outline-variant [&_blockquote]:ps-3 [&_blockquote]:italic [&_blockquote]:text-on-surface-variant [&_blockquote]:my-3',
|
|
51
|
+
'[&_code]:rounded [&_code]:bg-surface-container-highest [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-sm [&_code]:font-mono',
|
|
52
|
+
'[&_pre]:rounded-md [&_pre]:bg-surface-container-highest [&_pre]:p-3 [&_pre]:my-3 [&_pre]:overflow-x-auto',
|
|
53
|
+
'[&_pre_code]:bg-transparent [&_pre_code]:p-0',
|
|
54
|
+
'[&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2',
|
|
55
|
+
'[&_a:hover]:text-primary/80',
|
|
56
|
+
'[&_hr]:border-outline-variant [&_hr]:my-4',
|
|
57
|
+
'[&_strong]:font-semibold',
|
|
58
|
+
'[&_em]:italic',
|
|
59
|
+
// ----- Image -----
|
|
60
|
+
'[&_img]:max-w-full [&_img]:h-auto [&_img]:rounded-md [&_img]:my-2',
|
|
61
|
+
// ----- YouTube embed (responsive 16:9) -----
|
|
62
|
+
'[&_iframe[src*="youtube"]]:aspect-video [&_iframe[src*="youtube"]]:w-full [&_iframe[src*="youtube"]]:rounded-md [&_iframe[src*="youtube"]]:my-3 [&_iframe[src*="youtube"]]:border-0',
|
|
63
|
+
// ----- Mention chip -----
|
|
64
|
+
'[&_.sv5ui-editor-mention]:inline-flex [&_.sv5ui-editor-mention]:items-center',
|
|
65
|
+
'[&_.sv5ui-editor-mention]:rounded [&_.sv5ui-editor-mention]:bg-primary-container/60',
|
|
66
|
+
'[&_.sv5ui-editor-mention]:text-on-primary-container',
|
|
67
|
+
'[&_.sv5ui-editor-mention]:px-1.5 [&_.sv5ui-editor-mention]:py-0.5',
|
|
68
|
+
'[&_.sv5ui-editor-mention]:text-sm [&_.sv5ui-editor-mention]:font-medium',
|
|
69
|
+
// ----- Table -----
|
|
70
|
+
'[&_.tableWrapper]:my-3 [&_.tableWrapper]:overflow-x-auto',
|
|
71
|
+
'[&_table]:w-full [&_table]:border-collapse [&_table]:table-fixed',
|
|
72
|
+
'[&_table]:border [&_table]:border-outline-variant [&_table]:rounded-md',
|
|
73
|
+
'[&_th]:border [&_th]:border-outline-variant',
|
|
74
|
+
'[&_th]:bg-surface-container-low [&_th]:px-2 [&_th]:py-1.5',
|
|
75
|
+
'[&_th]:font-semibold [&_th]:text-start [&_th]:align-top',
|
|
76
|
+
'[&_th]:relative [&_th]:min-w-16',
|
|
77
|
+
'[&_td]:border [&_td]:border-outline-variant',
|
|
78
|
+
'[&_td]:px-2 [&_td]:py-1.5 [&_td]:align-top',
|
|
79
|
+
'[&_td]:relative [&_td]:min-w-16',
|
|
80
|
+
// Tiptap selectedCell highlight + column resize handle
|
|
81
|
+
'[&_.selectedCell]:bg-primary/10',
|
|
82
|
+
'[&_.column-resize-handle]:absolute [&_.column-resize-handle]:top-0 [&_.column-resize-handle]:bottom-[-2px]',
|
|
83
|
+
'[&_.column-resize-handle]:-right-0.5 [&_.column-resize-handle]:w-1',
|
|
84
|
+
'[&_.column-resize-handle]:bg-primary/40 [&_.column-resize-handle]:pointer-events-none',
|
|
85
|
+
// ----- Selection styling -----
|
|
86
|
+
'[&_::selection]:bg-primary/20'
|
|
87
|
+
],
|
|
88
|
+
footer: [
|
|
89
|
+
'flex items-center justify-between gap-2',
|
|
90
|
+
'border-t border-outline-variant',
|
|
91
|
+
'bg-surface-container-low',
|
|
92
|
+
'px-3 py-1.5 rounded-b-lg',
|
|
93
|
+
'text-xs text-on-surface-variant'
|
|
94
|
+
],
|
|
95
|
+
countLabel: 'tabular-nums',
|
|
96
|
+
bubbleMenu: [
|
|
97
|
+
// ⚠️ Tiptap's BubbleMenu only sets `visibility:hidden` initially,
|
|
98
|
+
// leaving the element in normal flow (taking up space below the editor)
|
|
99
|
+
// until the user first selects text. Setting `position:absolute` upfront
|
|
100
|
+
// keeps it out of flow from mount — extension's Floating UI positioning
|
|
101
|
+
// then overrides left/top inline on selection.
|
|
102
|
+
'absolute top-0 left-0 z-50 invisible',
|
|
103
|
+
'flex items-center gap-0.5 p-1',
|
|
104
|
+
'rounded-lg border border-outline-variant bg-surface',
|
|
105
|
+
'shadow-md'
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
variants: {
|
|
109
|
+
size: {
|
|
110
|
+
sm: {
|
|
111
|
+
toolbarButton: 'size-7 *:size-3.5',
|
|
112
|
+
content: 'text-sm [&_.ProseMirror]:p-3 [&_.ProseMirror]:min-h-24',
|
|
113
|
+
footer: 'text-[11px]'
|
|
114
|
+
},
|
|
115
|
+
md: {
|
|
116
|
+
toolbarButton: 'size-8 *:size-4',
|
|
117
|
+
content: 'text-sm',
|
|
118
|
+
footer: ''
|
|
119
|
+
},
|
|
120
|
+
lg: {
|
|
121
|
+
toolbarButton: 'size-9 *:size-5',
|
|
122
|
+
content: 'text-base [&_.ProseMirror]:p-5 [&_.ProseMirror]:min-h-40',
|
|
123
|
+
footer: ''
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
color: {
|
|
127
|
+
primary: { root: 'focus-within:ring-primary' },
|
|
128
|
+
secondary: { root: 'focus-within:ring-secondary' },
|
|
129
|
+
tertiary: { root: 'focus-within:ring-tertiary' },
|
|
130
|
+
success: { root: 'focus-within:ring-success' },
|
|
131
|
+
warning: { root: 'focus-within:ring-warning' },
|
|
132
|
+
error: { root: 'focus-within:ring-error' },
|
|
133
|
+
info: { root: 'focus-within:ring-info' },
|
|
134
|
+
surface: { root: 'focus-within:ring-outline' }
|
|
135
|
+
},
|
|
136
|
+
sticky: {
|
|
137
|
+
true: { toolbar: 'sticky top-0 z-10 backdrop-blur-sm' },
|
|
138
|
+
false: ''
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
defaultVariants: {
|
|
142
|
+
size: 'md',
|
|
143
|
+
color: 'primary',
|
|
144
|
+
sticky: false
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
export const editorDefaults = {
|
|
148
|
+
defaultVariants: editorVariants.defaultVariants,
|
|
149
|
+
slots: {}
|
|
150
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SV5UI Editor — rich-text WYSIWYG built on Tiptap v3 + ProseMirror.
|
|
3
|
+
*
|
|
4
|
+
* ## Installation
|
|
5
|
+
*
|
|
6
|
+
* Tiptap is declared as an optional peer dependency of sv5ui, so consumers
|
|
7
|
+
* who don't use `<Editor>` pay zero install/bundle cost. When you do use it,
|
|
8
|
+
* install Tiptap alongside sv5ui:
|
|
9
|
+
*
|
|
10
|
+
* ```bash
|
|
11
|
+
* pnpm add sv5ui \
|
|
12
|
+
* @tiptap/core @tiptap/pm @tiptap/starter-kit \
|
|
13
|
+
* @tiptap/extension-bubble-menu \
|
|
14
|
+
* @tiptap/extension-character-count \
|
|
15
|
+
* @tiptap/extension-drag-handle \
|
|
16
|
+
* @tiptap/extension-image \
|
|
17
|
+
* @tiptap/extension-mention \
|
|
18
|
+
* @tiptap/extension-placeholder \
|
|
19
|
+
* @tiptap/extension-table \
|
|
20
|
+
* @tiptap/extension-table-cell \
|
|
21
|
+
* @tiptap/extension-table-header \
|
|
22
|
+
* @tiptap/extension-table-row \
|
|
23
|
+
* @tiptap/extension-text-align \
|
|
24
|
+
* @tiptap/extension-typography \
|
|
25
|
+
* @tiptap/extension-youtube \
|
|
26
|
+
* @tiptap/suggestion \
|
|
27
|
+
* tiptap-markdown
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* (`npm add` / `yarn add` work equally well.)
|
|
31
|
+
*
|
|
32
|
+
* All 18 packages are required, even if you only enable a subset of
|
|
33
|
+
* features. Editor imports every extension at module load so toggling
|
|
34
|
+
* `image`, `tables`, `youtube`, etc. via props doesn't trigger a dynamic
|
|
35
|
+
* import. Missing peers fail at runtime with module-resolution errors.
|
|
36
|
+
*
|
|
37
|
+
* ## Usage
|
|
38
|
+
*
|
|
39
|
+
* ```svelte
|
|
40
|
+
* <script lang="ts">
|
|
41
|
+
* import { Editor, type EditorApi } from 'sv5ui/editor'
|
|
42
|
+
* let value = $state('<p>Hello</p>')
|
|
43
|
+
* let api = $state<EditorApi>()
|
|
44
|
+
* </script>
|
|
45
|
+
*
|
|
46
|
+
* <Editor bind:value bind:api placeholder="Write something…" />
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* See {@link EditorProps} for every option.
|
|
50
|
+
*/
|
|
51
|
+
export { default as Editor } from './Editor.svelte';
|
|
52
|
+
export { buildDefaultSlashCommands } from './editor.slash.svelte.js';
|
|
53
|
+
export type { EditorProps, EditorApi, EditorJSON, EditorReactiveState, MentionItem, SlashCommand, ToolbarAction } from './editor.types.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SV5UI Editor — rich-text WYSIWYG built on Tiptap v3 + ProseMirror.
|
|
3
|
+
*
|
|
4
|
+
* ## Installation
|
|
5
|
+
*
|
|
6
|
+
* Tiptap is declared as an optional peer dependency of sv5ui, so consumers
|
|
7
|
+
* who don't use `<Editor>` pay zero install/bundle cost. When you do use it,
|
|
8
|
+
* install Tiptap alongside sv5ui:
|
|
9
|
+
*
|
|
10
|
+
* ```bash
|
|
11
|
+
* pnpm add sv5ui \
|
|
12
|
+
* @tiptap/core @tiptap/pm @tiptap/starter-kit \
|
|
13
|
+
* @tiptap/extension-bubble-menu \
|
|
14
|
+
* @tiptap/extension-character-count \
|
|
15
|
+
* @tiptap/extension-drag-handle \
|
|
16
|
+
* @tiptap/extension-image \
|
|
17
|
+
* @tiptap/extension-mention \
|
|
18
|
+
* @tiptap/extension-placeholder \
|
|
19
|
+
* @tiptap/extension-table \
|
|
20
|
+
* @tiptap/extension-table-cell \
|
|
21
|
+
* @tiptap/extension-table-header \
|
|
22
|
+
* @tiptap/extension-table-row \
|
|
23
|
+
* @tiptap/extension-text-align \
|
|
24
|
+
* @tiptap/extension-typography \
|
|
25
|
+
* @tiptap/extension-youtube \
|
|
26
|
+
* @tiptap/suggestion \
|
|
27
|
+
* tiptap-markdown
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* (`npm add` / `yarn add` work equally well.)
|
|
31
|
+
*
|
|
32
|
+
* All 18 packages are required, even if you only enable a subset of
|
|
33
|
+
* features. Editor imports every extension at module load so toggling
|
|
34
|
+
* `image`, `tables`, `youtube`, etc. via props doesn't trigger a dynamic
|
|
35
|
+
* import. Missing peers fail at runtime with module-resolution errors.
|
|
36
|
+
*
|
|
37
|
+
* ## Usage
|
|
38
|
+
*
|
|
39
|
+
* ```svelte
|
|
40
|
+
* <script lang="ts">
|
|
41
|
+
* import { Editor, type EditorApi } from 'sv5ui/editor'
|
|
42
|
+
* let value = $state('<p>Hello</p>')
|
|
43
|
+
* let api = $state<EditorApi>()
|
|
44
|
+
* </script>
|
|
45
|
+
*
|
|
46
|
+
* <Editor bind:value bind:api placeholder="Write something…" />
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* See {@link EditorProps} for every option.
|
|
50
|
+
*/
|
|
51
|
+
export { default as Editor } from './Editor.svelte';
|
|
52
|
+
export { buildDefaultSlashCommands } from './editor.slash.svelte.js';
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { StepperProps } from './stepper.types.js'
|
|
3
|
+
|
|
4
|
+
export type Props = StepperProps
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import type { ClassNameValue } from 'tailwind-merge'
|
|
9
|
+
import { untrack } from 'svelte'
|
|
10
|
+
import { stepperVariants, stepperDefaults } from './stepper.variants.js'
|
|
11
|
+
import type { StepperItem, StepperItemState, StepperApi } from './stepper.types.js'
|
|
12
|
+
import { getComponentConfig } from '../config.js'
|
|
13
|
+
import Icon from '../Icon/Icon.svelte'
|
|
14
|
+
|
|
15
|
+
const config = getComponentConfig('stepper', stepperDefaults)
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
ref = $bindable(null),
|
|
19
|
+
as = 'div',
|
|
20
|
+
items = [],
|
|
21
|
+
value = $bindable(),
|
|
22
|
+
defaultValue,
|
|
23
|
+
onValueChange,
|
|
24
|
+
api = $bindable(),
|
|
25
|
+
linear = true,
|
|
26
|
+
disabled = false,
|
|
27
|
+
orientation = config.defaultVariants.orientation ?? 'horizontal',
|
|
28
|
+
size = config.defaultVariants.size ?? 'md',
|
|
29
|
+
color = config.defaultVariants.color ?? 'primary',
|
|
30
|
+
content: showContent = true,
|
|
31
|
+
class: className,
|
|
32
|
+
ui,
|
|
33
|
+
indicator: indicatorSlot,
|
|
34
|
+
titleSlot,
|
|
35
|
+
descriptionSlot,
|
|
36
|
+
body,
|
|
37
|
+
...restProps
|
|
38
|
+
}: Props = $props()
|
|
39
|
+
|
|
40
|
+
if (value === undefined) {
|
|
41
|
+
value = untrack(() => defaultValue ?? items[0]?.value ?? 0)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let triggers: (HTMLButtonElement | null)[] = $state([])
|
|
45
|
+
|
|
46
|
+
function getItemValue(item: StepperItem, index: number): string | number {
|
|
47
|
+
return item.value ?? index
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const activeIndex = $derived(items.findIndex((item, i) => getItemValue(item, i) === value))
|
|
51
|
+
|
|
52
|
+
function getState(index: number): StepperItemState {
|
|
53
|
+
if (activeIndex === -1) return 'pending'
|
|
54
|
+
if (index === activeIndex) return 'active'
|
|
55
|
+
return index < activeIndex ? 'completed' : 'pending'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function canActivate(index: number): boolean {
|
|
59
|
+
if (disabled || items[index]?.disabled) return false
|
|
60
|
+
if (!linear) return true
|
|
61
|
+
return index <= activeIndex + 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function setValue(next: string | number) {
|
|
65
|
+
if (next === value) return
|
|
66
|
+
value = next
|
|
67
|
+
onValueChange?.(next)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function handleClick(item: StepperItem, index: number) {
|
|
71
|
+
if (!canActivate(index)) return
|
|
72
|
+
setValue(getItemValue(item, index))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function findEnabled(start: number, dir: 1 | -1): number {
|
|
76
|
+
for (let i = start; i >= 0 && i < items.length; i += dir) {
|
|
77
|
+
if (!items[i]?.disabled) return i
|
|
78
|
+
}
|
|
79
|
+
return -1
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function handleKeydown(e: KeyboardEvent, index: number) {
|
|
83
|
+
const horizontal = orientation === 'horizontal'
|
|
84
|
+
const nextKey = horizontal ? 'ArrowRight' : 'ArrowDown'
|
|
85
|
+
const prevKey = horizontal ? 'ArrowLeft' : 'ArrowUp'
|
|
86
|
+
|
|
87
|
+
let target = -1
|
|
88
|
+
if (e.key === nextKey) target = findEnabled(index + 1, 1)
|
|
89
|
+
else if (e.key === prevKey) target = findEnabled(index - 1, -1)
|
|
90
|
+
else if (e.key === 'Home') target = findEnabled(0, 1)
|
|
91
|
+
else if (e.key === 'End') target = findEnabled(items.length - 1, -1)
|
|
92
|
+
else return
|
|
93
|
+
|
|
94
|
+
e.preventDefault()
|
|
95
|
+
if (target >= 0) triggers[target]?.focus()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const apiInstance: StepperApi = {
|
|
99
|
+
next() {
|
|
100
|
+
if (activeIndex >= items.length - 1) return
|
|
101
|
+
const target = items[activeIndex + 1]
|
|
102
|
+
if (!target) return
|
|
103
|
+
setValue(getItemValue(target, activeIndex + 1))
|
|
104
|
+
},
|
|
105
|
+
prev() {
|
|
106
|
+
if (activeIndex <= 0) return
|
|
107
|
+
const target = items[activeIndex - 1]
|
|
108
|
+
if (!target) return
|
|
109
|
+
setValue(getItemValue(target, activeIndex - 1))
|
|
110
|
+
},
|
|
111
|
+
goTo(next) {
|
|
112
|
+
setValue(next)
|
|
113
|
+
},
|
|
114
|
+
get hasNext() {
|
|
115
|
+
return activeIndex >= 0 && activeIndex < items.length - 1
|
|
116
|
+
},
|
|
117
|
+
get hasPrev() {
|
|
118
|
+
return activeIndex > 0
|
|
119
|
+
},
|
|
120
|
+
get activeIndex() {
|
|
121
|
+
return activeIndex
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
$effect.pre(() => {
|
|
126
|
+
api = apiInstance
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const variantSlots = $derived(stepperVariants({ orientation, size, color }))
|
|
130
|
+
|
|
131
|
+
type ItemUi = StepperItem['ui']
|
|
132
|
+
|
|
133
|
+
const classes = $derived.by(() => {
|
|
134
|
+
const c = config.slots
|
|
135
|
+
const slots = variantSlots
|
|
136
|
+
const u = ui ?? {}
|
|
137
|
+
|
|
138
|
+
const _item = slots.item({ class: [c.item, u.item] })
|
|
139
|
+
const _trigger = slots.trigger({ class: [c.trigger, u.trigger] })
|
|
140
|
+
const _container = slots.container({ class: [c.container, u.container] })
|
|
141
|
+
const _indicator = slots.indicator({ class: [c.indicator, u.indicator] })
|
|
142
|
+
const _separator = slots.separator({ class: [c.separator, u.separator] })
|
|
143
|
+
const _wrapper = slots.wrapper({ class: [c.wrapper, u.wrapper] })
|
|
144
|
+
const _title = slots.title({ class: [c.title, u.title] })
|
|
145
|
+
const _description = slots.description({ class: [c.description, u.description] })
|
|
146
|
+
const _content = slots.content({ class: [c.content, u.content] })
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
root: slots.root({ class: [c.root, className, u.root] }),
|
|
150
|
+
list: slots.list({ class: [c.list, u.list] }),
|
|
151
|
+
item: (itemClass?: ClassNameValue, itemUi?: ItemUi) =>
|
|
152
|
+
itemClass || itemUi
|
|
153
|
+
? slots.item({ class: [c.item, u.item, itemUi?.item, itemClass] })
|
|
154
|
+
: _item,
|
|
155
|
+
trigger: (itemUi?: ItemUi) =>
|
|
156
|
+
itemUi
|
|
157
|
+
? slots.trigger({ class: [c.trigger, u.trigger, itemUi.trigger] })
|
|
158
|
+
: _trigger,
|
|
159
|
+
container: (itemUi?: ItemUi) =>
|
|
160
|
+
itemUi
|
|
161
|
+
? slots.container({ class: [c.container, u.container, itemUi.container] })
|
|
162
|
+
: _container,
|
|
163
|
+
indicator: (itemUi?: ItemUi) =>
|
|
164
|
+
itemUi
|
|
165
|
+
? slots.indicator({ class: [c.indicator, u.indicator, itemUi.indicator] })
|
|
166
|
+
: _indicator,
|
|
167
|
+
separator: (itemUi?: ItemUi) =>
|
|
168
|
+
itemUi
|
|
169
|
+
? slots.separator({ class: [c.separator, u.separator, itemUi.separator] })
|
|
170
|
+
: _separator,
|
|
171
|
+
wrapper: (itemUi?: ItemUi) =>
|
|
172
|
+
itemUi
|
|
173
|
+
? slots.wrapper({ class: [c.wrapper, u.wrapper, itemUi.wrapper] })
|
|
174
|
+
: _wrapper,
|
|
175
|
+
title: (itemUi?: ItemUi) =>
|
|
176
|
+
itemUi ? slots.title({ class: [c.title, u.title, itemUi.title] }) : _title,
|
|
177
|
+
description: (itemUi?: ItemUi) =>
|
|
178
|
+
itemUi
|
|
179
|
+
? slots.description({
|
|
180
|
+
class: [c.description, u.description, itemUi.description]
|
|
181
|
+
})
|
|
182
|
+
: _description,
|
|
183
|
+
content: (itemUi?: ItemUi) =>
|
|
184
|
+
itemUi ? slots.content({ class: [c.content, u.content, itemUi.content] }) : _content
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<svelte:element
|
|
190
|
+
this={as}
|
|
191
|
+
bind:this={ref}
|
|
192
|
+
class={classes.root}
|
|
193
|
+
data-orientation={orientation}
|
|
194
|
+
{...restProps}
|
|
195
|
+
>
|
|
196
|
+
<ol class={classes.list}>
|
|
197
|
+
{#each items as item, index (item.value ?? index)}
|
|
198
|
+
{@const state = getState(index)}
|
|
199
|
+
{@const number = index + 1}
|
|
200
|
+
{@const active = state === 'active'}
|
|
201
|
+
{@const itemDisabled = disabled || item.disabled || !canActivate(index)}
|
|
202
|
+
{@const slotProps = { item, index, number, state, active }}
|
|
203
|
+
{@const hasVisibleText = !!(
|
|
204
|
+
titleSlot ||
|
|
205
|
+
descriptionSlot ||
|
|
206
|
+
item.title ||
|
|
207
|
+
item.description
|
|
208
|
+
)}
|
|
209
|
+
<li
|
|
210
|
+
class={classes.item(item.class, item.ui)}
|
|
211
|
+
data-state={state}
|
|
212
|
+
data-orientation={orientation}
|
|
213
|
+
data-stepper-item=""
|
|
214
|
+
>
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
bind:this={triggers[index]}
|
|
218
|
+
class={classes.trigger(item.ui)}
|
|
219
|
+
disabled={itemDisabled}
|
|
220
|
+
aria-current={active ? 'step' : undefined}
|
|
221
|
+
aria-label={hasVisibleText ? undefined : `Step ${number}`}
|
|
222
|
+
tabindex={active ? 0 : -1}
|
|
223
|
+
onclick={() => handleClick(item, index)}
|
|
224
|
+
onkeydown={(e) => handleKeydown(e, index)}
|
|
225
|
+
>
|
|
226
|
+
<span class={classes.container(item.ui)}>
|
|
227
|
+
{#if indicatorSlot}
|
|
228
|
+
{@render indicatorSlot(slotProps)}
|
|
229
|
+
{:else}
|
|
230
|
+
<span
|
|
231
|
+
class={classes.indicator(item.ui)}
|
|
232
|
+
data-stepper-indicator=""
|
|
233
|
+
aria-hidden="true"
|
|
234
|
+
>
|
|
235
|
+
{#if item.icon}
|
|
236
|
+
<Icon name={item.icon} />
|
|
237
|
+
{:else if state === 'completed'}
|
|
238
|
+
<Icon name="lucide:check" />
|
|
239
|
+
{:else}
|
|
240
|
+
{number}
|
|
241
|
+
{/if}
|
|
242
|
+
</span>
|
|
243
|
+
{/if}
|
|
244
|
+
|
|
245
|
+
{#if index < items.length - 1}
|
|
246
|
+
<span
|
|
247
|
+
class={classes.separator(item.ui)}
|
|
248
|
+
data-stepper-separator=""
|
|
249
|
+
aria-hidden="true"
|
|
250
|
+
></span>
|
|
251
|
+
{/if}
|
|
252
|
+
</span>
|
|
253
|
+
|
|
254
|
+
{#if hasVisibleText}
|
|
255
|
+
<span class={classes.wrapper(item.ui)}>
|
|
256
|
+
{#if titleSlot}
|
|
257
|
+
{@render titleSlot(slotProps)}
|
|
258
|
+
{:else if item.title}
|
|
259
|
+
<span class={classes.title(item.ui)}>{item.title}</span>
|
|
260
|
+
{/if}
|
|
261
|
+
|
|
262
|
+
{#if descriptionSlot}
|
|
263
|
+
{@render descriptionSlot(slotProps)}
|
|
264
|
+
{:else if item.description}
|
|
265
|
+
<span class={classes.description(item.ui)}>
|
|
266
|
+
{item.description}
|
|
267
|
+
</span>
|
|
268
|
+
{/if}
|
|
269
|
+
</span>
|
|
270
|
+
{/if}
|
|
271
|
+
</button>
|
|
272
|
+
</li>
|
|
273
|
+
{/each}
|
|
274
|
+
</ol>
|
|
275
|
+
|
|
276
|
+
{#if showContent}
|
|
277
|
+
{#each items as item, index (item.value ?? index)}
|
|
278
|
+
{@const state = getState(index)}
|
|
279
|
+
{#if state === 'active' && (body || item.content !== undefined)}
|
|
280
|
+
{@const number = index + 1}
|
|
281
|
+
{@const slotProps = { item, index, number, state, active: true }}
|
|
282
|
+
<div class={classes.content(item.ui)} role="region" data-stepper-content="">
|
|
283
|
+
{#if body}
|
|
284
|
+
{@render body(slotProps)}
|
|
285
|
+
{:else}
|
|
286
|
+
{item.content}
|
|
287
|
+
{/if}
|
|
288
|
+
</div>
|
|
289
|
+
{/if}
|
|
290
|
+
{/each}
|
|
291
|
+
{/if}
|
|
292
|
+
</svelte:element>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Stepper } from './Stepper.svelte';
|