@nqmcreative/ui 0.4.1 → 0.4.3
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/README.md +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/tones.d.ts +10 -0
- package/dist/core/tones.js +19 -0
- package/dist/core/trigger.d.ts +8 -0
- package/dist/core/trigger.js +11 -0
- package/dist/styles/matte/Alert.svelte +1 -1
- package/dist/styles/matte/Avatar.svelte +16 -3
- package/dist/styles/matte/Button.svelte +9 -2
- package/dist/styles/matte/Checkbox.svelte +49 -30
- package/dist/styles/matte/CheckboxGroup.svelte +15 -4
- package/dist/styles/matte/Combobox.svelte +12 -3
- package/dist/styles/matte/CommandPalette.svelte +1 -1
- package/dist/styles/matte/ConfirmDialog.svelte +10 -2
- package/dist/styles/matte/ContextMenu.svelte +1 -1
- package/dist/styles/matte/DatePicker.svelte +10 -3
- package/dist/styles/matte/Divider.svelte +1 -1
- package/dist/styles/matte/Drawer.svelte +10 -2
- package/dist/styles/matte/Dropdown.svelte +1 -1
- package/dist/styles/matte/Field.svelte +59 -3
- package/dist/styles/matte/Footer.svelte +14 -3
- package/dist/styles/matte/Input.svelte +9 -2
- package/dist/styles/matte/Label.svelte +3 -1
- package/dist/styles/matte/Link.svelte +13 -2
- package/dist/styles/matte/MenuItem.svelte +11 -4
- package/dist/styles/matte/Modal.svelte +11 -3
- package/dist/styles/matte/MultiSelect.svelte +1 -1
- package/dist/styles/matte/Navbar.svelte +2 -2
- package/dist/styles/matte/Popover.svelte +1 -1
- package/dist/styles/matte/Radio.svelte +41 -22
- package/dist/styles/matte/RadioGroup.svelte +15 -4
- package/dist/styles/matte/Select.svelte +4 -2
- package/dist/styles/matte/Skeleton.svelte +2 -1
- package/dist/styles/matte/Stat.svelte +27 -3
- package/dist/styles/matte/Switch.svelte +6 -1
- package/dist/styles/matte/Tabs.svelte +1 -1
- package/dist/styles/matte/Textarea.svelte +4 -2
- package/dist/styles/matte/Toaster.svelte +21 -3
- package/dist/styles/matte/Tooltip.svelte +8 -1
- package/dist/styles/matte/index.d.ts +1 -1
- package/dist/styles/matte/index.js +1 -1
- package/dist/styles/matte/theme.css +64 -1
- package/dist/styles/paper/AccordionItem.svelte +38 -11
- package/dist/styles/paper/AccordionItem.svelte.d.ts +1 -1
- package/dist/styles/paper/Alert.svelte +2 -2
- package/dist/styles/paper/Avatar.svelte +14 -3
- package/dist/styles/paper/Button.svelte +9 -2
- package/dist/styles/paper/Card.svelte +1 -1
- package/dist/styles/paper/Checkbox.svelte +55 -36
- package/dist/styles/paper/CheckboxGroup.svelte +15 -4
- package/dist/styles/paper/Combobox.svelte +12 -3
- package/dist/styles/paper/CommandPalette.svelte +1 -1
- package/dist/styles/paper/ConfirmDialog.svelte +8 -2
- package/dist/styles/paper/ContextMenu.svelte +1 -1
- package/dist/styles/paper/DatePicker.svelte +10 -3
- package/dist/styles/paper/Divider.svelte +1 -1
- package/dist/styles/paper/Drawer.svelte +8 -2
- package/dist/styles/paper/Dropdown.svelte +1 -1
- package/dist/styles/paper/Field.svelte +59 -3
- package/dist/styles/paper/Footer.svelte +14 -3
- package/dist/styles/paper/Input.svelte +9 -2
- package/dist/styles/paper/Label.svelte +3 -1
- package/dist/styles/paper/Link.svelte +13 -2
- package/dist/styles/paper/MenuItem.svelte +11 -4
- package/dist/styles/paper/Modal.svelte +9 -3
- package/dist/styles/paper/MultiSelect.svelte +1 -1
- package/dist/styles/paper/Navbar.svelte +2 -2
- package/dist/styles/paper/Popover.svelte +1 -1
- package/dist/styles/paper/Radio.svelte +41 -22
- package/dist/styles/paper/RadioGroup.svelte +15 -4
- package/dist/styles/paper/SegmentedControl.svelte +2 -2
- package/dist/styles/paper/Select.svelte +4 -2
- package/dist/styles/paper/Skeleton.svelte +2 -1
- package/dist/styles/paper/Stat.svelte +24 -2
- package/dist/styles/paper/Switch.svelte +6 -1
- package/dist/styles/paper/Tabs.svelte +1 -1
- package/dist/styles/paper/Textarea.svelte +4 -2
- package/dist/styles/paper/Toaster.svelte +19 -3
- package/dist/styles/paper/Tooltip.svelte +8 -1
- package/dist/styles/paper/index.d.ts +1 -1
- package/dist/styles/paper/index.js +1 -1
- package/dist/styles/paper/theme.css +85 -13
- package/dist/styles/sprout/Alert.svelte +3 -3
- package/dist/styles/sprout/Avatar.svelte +14 -3
- package/dist/styles/sprout/Breadcrumb.svelte +1 -1
- package/dist/styles/sprout/Button.svelte +8 -4
- package/dist/styles/sprout/Calendar.svelte +1 -1
- package/dist/styles/sprout/Card.svelte +2 -2
- package/dist/styles/sprout/Checkbox.svelte +55 -36
- package/dist/styles/sprout/CheckboxGroup.svelte +15 -4
- package/dist/styles/sprout/ColorInput.svelte +1 -1
- package/dist/styles/sprout/Combobox.svelte +12 -3
- package/dist/styles/sprout/ConfirmDialog.svelte +8 -2
- package/dist/styles/sprout/DatePicker.svelte +11 -4
- package/dist/styles/sprout/Divider.svelte +1 -1
- package/dist/styles/sprout/Drawer.svelte +8 -2
- package/dist/styles/sprout/Dropzone.svelte +1 -1
- package/dist/styles/sprout/Field.svelte +59 -3
- package/dist/styles/sprout/FileInput.svelte +3 -3
- package/dist/styles/sprout/Footer.svelte +14 -3
- package/dist/styles/sprout/Input.svelte +9 -2
- package/dist/styles/sprout/Label.svelte +3 -1
- package/dist/styles/sprout/Link.svelte +13 -2
- package/dist/styles/sprout/LogoCloud.svelte +1 -1
- package/dist/styles/sprout/MenuItem.svelte +11 -4
- package/dist/styles/sprout/Modal.svelte +8 -2
- package/dist/styles/sprout/MultiSelect.svelte +3 -3
- package/dist/styles/sprout/Navbar.svelte +2 -2
- package/dist/styles/sprout/Radio.svelte +41 -22
- package/dist/styles/sprout/RadioGroup.svelte +15 -4
- package/dist/styles/sprout/SearchInput.svelte +1 -1
- package/dist/styles/sprout/SegmentedControl.svelte +2 -2
- package/dist/styles/sprout/Select.svelte +4 -2
- package/dist/styles/sprout/Sidebar.svelte +2 -1
- package/dist/styles/sprout/Skeleton.svelte +2 -1
- package/dist/styles/sprout/Stat.svelte +24 -2
- package/dist/styles/sprout/Switch.svelte +6 -1
- package/dist/styles/sprout/Table.svelte +1 -1
- package/dist/styles/sprout/Tabs.svelte +1 -1
- package/dist/styles/sprout/TagsInput.svelte +1 -1
- package/dist/styles/sprout/Textarea.svelte +4 -2
- package/dist/styles/sprout/TimeInput.svelte +1 -1
- package/dist/styles/sprout/Toaster.svelte +22 -6
- package/dist/styles/sprout/Tooltip.svelte +8 -1
- package/dist/styles/sprout/index.d.ts +2 -2
- package/dist/styles/sprout/index.js +2 -2
- package/dist/styles/sprout/lift.d.ts +14 -0
- package/dist/styles/sprout/lift.js +23 -0
- package/dist/styles/sprout/theme.css +68 -24
- package/package.json +1 -1
- package/registry.json +4 -2
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
toneText,
|
|
15
15
|
type Tone
|
|
16
16
|
} from '../../core/tones.js';
|
|
17
|
-
import {
|
|
17
|
+
import { lift, liftSoft } from './lift.js';
|
|
18
18
|
|
|
19
19
|
export type ButtonVariant = 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'primary';
|
|
20
20
|
export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
@@ -92,11 +92,11 @@
|
|
|
92
92
|
case 'solid':
|
|
93
93
|
return `border-transparent ${toneSolid[tone]} ${toneSolidHover[tone]} ${lift[tone]}`;
|
|
94
94
|
case 'soft':
|
|
95
|
-
return `border-transparent ${toneSoft[tone]} ${toneSoftHover[tone]} ${
|
|
95
|
+
return `border-transparent ${toneSoft[tone]} ${toneSoftHover[tone]} ${liftSoft[tone]}`;
|
|
96
96
|
// Outline keeps its own colours on hover — swapping to a solid fill is
|
|
97
97
|
// matte's move, and it fights sprout's soft surfaces.
|
|
98
98
|
case 'outline':
|
|
99
|
-
return `bg-bg ${
|
|
99
|
+
return `bg-bg ${liftSoft[tone]} ${toneText[tone]} ${toneBorderSoft[tone]} hover:bg-bg-alt`;
|
|
100
100
|
// Nothing raised, so nothing to sink: ghost stays flat on the page.
|
|
101
101
|
case 'ghost':
|
|
102
102
|
return `border-transparent bg-transparent ${toneText[tone]} ${toneSoftHover[tone]} active:translate-y-0`;
|
|
@@ -116,6 +116,10 @@
|
|
|
116
116
|
const iconClass = $derived(size === 'sm' ? iconSm : iconMd);
|
|
117
117
|
|
|
118
118
|
async function handleClick(event: MouseEvent) {
|
|
119
|
+
if (inert) {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
119
123
|
const result = onclick?.(event);
|
|
120
124
|
// Anything else is a plain handler: nothing to wait for, nothing to lock.
|
|
121
125
|
if (!(result instanceof Promise)) return;
|
|
@@ -134,7 +138,7 @@
|
|
|
134
138
|
disabled={href ? undefined : inert}
|
|
135
139
|
aria-disabled={href && inert ? 'true' : undefined}
|
|
136
140
|
aria-busy={busy ? 'true' : undefined}
|
|
137
|
-
|
|
141
|
+
tabindex={href && inert ? -1 : undefined}
|
|
138
142
|
class="{base} {focusRing} {toneRing[tone]} {variantClass} {isLink
|
|
139
143
|
? ''
|
|
140
144
|
: iconOnly
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
type="button"
|
|
190
190
|
onclick={() => pick(now)}
|
|
191
191
|
disabled={blocked(now)}
|
|
192
|
-
class="self-start rounded px-1 text-[13px] font-medium transition-colors duration-150 disabled:pointer-events-none disabled:opacity-40 {toneText[
|
|
192
|
+
class="self-start rounded-md px-1 text-[13px] font-medium transition-colors duration-150 disabled:pointer-events-none disabled:opacity-40 {toneText[
|
|
193
193
|
tone
|
|
194
194
|
]} hover:underline hover:underline-offset-2 {focusRing} {toneRing[tone]}"
|
|
195
195
|
>
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
...rest
|
|
41
41
|
}: Props = $props();
|
|
42
42
|
|
|
43
|
-
// The default card is the one the style is named for:
|
|
43
|
+
// The default card is the one the style is named for: white, a hairline, and
|
|
44
44
|
// the lip. `filled` and `tinted` stay flat on purpose — a tinted panel that
|
|
45
45
|
// also floats is two claims about the same box.
|
|
46
46
|
const variants = $derived({
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
<div
|
|
54
54
|
class="flex flex-col rounded-[28px] {variants[variant]}
|
|
55
|
-
{accent ? `border-l-
|
|
55
|
+
{accent ? `border-l-2 ${toneBorderLeft[tone]}` : ''}
|
|
56
56
|
{interactive
|
|
57
57
|
? `transition-[box-shadow,transform,border-color] duration-150 ease-brand-out hover:-translate-y-0.5 ${softOnHover} ${toneHoverBorder[tone]}`
|
|
58
58
|
: ''}
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
...rest
|
|
27
27
|
}: Props = $props();
|
|
28
28
|
|
|
29
|
+
const uid = $props.id();
|
|
30
|
+
|
|
29
31
|
let el: HTMLInputElement | null = $state(null);
|
|
30
32
|
|
|
31
33
|
$effect(() => {
|
|
@@ -33,44 +35,61 @@
|
|
|
33
35
|
});
|
|
34
36
|
</script>
|
|
35
37
|
|
|
36
|
-
<
|
|
37
|
-
class="
|
|
38
|
-
|
|
38
|
+
<div
|
|
39
|
+
class="inline-block font-sans text-sm {disabled
|
|
40
|
+
? 'pointer-events-none opacity-50'
|
|
41
|
+
: ''} {className}"
|
|
39
42
|
>
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
<label class="group inline-flex items-start gap-2.5 {disabled ? '' : 'cursor-pointer'}">
|
|
44
|
+
<input
|
|
45
|
+
bind:this={el}
|
|
46
|
+
type="checkbox"
|
|
47
|
+
bind:checked
|
|
48
|
+
{disabled}
|
|
49
|
+
aria-describedby={description ? `${uid}-description` : undefined}
|
|
50
|
+
class="peer sr-only"
|
|
51
|
+
{...rest}
|
|
52
|
+
/>
|
|
53
|
+
<span
|
|
54
|
+
class="mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded-md border border-hairline-strong bg-bg {edge} transition-colors duration-150 ease-brand-out
|
|
43
55
|
{tonePeerChecked[tone]} {peerFocusRing} {tonePeerFocus[tone]}"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
{/if}
|
|
63
|
-
</span>
|
|
64
|
-
{#if label || description || children}
|
|
65
|
-
<span class="flex flex-col gap-0.5">
|
|
66
|
-
{#if children}
|
|
67
|
-
{@render children()}
|
|
68
|
-
{:else if label}
|
|
69
|
-
<span class="leading-snug text-text">{label}</span>
|
|
70
|
-
{/if}
|
|
71
|
-
{#if description}
|
|
72
|
-
<span class="text-[13px] leading-snug text-text-muted">{description}</span>
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
>
|
|
58
|
+
{#if indeterminate}
|
|
59
|
+
<span class="h-0.5 w-2.5 rounded-full bg-text-muted"></span>
|
|
60
|
+
{:else}
|
|
61
|
+
<svg
|
|
62
|
+
class="size-3 text-text-inverse opacity-0 transition-opacity duration-150 group-has-[:checked]:opacity-100"
|
|
63
|
+
viewBox="0 0 12 12"
|
|
64
|
+
fill="none"
|
|
65
|
+
>
|
|
66
|
+
<path
|
|
67
|
+
d="m2 6 2.6 2.6L10 3.2"
|
|
68
|
+
stroke="currentColor"
|
|
69
|
+
stroke-width="2"
|
|
70
|
+
stroke-linecap="round"
|
|
71
|
+
stroke-linejoin="round"
|
|
72
|
+
/>
|
|
73
|
+
</svg>
|
|
73
74
|
{/if}
|
|
74
75
|
</span>
|
|
76
|
+
{#if label || children}
|
|
77
|
+
<span class="flex flex-col gap-0.5">
|
|
78
|
+
{#if children}
|
|
79
|
+
{@render children()}
|
|
80
|
+
{:else if label}
|
|
81
|
+
<span class="leading-snug text-text">{label}</span>
|
|
82
|
+
{/if}
|
|
83
|
+
</span>
|
|
84
|
+
{/if}
|
|
85
|
+
</label>
|
|
86
|
+
{#if description}
|
|
87
|
+
<!-- ponytail: the indent tracks the 18px box plus the label's gap; move one, move both. -->
|
|
88
|
+
<span
|
|
89
|
+
id="{uid}-description"
|
|
90
|
+
class="mt-0.5 block pl-[28px] text-[13px] leading-snug text-text-muted"
|
|
91
|
+
>
|
|
92
|
+
{description}
|
|
93
|
+
</span>
|
|
75
94
|
{/if}
|
|
76
|
-
</
|
|
95
|
+
</div>
|
|
@@ -51,6 +51,11 @@
|
|
|
51
51
|
...rest
|
|
52
52
|
}: Props = $props();
|
|
53
53
|
|
|
54
|
+
const uid = $props.id();
|
|
55
|
+
// The group's own hint and error sit outside every control in it, so the
|
|
56
|
+
// fieldset is what has to point at them.
|
|
57
|
+
const messageId = $derived(error ? `${uid}-error` : hint ? `${uid}-hint` : undefined);
|
|
58
|
+
|
|
54
59
|
const full = $derived(max > 0 && value.length >= max);
|
|
55
60
|
|
|
56
61
|
function toggle(option: CheckboxOption) {
|
|
@@ -61,11 +66,17 @@
|
|
|
61
66
|
}
|
|
62
67
|
</script>
|
|
63
68
|
|
|
64
|
-
<fieldset
|
|
69
|
+
<fieldset
|
|
70
|
+
aria-describedby={messageId}
|
|
71
|
+
class="flex flex-col gap-3 border-0 p-0 {className}"
|
|
72
|
+
{...rest}
|
|
73
|
+
>
|
|
65
74
|
{#if legend}
|
|
66
75
|
<legend class="mb-1 font-sans text-sm font-medium text-text">
|
|
67
76
|
{legend}
|
|
68
|
-
{#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span
|
|
77
|
+
{#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span><span
|
|
78
|
+
class="sr-only">(required)</span
|
|
79
|
+
>{/if}
|
|
69
80
|
</legend>
|
|
70
81
|
{/if}
|
|
71
82
|
|
|
@@ -111,8 +122,8 @@
|
|
|
111
122
|
</div>
|
|
112
123
|
|
|
113
124
|
{#if error}
|
|
114
|
-
<p class="font-sans text-[13px] text-danger">{error}</p>
|
|
125
|
+
<p id="{uid}-error" role="alert" class="font-sans text-[13px] text-danger">{error}</p>
|
|
115
126
|
{:else if hint}
|
|
116
|
-
<p class="font-sans text-[13px] text-text-muted">{hint}</p>
|
|
127
|
+
<p id="{uid}-hint" class="font-sans text-[13px] text-text-muted">{hint}</p>
|
|
117
128
|
{/if}
|
|
118
129
|
</fieldset>
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
>
|
|
75
75
|
{#snippet prefix()}
|
|
76
76
|
<span
|
|
77
|
-
class="relative grid size-5 place-items-center overflow-hidden rounded border border-hairline-strong"
|
|
77
|
+
class="relative grid size-5 place-items-center overflow-hidden rounded-lg border border-hairline-strong"
|
|
78
78
|
>
|
|
79
79
|
<span class="absolute inset-0" style="background-color: {resolved}"></span>
|
|
80
80
|
<input
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
import { anchored } from '../../core/actions/anchor.js';
|
|
4
4
|
import { clickOutside, portal } from '../../core/actions/dismissable.js';
|
|
5
5
|
import { ListCursor, groupItems, matchQuery, revealIndex } from '../../core/list.svelte.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
focusWithinRing,
|
|
8
|
+
toneFocusWithinBorder,
|
|
9
|
+
toneFocusWithinRing,
|
|
10
|
+
toneSoft,
|
|
11
|
+
toneText,
|
|
12
|
+
type Tone
|
|
13
|
+
} from '../../core/tones.js';
|
|
7
14
|
import { iconMd } from './icon.js';
|
|
8
15
|
import { edge, soft } from './lift.js';
|
|
9
16
|
|
|
@@ -144,7 +151,9 @@
|
|
|
144
151
|
|
|
145
152
|
<div
|
|
146
153
|
bind:this={wrapper}
|
|
147
|
-
class="relative inline-flex w-full items-center gap-2 rounded-xl border bg-bg pr-1.5 pl-3 {edge} transition-colors duration-150 ease-brand-out
|
|
154
|
+
class="relative inline-flex w-full items-center gap-2 rounded-xl border bg-bg pr-1.5 pl-3 {edge} transition-colors duration-150 ease-brand-out {focusWithinRing} {toneFocusWithinRing[
|
|
155
|
+
tone
|
|
156
|
+
]}
|
|
148
157
|
{invalid ? 'border-danger' : `border-hairline-strong ${toneFocusWithinBorder[tone]}`}
|
|
149
158
|
{disabled ? 'pointer-events-none opacity-50' : ''} {className}"
|
|
150
159
|
>
|
|
@@ -178,7 +187,7 @@
|
|
|
178
187
|
type="button"
|
|
179
188
|
onclick={clear}
|
|
180
189
|
aria-label="Clear selection"
|
|
181
|
-
class="shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
190
|
+
class="shrink-0 rounded-xl p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
182
191
|
>
|
|
183
192
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
184
193
|
<path
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
children
|
|
37
37
|
}: Props = $props();
|
|
38
38
|
|
|
39
|
+
const uid = $props.id();
|
|
40
|
+
|
|
39
41
|
async function confirm() {
|
|
40
42
|
try {
|
|
41
43
|
loading = true;
|
|
@@ -55,6 +57,8 @@
|
|
|
55
57
|
|
|
56
58
|
<Modal
|
|
57
59
|
bind:open
|
|
60
|
+
aria-labelledby="{uid}-title"
|
|
61
|
+
aria-describedby={description ? `${uid}-description` : undefined}
|
|
58
62
|
size="sm"
|
|
59
63
|
dismissible={!loading}
|
|
60
64
|
showClose={false}
|
|
@@ -73,9 +77,11 @@
|
|
|
73
77
|
{/if}
|
|
74
78
|
</span>
|
|
75
79
|
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
76
|
-
<h2 class="font-heading text-base font-semibold text-text">{title}</h2>
|
|
80
|
+
<h2 id="{uid}-title" class="font-heading text-base font-semibold text-text">{title}</h2>
|
|
77
81
|
{#if description}
|
|
78
|
-
<p class="text-[13px] leading-relaxed text-text-secondary">
|
|
82
|
+
<p id="{uid}-description" class="text-[13px] leading-relaxed text-text-secondary">
|
|
83
|
+
{description}
|
|
84
|
+
</p>
|
|
79
85
|
{/if}
|
|
80
86
|
{#if children}
|
|
81
87
|
{@render children()}
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
import { clickOutside, portal } from '../../core/actions/dismissable.js';
|
|
4
4
|
import { dateHint } from '../../core/calendar.js';
|
|
5
5
|
import { formatISO, parseFormatted, type DateFormat } from '../../core/date.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
focusWithinRing,
|
|
8
|
+
toneFocusWithinBorder,
|
|
9
|
+
toneFocusWithinRing,
|
|
10
|
+
type Tone
|
|
11
|
+
} from '../../core/tones.js';
|
|
7
12
|
import Calendar from './Calendar.svelte';
|
|
8
13
|
import { edge, soft } from './lift.js';
|
|
9
14
|
|
|
@@ -95,7 +100,9 @@
|
|
|
95
100
|
|
|
96
101
|
<div
|
|
97
102
|
bind:this={wrapper}
|
|
98
|
-
class="inline-flex w-full items-center gap-1 rounded-xl border bg-bg pr-1 pl-3 {edge} transition-colors duration-150 ease-brand-out
|
|
103
|
+
class="inline-flex w-full items-center gap-1 rounded-xl border bg-bg pr-1 pl-3 {edge} transition-colors duration-150 ease-brand-out {focusWithinRing} {toneFocusWithinRing[
|
|
104
|
+
tone
|
|
105
|
+
]}
|
|
99
106
|
{invalid ? 'border-danger' : `border-hairline-strong ${toneFocusWithinBorder[tone]}`}
|
|
100
107
|
{disabled ? 'pointer-events-none opacity-50' : ''} {className}"
|
|
101
108
|
>
|
|
@@ -134,7 +141,7 @@
|
|
|
134
141
|
inputEl?.focus();
|
|
135
142
|
}}
|
|
136
143
|
aria-label="Clear selection"
|
|
137
|
-
class="shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
144
|
+
class="shrink-0 rounded-xl p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
138
145
|
>
|
|
139
146
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
140
147
|
<path
|
|
@@ -153,7 +160,7 @@
|
|
|
153
160
|
onclick={() => (open = !open)}
|
|
154
161
|
aria-label="Select a date"
|
|
155
162
|
aria-expanded={open}
|
|
156
|
-
class="shrink-0 rounded p-1.5 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
163
|
+
class="shrink-0 rounded-xl p-1.5 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
157
164
|
>
|
|
158
165
|
<svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
159
166
|
<rect x="3" y="4.5" width="14" height="12" rx="2" stroke="currentColor" stroke-width="1.4" />
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
{...rest}
|
|
21
21
|
></div>
|
|
22
22
|
{:else if label}
|
|
23
|
-
<div class="flex items-center gap-3 {className}" {...rest}>
|
|
23
|
+
<div role="separator" class="flex items-center gap-3 {className}" {...rest}>
|
|
24
24
|
<span class="h-px flex-1 bg-hairline"></span>
|
|
25
25
|
<span class="font-sans text-xs font-medium text-text-muted">{label}</span>
|
|
26
26
|
<span class="h-px flex-1 bg-hairline"></span>
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
...rest
|
|
40
40
|
}: Props = $props();
|
|
41
41
|
|
|
42
|
+
const uid = $props.id();
|
|
43
|
+
|
|
42
44
|
let dialog: HTMLDialogElement | null = $state(null);
|
|
43
45
|
|
|
44
46
|
const horizontal = $derived(side === 'left' || side === 'right');
|
|
@@ -81,6 +83,8 @@
|
|
|
81
83
|
|
|
82
84
|
<dialog
|
|
83
85
|
bind:this={dialog}
|
|
86
|
+
aria-labelledby={title ? `${uid}-title` : undefined}
|
|
87
|
+
aria-describedby={description ? `${uid}-description` : undefined}
|
|
84
88
|
onclose={close}
|
|
85
89
|
onclick={(event) => {
|
|
86
90
|
if (dismissible && isBackdropClick(event, dialog)) close();
|
|
@@ -98,10 +102,12 @@
|
|
|
98
102
|
{#if icon}<span class={iconLg}>{@render icon()}</span>{/if}
|
|
99
103
|
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
100
104
|
{#if title}
|
|
101
|
-
<h2 class="font-heading text-base font-semibold text-text">{title}</h2>
|
|
105
|
+
<h2 id="{uid}-title" class="font-heading text-base font-semibold text-text">{title}</h2>
|
|
102
106
|
{/if}
|
|
103
107
|
{#if description}
|
|
104
|
-
<p class="font-sans text-[13px] text-text-muted">
|
|
108
|
+
<p id="{uid}-description" class="font-sans text-[13px] text-text-muted">
|
|
109
|
+
{description}
|
|
110
|
+
</p>
|
|
105
111
|
{/if}
|
|
106
112
|
</div>
|
|
107
113
|
{#if dismissible}
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
type="button"
|
|
191
191
|
onclick={() => remove(i)}
|
|
192
192
|
aria-label="Remove {file.name}"
|
|
193
|
-
class="-m-1 shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger"
|
|
193
|
+
class="-m-1 shrink-0 rounded-xl p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger"
|
|
194
194
|
>
|
|
195
195
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
196
196
|
<path
|
|
@@ -24,9 +24,65 @@
|
|
|
24
24
|
children,
|
|
25
25
|
...rest
|
|
26
26
|
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
const uid = $props.id();
|
|
29
|
+
const messageId = $derived(error ? `${uid}-error` : hint ? `${uid}-hint` : undefined);
|
|
30
|
+
|
|
31
|
+
interface Described {
|
|
32
|
+
for?: string;
|
|
33
|
+
messageId?: string;
|
|
34
|
+
required: boolean;
|
|
35
|
+
invalid: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A hint nobody can hear is not a hint. `aria-describedby` has to sit on the
|
|
40
|
+
* control, and the control arrives through `children` — out of reach of this
|
|
41
|
+
* template — so the field finds it once the DOM exists and keeps the three
|
|
42
|
+
* attributes in step itself. It touches only what it set: an `aria-invalid`
|
|
43
|
+
* the control already carries from its own `invalid` prop stays that
|
|
44
|
+
* control's business.
|
|
45
|
+
*/
|
|
46
|
+
function describes(node: HTMLElement, state: Described) {
|
|
47
|
+
let ourId: string | undefined;
|
|
48
|
+
let ownsInvalid = false;
|
|
49
|
+
|
|
50
|
+
const apply = (next: Described) => {
|
|
51
|
+
const control =
|
|
52
|
+
(next.for ? node.querySelector<HTMLElement>(`#${CSS.escape(next.for)}`) : null) ??
|
|
53
|
+
node.querySelector<HTMLElement>('input, select, textarea');
|
|
54
|
+
if (!control) return;
|
|
55
|
+
|
|
56
|
+
const ids = (control.getAttribute('aria-describedby') ?? '')
|
|
57
|
+
.split(/\s+/)
|
|
58
|
+
.filter((id) => id && id !== ourId);
|
|
59
|
+
if (next.messageId) ids.push(next.messageId);
|
|
60
|
+
if (ids.length) control.setAttribute('aria-describedby', ids.join(' '));
|
|
61
|
+
else control.removeAttribute('aria-describedby');
|
|
62
|
+
ourId = next.messageId;
|
|
63
|
+
|
|
64
|
+
if (next.required) control.setAttribute('aria-required', 'true');
|
|
65
|
+
else control.removeAttribute('aria-required');
|
|
66
|
+
|
|
67
|
+
if (next.invalid) {
|
|
68
|
+
ownsInvalid ||= !control.hasAttribute('aria-invalid');
|
|
69
|
+
control.setAttribute('aria-invalid', 'true');
|
|
70
|
+
} else if (ownsInvalid) {
|
|
71
|
+
control.removeAttribute('aria-invalid');
|
|
72
|
+
ownsInvalid = false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
apply(state);
|
|
77
|
+
return { update: apply };
|
|
78
|
+
}
|
|
27
79
|
</script>
|
|
28
80
|
|
|
29
|
-
<div
|
|
81
|
+
<div
|
|
82
|
+
use:describes={{ for: htmlFor, messageId, required, invalid: Boolean(error) }}
|
|
83
|
+
class="flex w-full flex-col gap-1.5 {className}"
|
|
84
|
+
{...rest}
|
|
85
|
+
>
|
|
30
86
|
{#if label}
|
|
31
87
|
<label for={htmlFor} class="font-sans text-sm font-medium text-text">
|
|
32
88
|
{label}
|
|
@@ -35,8 +91,8 @@
|
|
|
35
91
|
{/if}
|
|
36
92
|
{@render children()}
|
|
37
93
|
{#if error}
|
|
38
|
-
<p class="font-sans text-[13px] text-danger">{error}</p>
|
|
94
|
+
<p id="{uid}-error" role="alert" class="font-sans text-[13px] text-danger">{error}</p>
|
|
39
95
|
{:else if hint}
|
|
40
|
-
<p class="font-sans text-[13px] text-text-muted">{hint}</p>
|
|
96
|
+
<p id="{uid}-hint" class="font-sans text-[13px] text-text-muted">{hint}</p>
|
|
41
97
|
{/if}
|
|
42
98
|
</div>
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
type="button"
|
|
120
120
|
{disabled}
|
|
121
121
|
onclick={() => input?.click()}
|
|
122
|
-
class="shrink-0 rounded-
|
|
122
|
+
class="shrink-0 rounded-xl border border-hairline-strong bg-bg-alt font-sans font-medium text-text-secondary transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current
|
|
123
123
|
{buttons[size]}"
|
|
124
124
|
>
|
|
125
125
|
{label ?? 'Choose file'}
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
type="button"
|
|
137
137
|
onclick={clear}
|
|
138
138
|
aria-label="Remove"
|
|
139
|
-
class="mr-1 shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger"
|
|
139
|
+
class="mr-1 shrink-0 rounded-xl p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger"
|
|
140
140
|
>
|
|
141
141
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
142
142
|
<path
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
type="button"
|
|
185
185
|
onclick={() => (files = files.filter((_, at) => at !== i))}
|
|
186
186
|
aria-label="Remove {file.name}"
|
|
187
|
-
class="-m-1 shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger"
|
|
187
|
+
class="-m-1 shrink-0 rounded-xl p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger"
|
|
188
188
|
>
|
|
189
189
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
190
190
|
<path
|
|
@@ -59,15 +59,26 @@
|
|
|
59
59
|
<a
|
|
60
60
|
href={item.href}
|
|
61
61
|
target={item.external ? '_blank' : undefined}
|
|
62
|
-
rel={item.external ? 'noreferrer' : undefined}
|
|
62
|
+
rel={item.external ? 'noopener noreferrer' : undefined}
|
|
63
63
|
class="inline-flex items-center gap-2 font-sans text-[13px] text-text-muted transition-colors duration-150 ease-brand-out {toneHoverText[
|
|
64
64
|
tone
|
|
65
65
|
]}"
|
|
66
66
|
>
|
|
67
67
|
{#if item.icon}<span class={iconSm}>{@render item.icon()}</span
|
|
68
68
|
>{/if}{item.label}
|
|
69
|
-
{#if item.external}<
|
|
70
|
-
|
|
69
|
+
{#if item.external}<svg
|
|
70
|
+
class="inline-block size-[0.85em] align-[-0.05em]"
|
|
71
|
+
viewBox="0 0 14 14"
|
|
72
|
+
fill="none"
|
|
73
|
+
aria-hidden="true"
|
|
74
|
+
><path
|
|
75
|
+
d="M5.5 2.5h6v6M11.5 2.5 3.5 10.5"
|
|
76
|
+
stroke="currentColor"
|
|
77
|
+
stroke-width="1.5"
|
|
78
|
+
stroke-linecap="round"
|
|
79
|
+
stroke-linejoin="round"
|
|
80
|
+
/></svg
|
|
81
|
+
><span class="sr-only">(opens in a new tab)</span>{/if}
|
|
71
82
|
</a>
|
|
72
83
|
</li>
|
|
73
84
|
{/each}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
focusWithinRing,
|
|
6
|
+
toneFocusWithinBorder,
|
|
7
|
+
toneFocusWithinRing,
|
|
8
|
+
type Tone
|
|
9
|
+
} from '../../core/tones.js';
|
|
5
10
|
import { edge } from './lift.js';
|
|
6
11
|
|
|
7
12
|
export type InputSize = 'sm' | 'md' | 'lg';
|
|
@@ -38,7 +43,9 @@
|
|
|
38
43
|
</script>
|
|
39
44
|
|
|
40
45
|
<span
|
|
41
|
-
class="inline-flex w-full items-center gap-2 rounded-xl border bg-bg px-3 {edge} transition-colors duration-150 ease-brand-out
|
|
46
|
+
class="inline-flex w-full items-center gap-2 rounded-xl border bg-bg px-3 {edge} transition-colors duration-150 ease-brand-out {focusWithinRing} {toneFocusWithinRing[
|
|
47
|
+
tone
|
|
48
|
+
]}
|
|
42
49
|
{sizes[size]}
|
|
43
50
|
{invalid
|
|
44
51
|
? 'border-danger focus-within:border-danger'
|
|
@@ -12,5 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
<label class="font-sans text-sm font-medium text-text {className}" {...rest}>
|
|
14
14
|
{@render children()}
|
|
15
|
-
{#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span
|
|
15
|
+
{#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span><span class="sr-only"
|
|
16
|
+
>(required)</span
|
|
17
|
+
>{/if}
|
|
16
18
|
</label>
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
<a
|
|
42
42
|
target={external ? '_blank' : undefined}
|
|
43
|
-
rel={external ? 'noreferrer' : undefined}
|
|
43
|
+
rel={external ? 'noopener noreferrer' : undefined}
|
|
44
44
|
class="inline-flex items-center gap-1 rounded-md font-sans transition-colors duration-150 ease-brand-out
|
|
45
45
|
{muted ? `text-text-secondary ${toneHoverText[tone]}` : toneText[tone]}
|
|
46
46
|
{underlines[underline]} {focusRing} {toneRing[tone]} {className}"
|
|
@@ -49,5 +49,16 @@
|
|
|
49
49
|
{#if icon}<span class={iconSm}>{@render icon()}</span>{/if}
|
|
50
50
|
{@render children()}
|
|
51
51
|
{#if iconEnd}<span class={iconSm}>{@render iconEnd()}</span>{/if}
|
|
52
|
-
{#if external}
|
|
52
|
+
{#if external}
|
|
53
|
+
<svg class="size-[0.85em] shrink-0" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
54
|
+
<path
|
|
55
|
+
d="M5.5 2.5h6v6M11.5 2.5 3.5 10.5"
|
|
56
|
+
stroke="currentColor"
|
|
57
|
+
stroke-width="1.5"
|
|
58
|
+
stroke-linecap="round"
|
|
59
|
+
stroke-linejoin="round"
|
|
60
|
+
/>
|
|
61
|
+
</svg>
|
|
62
|
+
<span class="sr-only">(opens in a new tab)</span>
|
|
63
|
+
{/if}
|
|
53
64
|
</a>
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
<svelte:element
|
|
43
43
|
this={logo.href ? 'a' : 'div'}
|
|
44
44
|
href={logo.href}
|
|
45
|
-
class="flex items-center rounded transition-all duration-200 ease-brand-out {muted
|
|
45
|
+
class="flex items-center rounded-md transition-all duration-200 ease-brand-out {muted
|
|
46
46
|
? 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0'
|
|
47
47
|
: ''}"
|
|
48
48
|
>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
-
import { toneSoftHover, toneText, type Tone } from '../../core/tones.js';
|
|
4
|
+
import { toneSoft, toneSoftHover, toneText, type Tone } from '../../core/tones.js';
|
|
5
5
|
import { iconMd } from './icon.js';
|
|
6
6
|
|
|
7
7
|
interface Props extends HTMLButtonAttributes {
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
children,
|
|
29
29
|
...rest
|
|
30
30
|
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
// A chosen row speaks in the brand, the same as an active nav link — a tick
|
|
33
|
+
// alone leaves it the same grey as every row above it. An item that already
|
|
34
|
+
// carries a tone (a red `Delete`) keeps its own.
|
|
35
|
+
const chosen = $derived(toneSoft[tone === 'neutral' ? 'brand' : tone]);
|
|
31
36
|
</script>
|
|
32
37
|
|
|
33
38
|
<svelte:element
|
|
@@ -39,9 +44,11 @@
|
|
|
39
44
|
aria-disabled={disabled ? 'true' : undefined}
|
|
40
45
|
tabindex={disabled ? undefined : -1}
|
|
41
46
|
class="mx-1 flex items-center gap-2.5 rounded-xl px-2.5 py-1.5 text-left text-sm transition-colors duration-100 ease-brand-out focus:outline-none disabled:pointer-events-none disabled:opacity-40
|
|
42
|
-
{
|
|
43
|
-
?
|
|
44
|
-
:
|
|
47
|
+
{selected
|
|
48
|
+
? `${chosen} font-medium`
|
|
49
|
+
: tone === 'neutral'
|
|
50
|
+
? 'text-text-secondary hover:bg-bg-inset focus-visible:bg-bg-inset'
|
|
51
|
+
: `${toneText[tone]} ${toneSoftHover[tone]}`}
|
|
45
52
|
{className}"
|
|
46
53
|
{...rest}
|
|
47
54
|
>
|