@mrintel/villain-ui 0.3.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3490 -1296
  3. package/dist/components/buttons/Button.svelte +27 -33
  4. package/dist/components/buttons/Button.svelte.d.ts +4 -1
  5. package/dist/components/buttons/ButtonGroup.svelte +17 -30
  6. package/dist/components/buttons/FloatingActionButton.svelte +20 -44
  7. package/dist/components/buttons/FloatingActionButton.svelte.d.ts +2 -1
  8. package/dist/components/buttons/IconButton.svelte +23 -53
  9. package/dist/components/buttons/IconButton.svelte.d.ts +2 -1
  10. package/dist/components/buttons/LinkButton.svelte +24 -37
  11. package/dist/components/buttons/LinkButton.svelte.d.ts +4 -1
  12. package/dist/components/buttons/buttonClasses.d.ts +5 -0
  13. package/dist/components/buttons/buttonClasses.js +8 -3
  14. package/dist/components/cards/Card.svelte +54 -46
  15. package/dist/components/cards/Card.svelte.d.ts +9 -2
  16. package/dist/components/cards/Container.svelte +17 -33
  17. package/dist/components/cards/Divider.svelte +36 -52
  18. package/dist/components/cards/Divider.svelte.d.ts +2 -0
  19. package/dist/components/cards/Grid.svelte +55 -44
  20. package/dist/components/cards/Panel.svelte +18 -32
  21. package/dist/components/cards/Panel.svelte.d.ts +2 -1
  22. package/dist/components/cards/SectionHeader.svelte +24 -38
  23. package/dist/components/cards/SectionHeader.svelte.d.ts +1 -0
  24. package/dist/components/data/Avatar.svelte +48 -67
  25. package/dist/components/data/Badge.svelte +45 -32
  26. package/dist/components/data/Badge.svelte.d.ts +7 -1
  27. package/dist/components/data/CalendarGrid.svelte +433 -0
  28. package/dist/components/data/CalendarGrid.svelte.d.ts +25 -0
  29. package/dist/components/data/CalendarGrid.types.d.ts +7 -0
  30. package/dist/components/data/CalendarGrid.types.js +1 -0
  31. package/dist/components/data/CodeBlock.svelte +119 -121
  32. package/dist/components/data/CodeBlock.svelte.d.ts +8 -0
  33. package/dist/components/data/List.svelte +87 -64
  34. package/dist/components/data/List.svelte.d.ts +7 -0
  35. package/dist/components/data/Pagination.svelte +121 -123
  36. package/dist/components/data/Pagination.svelte.d.ts +5 -0
  37. package/dist/components/data/Sparkline.svelte +117 -0
  38. package/dist/components/data/Sparkline.svelte.d.ts +43 -0
  39. package/dist/components/data/Stat.svelte +92 -103
  40. package/dist/components/data/Table.svelte +443 -76
  41. package/dist/components/data/Table.svelte.d.ts +23 -2
  42. package/dist/components/data/Table.types.d.ts +14 -0
  43. package/dist/components/data/Table.types.js +1 -0
  44. package/dist/components/data/Tag.svelte +51 -53
  45. package/dist/components/data/Tag.svelte.d.ts +5 -1
  46. package/dist/components/data/index.d.ts +4 -0
  47. package/dist/components/data/index.js +2 -0
  48. package/dist/components/forms/Checkbox.svelte +39 -51
  49. package/dist/components/forms/Checkbox.svelte.d.ts +3 -1
  50. package/dist/components/forms/DatePicker.svelte +61 -0
  51. package/dist/components/forms/DatePicker.svelte.d.ts +15 -0
  52. package/dist/components/forms/DateTimePicker.svelte +63 -0
  53. package/dist/components/forms/DateTimePicker.svelte.d.ts +16 -0
  54. package/dist/components/forms/FileUpload.svelte +136 -164
  55. package/dist/components/forms/FileUpload.svelte.d.ts +1 -0
  56. package/dist/components/forms/Input.svelte +284 -57
  57. package/dist/components/forms/Input.svelte.d.ts +10 -3
  58. package/dist/components/forms/InputGroup.svelte +7 -7
  59. package/dist/components/forms/RadioGroup.svelte +77 -87
  60. package/dist/components/forms/RadioGroup.svelte.d.ts +3 -1
  61. package/dist/components/forms/RangeSlider.svelte +90 -116
  62. package/dist/components/forms/Select.svelte +106 -71
  63. package/dist/components/forms/Select.svelte.d.ts +3 -1
  64. package/dist/components/forms/Step.svelte +25 -0
  65. package/dist/components/forms/Step.svelte.d.ts +12 -0
  66. package/dist/components/forms/StepContext.d.ts +3 -0
  67. package/dist/components/forms/StepContext.js +5 -0
  68. package/dist/components/forms/Stepper.types.d.ts +37 -0
  69. package/dist/components/forms/Stepper.types.js +1 -0
  70. package/dist/components/forms/StepperForm.svelte +183 -0
  71. package/dist/components/forms/StepperForm.svelte.d.ts +17 -0
  72. package/dist/components/forms/Switch.svelte +44 -56
  73. package/dist/components/forms/Switch.svelte.d.ts +3 -1
  74. package/dist/components/forms/Textarea.svelte +52 -57
  75. package/dist/components/forms/Textarea.svelte.d.ts +3 -1
  76. package/dist/components/forms/TimePicker.svelte +63 -0
  77. package/dist/components/forms/TimePicker.svelte.d.ts +16 -0
  78. package/dist/components/forms/formClasses.d.ts +3 -0
  79. package/dist/components/forms/formClasses.js +3 -0
  80. package/dist/components/forms/index.d.ts +6 -0
  81. package/dist/components/forms/index.js +5 -0
  82. package/dist/components/navigation/Breadcrumbs.svelte +56 -59
  83. package/dist/components/navigation/Breadcrumbs.svelte.d.ts +1 -0
  84. package/dist/components/navigation/ContextMenu.svelte +133 -83
  85. package/dist/components/navigation/ContextMenu.svelte.d.ts +8 -1
  86. package/dist/components/navigation/DropdownMenu.svelte +139 -80
  87. package/dist/components/navigation/DropdownMenu.svelte.d.ts +8 -1
  88. package/dist/components/navigation/Menu.svelte +72 -48
  89. package/dist/components/navigation/Navbar.svelte +111 -32
  90. package/dist/components/navigation/Navbar.svelte.d.ts +6 -0
  91. package/dist/components/navigation/Sidebar.svelte +236 -35
  92. package/dist/components/navigation/Sidebar.svelte.d.ts +2 -0
  93. package/dist/components/navigation/Stepper.svelte +204 -0
  94. package/dist/components/navigation/Stepper.svelte.d.ts +34 -0
  95. package/dist/components/navigation/Tabs.svelte +86 -54
  96. package/dist/components/navigation/Tabs.svelte.d.ts +5 -1
  97. package/dist/components/navigation/index.d.ts +1 -0
  98. package/dist/components/navigation/index.js +1 -0
  99. package/dist/components/overlays/Alert.svelte +81 -99
  100. package/dist/components/overlays/Alert.svelte.d.ts +5 -1
  101. package/dist/components/overlays/CommandPalette.svelte +182 -217
  102. package/dist/components/overlays/Drawer.svelte +158 -167
  103. package/dist/components/overlays/Drawer.svelte.d.ts +3 -1
  104. package/dist/components/overlays/Dropdown.svelte +62 -30
  105. package/dist/components/overlays/Dropdown.svelte.d.ts +2 -0
  106. package/dist/components/overlays/Modal.svelte +125 -130
  107. package/dist/components/overlays/Modal.svelte.d.ts +3 -1
  108. package/dist/components/overlays/Popover.svelte +106 -131
  109. package/dist/components/overlays/ProgressBar.svelte +29 -45
  110. package/dist/components/overlays/SkeletonLoader.svelte +66 -82
  111. package/dist/components/overlays/Spinner.svelte +33 -43
  112. package/dist/components/overlays/Toast.svelte +111 -140
  113. package/dist/components/overlays/Toast.svelte.d.ts +3 -0
  114. package/dist/components/overlays/Tooltip.svelte +94 -115
  115. package/dist/components/overlays/Tooltip.svelte.d.ts +3 -1
  116. package/dist/components/typography/Code.svelte +10 -14
  117. package/dist/components/typography/Heading.svelte +15 -22
  118. package/dist/components/typography/Heading.svelte.d.ts +1 -0
  119. package/dist/components/typography/Text.svelte +21 -24
  120. package/dist/components/typography/Text.svelte.d.ts +2 -1
  121. package/dist/components/utilities/Accordion.svelte +54 -67
  122. package/dist/components/utilities/Accordion.svelte.d.ts +4 -1
  123. package/dist/components/utilities/Carousel.svelte +124 -152
  124. package/dist/components/utilities/Collapse.svelte +46 -60
  125. package/dist/components/utilities/Hero.svelte +42 -0
  126. package/dist/components/utilities/Hero.svelte.d.ts +10 -0
  127. package/dist/components/utilities/Portal.svelte +47 -72
  128. package/dist/components/utilities/ScrollArea.svelte +33 -41
  129. package/dist/components/utilities/SystemConsole.svelte +310 -0
  130. package/dist/components/utilities/SystemConsole.svelte.d.ts +20 -0
  131. package/dist/components/utilities/SystemInterface.svelte +726 -0
  132. package/dist/components/utilities/SystemInterface.svelte.d.ts +19 -0
  133. package/dist/components/utilities/index.d.ts +4 -0
  134. package/dist/components/utilities/index.js +3 -0
  135. package/dist/components/utilities/utilities.types.d.ts +46 -0
  136. package/dist/components/utilities/utilities.types.js +4 -0
  137. package/dist/index.d.ts +57 -5
  138. package/dist/index.js +5 -5
  139. package/dist/theme.css +2889 -218
  140. package/package.json +83 -76
@@ -1,116 +1,90 @@
1
- <script lang="ts">
2
- import { createId } from '../../lib/internal/id.js';
3
-
4
- interface Props {
5
- value?: number;
6
- min?: number;
7
- max?: number;
8
- step?: number;
9
- disabled?: boolean;
10
- label?: string;
11
- showValue?: boolean;
12
- id?: string;
13
- oninput?: (event: Event) => void;
14
- }
15
-
16
- let {
17
- value = $bindable(0),
18
- min = 0,
19
- max = 100,
20
- step = 1,
21
- disabled = false,
22
- label,
23
- showValue = true,
24
- id = createId('range'),
25
- oninput
26
- }: Props = $props();
27
-
28
- const percentage = $derived(max === min ? 0 : ((value - min) / (max - min)) * 100);
29
- </script>
30
-
31
- <div>
32
- {#if label || showValue}
33
- <div class="flex justify-between items-center mb-2">
34
- {#if label}
35
- <label for={id} class="text-text-soft text-sm">
36
- {label}
37
- </label>
38
- {/if}
39
- {#if showValue}
40
- <span class="text-text-soft text-sm">
41
- {value}
42
- </span>
43
- {/if}
44
- </div>
45
- {/if}
46
- <input
47
- type="range"
48
- {id}
49
- {min}
50
- {max}
51
- {step}
52
- {disabled}
53
- bind:value
54
- oninput={oninput}
55
- aria-valuemin={min}
56
- aria-valuemax={max}
57
- aria-valuenow={value}
58
- class="w-full h-2 rounded-pill appearance-none cursor-pointer transition-opacity duration-200 {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
59
- style="background: linear-gradient(to right, var(--color-accent) 0%, var(--color-accent) {percentage}%, var(--color-base-3) {percentage}%, var(--color-base-3) 100%); border: 1px solid var(--color-border);"
60
- />
61
- </div>
62
-
63
- <style>
64
- input[type="range"]::-webkit-slider-thumb {
65
- appearance: none;
66
- width: 1.25rem;
67
- height: 1.25rem;
68
- border-radius: var(--radius-pill);
69
- background: var(--color-accent);
70
- box-shadow: var(--shadow-accent-glow);
71
- cursor: pointer;
72
- transition: transform 0.2s var(--ease-luxe);
73
- }
74
-
75
- input[type="range"]::-webkit-slider-thumb:hover {
76
- transform: scale(1.1);
77
- }
78
-
79
- input[type="range"]::-moz-range-thumb {
80
- appearance: none;
81
- width: 1.25rem;
82
- height: 1.25rem;
83
- border-radius: var(--radius-pill);
84
- background: var(--color-accent);
85
- box-shadow: var(--shadow-accent-glow);
86
- cursor: pointer;
87
- transition: transform 0.2s var(--ease-luxe);
88
- border: none;
89
- }
90
-
91
- input[type="range"]::-moz-range-thumb:hover {
92
- transform: scale(1.1);
93
- }
94
-
95
- input[type="range"]:focus::-webkit-slider-thumb {
96
- box-shadow:
97
- var(--shadow-accent-glow),
98
- 0 0 0 3px var(--color-base-1),
99
- 0 0 0 5px var(--color-accent);
100
- }
101
-
102
- input[type="range"]:focus::-moz-range-thumb {
103
- box-shadow:
104
- var(--shadow-accent-glow),
105
- 0 0 0 3px var(--color-base-1),
106
- 0 0 0 5px var(--color-accent);
107
- }
108
-
109
- input[type="range"]:disabled::-webkit-slider-thumb {
110
- cursor: not-allowed;
111
- }
112
-
113
- input[type="range"]:disabled::-moz-range-thumb {
114
- cursor: not-allowed;
115
- }
116
- </style>
1
+ <script lang="ts">import { createId } from '../../lib/internal/id.js';
2
+ let { value = $bindable(0), min = 0, max = 100, step = 1, disabled = false, label, showValue = true, id = createId('range'), oninput } = $props();
3
+ const percentage = $derived(max === min ? 0 : ((value - min) / (max - min)) * 100);
4
+ </script>
5
+
6
+ <div>
7
+ {#if label || showValue}
8
+ <div class="flex justify-between items-center mb-2">
9
+ {#if label}
10
+ <label for={id} class="text-text-soft text-sm">
11
+ {label}
12
+ </label>
13
+ {/if}
14
+ {#if showValue}
15
+ <span class="text-text-soft text-sm">
16
+ {value}
17
+ </span>
18
+ {/if}
19
+ </div>
20
+ {/if}
21
+ <input
22
+ type="range"
23
+ {id}
24
+ {min}
25
+ {max}
26
+ {step}
27
+ {disabled}
28
+ bind:value
29
+ oninput={oninput}
30
+ aria-valuemin={min}
31
+ aria-valuemax={max}
32
+ aria-valuenow={value}
33
+ class="w-full h-2 rounded-pill appearance-none cursor-pointer transition-opacity duration-200 {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
34
+ style="background: linear-gradient(to right, var(--color-accent) 0%, var(--color-accent) {percentage}%, var(--color-base-3) {percentage}%, var(--color-base-3) 100%); border: 1px solid var(--color-border);"
35
+ />
36
+ </div>
37
+
38
+ <style>
39
+ input[type="range"]::-webkit-slider-thumb {
40
+ appearance: none;
41
+ width: 1.25rem;
42
+ height: 1.25rem;
43
+ border-radius: var(--radius-pill);
44
+ background: var(--color-accent);
45
+ box-shadow: var(--shadow-accent-glow);
46
+ cursor: pointer;
47
+ transition: transform 0.2s var(--ease-luxe);
48
+ }
49
+
50
+ input[type="range"]::-webkit-slider-thumb:hover {
51
+ transform: scale(1.1);
52
+ }
53
+
54
+ input[type="range"]::-moz-range-thumb {
55
+ appearance: none;
56
+ width: 1.25rem;
57
+ height: 1.25rem;
58
+ border-radius: var(--radius-pill);
59
+ background: var(--color-accent);
60
+ box-shadow: var(--shadow-accent-glow);
61
+ cursor: pointer;
62
+ transition: transform 0.2s var(--ease-luxe);
63
+ border: none;
64
+ }
65
+
66
+ input[type="range"]::-moz-range-thumb:hover {
67
+ transform: scale(1.1);
68
+ }
69
+
70
+ input[type="range"]:focus::-webkit-slider-thumb {
71
+ box-shadow:
72
+ var(--shadow-accent-glow),
73
+ 0 0 0 3px var(--color-base-1),
74
+ 0 0 0 5px var(--color-accent);
75
+ }
76
+
77
+ input[type="range"]:focus::-moz-range-thumb {
78
+ box-shadow:
79
+ var(--shadow-accent-glow),
80
+ 0 0 0 3px var(--color-base-1),
81
+ 0 0 0 5px var(--color-accent);
82
+ }
83
+
84
+ input[type="range"]:disabled::-webkit-slider-thumb {
85
+ cursor: not-allowed;
86
+ }
87
+
88
+ input[type="range"]:disabled::-moz-range-thumb {
89
+ cursor: not-allowed;
90
+ }</style>
@@ -1,71 +1,106 @@
1
- <script lang="ts">
2
- import { createId } from '../../lib/internal/id.js';
3
-
4
- interface Props {
5
- value?: string;
6
- options: Array<{ value: string; label: string }>;
7
- placeholder?: string;
8
- disabled?: boolean;
9
- error?: boolean;
10
- label?: string;
11
- id?: string;
12
- onchange?: (event: Event) => void;
13
- }
14
-
15
- let {
16
- value = $bindable(''),
17
- options,
18
- placeholder,
19
- disabled = false,
20
- error = false,
21
- label,
22
- id = createId('select'),
23
- onchange
24
- }: Props = $props();
25
-
26
- const chevronIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M2.5 4.5L6 8L9.5 4.5' stroke='%23ADADAD' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E";
27
-
28
- const baseClasses = `glass-panel rounded-lg px-4 py-3 pr-10 font-body text-text transition-all duration-300 ease-luxe w-full appearance-none bg-no-repeat bg-[right_1rem_center] bg-[length:12px]`;
29
- const focusClasses = 'focus:outline-none focus:border-accent focus:accent-glow';
30
- const errorClasses = error ? 'border-error' : '';
31
- const disabledClasses = disabled ? 'opacity-50 cursor-not-allowed' : '';
32
- </script>
33
-
34
- {#if label}
35
- <div>
36
- <label for={id} class="text-text-soft text-sm mb-2 block">
37
- {label}
38
- </label>
39
- <select
40
- {id}
41
- {disabled}
42
- bind:value
43
- onchange={onchange}
44
- class="{baseClasses} {focusClasses} {errorClasses} {disabledClasses}"
45
- style={`background-image: url('${chevronIcon}')`}
46
- >
47
- {#if placeholder}
48
- <option disabled value="">{placeholder}</option>
49
- {/if}
50
- {#each options as option}
51
- <option value={option.value}>{option.label}</option>
52
- {/each}
53
- </select>
54
- </div>
55
- {:else}
56
- <select
57
- {id}
58
- {disabled}
59
- bind:value
60
- onchange={onchange}
61
- class="{baseClasses} {focusClasses} {errorClasses} {disabledClasses}"
62
- style={`background-image: url('${chevronIcon}')`}
63
- >
64
- {#if placeholder}
65
- <option disabled value="">{placeholder}</option>
66
- {/if}
67
- {#each options as option}
68
- <option value={option.value}>{option.label}</option>
69
- {/each}
70
- </select>
71
- {/if}
1
+ <script lang="ts">import { createId } from '../../lib/internal/id.js';
2
+ import { baseInputClasses, focusClasses, disabledClasses, } from './formClasses';
3
+ let { value = $bindable(''), options, placeholder, disabled = false, error = false, label, id = createId('select'), onchange, iconBefore, class: className = '', } = $props();
4
+ const chevronIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none'%3E%3Cpath d='M6 9l6 6 6-6' stroke='%23A855F7' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E";
5
+ const selectClasses = `${baseInputClasses} pr-12 appearance-none bg-no-repeat bg-[right_1rem_center] bg-[length:16px] cursor-pointer`;
6
+ const errorClasses = $derived(error ? 'error-state' : '');
7
+ </script>
8
+
9
+ {#if label}
10
+ <div>
11
+ <label for={id} class="text-text-soft text-sm mb-2 block">
12
+ {label}
13
+ </label>
14
+ <div class="relative">
15
+ {#if iconBefore}
16
+ <span
17
+ class="absolute left-4 top-1/2 -translate-y-1/2 z-10 inline-flex items-center justify-center text-text-soft pointer-events-none"
18
+ >
19
+ {@render iconBefore()}
20
+ </span>
21
+ {/if}
22
+
23
+ <select
24
+ {id}
25
+ {disabled}
26
+ bind:value
27
+ {onchange}
28
+ class="{selectClasses} {focusClasses} {errorClasses} {disabled
29
+ ? disabledClasses
30
+ : ''} {className}"
31
+ class:pl-12={iconBefore}
32
+ style={`background-image: url('${chevronIcon}')`}
33
+ >
34
+ {#if placeholder}
35
+ <option disabled value="" aria-hidden="true" hidden
36
+ >{placeholder}</option
37
+ >
38
+ {/if}
39
+
40
+ {#each options as option}
41
+ <option value={option.value}>{option.label}</option>
42
+ {/each}
43
+ </select>
44
+ </div>
45
+ </div>
46
+ {:else}
47
+ <div class="relative">
48
+ {#if iconBefore}
49
+ <span
50
+ class="absolute left-4 top-1/2 -translate-y-1/2 z-10 inline-flex items-center justify-center text-text-soft pointer-events-none"
51
+ >
52
+ {@render iconBefore()}
53
+ </span>
54
+ {/if}
55
+
56
+ <select
57
+ {id}
58
+ {disabled}
59
+ bind:value
60
+ {onchange}
61
+ class="{selectClasses} {focusClasses} {errorClasses} {disabled
62
+ ? disabledClasses
63
+ : ''} {className}"
64
+ class:pl-12={iconBefore}
65
+ style={`background-image: url('${chevronIcon}')`}
66
+ >
67
+ {#if placeholder}
68
+ <option disabled value="" aria-hidden="true" hidden
69
+ >{placeholder}</option
70
+ >
71
+ {/if}
72
+
73
+ {#each options as option}
74
+ <option value={option.value}>{option.label}</option>
75
+ {/each}
76
+ </select>
77
+ </div>
78
+ {/if}
79
+
80
+ <style>
81
+ /* Force dark dropdown (Firefox + Chromium) */
82
+ select {
83
+ color-scheme: var(--color-base-0);
84
+ }
85
+
86
+ /* Option styling */
87
+ select option {
88
+ background-color: var(--color-base-2);
89
+ color: var(--color-text);
90
+ padding: 0.5rem;
91
+ }
92
+
93
+ select option:hover {
94
+ background-color: var(--color-accent-overlay-50);
95
+ color: var(--color-text);
96
+ }
97
+ select option:focus,
98
+ select option:checked {
99
+ background-color: var(--color-accent);
100
+ color: var(--color-text);
101
+ }
102
+
103
+ select option:disabled {
104
+ color: var(--color-text-muted);
105
+ opacity: 0.5;
106
+ }</style>
@@ -1,4 +1,4 @@
1
- interface Props {
1
+ export interface Props {
2
2
  value?: string;
3
3
  options: Array<{
4
4
  value: string;
@@ -10,6 +10,8 @@ interface Props {
10
10
  label?: string;
11
11
  id?: string;
12
12
  onchange?: (event: Event) => void;
13
+ iconBefore?: import('svelte').Snippet;
14
+ class?: string;
13
15
  }
14
16
  declare const Select: import("svelte").Component<Props, {}, "value">;
15
17
  type Select = ReturnType<typeof Select>;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">import { onMount, onDestroy } from 'svelte';
2
+ import { getStepperContext } from './StepContext';
3
+ let { id, label = id, fields = [], optional = false, order, children, } = $props();
4
+ const context = getStepperContext();
5
+ onMount(() => {
6
+ context?.registerStep({ id, label, fields, optional, order });
7
+ });
8
+ onDestroy(() => {
9
+ context?.unregisterStep(id);
10
+ });
11
+ const isActive = $derived(context?.currentStepId === id);
12
+ const state = $derived(context?.getStepState(id) ?? 'idle');
13
+ </script>
14
+
15
+ {#if isActive && children}
16
+ <div
17
+ class="step-content"
18
+ data-step-id={id}
19
+ data-step-state={state}
20
+ role="tabpanel"
21
+ aria-labelledby={`step-${id}`}
22
+ >
23
+ {@render children()}
24
+ </div>
25
+ {/if}
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ id: string;
4
+ label?: string;
5
+ fields?: string[];
6
+ optional?: boolean;
7
+ order?: number;
8
+ children?: Snippet;
9
+ }
10
+ declare const Step: import("svelte").Component<Props, {}, "">;
11
+ type Step = ReturnType<typeof Step>;
12
+ export default Step;
@@ -0,0 +1,3 @@
1
+ import type { StepContext } from './Stepper.types';
2
+ export declare const STEPPER_CONTEXT_KEY: unique symbol;
3
+ export declare function getStepperContext(): StepContext | undefined;
@@ -0,0 +1,5 @@
1
+ import { getContext } from 'svelte';
2
+ export const STEPPER_CONTEXT_KEY = Symbol('stepper-form-context');
3
+ export function getStepperContext() {
4
+ return getContext(STEPPER_CONTEXT_KEY);
5
+ }
@@ -0,0 +1,37 @@
1
+ export type StepState = 'idle' | 'active' | 'completed' | 'error';
2
+ export type ValidationMode = 'strict' | 'loose' | 'submit-only';
3
+ export interface StepRegistration {
4
+ id: string;
5
+ label: string;
6
+ fields?: string[];
7
+ optional?: boolean;
8
+ order?: number;
9
+ }
10
+ export interface StepContext {
11
+ registerStep: (step: StepRegistration) => void;
12
+ unregisterStep: (id: string) => void;
13
+ currentStepId: string;
14
+ getStepState: (id: string) => StepState;
15
+ }
16
+ export interface StepMeta {
17
+ id: string;
18
+ label: string;
19
+ fields: string[];
20
+ optional: boolean;
21
+ state: StepState;
22
+ }
23
+ export interface StepperFormContext {
24
+ next: () => Promise<boolean>;
25
+ back: () => void;
26
+ goto: (stepId: string) => Promise<boolean>;
27
+ currentStep: number;
28
+ currentStepId: string;
29
+ totalSteps: number;
30
+ canNext: boolean;
31
+ canBack: boolean;
32
+ isFirstStep: boolean;
33
+ isLastStep: boolean;
34
+ steps: StepMeta[];
35
+ validateCurrentStep: () => Promise<boolean>;
36
+ getStepErrors: (stepId: string) => string[];
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,183 @@
1
+ <script lang="ts">import { setContext, tick } from 'svelte';
2
+ import { STEPPER_CONTEXT_KEY } from './StepContext';
3
+ let { form, validationMode = 'strict', initialStep = 0, onStepChange, onValidationError, onComplete, header, footer, children, class: className = '', } = $props();
4
+ let registeredSteps = $state([]);
5
+ let currentStepIndex = $state(initialStep);
6
+ let stepStates = $state(new Map());
7
+ let stepErrors = $state(new Map());
8
+ const totalSteps = $derived(registeredSteps.length);
9
+ const currentStepId = $derived(registeredSteps[currentStepIndex]?.id ?? '');
10
+ const isFirstStep = $derived(currentStepIndex === 0);
11
+ const isLastStep = $derived(currentStepIndex === totalSteps - 1);
12
+ const canBack = $derived(!isFirstStep);
13
+ const canNext = $derived(validationMode === 'submit-only'
14
+ ? true
15
+ : (() => {
16
+ const step = registeredSteps[currentStepIndex];
17
+ if (!step)
18
+ return false;
19
+ return (stepErrors.get(step.id) ?? []).length === 0;
20
+ })());
21
+ const steps = $derived(registeredSteps.map((step, i) => ({
22
+ id: step.id,
23
+ label: step.label,
24
+ fields: step.fields ?? [],
25
+ optional: step.optional ?? false,
26
+ state: stepStates.get(step.id) ??
27
+ (i < currentStepIndex
28
+ ? 'completed'
29
+ : i === currentStepIndex
30
+ ? 'active'
31
+ : 'idle'),
32
+ })));
33
+ async function validateStepFields(stepId) {
34
+ const step = registeredSteps.find((s) => s.id === stepId);
35
+ if (!step?.fields?.length)
36
+ return true;
37
+ const formErrors = form?.errors;
38
+ if (!formErrors)
39
+ return true;
40
+ const errors = [];
41
+ for (const field of step.fields) {
42
+ const fieldErrors = $state.snapshot(formErrors)?.[field];
43
+ if (Array.isArray(fieldErrors))
44
+ errors.push(...fieldErrors);
45
+ }
46
+ if (errors.length) {
47
+ stepErrors.set(stepId, errors);
48
+ stepStates.set(stepId, 'error');
49
+ onValidationError?.(stepId, errors);
50
+ return false;
51
+ }
52
+ stepErrors.delete(stepId);
53
+ stepStates.set(stepId, 'active');
54
+ return true;
55
+ }
56
+ async function validateCurrentStep() {
57
+ if (validationMode === 'submit-only')
58
+ return true;
59
+ return validateStepFields(currentStepId);
60
+ }
61
+ async function next() {
62
+ if (isLastStep) {
63
+ onComplete?.();
64
+ return true;
65
+ }
66
+ if (validationMode !== 'submit-only') {
67
+ const valid = validationMode === 'strict'
68
+ ? await validateCurrentStep()
69
+ : (await validateCurrentStep(), true);
70
+ if (!valid)
71
+ return false;
72
+ }
73
+ stepStates.set(currentStepId, 'completed');
74
+ currentStepIndex++;
75
+ const newStepId = registeredSteps[currentStepIndex].id;
76
+ stepStates.set(newStepId, 'active');
77
+ await tick();
78
+ onStepChange?.(currentStepIndex, newStepId);
79
+ return true;
80
+ }
81
+ function back() {
82
+ if (isFirstStep)
83
+ return;
84
+ stepStates.set(currentStepId, 'idle');
85
+ currentStepIndex--;
86
+ const newStepId = registeredSteps[currentStepIndex].id;
87
+ stepStates.set(newStepId, 'active');
88
+ onStepChange?.(currentStepIndex, newStepId);
89
+ }
90
+ async function goto(stepId) {
91
+ const targetIndex = registeredSteps.findIndex((s) => s.id === stepId);
92
+ if (targetIndex === -1)
93
+ return false;
94
+ if (targetIndex > currentStepIndex && validationMode === 'strict') {
95
+ for (let i = currentStepIndex; i < targetIndex; i++) {
96
+ const ok = await validateStepFields(registeredSteps[i].id);
97
+ if (!ok)
98
+ return false;
99
+ stepStates.set(registeredSteps[i].id, 'completed');
100
+ }
101
+ }
102
+ for (let i = 0; i < registeredSteps.length; i++) {
103
+ stepStates.set(registeredSteps[i].id, i < targetIndex
104
+ ? 'completed'
105
+ : i === targetIndex
106
+ ? 'active'
107
+ : 'idle');
108
+ }
109
+ currentStepIndex = targetIndex;
110
+ await tick();
111
+ onStepChange?.(targetIndex, stepId);
112
+ return true;
113
+ }
114
+ function getStepErrors(stepId) {
115
+ return stepErrors.get(stepId) ?? [];
116
+ }
117
+ function getStepState(id) {
118
+ return stepStates.get(id) ?? 'idle';
119
+ }
120
+ function registerStep(step) {
121
+ if (registeredSteps.some((s) => s.id === step.id))
122
+ return;
123
+ registeredSteps = [...registeredSteps, step].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
124
+ if (!registeredSteps[currentStepIndex]) {
125
+ currentStepIndex = 0;
126
+ }
127
+ const id = registeredSteps[currentStepIndex].id;
128
+ stepStates.set(id, 'active');
129
+ }
130
+ function unregisterStep(id) {
131
+ registeredSteps = registeredSteps.filter((s) => s.id !== id);
132
+ stepStates.delete(id);
133
+ stepErrors.delete(id);
134
+ if (currentStepIndex >= registeredSteps.length) {
135
+ currentStepIndex = Math.max(registeredSteps.length - 1, 0);
136
+ }
137
+ const active = registeredSteps[currentStepIndex];
138
+ if (active)
139
+ stepStates.set(active.id, 'active');
140
+ }
141
+ const stepContext = {
142
+ registerStep,
143
+ unregisterStep,
144
+ get currentStepId() {
145
+ return currentStepId;
146
+ },
147
+ getStepState,
148
+ };
149
+ setContext(STEPPER_CONTEXT_KEY, stepContext);
150
+ const slotContext = $derived({
151
+ next,
152
+ back,
153
+ goto,
154
+ currentStep: currentStepIndex,
155
+ currentStepId,
156
+ totalSteps,
157
+ canNext,
158
+ canBack,
159
+ isFirstStep,
160
+ isLastStep,
161
+ steps,
162
+ validateCurrentStep,
163
+ getStepErrors,
164
+ });
165
+ </script>
166
+
167
+ <div class={`stepper-form ${className}`} data-current-step={currentStepId}>
168
+ {#if header}
169
+ <div class="stepper-form-header">
170
+ {@render header(slotContext)}
171
+ </div>
172
+ {/if}
173
+
174
+ <div class="stepper-form-content">
175
+ {@render children?.(slotContext)}
176
+ </div>
177
+
178
+ {#if footer}
179
+ <div class="stepper-form-footer">
180
+ {@render footer(slotContext)}
181
+ </div>
182
+ {/if}
183
+ </div>