@mrintel/villain-ui 0.7.9 → 0.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.
Files changed (34) hide show
  1. package/dist/components/buttons/Button.svelte.d.ts +1 -1
  2. package/dist/components/buttons/IconButton.svelte.d.ts +1 -1
  3. package/dist/components/buttons/LinkButton.svelte.d.ts +1 -1
  4. package/dist/components/buttons/buttonClasses.d.ts +2 -0
  5. package/dist/components/buttons/buttonClasses.js +2 -1
  6. package/dist/components/data/Data.types.d.ts +10 -0
  7. package/dist/components/data/EmptyState.svelte +42 -0
  8. package/dist/components/data/EmptyState.svelte.d.ts +13 -0
  9. package/dist/components/data/Stat.svelte +30 -2
  10. package/dist/components/data/Stat.svelte.d.ts +9 -0
  11. package/dist/components/data/WeekHeatmap.svelte +150 -0
  12. package/dist/components/data/WeekHeatmap.svelte.d.ts +47 -0
  13. package/dist/components/data/index.d.ts +3 -1
  14. package/dist/components/data/index.js +2 -0
  15. package/dist/components/forms/Input.svelte +94 -9
  16. package/dist/components/forms/Input.svelte.d.ts +8 -0
  17. package/dist/components/overlays/ToastHost.svelte +19 -0
  18. package/dist/components/overlays/ToastHost.svelte.d.ts +8 -0
  19. package/dist/components/overlays/index.d.ts +3 -0
  20. package/dist/components/overlays/index.js +2 -0
  21. package/dist/components/overlays/toast.store.d.ts +32 -0
  22. package/dist/components/overlays/toast.store.js +34 -0
  23. package/dist/components/typography/Heading.svelte +3 -3
  24. package/dist/components/typography/Heading.svelte.d.ts +1 -0
  25. package/dist/components/typography/Text.svelte +2 -2
  26. package/dist/components/typography/Text.svelte.d.ts +1 -0
  27. package/dist/components/wizard/StepForm.svelte +21 -1
  28. package/dist/components/wizard/StepForm.svelte.d.ts +8 -1
  29. package/dist/components/wizard/step.service.js +89 -21
  30. package/dist/components/wizard/step.types.d.ts +31 -0
  31. package/dist/index.d.ts +7 -4
  32. package/dist/index.js +3 -3
  33. package/dist/theme.css +122 -0
  34. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  export interface Props {
2
- variant?: 'primary' | 'secondary' | 'ghost';
2
+ variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
3
3
  size?: 'sm' | 'md' | 'lg';
4
4
  disabled?: boolean;
5
5
  type?: 'button' | 'submit' | 'reset';
@@ -1,5 +1,5 @@
1
1
  export interface Props {
2
- variant?: 'primary' | 'secondary' | 'ghost';
2
+ variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
3
3
  size?: 'sm' | 'md' | 'lg';
4
4
  shape?: 'circle' | 'square';
5
5
  disabled?: boolean;
@@ -1,6 +1,6 @@
1
1
  export interface Props {
2
2
  href: string;
3
- variant?: 'primary' | 'secondary' | 'ghost';
3
+ variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
4
4
  size?: 'sm' | 'md' | 'lg';
5
5
  disabled?: boolean;
6
6
  target?: '_self' | '_blank' | '_parent' | '_top';
@@ -2,7 +2,9 @@ export declare const variantClasses: {
2
2
  primary: string;
3
3
  secondary: string;
4
4
  ghost: string;
5
+ danger: string;
5
6
  };
7
+ export type ButtonVariant = keyof typeof variantClasses;
6
8
  export declare const sizeClasses: {
7
9
  sm: string;
8
10
  md: string;
@@ -1,7 +1,8 @@
1
1
  export const variantClasses = {
2
2
  primary: 'bg-[var(--color-accent)] text-[var(--color-text)] accent-glow hover-lift',
3
3
  secondary: 'bg-transparent text-[var(--color-text)] border border-[var(--color-border-strong)] hover:border-[var(--color-accent)] hover-lift',
4
- ghost: 'bg-transparent text-[var(--color-text)] hover:bg-[var(--color-base-2)] hover-lift'
4
+ ghost: 'bg-transparent text-[var(--color-text)] hover:bg-[var(--color-base-2)] hover-lift',
5
+ danger: 'bg-[var(--color-error)] text-[var(--color-text)] shadow-[0_0_20px_var(--color-error-overlay-30)] hover-lift'
5
6
  };
6
7
  export const sizeClasses = {
7
8
  sm: 'px-3 py-1.5 text-sm font-medium',
@@ -25,3 +25,13 @@ export interface SelectionState {
25
25
  allSelected: boolean;
26
26
  someSelected: boolean;
27
27
  }
28
+ export interface WeekHeatmapCell {
29
+ /** Matrix row index (0 = Sunday, matching the SDK PeakTimesHeatmap contract) */
30
+ day: number;
31
+ /** Hour of day, 0–23 */
32
+ hour: number;
33
+ /** Raw matrix value */
34
+ value: number;
35
+ /** 0–100, after normalization */
36
+ intensity: number;
37
+ }
@@ -0,0 +1,42 @@
1
+ <script lang="ts">"use strict";
2
+ let { title, message, icon, action, size = 'md', class: className = '' } = $props();
3
+ const sizeClasses = {
4
+ sm: 'py-6 gap-2',
5
+ md: 'py-12 gap-3',
6
+ lg: 'py-20 gap-4'
7
+ };
8
+ const iconSizeClasses = {
9
+ sm: 'text-3xl',
10
+ md: 'text-5xl',
11
+ lg: 'text-6xl'
12
+ };
13
+ </script>
14
+
15
+ <div class="flex flex-col items-center justify-center text-center {sizeClasses[size]} {className}">
16
+ {#if icon}
17
+ <div class="{iconSizeClasses[size]} text-[var(--color-text-muted)]" aria-hidden="true">
18
+ {@render icon()}
19
+ </div>
20
+ {/if}
21
+
22
+ {#if title}
23
+ <p
24
+ class="font-semibold"
25
+ style="color: var(--color-text); font-family: var(--font-heading); font-size: var(--text-h5-size);"
26
+ >
27
+ {title}
28
+ </p>
29
+ {/if}
30
+
31
+ {#if message}
32
+ <p class="max-w-sm text-sm" style="color: var(--color-text-muted); font-family: var(--font-body);">
33
+ {message}
34
+ </p>
35
+ {/if}
36
+
37
+ {#if action}
38
+ <div class="mt-2">
39
+ {@render action()}
40
+ </div>
41
+ {/if}
42
+ </div>
@@ -0,0 +1,13 @@
1
+ interface Props {
2
+ title?: string;
3
+ message?: string;
4
+ /** Icon or illustration shown above the title. */
5
+ icon?: import('svelte').Snippet;
6
+ /** Call-to-action area (typically a Button or LinkButton). */
7
+ action?: import('svelte').Snippet;
8
+ size?: 'sm' | 'md' | 'lg';
9
+ class?: string;
10
+ }
11
+ declare const EmptyState: import("svelte").Component<Props, {}, "">;
12
+ type EmptyState = ReturnType<typeof EmptyState>;
13
+ export default EmptyState;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">"use strict";
2
- let { label, value, change, trend, icon, description } = $props();
2
+ let { label, value, change, trend, icon, description, countUp = false, countUpDuration = 800, format } = $props();
3
3
  const trendColors = {
4
4
  up: 'var(--color-success)',
5
5
  down: 'var(--color-error)',
@@ -7,6 +7,34 @@ const trendColors = {
7
7
  };
8
8
  const trendColor = $derived(trend ? trendColors[trend] : 'var(--color-text-muted)');
9
9
  const showGlow = $derived(trend === 'up');
10
+ // Count-up: animate toward the current numeric value; re-runs when value changes.
11
+ let animatedValue = $state(0);
12
+ $effect(() => {
13
+ if (!countUp || typeof value !== 'number')
14
+ return;
15
+ const target = value;
16
+ const reduced = typeof window !== 'undefined' &&
17
+ window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
18
+ if (reduced) {
19
+ animatedValue = target;
20
+ return;
21
+ }
22
+ const from = animatedValue;
23
+ const start = performance.now();
24
+ let raf = 0;
25
+ const tick = (t) => {
26
+ const p = Math.min(1, (t - start) / countUpDuration);
27
+ const eased = 1 - Math.pow(1 - p, 3);
28
+ animatedValue = Math.round(from + (target - from) * eased);
29
+ if (p < 1)
30
+ raf = requestAnimationFrame(tick);
31
+ };
32
+ raf = requestAnimationFrame(tick);
33
+ return () => cancelAnimationFrame(raf);
34
+ });
35
+ const displayValue = $derived(countUp && typeof value === 'number'
36
+ ? (format ? format(animatedValue) : animatedValue)
37
+ : value);
10
38
  </script>
11
39
 
12
40
  <div class="panel-spectral rounded-[var(--radius-xl)] p-6">
@@ -34,7 +62,7 @@ const showGlow = $derived(trend === 'up');
34
62
  class:text-glow={showGlow}
35
63
  style="color: var(--color-text); font-family: var(--font-heading); font-size: 2.5rem; font-weight: 700; line-height: 1.2;"
36
64
  >
37
- {value}
65
+ {displayValue}
38
66
  </div>
39
67
 
40
68
  <!-- Change Indicator & Description -->
@@ -5,6 +5,15 @@ interface Props {
5
5
  trend?: 'up' | 'down' | 'neutral';
6
6
  icon?: import('svelte').Snippet;
7
7
  description?: string;
8
+ /**
9
+ * Animate numeric values counting up from 0 on mount (easeOutCubic).
10
+ * Ignored for string values; disabled under prefers-reduced-motion.
11
+ */
12
+ countUp?: boolean;
13
+ /** Count-up duration in ms. @default 800 */
14
+ countUpDuration?: number;
15
+ /** Formatter for the displayed number while counting (e.g. toLocaleString). */
16
+ format?: (value: number) => string;
8
17
  }
9
18
  declare const Stat: import("svelte").Component<Props, {}, "">;
10
19
  type Stat = ReturnType<typeof Stat>;
@@ -0,0 +1,150 @@
1
+ <script lang="ts">let { matrix, rowOrder, dayLabels, hourLabel = (h) => String(h), hourLabelEvery = 1, normalize = true, highlight, markNow = false, cellLabel, onCellSelect, selected = null, ariaLabel = 'Weekly traffic heatmap', class: className = '' } = $props();
2
+ const BASE_DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
3
+ const hours = Array.from({ length: 24 }, (_, h) => h);
4
+ // Captured once at init — a long-lived page drifts; accepted (matches hand-rolled versions).
5
+ const now = new Date();
6
+ const nowDay = now.getDay();
7
+ const nowHour = now.getHours();
8
+ const order = $derived(rowOrder ?? [0, 1, 2, 3, 4, 5, 6]);
9
+ const labels = $derived(dayLabels ?? order.map((d) => BASE_DAY_NAMES[d] ?? String(d)));
10
+ const rows = $derived(order.map((day, i) => ({ day, label: labels[i] ?? String(day) })));
11
+ const maxValue = $derived(normalize ? Math.max(1, ...matrix.flat()) : 100);
12
+ function getCell(day, hour) {
13
+ const value = matrix[day]?.[hour] ?? 0;
14
+ const intensity = Math.min(100, Math.max(0, Math.round((value / maxValue) * 100)));
15
+ return { day, hour, value, intensity };
16
+ }
17
+ function labelFor(cell, dayLabel, isNow) {
18
+ const text = cellLabel ? cellLabel(cell) : `${dayLabel} ${hourLabel(cell.hour)} — ${cell.value}`;
19
+ return isNow ? `${text} (now)` : text;
20
+ }
21
+ const fill = (intensity) => `color-mix(in srgb, var(--color-accent) ${intensity}%, var(--color-base-2))`;
22
+ export {};
23
+ </script>
24
+
25
+ {#if matrix.length > 0}
26
+ <div class="heatmap-scroll {className}">
27
+ <div
28
+ class="heatmap-grid"
29
+ role={onCellSelect ? undefined : 'img'}
30
+ aria-label={onCellSelect ? undefined : ariaLabel}
31
+ >
32
+ <!-- Corner + hour axis -->
33
+ <div class="heatmap-corner" aria-hidden="true"></div>
34
+ {#each hours as hour (hour)}
35
+ <div class="heatmap-axis heatmap-hour" aria-hidden="true">
36
+ {hour % hourLabelEvery === 0 ? hourLabel(hour) : ''}
37
+ </div>
38
+ {/each}
39
+
40
+ <!-- Day rows -->
41
+ {#each rows as row (row.day)}
42
+ <div class="heatmap-axis heatmap-day" aria-hidden="true">{row.label}</div>
43
+ {#each hours as hour (hour)}
44
+ {@const cell = getCell(row.day, hour)}
45
+ {@const hl = highlight?.(row.day, hour) ?? false}
46
+ {@const isNow = markNow && row.day === nowDay && hour === nowHour}
47
+ {@const isSel = !!selected && selected.day === row.day && selected.hour === hour}
48
+ {@const label = labelFor(cell, row.label, isNow)}
49
+ {#if onCellSelect}
50
+ <button
51
+ type="button"
52
+ class="heatmap-cell"
53
+ class:is-highlight={hl}
54
+ class:is-ring={isNow || isSel}
55
+ style="background-color: {fill(cell.intensity)}"
56
+ aria-label={label}
57
+ aria-pressed={isSel}
58
+ onclick={() => onCellSelect(cell)}
59
+ ></button>
60
+ {:else}
61
+ <div
62
+ class="heatmap-cell"
63
+ class:is-highlight={hl}
64
+ class:is-ring={isNow || isSel}
65
+ style="background-color: {fill(cell.intensity)}"
66
+ title={label}
67
+ aria-hidden="true"
68
+ ></div>
69
+ {/if}
70
+ {/each}
71
+ {/each}
72
+ </div>
73
+ </div>
74
+ {/if}
75
+
76
+ <style>
77
+ .heatmap-scroll {
78
+ overflow-x: auto;
79
+ }
80
+
81
+ .heatmap-grid {
82
+ display: grid;
83
+ grid-template-columns: 3rem repeat(24, 1fr);
84
+ gap: 2px;
85
+ min-width: 640px;
86
+ }
87
+
88
+ .heatmap-axis {
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ font-size: 0.6rem;
93
+ color: var(--color-text-muted);
94
+ text-align: center;
95
+ }
96
+
97
+ .heatmap-corner {
98
+ min-width: 3rem;
99
+ }
100
+
101
+ .heatmap-cell {
102
+ height: var(--heatmap-cell-size, 1.5rem);
103
+ min-width: var(--heatmap-cell-size, 1.5rem);
104
+ border-radius: 2px;
105
+ border: 0;
106
+ padding: 0;
107
+ }
108
+
109
+ button.heatmap-cell {
110
+ cursor: pointer;
111
+ }
112
+
113
+ button.heatmap-cell:focus-visible {
114
+ outline: 2px solid var(--color-accent);
115
+ outline-offset: 2px;
116
+ }
117
+
118
+ .heatmap-cell.is-highlight {
119
+ outline: 1.5px solid color-mix(in srgb, var(--color-text) 85%, transparent);
120
+ outline-offset: -1.5px;
121
+ }
122
+
123
+ /* Ring (now / selected) wins over the highlight outline. */
124
+ .heatmap-cell.is-ring {
125
+ box-shadow: 0 0 0 2px var(--color-accent);
126
+ position: relative;
127
+ z-index: 1;
128
+ }
129
+
130
+ @media (hover: hover) {
131
+ .heatmap-cell {
132
+ transition: transform 150ms var(--ease-sharp);
133
+ }
134
+
135
+ .heatmap-cell:hover {
136
+ transform: scale(1.2);
137
+ position: relative;
138
+ z-index: 1;
139
+ }
140
+ }
141
+
142
+ @media (prefers-reduced-motion: reduce) {
143
+ .heatmap-cell {
144
+ transition: none;
145
+ }
146
+
147
+ .heatmap-cell:hover {
148
+ transform: none;
149
+ }
150
+ }</style>
@@ -0,0 +1,47 @@
1
+ import type { WeekHeatmapCell } from './Data.types';
2
+ interface Props {
3
+ /** 7 rows × 24 cols. Row index 0 = Sunday. */
4
+ matrix: number[][];
5
+ /**
6
+ * Display order of matrix row indices. Default Sunday-first [0..6].
7
+ * Monday-first = [1, 2, 3, 4, 5, 6, 0].
8
+ */
9
+ rowOrder?: number[];
10
+ /** Labels aligned to rowOrder. Default ['Sun','Mon',...] reordered by rowOrder. */
11
+ dayLabels?: string[];
12
+ /** Hour column label formatter. Default String(hour). E.g. h => '12a'/'6a'/'12p'/'6p'. */
13
+ hourLabel?: (hour: number) => string;
14
+ /**
15
+ * Show a label only every N hour columns (others render empty) to reduce axis noise.
16
+ * Default 1 (label every hour).
17
+ */
18
+ hourLabelEvery?: number;
19
+ /**
20
+ * true (default): scale intensity to max(matrix) — raw-count matrices.
21
+ * false: values are already 0–100 — percentage matrices.
22
+ */
23
+ normalize?: boolean;
24
+ /** Outline overlay predicate (e.g. member's frequent cells). */
25
+ highlight?: (day: number, hour: number) => boolean;
26
+ /** Ring the current weekday+hour cell. Default false. */
27
+ markNow?: boolean;
28
+ /** Tooltip/aria text per cell. Default: `${dayLabel} ${hourLabel} — ${value}`. */
29
+ cellLabel?: (cell: WeekHeatmapCell) => string;
30
+ /**
31
+ * When provided, cells render as <button> and clicking/tapping calls this —
32
+ * the consumer renders its own readout. When absent, cells are inert divs
33
+ * and the grid gets role="img".
34
+ */
35
+ onCellSelect?: (cell: WeekHeatmapCell) => void;
36
+ /** Currently selected cell, ring-highlighted. Consumer-managed (pairs with onCellSelect). */
37
+ selected?: {
38
+ day: number;
39
+ hour: number;
40
+ } | null;
41
+ /** Accessible name for the grid. Default "Weekly traffic heatmap". */
42
+ ariaLabel?: string;
43
+ class?: string;
44
+ }
45
+ declare const WeekHeatmap: import("svelte").Component<Props, {}, "">;
46
+ type WeekHeatmap = ReturnType<typeof WeekHeatmap>;
47
+ export default WeekHeatmap;
@@ -1,5 +1,5 @@
1
1
  export { default as Table } from './Table.svelte';
2
- export type { Column as TableColumn, SortDirection, RowKey, SelectionState, CalendarEvent, ListItem } from './Data.types';
2
+ export type { Column as TableColumn, SortDirection, RowKey, SelectionState, CalendarEvent, ListItem, WeekHeatmapCell } from './Data.types';
3
3
  export { default as Pagination } from './Pagination.svelte';
4
4
  export { default as Badge } from './Badge.svelte';
5
5
  export { default as Tag } from './Tag.svelte';
@@ -9,3 +9,5 @@ export { default as CodeBlock } from './CodeBlock.svelte';
9
9
  export { default as Stat } from './Stat.svelte';
10
10
  export { default as CalendarGrid } from './CalendarGrid.svelte';
11
11
  export { default as Sparkline } from './Sparkline.svelte';
12
+ export { default as EmptyState } from './EmptyState.svelte';
13
+ export { default as WeekHeatmap } from './WeekHeatmap.svelte';
@@ -8,3 +8,5 @@ export { default as CodeBlock } from './CodeBlock.svelte';
8
8
  export { default as Stat } from './Stat.svelte';
9
9
  export { default as CalendarGrid } from './CalendarGrid.svelte';
10
10
  export { default as Sparkline } from './Sparkline.svelte';
11
+ export { default as EmptyState } from './EmptyState.svelte';
12
+ export { default as WeekHeatmap } from './WeekHeatmap.svelte';
@@ -1,6 +1,10 @@
1
1
  <script lang="ts">import { createId } from '../../lib/internal/id';
2
2
  import { baseInputClasses, focusClasses, disabledClasses, } from './formClasses';
3
- let { type = 'text', name, value = $bindable(), placeholder, disabled = false, error = false, label, id = createId('input'), autocomplete, 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'), autocomplete, oninput, iconBefore, iconAfter, validate, validationMessage, showValidation = true, revealable = false, min, max, step = 1, class: className = '', } = $props();
4
+ // Password reveal toggle (revealable only applies to type="password")
5
+ let revealed = $state(false);
6
+ const showReveal = $derived(revealable && type === 'password');
7
+ const effectiveType = $derived(showReveal && revealed ? 'text' : type);
4
8
  // Built-in validation patterns
5
9
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6
10
  const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
@@ -65,15 +69,22 @@ $effect(() => {
65
69
  });
66
70
  const hasError = $derived(error || (validationError !== null));
67
71
  const errorClasses = $derived(hasError ? 'error-state' : '');
72
+ function clamp(n) {
73
+ if (min !== undefined && n < min)
74
+ return min;
75
+ if (max !== undefined && n > max)
76
+ return max;
77
+ return n;
78
+ }
68
79
  function increment() {
69
80
  if (disabled || type !== 'number')
70
81
  return;
71
- value = Number(value || 0) + 1;
82
+ value = clamp(Number(value || 0) + step);
72
83
  }
73
84
  function decrement() {
74
85
  if (disabled || type !== 'number')
75
86
  return;
76
- value = Number(value || 0) - 1;
87
+ value = clamp(Number(value || 0) - step);
77
88
  }
78
89
  </script>
79
90
 
@@ -94,23 +105,60 @@ function decrement() {
94
105
 
95
106
  <!-- INPUT FIELD -->
96
107
  <input
97
- {type}
108
+ type={effectiveType}
98
109
  {name}
99
110
  {id}
100
111
  {placeholder}
101
112
  {disabled}
102
113
  {autocomplete}
114
+ min={type === 'number' ? min : undefined}
115
+ max={type === 'number' ? max : undefined}
116
+ step={type === 'number' ? step : undefined}
103
117
  bind:value
104
118
  {oninput}
105
119
  class="{baseInputClasses} {focusClasses} {errorClasses} {disabled
106
120
  ? disabledClasses
107
121
  : ''} {className}"
108
122
  class:pl-12={iconBefore}
109
- class:pr-12={iconAfter || type === 'number'}
123
+ class:pr-12={iconAfter || showReveal || type === 'number'}
110
124
  />
111
125
 
112
126
  <!-- AFTER ICON (non-number) -->
113
- {#if iconAfter && type !== 'number'}
127
+ {#if showReveal}
128
+ <button
129
+ type="button"
130
+ onclick={() => (revealed = !revealed)}
131
+ class="absolute right-4 z-10 inline-flex cursor-pointer items-center justify-center text-text-soft transition-colors hover:text-[var(--color-text)]"
132
+ aria-label={revealed ? 'Hide password' : 'Show password'}
133
+ aria-pressed={revealed}
134
+ >
135
+ {#if revealed}
136
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
137
+ <path
138
+ stroke-linecap="round"
139
+ stroke-linejoin="round"
140
+ stroke-width="2"
141
+ d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
142
+ />
143
+ </svg>
144
+ {:else}
145
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
146
+ <path
147
+ stroke-linecap="round"
148
+ stroke-linejoin="round"
149
+ stroke-width="2"
150
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
151
+ />
152
+ <path
153
+ stroke-linecap="round"
154
+ stroke-linejoin="round"
155
+ stroke-width="2"
156
+ d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
157
+ />
158
+ </svg>
159
+ {/if}
160
+ </button>
161
+ {:else if iconAfter && type !== 'number'}
114
162
  <span
115
163
  class="absolute right-4 z-10 inline-flex items-center justify-center text-text-soft pointer-events-none"
116
164
  >
@@ -158,22 +206,59 @@ function decrement() {
158
206
  {/if}
159
207
 
160
208
  <input
161
- {type}
209
+ type={effectiveType}
162
210
  {name}
163
211
  {id}
164
212
  {placeholder}
165
213
  {disabled}
166
214
  {autocomplete}
215
+ min={type === 'number' ? min : undefined}
216
+ max={type === 'number' ? max : undefined}
217
+ step={type === 'number' ? step : undefined}
167
218
  bind:value
168
219
  {oninput}
169
220
  class="{baseInputClasses} {focusClasses} {errorClasses} {disabled
170
221
  ? disabledClasses
171
222
  : ''} {className}"
172
223
  class:pl-12={iconBefore}
173
- class:pr-12={iconAfter || type === 'number'}
224
+ class:pr-12={iconAfter || showReveal || type === 'number'}
174
225
  />
175
226
 
176
- {#if iconAfter && type !== 'number'}
227
+ {#if showReveal}
228
+ <button
229
+ type="button"
230
+ onclick={() => (revealed = !revealed)}
231
+ class="absolute right-4 z-10 inline-flex cursor-pointer items-center justify-center text-text-soft transition-colors hover:text-[var(--color-text)]"
232
+ aria-label={revealed ? 'Hide password' : 'Show password'}
233
+ aria-pressed={revealed}
234
+ >
235
+ {#if revealed}
236
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
237
+ <path
238
+ stroke-linecap="round"
239
+ stroke-linejoin="round"
240
+ stroke-width="2"
241
+ d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
242
+ />
243
+ </svg>
244
+ {:else}
245
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
246
+ <path
247
+ stroke-linecap="round"
248
+ stroke-linejoin="round"
249
+ stroke-width="2"
250
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
251
+ />
252
+ <path
253
+ stroke-linecap="round"
254
+ stroke-linejoin="round"
255
+ stroke-width="2"
256
+ d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
257
+ />
258
+ </svg>
259
+ {/if}
260
+ </button>
261
+ {:else if iconAfter && type !== 'number'}
177
262
  <span
178
263
  class="absolute right-4 z-10 inline-flex items-center justify-center text-text-soft pointer-events-none"
179
264
  >
@@ -14,6 +14,14 @@ export interface Props {
14
14
  validate?: (value: string | number) => boolean | string;
15
15
  validationMessage?: string;
16
16
  showValidation?: boolean;
17
+ /** Password inputs only: show an eye toggle to reveal the value. Replaces iconAfter. */
18
+ revealable?: boolean;
19
+ /** Number inputs only: lower bound enforced by the stepper arrows. */
20
+ min?: number;
21
+ /** Number inputs only: upper bound enforced by the stepper arrows. */
22
+ max?: number;
23
+ /** Number inputs only: stepper arrow increment. @default 1 */
24
+ step?: number;
17
25
  class?: string;
18
26
  }
19
27
  declare const Input: import("svelte").Component<Props, {}, "value">;
@@ -0,0 +1,19 @@
1
+ <script lang="ts">import Toast from './Toast.svelte';
2
+ import { toasts, dismissToast } from './toast.store';
3
+ let { position = 'bottom-right', maxVisible = 4 } = $props();
4
+ let items = $state(toasts.get());
5
+ $effect(() => toasts.subscribe((value) => (items = value)));
6
+ const visible = $derived(items.slice(0, maxVisible));
7
+ </script>
8
+
9
+ {#each visible as item, index (item.id)}
10
+ <Toast
11
+ message={item.message}
12
+ variant={item.variant}
13
+ duration={item.duration}
14
+ dismissible={item.dismissible}
15
+ {position}
16
+ {index}
17
+ onClose={() => dismissToast(item.id)}
18
+ />
19
+ {/each}
@@ -0,0 +1,8 @@
1
+ interface Props {
2
+ position?: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
3
+ /** Newest toasts beyond this count are queued until older ones dismiss. */
4
+ maxVisible?: number;
5
+ }
6
+ declare const ToastHost: import("svelte").Component<Props, {}, "">;
7
+ type ToastHost = ReturnType<typeof ToastHost>;
8
+ export default ToastHost;
@@ -5,6 +5,9 @@ export { default as Tooltip } from './Tooltip.svelte';
5
5
  export { default as ProgressBar } from './ProgressBar.svelte';
6
6
  export { default as SkeletonLoader } from './SkeletonLoader.svelte';
7
7
  export { default as Toast } from './Toast.svelte';
8
+ export { default as ToastHost } from './ToastHost.svelte';
9
+ export { toast, toasts, dismissToast } from './toast.store';
10
+ export type { ToastItem, ToastOptions, ToastVariant } from './toast.store';
8
11
  export { default as Drawer } from './Drawer.svelte';
9
12
  export { default as Popover } from './Popover.svelte';
10
13
  export { default as Dropdown } from './Dropdown.svelte';
@@ -5,6 +5,8 @@ export { default as Tooltip } from './Tooltip.svelte';
5
5
  export { default as ProgressBar } from './ProgressBar.svelte';
6
6
  export { default as SkeletonLoader } from './SkeletonLoader.svelte';
7
7
  export { default as Toast } from './Toast.svelte';
8
+ export { default as ToastHost } from './ToastHost.svelte';
9
+ export { toast, toasts, dismissToast } from './toast.store';
8
10
  export { default as Drawer } from './Drawer.svelte';
9
11
  export { default as Popover } from './Popover.svelte';
10
12
  export { default as Dropdown } from './Dropdown.svelte';
@@ -0,0 +1,32 @@
1
+ export type ToastVariant = 'info' | 'success' | 'warning' | 'error';
2
+ export interface ToastOptions {
3
+ variant?: ToastVariant;
4
+ /** Auto-dismiss delay in ms. 0 disables auto-dismiss. Default 3000. */
5
+ duration?: number;
6
+ dismissible?: boolean;
7
+ }
8
+ export interface ToastItem extends Required<Pick<ToastOptions, 'variant' | 'duration' | 'dismissible'>> {
9
+ id: string;
10
+ message: string;
11
+ }
12
+ /** Active toasts, oldest first. Rendered by `<ToastHost />`. */
13
+ export declare const toasts: import("nanostores").PreinitializedWritableAtom<ToastItem[]> & object;
14
+ export declare function dismissToast(id: string): void;
15
+ declare function push(message: string, options?: ToastOptions): string;
16
+ /**
17
+ * Show a toast. Requires a single `<ToastHost />` mounted in the app's root
18
+ * layout. Returns the toast id (usable with `dismissToast`).
19
+ *
20
+ * ```ts
21
+ * toast('Saved');
22
+ * toast.success('Added to cart');
23
+ * toast.error('Could not save', { duration: 0 });
24
+ * ```
25
+ */
26
+ export declare const toast: typeof push & {
27
+ info: (message: string, options?: Omit<ToastOptions, "variant">) => string;
28
+ success: (message: string, options?: Omit<ToastOptions, "variant">) => string;
29
+ warning: (message: string, options?: Omit<ToastOptions, "variant">) => string;
30
+ error: (message: string, options?: Omit<ToastOptions, "variant">) => string;
31
+ };
32
+ export {};