@mrintel/villain-ui 0.6.3 → 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.
@@ -1,4 +1,4 @@
1
- <script lang="ts">let { padding = 'md', href, target = '_self', rel, class: className = '', children, header, footer, iconBefore, ...restProps } = $props();
1
+ <script lang="ts">let { padding = 'md', href, target = '_self', rel, onclick, selected = false, disabled = false, class: className = '', children, header, footer, iconBefore, ...restProps } = $props();
2
2
  // Compute rel attribute: use explicit rel if provided, otherwise default to 'noopener noreferrer' for target='_blank'
3
3
  const relValue = $derived(rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined));
4
4
  const paddingClasses = {
@@ -7,54 +7,48 @@ const paddingClasses = {
7
7
  md: 'p-6',
8
8
  lg: 'p-8'
9
9
  };
10
- const baseClasses = $derived(`panel-raised rounded-[var(--radius-lg)] transition-all duration-300 ease-[var(--ease-luxe)] ${href ? 'no-underline' : ''}`);
10
+ const isInteractive = $derived(!!href || !!onclick);
11
+ const baseClasses = $derived(`panel-raised rounded-[var(--radius-lg)] transition-all duration-300 ease-[var(--ease-luxe)]` +
12
+ `${href ? ' no-underline' : ''}` +
13
+ `${isInteractive ? ' cursor-pointer hover-lift' : ''}` +
14
+ `${selected ? ' ring-2 ring-accent ring-offset-2 ring-offset-base-0' : ''}` +
15
+ `${disabled ? ' opacity-50 pointer-events-none' : ''}`);
11
16
  export {};
12
17
  </script>
13
18
 
14
- {#if href}
15
- <a {href} {target} rel={relValue} class="{baseClasses} {paddingClasses[padding]} {className}" {...restProps}>
16
- {#if iconBefore}
17
- <div class="card-icon">
18
- {@render iconBefore()}
19
- </div>
20
- {/if}
21
- {#if header}
22
- <div class="pb-3 mb-3 border-b border-border-strong">
23
- {@render header()}
24
- </div>
25
- {/if}
19
+ {#snippet cardContent()}
20
+ {#if iconBefore}
21
+ <div class="card-icon">
22
+ {@render iconBefore()}
23
+ </div>
24
+ {/if}
25
+ {#if header}
26
+ <div class="pb-3 mb-3 border-b border-border-strong">
27
+ {@render header()}
28
+ </div>
29
+ {/if}
30
+
31
+ <div>
32
+ {@render children?.()}
33
+ </div>
26
34
 
27
- <div>
28
- {@render children?.()}
35
+ {#if footer}
36
+ <div class="pt-3 mt-3 border-t border-border-strong">
37
+ {@render footer()}
29
38
  </div>
39
+ {/if}
40
+ {/snippet}
30
41
 
31
- {#if footer}
32
- <div class="pt-3 mt-3 border-t border-border-strong">
33
- {@render footer()}
34
- </div>
35
- {/if}
42
+ {#if href}
43
+ <a {href} {target} rel={relValue} class="{baseClasses} {paddingClasses[padding]} {className}" aria-disabled={disabled || undefined} {...restProps}>
44
+ {@render cardContent()}
36
45
  </a>
46
+ {:else if onclick}
47
+ <button type="button" {onclick} {disabled} aria-pressed={selected} class="{baseClasses} {paddingClasses[padding]} {className} text-left w-full" {...restProps}>
48
+ {@render cardContent()}
49
+ </button>
37
50
  {:else}
38
51
  <div class="{baseClasses} {paddingClasses[padding]} {className}" {...restProps}>
39
- {#if iconBefore}
40
- <div class="card-icon">
41
- {@render iconBefore()}
42
- </div>
43
- {/if}
44
- {#if header}
45
- <div class="pb-3 mb-3 border-b border-border-strong">
46
- {@render header()}
47
- </div>
48
- {/if}
49
-
50
- <div>
51
- {@render children?.()}
52
- </div>
53
-
54
- {#if footer}
55
- <div class="pt-3 mt-3 border-t border-border-strong">
56
- {@render footer()}
57
- </div>
58
- {/if}
52
+ {@render cardContent()}
59
53
  </div>
60
54
  {/if}
@@ -4,6 +4,9 @@ export interface Props {
4
4
  href?: string;
5
5
  target?: '_blank' | '_self' | '_parent' | '_top';
6
6
  rel?: string;
7
+ onclick?: (event: MouseEvent) => void;
8
+ selected?: boolean;
9
+ disabled?: boolean;
7
10
  class?: string;
8
11
  children?: Snippet;
9
12
  header?: Snippet;
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">import { createId } from '../../lib/internal/id.js';
2
2
  import { baseInputClasses, focusClasses, disabledClasses, } from './formClasses';
3
- let { type = 'text', value = $bindable(''), placeholder, disabled = false, error = false, label, id = createId('input'), oninput, iconBefore, iconAfter, validate, validationMessage, showValidation = true, class: className = '', } = $props();
3
+ let { type = 'text', name, value = $bindable(''), placeholder, disabled = false, error = false, label, id = createId('input'), oninput, iconBefore, iconAfter, validate, validationMessage, showValidation = true, class: className = '', } = $props();
4
4
  // Built-in validation patterns
5
5
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6
6
  const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
@@ -91,6 +91,7 @@ function decrement() {
91
91
  <!-- INPUT FIELD -->
92
92
  <input
93
93
  {type}
94
+ {name}
94
95
  {id}
95
96
  {placeholder}
96
97
  {disabled}
@@ -153,6 +154,7 @@ function decrement() {
153
154
 
154
155
  <input
155
156
  {type}
157
+ {name}
156
158
  {id}
157
159
  {placeholder}
158
160
  {disabled}
@@ -1,5 +1,6 @@
1
1
  export interface Props {
2
2
  type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'color' | 'search';
3
+ name?: string;
3
4
  value?: string | number;
4
5
  placeholder?: string;
5
6
  disabled?: boolean;
@@ -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>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ValidationMode, StepperFormContext } from './Stepper.types';
3
+ interface Props {
4
+ form: any;
5
+ validationMode?: ValidationMode;
6
+ initialStep?: number;
7
+ onStepChange?: (stepIndex: number, stepId: string) => void;
8
+ onValidationError?: (stepId: string, errors: string[]) => void;
9
+ onComplete?: () => void;
10
+ header?: Snippet<[StepperFormContext]>;
11
+ footer?: Snippet<[StepperFormContext]>;
12
+ children?: Snippet<[StepperFormContext]>;
13
+ class?: string;
14
+ }
15
+ declare const StepperForm: import("svelte").Component<Props, {}, "">;
16
+ type StepperForm = ReturnType<typeof StepperForm>;
17
+ export default StepperForm;
@@ -10,3 +10,6 @@ export { default as InputGroup } from './InputGroup.svelte';
10
10
  export { default as DatePicker } from './DatePicker.svelte';
11
11
  export { default as TimePicker } from './TimePicker.svelte';
12
12
  export { default as DateTimePicker } from './DateTimePicker.svelte';
13
+ export { default as Step } from './Step.svelte';
14
+ export { default as StepperForm } from './StepperForm.svelte';
15
+ export type { StepState, ValidationMode, StepRegistration, StepperFormContext, StepMeta, StepContext } from './Stepper.types';
@@ -10,3 +10,5 @@ export { default as InputGroup } from './InputGroup.svelte';
10
10
  export { default as DatePicker } from './DatePicker.svelte';
11
11
  export { default as TimePicker } from './TimePicker.svelte';
12
12
  export { default as DateTimePicker } from './DateTimePicker.svelte';
13
+ export { default as Step } from './Step.svelte';
14
+ export { default as StepperForm } from './StepperForm.svelte';
@@ -0,0 +1,204 @@
1
+ <script lang="ts">let { steps, current = 0, states, variant = 'default', orientation = 'horizontal', showNumbers = true, showProgress = true, clickable = false, onStepClick, class: className = '' } = $props();
2
+ /* ---------------------------------------------
3
+ * Normalize steps (stable IDs, StepForm friendly)
4
+ * --------------------------------------------- */
5
+ const normalizedSteps = $derived(steps.map((step, _) => typeof step === 'string'
6
+ ? {
7
+ id: step.toLowerCase().replace(/\s+/g, '-'),
8
+ label: step
9
+ }
10
+ : step));
11
+ /* ---------------------------------------------
12
+ * Resolve step states
13
+ * Priority:
14
+ * 1. explicit `states` prop
15
+ * 2. per-step `state`
16
+ * 3. derive from `current`
17
+ * --------------------------------------------- */
18
+ const derivedStates = $derived(states && states.length === normalizedSteps.length
19
+ ? states
20
+ : normalizedSteps.map((step, i) => {
21
+ if (step.state)
22
+ return step.state;
23
+ if (i < current)
24
+ return 'completed';
25
+ if (i === current)
26
+ return 'active';
27
+ return 'idle';
28
+ }));
29
+ /* ---------------------------------------------
30
+ * Progress bar percentage
31
+ * --------------------------------------------- */
32
+ const progressPercent = $derived(normalizedSteps.length > 1
33
+ ? ((current + 1) / normalizedSteps.length) * 100
34
+ : 100);
35
+ /* ---------------------------------------------
36
+ * Interaction handlers
37
+ * --------------------------------------------- */
38
+ function handleStepClick(index) {
39
+ if (!clickable)
40
+ return;
41
+ const state = derivedStates[index];
42
+ if (state !== 'completed')
43
+ return;
44
+ onStepClick?.(index);
45
+ }
46
+ function handleKeyDown(event, index) {
47
+ if (event.key === 'Enter' || event.key === ' ') {
48
+ event.preventDefault();
49
+ handleStepClick(index);
50
+ }
51
+ }
52
+ /* ---------------------------------------------
53
+ * Styling maps
54
+ * --------------------------------------------- */
55
+ const stateStyles = {
56
+ idle: {
57
+ circle: 'bg-[var(--color-base-2)] text-[var(--color-text-tertiary)]',
58
+ label: 'text-[var(--color-text-tertiary)]',
59
+ connector: 'bg-[var(--color-base-2)]'
60
+ },
61
+ active: {
62
+ circle: 'bg-[var(--color-accent-primary)] text-white shadow-[0_0_0_4px_rgba(139,92,246,0.3)]',
63
+ label: 'text-[var(--color-accent-primary)]',
64
+ connector: 'bg-[var(--color-base-2)]'
65
+ },
66
+ completed: {
67
+ circle: 'bg-[var(--color-accent-primary)] text-white',
68
+ label: 'text-[var(--color-text-secondary)]',
69
+ connector: 'bg-[var(--color-accent-primary)]'
70
+ },
71
+ error: {
72
+ circle: 'bg-[var(--color-error)] text-white',
73
+ label: 'text-[var(--color-error)]',
74
+ connector: 'bg-[var(--color-error)]'
75
+ },
76
+ disabled: {
77
+ circle: 'bg-[var(--color-base-1)] text-[var(--color-text-tertiary)] opacity-50',
78
+ label: 'text-[var(--color-text-tertiary)] opacity-50',
79
+ connector: 'bg-[var(--color-base-1)]'
80
+ }
81
+ };
82
+ const sizeConfig = {
83
+ default: { circle: 'w-8 h-8', text: 'text-sm', connector: 'h-0.5', gap: 'gap-3' },
84
+ compact: { circle: 'w-6 h-6', text: 'text-xs', connector: 'h-0.5', gap: 'gap-2' },
85
+ minimal: { circle: 'w-3 h-3', text: 'text-xs', connector: 'h-0.5', gap: 'gap-1' }
86
+ };
87
+ const sizes = $derived(sizeConfig[variant]);
88
+ export {};
89
+ </script>
90
+
91
+ <!-- ========================================================= -->
92
+ <!-- Root -->
93
+ <!-- ========================================================= -->
94
+ <div
95
+ class="stepper {className}"
96
+ class:stepper-horizontal={orientation === 'horizontal'}
97
+ class:stepper-vertical={orientation === 'vertical'}
98
+ role="group"
99
+ aria-label="Progress"
100
+ >
101
+ <!-- ========================================================= -->
102
+ <!-- Horizontal -->
103
+ <!-- ========================================================= -->
104
+ {#if orientation === 'horizontal'}
105
+ <div class="w-full">
106
+ {#if showProgress && variant !== 'minimal'}
107
+ <div class="relative mb-8">
108
+ <div class="h-2 rounded-full overflow-hidden bg-[var(--color-base-2)]">
109
+ <div
110
+ class="h-full transition-all duration-500 ease-out bg-[var(--color-accent-primary)]"
111
+ style="width: {progressPercent}%"
112
+ ></div>
113
+ </div>
114
+
115
+ <div class="absolute top-0 left-0 right-0 flex justify-between items-center -mt-3">
116
+ {#each normalizedSteps as step, i}
117
+ {@const state = derivedStates[i]}
118
+ {@const styles = stateStyles[state]}
119
+ <div class="flex flex-col items-center">
120
+ <button
121
+ type="button"
122
+ class="{sizes.circle} rounded-full flex items-center justify-center transition-all duration-300 {styles.circle}"
123
+ class:cursor-pointer={clickable && state === 'completed'}
124
+ disabled={state === 'disabled'}
125
+ onclick={() => handleStepClick(i)}
126
+ onkeydown={(e) => handleKeyDown(e, i)}
127
+ aria-current={state === 'active' ? 'step' : undefined}
128
+ >
129
+ {#if state === 'completed' && variant !== 'minimal'}
130
+
131
+ {:else if step.icon}
132
+ {@render step.icon()}
133
+ {:else if showNumbers && variant !== 'minimal'}
134
+ <span class="{sizes.text} font-bold">{i + 1}</span>
135
+ {/if}
136
+ </button>
137
+
138
+ {#if variant !== 'minimal'}
139
+ <span class="hidden md:block mt-2 {sizes.text} font-medium {styles.label}">
140
+ {step.label}
141
+ </span>
142
+ {/if}
143
+ </div>
144
+ {/each}
145
+ </div>
146
+ </div>
147
+ {/if}
148
+ </div>
149
+
150
+ <!-- ========================================================= -->
151
+ <!-- Vertical -->
152
+ <!-- ========================================================= -->
153
+ {:else}
154
+ <div class="flex flex-col {sizes.gap}">
155
+ {#each normalizedSteps as step, i}
156
+ {@const state = derivedStates[i]}
157
+ {@const styles = stateStyles[state]}
158
+ {@const isLast = i === normalizedSteps.length - 1}
159
+
160
+ <div class="flex">
161
+ <div class="flex flex-col items-center mr-4">
162
+ <button
163
+ type="button"
164
+ class="{sizes.circle} rounded-full flex items-center justify-center transition-all duration-300 {styles.circle}"
165
+ class:cursor-pointer={clickable && state === 'completed'}
166
+ disabled={state === 'disabled'}
167
+ onclick={() => handleStepClick(i)}
168
+ onkeydown={(e) => handleKeyDown(e, i)}
169
+ aria-current={state === 'active' ? 'step' : undefined}
170
+ >
171
+ {#if state === 'completed' && variant !== 'minimal'}✓{/if}
172
+ </button>
173
+
174
+ {#if !isLast}
175
+ <div
176
+ class="w-0.5 flex-1 min-h-8 my-2 rounded-full transition-colors duration-300"
177
+ class:bg-[var(--color-accent-primary)]={state === 'completed'}
178
+ class:bg-[var(--color-base-2)]={state !== 'completed'}
179
+ ></div>
180
+ {/if}
181
+ </div>
182
+
183
+ <div class="flex-1 pb-8">
184
+ <span class="{sizes.text} font-medium {styles.label}">
185
+ {step.label}
186
+ </span>
187
+ {#if step.description && variant === 'default'}
188
+ <p class="mt-1 text-xs text-[var(--color-text-tertiary)]">
189
+ {step.description}
190
+ </p>
191
+ {/if}
192
+ </div>
193
+ </div>
194
+ {/each}
195
+ </div>
196
+ {/if}
197
+ </div>
198
+
199
+ <style>
200
+ @media (prefers-reduced-motion: reduce) {
201
+ .stepper * {
202
+ transition-duration: 0.01ms !important;
203
+ }
204
+ }</style>
@@ -0,0 +1,34 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type StepState = 'idle' | 'active' | 'completed' | 'error' | 'disabled';
3
+ export interface StepConfig {
4
+ id: string;
5
+ label: string;
6
+ description?: string;
7
+ icon?: Snippet;
8
+ state?: StepState;
9
+ }
10
+ interface Props {
11
+ /** Array of step labels or full step configs */
12
+ steps: (string | StepConfig)[];
13
+ /** Current step index (0-based) */
14
+ current?: number;
15
+ /** Optional explicit states (must match steps length) */
16
+ states?: StepState[];
17
+ /** Visual variant */
18
+ variant?: 'default' | 'compact' | 'minimal';
19
+ /** Orientation */
20
+ orientation?: 'horizontal' | 'vertical';
21
+ /** Show step numbers */
22
+ showNumbers?: boolean;
23
+ /** Show progress bar (horizontal only) */
24
+ showProgress?: boolean;
25
+ /** Allow clicking on completed steps */
26
+ clickable?: boolean;
27
+ /** Callback when a step is clicked */
28
+ onStepClick?: (index: number) => void;
29
+ /** Additional CSS class */
30
+ class?: string;
31
+ }
32
+ declare const Stepper: import("svelte").Component<Props, {}, "">;
33
+ type Stepper = ReturnType<typeof Stepper>;
34
+ export default Stepper;
@@ -5,3 +5,4 @@ export { default as Breadcrumbs } from './Breadcrumbs.svelte';
5
5
  export { default as Menu } from './Menu.svelte';
6
6
  export { default as DropdownMenu } from './DropdownMenu.svelte';
7
7
  export { default as ContextMenu } from './ContextMenu.svelte';
8
+ export { default as Stepper } from './Stepper.svelte';
@@ -5,3 +5,4 @@ export { default as Breadcrumbs } from './Breadcrumbs.svelte';
5
5
  export { default as Menu } from './Menu.svelte';
6
6
  export { default as DropdownMenu } from './DropdownMenu.svelte';
7
7
  export { default as ContextMenu } from './ContextMenu.svelte';
8
+ export { default as Stepper } from './Stepper.svelte';
package/dist/index.d.ts CHANGED
@@ -4,11 +4,12 @@
4
4
  * This is the main entry point for the component library.
5
5
  */
6
6
  import './theme.css';
7
- export declare const version = "0.6.0";
7
+ export declare const version = "0.7.1";
8
8
  export { Button, IconButton, ButtonGroup, LinkButton, FloatingActionButton } from './components/buttons';
9
- export { Input, Textarea, Select, Checkbox, Switch, RadioGroup, RangeSlider, FileUpload, InputGroup, DatePicker, TimePicker, DateTimePicker } from './components/forms';
9
+ export { Input, Textarea, Select, Checkbox, Switch, RadioGroup, RangeSlider, FileUpload, InputGroup, DatePicker, TimePicker, DateTimePicker, StepperForm, Step } from './components/forms';
10
+ export type { StepState, ValidationMode, StepRegistration, StepperFormContext, StepMeta, StepContext } from './components/forms';
10
11
  export { Card, Panel, Grid, Container, SectionHeader, Divider } from './components/cards';
11
- export { Navbar, Sidebar, Tabs, Breadcrumbs, Menu, DropdownMenu, ContextMenu } from './components/navigation';
12
+ export { Navbar, Sidebar, Tabs, Breadcrumbs, Menu, DropdownMenu, ContextMenu, Stepper } from './components/navigation';
12
13
  export { Modal, Alert, Spinner, Tooltip, ProgressBar, SkeletonLoader, Toast, Drawer, Popover, Dropdown, CommandPalette } from './components/overlays';
13
14
  export { Heading, Text, Code } from './components/typography';
14
15
  export { Table, Pagination, Badge, Tag, List, Avatar, CodeBlock, Stat, CalendarGrid, Sparkline } from './components/data';
@@ -29,6 +30,8 @@ import type RadioGroup from './components/forms/RadioGroup.svelte';
29
30
  import type DatePicker from './components/forms/DatePicker.svelte';
30
31
  import type TimePicker from './components/forms/TimePicker.svelte';
31
32
  import type DateTimePicker from './components/forms/DateTimePicker.svelte';
33
+ import type StepperForm from './components/forms/StepperForm.svelte';
34
+ import type Step from './components/forms/Step.svelte';
32
35
  import type Card from './components/cards/Card.svelte';
33
36
  import type Tabs from './components/navigation/Tabs.svelte';
34
37
  import type Modal from './components/overlays/Modal.svelte';
@@ -37,6 +40,7 @@ import type Alert from './components/overlays/Alert.svelte';
37
40
  import type Tooltip from './components/overlays/Tooltip.svelte';
38
41
  import type Accordion from './components/utilities/Accordion.svelte';
39
42
  import type Sparkline from './components/data/Sparkline.svelte';
43
+ import type Stepper from './components/navigation/Stepper.svelte';
40
44
  export type ButtonProps = ComponentProps<Button>;
41
45
  export type IconButtonProps = ComponentProps<IconButton>;
42
46
  export type FloatingActionButtonProps = ComponentProps<FloatingActionButton>;
@@ -50,6 +54,8 @@ export type RadioGroupProps = ComponentProps<RadioGroup>;
50
54
  export type DatePickerProps = ComponentProps<DatePicker>;
51
55
  export type TimePickerProps = ComponentProps<TimePicker>;
52
56
  export type DateTimePickerProps = ComponentProps<DateTimePicker>;
57
+ export type StepperFormProps = ComponentProps<StepperForm>;
58
+ export type StepProps = ComponentProps<Step>;
53
59
  export type CardProps = ComponentProps<Card>;
54
60
  export type TabsProps = ComponentProps<Tabs>;
55
61
  export type ModalProps = ComponentProps<Modal>;
@@ -58,3 +64,4 @@ export type AlertProps = ComponentProps<Alert>;
58
64
  export type TooltipProps = ComponentProps<Tooltip>;
59
65
  export type AccordionProps = ComponentProps<Accordion>;
60
66
  export type SparklineProps = ComponentProps<Sparkline>;
67
+ export type StepperProps = ComponentProps<Stepper>;
package/dist/index.js CHANGED
@@ -5,15 +5,15 @@
5
5
  */
6
6
  // Import theme CSS to ensure it's bundled
7
7
  import './theme.css';
8
- export const version = '0.6.0';
8
+ export const version = '0.7.1';
9
9
  // ===== Button Components =====
10
10
  export { Button, IconButton, ButtonGroup, LinkButton, FloatingActionButton } from './components/buttons';
11
11
  // ===== Form Components =====
12
- export { Input, Textarea, Select, Checkbox, Switch, RadioGroup, RangeSlider, FileUpload, InputGroup, DatePicker, TimePicker, DateTimePicker } from './components/forms';
12
+ export { Input, Textarea, Select, Checkbox, Switch, RadioGroup, RangeSlider, FileUpload, InputGroup, DatePicker, TimePicker, DateTimePicker, StepperForm, Step } from './components/forms';
13
13
  // ===== Layout Components =====
14
14
  export { Card, Panel, Grid, Container, SectionHeader, Divider } from './components/cards';
15
15
  // ===== Navigation Components =====
16
- export { Navbar, Sidebar, Tabs, Breadcrumbs, Menu, DropdownMenu, ContextMenu } from './components/navigation';
16
+ export { Navbar, Sidebar, Tabs, Breadcrumbs, Menu, DropdownMenu, ContextMenu, Stepper } from './components/navigation';
17
17
  // ===== Overlay & Feedback Components =====
18
18
  export { Modal, Alert, Spinner, Tooltip, ProgressBar, SkeletonLoader, Toast, Drawer, Popover, Dropdown, CommandPalette } from './components/overlays';
19
19
  // ===== Typography Components =====
package/dist/theme.css CHANGED
@@ -9,6 +9,7 @@
9
9
  --color-red-400: oklch(70.4% 0.191 22.216);
10
10
  --color-red-500: oklch(63.7% 0.237 25.331);
11
11
  --color-neutral-400: oklch(70.8% 0 0);
12
+ --color-white: #fff;
12
13
  --spacing: 0.25rem;
13
14
  --breakpoint-sm: 40rem;
14
15
  --breakpoint-md: 48rem;
@@ -43,6 +44,7 @@
43
44
  --radius-lg: 14px;
44
45
  --radius-xl: 18px;
45
46
  --radius-2xl: 24px;
47
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
46
48
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
47
49
  --animate-spin: spin 1s linear infinite;
48
50
  --blur-md: 12px;
@@ -701,6 +703,9 @@
701
703
  .my-6 {
702
704
  margin-block: calc(var(--spacing) * 6);
703
705
  }
706
+ .-mt-3 {
707
+ margin-top: calc(var(--spacing) * -3);
708
+ }
704
709
  .mt-1 {
705
710
  margin-top: calc(var(--spacing) * 1);
706
711
  }
@@ -719,6 +724,9 @@
719
724
  .mr-2 {
720
725
  margin-right: calc(var(--spacing) * 2);
721
726
  }
727
+ .mr-4 {
728
+ margin-right: calc(var(--spacing) * 4);
729
+ }
722
730
  .mb-1 {
723
731
  margin-bottom: calc(var(--spacing) * 1);
724
732
  }
@@ -788,6 +796,9 @@
788
796
  .table {
789
797
  display: table;
790
798
  }
799
+ .h-0\.5 {
800
+ height: calc(var(--spacing) * 0.5);
801
+ }
791
802
  .h-2 {
792
803
  height: calc(var(--spacing) * 2);
793
804
  }
@@ -848,9 +859,15 @@
848
859
  .max-h-\[400px\] {
849
860
  max-height: 400px;
850
861
  }
862
+ .min-h-8 {
863
+ min-height: calc(var(--spacing) * 8);
864
+ }
851
865
  .min-h-\[100px\] {
852
866
  min-height: 100px;
853
867
  }
868
+ .w-0\.5 {
869
+ width: calc(var(--spacing) * 0.5);
870
+ }
854
871
  .w-2 {
855
872
  width: calc(var(--spacing) * 2);
856
873
  }
@@ -1313,12 +1330,21 @@
1313
1330
  .bg-\[var\(--color-accent-overlay-20\)\] {
1314
1331
  background-color: var(--color-accent-overlay-20);
1315
1332
  }
1333
+ .bg-\[var\(--color-accent-primary\)\] {
1334
+ background-color: var(--color-accent-primary);
1335
+ }
1336
+ .bg-\[var\(--color-base-1\)\] {
1337
+ background-color: var(--color-base-1);
1338
+ }
1316
1339
  .bg-\[var\(--color-base-2\)\] {
1317
1340
  background-color: var(--color-base-2);
1318
1341
  }
1319
1342
  .bg-\[var\(--color-base-3\)\] {
1320
1343
  background-color: var(--color-base-3);
1321
1344
  }
1345
+ .bg-\[var\(--color-error\)\] {
1346
+ background-color: var(--color-error);
1347
+ }
1322
1348
  .bg-\[var\(--color-error-overlay-15\)\] {
1323
1349
  background-color: var(--color-error-overlay-15);
1324
1350
  }
@@ -1451,6 +1477,9 @@
1451
1477
  .pb-3 {
1452
1478
  padding-bottom: calc(var(--spacing) * 3);
1453
1479
  }
1480
+ .pb-8 {
1481
+ padding-bottom: calc(var(--spacing) * 8);
1482
+ }
1454
1483
  .pl-4 {
1455
1484
  padding-left: calc(var(--spacing) * 4);
1456
1485
  }
@@ -1545,6 +1574,9 @@
1545
1574
  .text-\[var\(--color-accent\)\] {
1546
1575
  color: var(--color-accent);
1547
1576
  }
1577
+ .text-\[var\(--color-accent-primary\)\] {
1578
+ color: var(--color-accent-primary);
1579
+ }
1548
1580
  .text-\[var\(--color-accent-soft\)\] {
1549
1581
  color: var(--color-accent-soft);
1550
1582
  }
@@ -1563,9 +1595,15 @@
1563
1595
  .text-\[var\(--color-text-muted\)\] {
1564
1596
  color: var(--color-text-muted);
1565
1597
  }
1598
+ .text-\[var\(--color-text-secondary\)\] {
1599
+ color: var(--color-text-secondary);
1600
+ }
1566
1601
  .text-\[var\(--color-text-soft\)\] {
1567
1602
  color: var(--color-text-soft);
1568
1603
  }
1604
+ .text-\[var\(--color-text-tertiary\)\] {
1605
+ color: var(--color-text-tertiary);
1606
+ }
1569
1607
  .text-\[var\(--color-warning\)\] {
1570
1608
  color: var(--color-warning);
1571
1609
  }
@@ -1602,6 +1640,9 @@
1602
1640
  .text-warning {
1603
1641
  color: var(--color-warning);
1604
1642
  }
1643
+ .text-white {
1644
+ color: var(--color-white);
1645
+ }
1605
1646
  .lowercase {
1606
1647
  text-transform: lowercase;
1607
1648
  }
@@ -1636,6 +1677,10 @@
1636
1677
  --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
1637
1678
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1638
1679
  }
1680
+ .shadow-\[0_0_0_4px_rgba\(139\,92\,246\,0\.3\)\] {
1681
+ --tw-shadow: 0 0 0 4px var(--tw-shadow-color, rgba(139,92,246,0.3));
1682
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1683
+ }
1639
1684
  .shadow-\[0_0_12px_var\(--color-info-overlay-20\)\] {
1640
1685
  --tw-shadow: 0 0 12px var(--tw-shadow-color, var(--color-info-overlay-20));
1641
1686
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -1656,6 +1701,20 @@
1656
1701
  --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
1657
1702
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1658
1703
  }
1704
+ .ring-2 {
1705
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
1706
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1707
+ }
1708
+ .ring-accent {
1709
+ --tw-ring-color: var(--color-accent);
1710
+ }
1711
+ .ring-offset-2 {
1712
+ --tw-ring-offset-width: 2px;
1713
+ --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
1714
+ }
1715
+ .ring-offset-base-0 {
1716
+ --tw-ring-offset-color: var(--color-base-0);
1717
+ }
1659
1718
  .outline {
1660
1719
  outline-style: var(--tw-outline-style);
1661
1720
  outline-width: 1px;
@@ -1738,6 +1797,10 @@
1738
1797
  --tw-ease: var(--ease-luxe);
1739
1798
  transition-timing-function: var(--ease-luxe);
1740
1799
  }
1800
+ .ease-out {
1801
+ --tw-ease: var(--ease-out);
1802
+ transition-timing-function: var(--ease-out);
1803
+ }
1741
1804
  .outline-none {
1742
1805
  --tw-outline-style: none;
1743
1806
  outline-style: none;
@@ -1997,6 +2060,11 @@
1997
2060
  margin-left: calc(var(--spacing) * 64);
1998
2061
  }
1999
2062
  }
2063
+ .md\:block {
2064
+ @media (width >= 48rem) {
2065
+ display: block;
2066
+ }
2067
+ }
2000
2068
  .md\:grid-cols-2 {
2001
2069
  @media (width >= 48rem) {
2002
2070
  grid-template-columns: repeat(2, minmax(0, 1fr));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrintel/villain-ui",
3
- "version": "0.6.3",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "A luxury Svelte 5 component library with a distinctive dark aesthetic, built with TypeScript and Tailwind CSS v4",