@dorsk/tsumikit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/autoresize.d.ts +11 -0
- package/dist/autoresize.js +24 -0
- package/dist/components/atoms/Badge.svelte +72 -0
- package/dist/components/atoms/Badge.svelte.d.ts +12 -0
- package/dist/components/atoms/Button.svelte +156 -0
- package/dist/components/atoms/Button.svelte.d.ts +13 -0
- package/dist/components/atoms/Card.svelte +46 -0
- package/dist/components/atoms/Card.svelte.d.ts +11 -0
- package/dist/components/atoms/Checkbox.svelte +99 -0
- package/dist/components/atoms/Checkbox.svelte.d.ts +10 -0
- package/dist/components/atoms/Chip.svelte +53 -0
- package/dist/components/atoms/Chip.svelte.d.ts +11 -0
- package/dist/components/atoms/Heading.svelte +66 -0
- package/dist/components/atoms/Heading.svelte.d.ts +13 -0
- package/dist/components/atoms/Icon.svelte +151 -0
- package/dist/components/atoms/Icon.svelte.d.ts +18 -0
- package/dist/components/atoms/Input.svelte +42 -0
- package/dist/components/atoms/Input.svelte.d.ts +10 -0
- package/dist/components/atoms/Link.svelte +31 -0
- package/dist/components/atoms/Link.svelte.d.ts +10 -0
- package/dist/components/atoms/Progress.svelte +59 -0
- package/dist/components/atoms/Progress.svelte.d.ts +9 -0
- package/dist/components/atoms/Select.svelte +95 -0
- package/dist/components/atoms/Select.svelte.d.ts +11 -0
- package/dist/components/atoms/Slider.svelte +136 -0
- package/dist/components/atoms/Slider.svelte.d.ts +14 -0
- package/dist/components/atoms/Switch.svelte +64 -0
- package/dist/components/atoms/Switch.svelte.d.ts +8 -0
- package/dist/components/atoms/Text.svelte +127 -0
- package/dist/components/atoms/Text.svelte.d.ts +16 -0
- package/dist/components/atoms/Textarea.svelte +62 -0
- package/dist/components/atoms/Textarea.svelte.d.ts +11 -0
- package/dist/components/layouts/AppShell.svelte +304 -0
- package/dist/components/layouts/AppShell.svelte.d.ts +21 -0
- package/dist/components/layouts/AutoGrid.svelte +36 -0
- package/dist/components/layouts/AutoGrid.svelte.d.ts +12 -0
- package/dist/components/layouts/Cluster.svelte +45 -0
- package/dist/components/layouts/Cluster.svelte.d.ts +14 -0
- package/dist/components/layouts/Container.svelte +40 -0
- package/dist/components/layouts/Container.svelte.d.ts +13 -0
- package/dist/components/layouts/NavItem.svelte +95 -0
- package/dist/components/layouts/NavItem.svelte.d.ts +14 -0
- package/dist/components/layouts/Stack.svelte +44 -0
- package/dist/components/layouts/Stack.svelte.d.ts +13 -0
- package/dist/components/molecules/Accordion.svelte +94 -0
- package/dist/components/molecules/Accordion.svelte.d.ts +16 -0
- package/dist/components/molecules/CodeBlock.svelte +119 -0
- package/dist/components/molecules/CodeBlock.svelte.d.ts +17 -0
- package/dist/components/molecules/CopyButton.svelte +80 -0
- package/dist/components/molecules/CopyButton.svelte.d.ts +13 -0
- package/dist/components/molecules/Dropzone.svelte +140 -0
- package/dist/components/molecules/Dropzone.svelte.d.ts +13 -0
- package/dist/components/molecules/Field.svelte +57 -0
- package/dist/components/molecules/Field.svelte.d.ts +12 -0
- package/dist/components/molecules/FileButton.svelte +68 -0
- package/dist/components/molecules/FileButton.svelte.d.ts +14 -0
- package/dist/components/molecules/FontScalePicker.svelte +21 -0
- package/dist/components/molecules/FontScalePicker.svelte.d.ts +6 -0
- package/dist/components/molecules/IconButton.svelte +36 -0
- package/dist/components/molecules/IconButton.svelte.d.ts +13 -0
- package/dist/components/molecules/Menu.svelte +120 -0
- package/dist/components/molecules/Menu.svelte.d.ts +17 -0
- package/dist/components/molecules/Modal.svelte +263 -0
- package/dist/components/molecules/Modal.svelte.d.ts +13 -0
- package/dist/components/molecules/OptionButton.svelte +76 -0
- package/dist/components/molecules/OptionButton.svelte.d.ts +10 -0
- package/dist/components/molecules/Popover.svelte +125 -0
- package/dist/components/molecules/Popover.svelte.d.ts +18 -0
- package/dist/components/molecules/RadioGroup.svelte +110 -0
- package/dist/components/molecules/RadioGroup.svelte.d.ts +16 -0
- package/dist/components/molecules/SelectButton.svelte +52 -0
- package/dist/components/molecules/SelectButton.svelte.d.ts +15 -0
- package/dist/components/molecules/Tabs.svelte +119 -0
- package/dist/components/molecules/Tabs.svelte.d.ts +15 -0
- package/dist/components/molecules/ThemePicker.svelte +22 -0
- package/dist/components/molecules/ThemePicker.svelte.d.ts +6 -0
- package/dist/components/molecules/Toaster.svelte +73 -0
- package/dist/components/molecules/Toaster.svelte.d.ts +18 -0
- package/dist/components/molecules/Toggle.svelte +68 -0
- package/dist/components/molecules/Toggle.svelte.d.ts +11 -0
- package/dist/components/molecules/Tooltip.svelte +106 -0
- package/dist/components/molecules/Tooltip.svelte.d.ts +10 -0
- package/dist/components/organisms/DataTable.svelte +145 -0
- package/dist/components/organisms/DataTable.svelte.d.ts +43 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +4 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +56 -0
- package/dist/stores/fontscale.svelte.d.ts +15 -0
- package/dist/stores/fontscale.svelte.js +49 -0
- package/dist/stores/theme.svelte.d.ts +96 -0
- package/dist/stores/theme.svelte.js +71 -0
- package/dist/stores/toast.svelte.d.ts +19 -0
- package/dist/stores/toast.svelte.js +26 -0
- package/dist/styles/app.css +522 -0
- package/dist/styles/variables.css +651 -0
- package/package.json +71 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export interface RadioOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
hint?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
// Radio group built on native <input type="radio"> sharing one `name`, so the
|
|
12
|
+
// browser handles single-selection, arrow-key navigation and form
|
|
13
|
+
// participation. Wrapped in a labelled `radiogroup` with token-styled dots.
|
|
14
|
+
// `value` is bindable.
|
|
15
|
+
let {
|
|
16
|
+
options,
|
|
17
|
+
value = $bindable(),
|
|
18
|
+
name = `radio-${Math.random().toString(36).slice(2, 8)}`,
|
|
19
|
+
label,
|
|
20
|
+
class: klass = ''
|
|
21
|
+
}: {
|
|
22
|
+
options: RadioOption[];
|
|
23
|
+
value?: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
label: string;
|
|
26
|
+
class?: string;
|
|
27
|
+
} = $props();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<div role="radiogroup" aria-label={label} class="radio-group {klass}">
|
|
31
|
+
{#each options as o (o.value)}
|
|
32
|
+
<label class="radio" class:disabled={o.disabled}>
|
|
33
|
+
<input type="radio" {name} value={o.value} bind:group={value} disabled={o.disabled} />
|
|
34
|
+
<span class="dot-ctl" aria-hidden="true"></span>
|
|
35
|
+
<span class="texts">
|
|
36
|
+
<span class="label-text">{o.label}</span>
|
|
37
|
+
{#if o.hint}<span class="hint">{o.hint}</span>{/if}
|
|
38
|
+
</span>
|
|
39
|
+
</label>
|
|
40
|
+
{/each}
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<style>
|
|
44
|
+
.radio-group {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
gap: var(--sp-2);
|
|
48
|
+
}
|
|
49
|
+
.radio {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: flex-start;
|
|
52
|
+
gap: var(--sp-2);
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
font-size: var(--fs-sm);
|
|
55
|
+
color: var(--text);
|
|
56
|
+
}
|
|
57
|
+
.radio.disabled {
|
|
58
|
+
cursor: not-allowed;
|
|
59
|
+
opacity: 0.45;
|
|
60
|
+
}
|
|
61
|
+
input {
|
|
62
|
+
position: absolute;
|
|
63
|
+
width: 1px;
|
|
64
|
+
height: 1px;
|
|
65
|
+
opacity: 0;
|
|
66
|
+
margin: 0;
|
|
67
|
+
}
|
|
68
|
+
.dot-ctl {
|
|
69
|
+
position: relative;
|
|
70
|
+
flex: none;
|
|
71
|
+
width: 1.15rem;
|
|
72
|
+
height: 1.15rem;
|
|
73
|
+
margin-top: 0.1rem;
|
|
74
|
+
border: 1px solid var(--border-strong);
|
|
75
|
+
border-radius: var(--r-pill);
|
|
76
|
+
background: var(--bg);
|
|
77
|
+
transition: border-color 0.12s var(--ease);
|
|
78
|
+
}
|
|
79
|
+
.dot-ctl::after {
|
|
80
|
+
content: '';
|
|
81
|
+
position: absolute;
|
|
82
|
+
inset: 50%;
|
|
83
|
+
width: 0.55rem;
|
|
84
|
+
height: 0.55rem;
|
|
85
|
+
margin: -0.275rem 0 0 -0.275rem;
|
|
86
|
+
border-radius: var(--r-pill);
|
|
87
|
+
background: var(--accent);
|
|
88
|
+
transform: scale(0);
|
|
89
|
+
transition: transform 0.1s var(--ease);
|
|
90
|
+
}
|
|
91
|
+
input:checked + .dot-ctl {
|
|
92
|
+
border-color: var(--accent);
|
|
93
|
+
}
|
|
94
|
+
input:checked + .dot-ctl::after {
|
|
95
|
+
transform: scale(1);
|
|
96
|
+
}
|
|
97
|
+
input:focus-visible + .dot-ctl {
|
|
98
|
+
outline: 2px solid var(--accent);
|
|
99
|
+
outline-offset: 2px;
|
|
100
|
+
}
|
|
101
|
+
.texts {
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-direction: column;
|
|
104
|
+
gap: 1px;
|
|
105
|
+
}
|
|
106
|
+
.hint {
|
|
107
|
+
font-size: var(--fs-xs);
|
|
108
|
+
color: var(--text-faint);
|
|
109
|
+
}
|
|
110
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface RadioOption {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
hint?: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
type $$ComponentProps = {
|
|
8
|
+
options: RadioOption[];
|
|
9
|
+
value?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
label: string;
|
|
12
|
+
class?: string;
|
|
13
|
+
};
|
|
14
|
+
declare const RadioGroup: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
15
|
+
type RadioGroup = ReturnType<typeof RadioGroup>;
|
|
16
|
+
export default RadioGroup;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// An icon/glyph button with a native <select> overlaid transparently on top:
|
|
3
|
+
// it gets the platform dropdown UI while keeping the compact icon-button
|
|
4
|
+
// affordance. Used by the header font-size + theme pickers and the drawer
|
|
5
|
+
// font-size picker (CCT-250 #5 / CCT-297 #11). Composes the Button primitive
|
|
6
|
+
// for the base look.
|
|
7
|
+
import Button from '../atoms/Button.svelte';
|
|
8
|
+
import Select from '../atoms/Select.svelte';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
glyph,
|
|
12
|
+
label,
|
|
13
|
+
title,
|
|
14
|
+
value,
|
|
15
|
+
options,
|
|
16
|
+
onchange,
|
|
17
|
+
class: klass = ''
|
|
18
|
+
}: {
|
|
19
|
+
// Visible content of the button (a letter like "A" or an emoji icon).
|
|
20
|
+
glyph: string;
|
|
21
|
+
label: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
value: string;
|
|
24
|
+
options: { value: string; label: string }[];
|
|
25
|
+
onchange: (value: string) => void;
|
|
26
|
+
class?: string;
|
|
27
|
+
} = $props();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<Button variant="ghost" class="select-button btn-icon {klass}" {title} aria-label={label}>
|
|
31
|
+
<span aria-hidden="true">{glyph}</span>
|
|
32
|
+
<Select
|
|
33
|
+
variant="ghost"
|
|
34
|
+
aria-label={label}
|
|
35
|
+
{value}
|
|
36
|
+
onchange={(e) => onchange((e.currentTarget as HTMLSelectElement).value)}
|
|
37
|
+
>
|
|
38
|
+
{#each options as o (o.value)}
|
|
39
|
+
<option value={o.value}>{o.label}</option>
|
|
40
|
+
{/each}
|
|
41
|
+
</Select>
|
|
42
|
+
</Button>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
/* The button must clip + position the overlaid select. It's rendered by
|
|
46
|
+
Button (child component), so target it via :global. */
|
|
47
|
+
:global(.select-button) {
|
|
48
|
+
position: relative;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
font-weight: var(--fw-bold);
|
|
51
|
+
}
|
|
52
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
glyph: string;
|
|
3
|
+
label: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
value: string;
|
|
6
|
+
options: {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}[];
|
|
10
|
+
onchange: (value: string) => void;
|
|
11
|
+
class?: string;
|
|
12
|
+
};
|
|
13
|
+
declare const SelectButton: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
14
|
+
type SelectButton = ReturnType<typeof SelectButton>;
|
|
15
|
+
export default SelectButton;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export interface TabItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: import('../atoms/Icon.svelte').IconName;
|
|
6
|
+
}
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
// WAI-ARIA tabs. A `tablist` of `tab`s controlling a single `tabpanel`.
|
|
11
|
+
// Follows the automatic-activation pattern: ←/→ (and Home/End) move selection
|
|
12
|
+
// and reveal the panel in one step; roving tabindex keeps exactly one tab in
|
|
13
|
+
// the Tab order. `value` is bindable; `panel` is a snippet that receives the
|
|
14
|
+
// active id so the caller renders the matching content.
|
|
15
|
+
import type { Snippet } from 'svelte';
|
|
16
|
+
import Icon from '../atoms/Icon.svelte';
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
tabs,
|
|
20
|
+
value = $bindable(),
|
|
21
|
+
label = 'Tabs',
|
|
22
|
+
panel
|
|
23
|
+
}: {
|
|
24
|
+
tabs: TabItem[];
|
|
25
|
+
value?: string;
|
|
26
|
+
label?: string;
|
|
27
|
+
panel: Snippet<[string]>;
|
|
28
|
+
} = $props();
|
|
29
|
+
|
|
30
|
+
// Default to the first tab when no value is supplied.
|
|
31
|
+
$effect(() => {
|
|
32
|
+
if (value === undefined && tabs.length) value = tabs[0].id;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let listEl = $state<HTMLDivElement | null>(null);
|
|
36
|
+
const baseId = `tabs-${Math.random().toString(36).slice(2, 8)}`;
|
|
37
|
+
|
|
38
|
+
function select(id: string, focus = false) {
|
|
39
|
+
value = id;
|
|
40
|
+
if (focus) {
|
|
41
|
+
queueMicrotask(() => listEl?.querySelector<HTMLButtonElement>(`#${baseId}-tab-${id}`)?.focus());
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function onkeydown(e: KeyboardEvent) {
|
|
45
|
+
const i = tabs.findIndex((t) => t.id === value);
|
|
46
|
+
if (i < 0) return;
|
|
47
|
+
let next = i;
|
|
48
|
+
if (e.key === 'ArrowRight') next = (i + 1) % tabs.length;
|
|
49
|
+
else if (e.key === 'ArrowLeft') next = (i - 1 + tabs.length) % tabs.length;
|
|
50
|
+
else if (e.key === 'Home') next = 0;
|
|
51
|
+
else if (e.key === 'End') next = tabs.length - 1;
|
|
52
|
+
else return;
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
select(tabs[next].id, true);
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<div class="tabs">
|
|
59
|
+
<div bind:this={listEl} role="tablist" aria-label={label} tabindex="-1" class="tablist" {onkeydown}>
|
|
60
|
+
{#each tabs as t (t.id)}
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
role="tab"
|
|
64
|
+
id="{baseId}-tab-{t.id}"
|
|
65
|
+
aria-selected={value === t.id}
|
|
66
|
+
aria-controls="{baseId}-panel"
|
|
67
|
+
tabindex={value === t.id ? 0 : -1}
|
|
68
|
+
class="tab"
|
|
69
|
+
class:selected={value === t.id}
|
|
70
|
+
onclick={() => select(t.id)}
|
|
71
|
+
>
|
|
72
|
+
{#if t.icon}<Icon name={t.icon} />{/if}
|
|
73
|
+
<span>{t.label}</span>
|
|
74
|
+
</button>
|
|
75
|
+
{/each}
|
|
76
|
+
</div>
|
|
77
|
+
<div role="tabpanel" id="{baseId}-panel" tabindex="0" class="tabpanel">
|
|
78
|
+
{#if value !== undefined}{@render panel(value)}{/if}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<style>
|
|
83
|
+
.tablist {
|
|
84
|
+
display: flex;
|
|
85
|
+
gap: var(--sp-1);
|
|
86
|
+
border-bottom: 1px solid var(--border);
|
|
87
|
+
}
|
|
88
|
+
.tab {
|
|
89
|
+
display: inline-flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: var(--sp-2);
|
|
92
|
+
padding: var(--sp-2) var(--sp-3);
|
|
93
|
+
border: none;
|
|
94
|
+
background: none;
|
|
95
|
+
color: var(--text-muted);
|
|
96
|
+
font-size: var(--fs-sm);
|
|
97
|
+
font-weight: var(--fw-medium);
|
|
98
|
+
border-bottom: 2px solid transparent;
|
|
99
|
+
margin-bottom: -1px;
|
|
100
|
+
transition:
|
|
101
|
+
color 0.12s var(--ease),
|
|
102
|
+
border-color 0.12s var(--ease);
|
|
103
|
+
}
|
|
104
|
+
.tab:hover {
|
|
105
|
+
color: var(--text);
|
|
106
|
+
}
|
|
107
|
+
.tab.selected {
|
|
108
|
+
color: var(--accent);
|
|
109
|
+
border-bottom-color: var(--accent);
|
|
110
|
+
}
|
|
111
|
+
.tabpanel {
|
|
112
|
+
padding-top: var(--sp-4);
|
|
113
|
+
}
|
|
114
|
+
.tabpanel:focus-visible {
|
|
115
|
+
outline: 2px solid var(--accent);
|
|
116
|
+
outline-offset: 2px;
|
|
117
|
+
border-radius: var(--r-sm);
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface TabItem {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
icon?: import('../atoms/Icon.svelte').IconName;
|
|
5
|
+
}
|
|
6
|
+
import type { Snippet } from 'svelte';
|
|
7
|
+
type $$ComponentProps = {
|
|
8
|
+
tabs: TabItem[];
|
|
9
|
+
value?: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
panel: Snippet<[string]>;
|
|
12
|
+
};
|
|
13
|
+
declare const Tabs: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
14
|
+
type Tabs = ReturnType<typeof Tabs>;
|
|
15
|
+
export default Tabs;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Theme switcher: a compact icon-button hosting a native <select> over the
|
|
3
|
+
// full theme registry, wired to the global theme store. The native control
|
|
4
|
+
// gives correct mobile/keyboard behaviour and outside-click handling for free.
|
|
5
|
+
// The button glyph reflects the active theme's icon.
|
|
6
|
+
import SelectButton from './SelectButton.svelte';
|
|
7
|
+
import { theme, THEMES } from '../../stores/theme.svelte';
|
|
8
|
+
|
|
9
|
+
let { class: klass = '' }: { class?: string } = $props();
|
|
10
|
+
|
|
11
|
+
const options = THEMES.map((t) => ({ value: t.id, label: `${t.icon} ${t.label}` }));
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<SelectButton
|
|
15
|
+
class={klass}
|
|
16
|
+
glyph={theme.icon}
|
|
17
|
+
label="Theme"
|
|
18
|
+
title={`Theme: ${theme.label}`}
|
|
19
|
+
value={theme.current}
|
|
20
|
+
{options}
|
|
21
|
+
onchange={(v) => theme.set(v as (typeof THEMES)[number]['id'])}
|
|
22
|
+
/>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Renders the toast queue in a bottom-centered, polite live region. Mount once
|
|
3
|
+
// near the app root. Each toast is tap/click-dismissible; screen readers
|
|
4
|
+
// announce additions via aria-live. Sits in its own stacking context above
|
|
5
|
+
// page content but below modals' top layer.
|
|
6
|
+
import { toasts } from '../../stores/toast.svelte';
|
|
7
|
+
import Icon from '../atoms/Icon.svelte';
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<div class="toaster" role="status" aria-live="polite" aria-relevant="additions">
|
|
11
|
+
{#each toasts.items as t (t.id)}
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
class="toast"
|
|
15
|
+
class:ok={t.tone === 'ok'}
|
|
16
|
+
class:err={t.tone === 'error'}
|
|
17
|
+
onclick={() => toasts.dismiss(t.id)}
|
|
18
|
+
>
|
|
19
|
+
{#if t.tone === 'ok'}<Icon name="check" />{:else if t.tone === 'error'}<Icon name="warning" />{/if}
|
|
20
|
+
<span class="msg">{t.message}</span>
|
|
21
|
+
<Icon name="x" />
|
|
22
|
+
</button>
|
|
23
|
+
{/each}
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
.toaster {
|
|
28
|
+
position: fixed;
|
|
29
|
+
left: 50%;
|
|
30
|
+
transform: translateX(-50%);
|
|
31
|
+
bottom: calc(var(--safe-bottom) + var(--sp-4));
|
|
32
|
+
z-index: var(--z-toast);
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
gap: var(--sp-2);
|
|
36
|
+
width: calc(100% - var(--sp-8));
|
|
37
|
+
max-width: 30rem;
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
}
|
|
40
|
+
.toast {
|
|
41
|
+
pointer-events: auto;
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: var(--sp-2);
|
|
45
|
+
width: 100%;
|
|
46
|
+
text-align: left;
|
|
47
|
+
color: var(--text);
|
|
48
|
+
background: var(--bg-elevated-2);
|
|
49
|
+
border: 1px solid var(--border-strong);
|
|
50
|
+
border-radius: var(--r-md);
|
|
51
|
+
padding: var(--sp-3) var(--sp-4);
|
|
52
|
+
box-shadow: var(--shadow-md);
|
|
53
|
+
font-size: var(--fs-sm);
|
|
54
|
+
animation: toast-in 0.18s var(--ease);
|
|
55
|
+
}
|
|
56
|
+
.toast .msg {
|
|
57
|
+
flex: 1;
|
|
58
|
+
}
|
|
59
|
+
.toast.ok {
|
|
60
|
+
border-color: var(--ok);
|
|
61
|
+
color: var(--ok);
|
|
62
|
+
}
|
|
63
|
+
.toast.err {
|
|
64
|
+
border-color: var(--danger);
|
|
65
|
+
color: var(--danger);
|
|
66
|
+
}
|
|
67
|
+
@keyframes toast-in {
|
|
68
|
+
from {
|
|
69
|
+
transform: translateY(8px);
|
|
70
|
+
opacity: 0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const Toaster: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type Toaster = InstanceType<typeof Toaster>;
|
|
18
|
+
export default Toaster;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Toggle chip — a muted pill/chip that lights up (tinted) when `pressed`.
|
|
3
|
+
// The "on" tint is driven by the `--toggle-accent` CSS var (default accent),
|
|
4
|
+
// so callers recolor it per-use (e.g. warm for behavior, role color for
|
|
5
|
+
// message-type filters) without restyling the base. Used across the
|
|
6
|
+
// conversation toolbar (filters / formatting / behavior / mobile tabs).
|
|
7
|
+
//
|
|
8
|
+
// Specializes the Button atom (ghost variant) for shared disabled/focus/
|
|
9
|
+
// transition behaviour; the chip surface + "on" tint are overrides.
|
|
10
|
+
// `.btn.toggle` outranks the atom's :where()-scoped variant classes.
|
|
11
|
+
import type { Snippet } from 'svelte';
|
|
12
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
13
|
+
import Button from '../atoms/Button.svelte';
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
pressed = false,
|
|
17
|
+
pill = false,
|
|
18
|
+
struck = false,
|
|
19
|
+
class: klass = '',
|
|
20
|
+
children,
|
|
21
|
+
...rest
|
|
22
|
+
}: HTMLButtonAttributes & {
|
|
23
|
+
pressed?: boolean;
|
|
24
|
+
pill?: boolean;
|
|
25
|
+
struck?: boolean;
|
|
26
|
+
children?: Snippet;
|
|
27
|
+
} = $props();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<Button
|
|
31
|
+
{...rest}
|
|
32
|
+
variant="ghost"
|
|
33
|
+
class="toggle {pill ? 'pill' : ''} {struck ? 'struck' : ''} {pressed ? 'on' : ''} {klass}"
|
|
34
|
+
aria-pressed={pressed}
|
|
35
|
+
>
|
|
36
|
+
{@render children?.()}
|
|
37
|
+
</Button>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
:global(.btn.toggle) {
|
|
41
|
+
gap: 4px;
|
|
42
|
+
min-height: 0;
|
|
43
|
+
padding: 0.15rem var(--sp-2);
|
|
44
|
+
border-radius: var(--r-sm);
|
|
45
|
+
font-size: var(--fs-xs);
|
|
46
|
+
font-weight: var(--fw-medium);
|
|
47
|
+
line-height: 1.4;
|
|
48
|
+
background: var(--bg-elevated-2);
|
|
49
|
+
color: var(--text-muted);
|
|
50
|
+
border: 1px solid var(--border);
|
|
51
|
+
}
|
|
52
|
+
:global(.btn.toggle.pill) {
|
|
53
|
+
border-radius: var(--r-pill);
|
|
54
|
+
}
|
|
55
|
+
:global(.btn.toggle:hover:not(:disabled)) {
|
|
56
|
+
border-color: var(--border-strong);
|
|
57
|
+
background: var(--bg-elevated-2);
|
|
58
|
+
}
|
|
59
|
+
:global(.btn.toggle.on) {
|
|
60
|
+
--tc: var(--toggle-accent, var(--accent));
|
|
61
|
+
color: var(--tc);
|
|
62
|
+
border-color: color-mix(in srgb, var(--tc) 55%, transparent);
|
|
63
|
+
background: color-mix(in srgb, var(--tc) 16%, transparent);
|
|
64
|
+
}
|
|
65
|
+
:global(.btn.toggle.struck) {
|
|
66
|
+
text-decoration: line-through;
|
|
67
|
+
}
|
|
68
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
+
type $$ComponentProps = HTMLButtonAttributes & {
|
|
4
|
+
pressed?: boolean;
|
|
5
|
+
pill?: boolean;
|
|
6
|
+
struck?: boolean;
|
|
7
|
+
children?: Snippet;
|
|
8
|
+
};
|
|
9
|
+
declare const Toggle: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type Toggle = ReturnType<typeof Toggle>;
|
|
11
|
+
export default Toggle;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Lightweight tooltip. Shows on hover (after a short delay) and on keyboard
|
|
3
|
+
// focus of the trigger; hides on blur / mouseleave / Escape. The trigger is
|
|
4
|
+
// any snippet; an action wires `aria-describedby` onto its first focusable
|
|
5
|
+
// element so screen readers announce the text. CSS-positioned above the
|
|
6
|
+
// trigger (centered). Dependency-free.
|
|
7
|
+
import type { Snippet } from 'svelte';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
text,
|
|
11
|
+
placement = 'top',
|
|
12
|
+
delay = 200,
|
|
13
|
+
trigger
|
|
14
|
+
}: {
|
|
15
|
+
text: string;
|
|
16
|
+
placement?: 'top' | 'bottom';
|
|
17
|
+
delay?: number;
|
|
18
|
+
trigger: Snippet;
|
|
19
|
+
} = $props();
|
|
20
|
+
|
|
21
|
+
const id = `tip-${Math.random().toString(36).slice(2, 8)}`;
|
|
22
|
+
let open = $state(false);
|
|
23
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
24
|
+
|
|
25
|
+
function show() {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
timer = setTimeout(() => (open = true), delay);
|
|
28
|
+
}
|
|
29
|
+
function hide() {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
open = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const FOCUSABLE = 'a[href],button,input,select,textarea,[tabindex]';
|
|
35
|
+
// Action: wire aria-describedby onto the trigger's focusable element AND the
|
|
36
|
+
// hover/focus listeners (attached via the DOM, so the wrapper stays a plain,
|
|
37
|
+
// role-less container — the interactive element is the trigger inside).
|
|
38
|
+
function tooltip(node: HTMLElement) {
|
|
39
|
+
const target = (node.matches(FOCUSABLE) ? node : node.querySelector(FOCUSABLE)) as
|
|
40
|
+
| HTMLElement
|
|
41
|
+
| null;
|
|
42
|
+
target?.setAttribute('aria-describedby', id);
|
|
43
|
+
const onkey = (e: KeyboardEvent) => e.key === 'Escape' && hide();
|
|
44
|
+
node.addEventListener('mouseenter', show);
|
|
45
|
+
node.addEventListener('mouseleave', hide);
|
|
46
|
+
node.addEventListener('focusin', show);
|
|
47
|
+
node.addEventListener('focusout', hide);
|
|
48
|
+
node.addEventListener('keydown', onkey);
|
|
49
|
+
return {
|
|
50
|
+
destroy() {
|
|
51
|
+
target?.removeAttribute('aria-describedby');
|
|
52
|
+
node.removeEventListener('mouseenter', show);
|
|
53
|
+
node.removeEventListener('mouseleave', hide);
|
|
54
|
+
node.removeEventListener('focusin', show);
|
|
55
|
+
node.removeEventListener('focusout', hide);
|
|
56
|
+
node.removeEventListener('keydown', onkey);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<span class="tip-wrap" use:tooltip>
|
|
63
|
+
{@render trigger()}
|
|
64
|
+
<span {id} role="tooltip" class="tip tip-{placement}" class:open aria-hidden={!open}>
|
|
65
|
+
{text}
|
|
66
|
+
</span>
|
|
67
|
+
</span>
|
|
68
|
+
|
|
69
|
+
<style>
|
|
70
|
+
.tip-wrap {
|
|
71
|
+
position: relative;
|
|
72
|
+
display: inline-flex;
|
|
73
|
+
}
|
|
74
|
+
.tip {
|
|
75
|
+
position: absolute;
|
|
76
|
+
left: 50%;
|
|
77
|
+
transform: translateX(-50%) scale(0.96);
|
|
78
|
+
z-index: var(--z-toast);
|
|
79
|
+
max-width: 16rem;
|
|
80
|
+
width: max-content;
|
|
81
|
+
padding: var(--sp-1) var(--sp-2);
|
|
82
|
+
background: var(--bg-elevated-2);
|
|
83
|
+
color: var(--text);
|
|
84
|
+
border: 1px solid var(--border-strong);
|
|
85
|
+
border-radius: var(--r-sm);
|
|
86
|
+
box-shadow: var(--shadow-md);
|
|
87
|
+
font-size: var(--fs-xs);
|
|
88
|
+
line-height: 1.4;
|
|
89
|
+
white-space: normal;
|
|
90
|
+
pointer-events: none;
|
|
91
|
+
opacity: 0;
|
|
92
|
+
transition:
|
|
93
|
+
opacity 0.12s var(--ease),
|
|
94
|
+
transform 0.12s var(--ease);
|
|
95
|
+
}
|
|
96
|
+
.tip-top {
|
|
97
|
+
bottom: calc(100% + 6px);
|
|
98
|
+
}
|
|
99
|
+
.tip-bottom {
|
|
100
|
+
top: calc(100% + 6px);
|
|
101
|
+
}
|
|
102
|
+
.tip.open {
|
|
103
|
+
opacity: 1;
|
|
104
|
+
transform: translateX(-50%) scale(1);
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
text: string;
|
|
4
|
+
placement?: 'top' | 'bottom';
|
|
5
|
+
delay?: number;
|
|
6
|
+
trigger: Snippet;
|
|
7
|
+
};
|
|
8
|
+
declare const Tooltip: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type Tooltip = ReturnType<typeof Tooltip>;
|
|
10
|
+
export default Tooltip;
|