@mrintel/villain-ui 0.7.9 → 0.8.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.
- package/dist/components/buttons/Button.svelte.d.ts +1 -1
- package/dist/components/buttons/IconButton.svelte.d.ts +1 -1
- package/dist/components/buttons/LinkButton.svelte.d.ts +1 -1
- package/dist/components/buttons/buttonClasses.d.ts +2 -0
- package/dist/components/buttons/buttonClasses.js +2 -1
- package/dist/components/data/Data.types.d.ts +10 -0
- package/dist/components/data/EmptyState.svelte +42 -0
- package/dist/components/data/EmptyState.svelte.d.ts +13 -0
- package/dist/components/data/Stat.svelte +30 -2
- package/dist/components/data/Stat.svelte.d.ts +9 -0
- package/dist/components/data/WeekHeatmap.svelte +155 -0
- package/dist/components/data/WeekHeatmap.svelte.d.ts +47 -0
- package/dist/components/data/index.d.ts +3 -1
- package/dist/components/data/index.js +2 -0
- package/dist/components/forms/Input.svelte +94 -9
- package/dist/components/forms/Input.svelte.d.ts +8 -0
- package/dist/components/overlays/ToastHost.svelte +19 -0
- package/dist/components/overlays/ToastHost.svelte.d.ts +8 -0
- package/dist/components/overlays/index.d.ts +3 -0
- package/dist/components/overlays/index.js +2 -0
- package/dist/components/overlays/toast.store.d.ts +32 -0
- package/dist/components/overlays/toast.store.js +34 -0
- package/dist/components/typography/Heading.svelte +3 -3
- package/dist/components/typography/Heading.svelte.d.ts +1 -0
- package/dist/components/typography/Text.svelte +2 -2
- package/dist/components/typography/Text.svelte.d.ts +1 -0
- package/dist/components/wizard/StepForm.svelte +21 -1
- package/dist/components/wizard/StepForm.svelte.d.ts +8 -1
- package/dist/components/wizard/step.service.js +89 -21
- package/dist/components/wizard/step.types.d.ts +31 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +3 -3
- package/dist/theme.css +122 -0
- package/package.json +1 -1
|
@@ -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
|
-
{
|
|
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,155 @@
|
|
|
1
|
+
<script lang="ts">import Tooltip from '../overlays/Tooltip.svelte';
|
|
2
|
+
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();
|
|
3
|
+
const BASE_DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
4
|
+
const hours = Array.from({ length: 24 }, (_, h) => h);
|
|
5
|
+
// Captured once at init — a long-lived page drifts; accepted (matches hand-rolled versions).
|
|
6
|
+
const now = new Date();
|
|
7
|
+
const nowDay = now.getDay();
|
|
8
|
+
const nowHour = now.getHours();
|
|
9
|
+
const order = $derived(rowOrder ?? [0, 1, 2, 3, 4, 5, 6]);
|
|
10
|
+
const labels = $derived(dayLabels ?? order.map((d) => BASE_DAY_NAMES[d] ?? String(d)));
|
|
11
|
+
const rows = $derived(order.map((day, i) => ({ day, label: labels[i] ?? String(day) })));
|
|
12
|
+
const maxValue = $derived(normalize ? Math.max(1, ...matrix.flat()) : 100);
|
|
13
|
+
function getCell(day, hour) {
|
|
14
|
+
const value = matrix[day]?.[hour] ?? 0;
|
|
15
|
+
const intensity = Math.min(100, Math.max(0, Math.round((value / maxValue) * 100)));
|
|
16
|
+
return { day, hour, value, intensity };
|
|
17
|
+
}
|
|
18
|
+
function labelFor(cell, dayLabel, isNow) {
|
|
19
|
+
const text = cellLabel ? cellLabel(cell) : `${dayLabel} ${hourLabel(cell.hour)}: ${cell.value}`;
|
|
20
|
+
return isNow ? `${text} (now)` : text;
|
|
21
|
+
}
|
|
22
|
+
const fill = (intensity) => `color-mix(in srgb, var(--color-accent) ${intensity}%, var(--color-base-2))`;
|
|
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
|
+
<!-- ponytail: one Tooltip per cell (168 total). Fine for a heatmap rendered
|
|
50
|
+
once per page; swap for a single hovered-cell-driven tooltip if it ever bites. -->
|
|
51
|
+
<Tooltip content={label}>
|
|
52
|
+
{#if onCellSelect}
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
class="heatmap-cell"
|
|
56
|
+
class:is-highlight={hl}
|
|
57
|
+
class:is-ring={isNow || isSel}
|
|
58
|
+
style="background-color: {fill(cell.intensity)}"
|
|
59
|
+
aria-label={label}
|
|
60
|
+
aria-pressed={isSel}
|
|
61
|
+
onclick={() => onCellSelect(cell)}
|
|
62
|
+
></button>
|
|
63
|
+
{:else}
|
|
64
|
+
<div
|
|
65
|
+
class="heatmap-cell"
|
|
66
|
+
class:is-highlight={hl}
|
|
67
|
+
class:is-ring={isNow || isSel}
|
|
68
|
+
style="background-color: {fill(cell.intensity)}"
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
></div>
|
|
71
|
+
{/if}
|
|
72
|
+
</Tooltip>
|
|
73
|
+
{/each}
|
|
74
|
+
{/each}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
{/if}
|
|
78
|
+
|
|
79
|
+
<style>
|
|
80
|
+
.heatmap-scroll {
|
|
81
|
+
overflow-x: auto;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.heatmap-grid {
|
|
85
|
+
display: grid;
|
|
86
|
+
grid-template-columns: 3rem repeat(24, 1fr);
|
|
87
|
+
gap: 2px;
|
|
88
|
+
min-width: 640px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.heatmap-axis {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
font-size: 0.6rem;
|
|
96
|
+
color: var(--color-text-muted);
|
|
97
|
+
text-align: center;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.heatmap-corner {
|
|
101
|
+
min-width: 3rem;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.heatmap-cell {
|
|
105
|
+
display: block;
|
|
106
|
+
width: 100%;
|
|
107
|
+
height: var(--heatmap-cell-size, 1.5rem);
|
|
108
|
+
min-width: var(--heatmap-cell-size, 1.5rem);
|
|
109
|
+
border-radius: 2px;
|
|
110
|
+
border: 0;
|
|
111
|
+
padding: 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
button.heatmap-cell {
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
button.heatmap-cell:focus-visible {
|
|
119
|
+
outline: 2px solid var(--color-accent);
|
|
120
|
+
outline-offset: 2px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.heatmap-cell.is-highlight {
|
|
124
|
+
outline: 1.5px solid color-mix(in srgb, var(--color-text) 85%, transparent);
|
|
125
|
+
outline-offset: -1.5px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Ring (now / selected) wins over the highlight outline. */
|
|
129
|
+
.heatmap-cell.is-ring {
|
|
130
|
+
box-shadow: 0 0 0 2px var(--color-accent);
|
|
131
|
+
position: relative;
|
|
132
|
+
z-index: 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@media (hover: hover) {
|
|
136
|
+
.heatmap-cell {
|
|
137
|
+
transition: transform 150ms var(--ease-sharp);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.heatmap-cell:hover {
|
|
141
|
+
transform: scale(1.2);
|
|
142
|
+
position: relative;
|
|
143
|
+
z-index: 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@media (prefers-reduced-motion: reduce) {
|
|
148
|
+
.heatmap-cell {
|
|
149
|
+
transition: none;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.heatmap-cell:hover {
|
|
153
|
+
transform: none;
|
|
154
|
+
}
|
|
155
|
+
}</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) +
|
|
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) -
|
|
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
|
-
{
|
|
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
|
|
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
|
-
{
|
|
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
|
|
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';
|