@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
import { type Tone } from '../../core/tones.js';
|
|
4
|
-
interface Props extends HTMLAttributes<
|
|
4
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
5
5
|
title: string;
|
|
6
6
|
open?: boolean;
|
|
7
7
|
tone?: Tone;
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
const variants = $derived({
|
|
41
41
|
soft: `${toneSurface[tone]} border ${toneBorderSoft[tone]}`,
|
|
42
42
|
outline: `bg-bg shadow-xs border ${toneBorderSoft[tone]}`,
|
|
43
|
-
accent: `${toneSurface[tone]} border-l-
|
|
43
|
+
accent: `${toneSurface[tone]} border-l-2 ${toneBorder[tone]}`
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
function dismiss() {
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
|
|
70
70
|
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
71
71
|
{#if title}
|
|
72
|
-
<p class="text-sm leading-snug font-semibold {toneText[tone]}">{title}</p>
|
|
72
|
+
<p class="font-heading text-sm leading-snug font-semibold {toneText[tone]}">{title}</p>
|
|
73
73
|
{/if}
|
|
74
74
|
{#if children}
|
|
75
75
|
<div class="text-[13px] leading-relaxed text-text-secondary">{@render children()}</div>
|
|
@@ -47,6 +47,12 @@
|
|
|
47
47
|
xl: 'grid place-items-center *:size-8'
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
// A src that 404s left the reader with the browser's broken-image mark while
|
|
51
|
+
// the initials sat right there unused. Holding the failed URL rather than a
|
|
52
|
+
// flag means a new src is tried again on its own.
|
|
53
|
+
let failed = $state('');
|
|
54
|
+
const broken = $derived(failed !== '' && failed === src);
|
|
55
|
+
|
|
50
56
|
const initials = $derived(
|
|
51
57
|
name
|
|
52
58
|
.trim()
|
|
@@ -60,13 +66,18 @@
|
|
|
60
66
|
|
|
61
67
|
<span
|
|
62
68
|
class="inline-flex shrink-0 items-center justify-center overflow-hidden font-sans font-semibold select-none
|
|
63
|
-
{squared ? 'rounded-lg' : 'rounded-full'} {sizes[size]} {src
|
|
69
|
+
{squared ? 'rounded-lg' : 'rounded-full'} {sizes[size]} {src && !broken
|
|
64
70
|
? 'bg-bg-inset'
|
|
65
71
|
: toneSoft[tone]} {className}"
|
|
66
72
|
{...rest}
|
|
67
73
|
>
|
|
68
|
-
{#if src}
|
|
69
|
-
<img
|
|
74
|
+
{#if src && !broken}
|
|
75
|
+
<img
|
|
76
|
+
{src}
|
|
77
|
+
alt={alt ?? name}
|
|
78
|
+
onerror={() => (failed = src ?? '')}
|
|
79
|
+
class="size-full object-cover"
|
|
80
|
+
/>
|
|
70
81
|
{:else if fallback}
|
|
71
82
|
<span class={glyphs[size]}>{@render fallback()}</span>
|
|
72
83
|
{:else}
|
|
@@ -63,7 +63,10 @@
|
|
|
63
63
|
}: Props = $props();
|
|
64
64
|
|
|
65
65
|
const base =
|
|
66
|
-
|
|
66
|
+
// The press: shadow is paper's language, so a click flattens the button
|
|
67
|
+
// onto the page (shadow drops) and dims it a notch. Snaps in at 75ms,
|
|
68
|
+
// settles back over the base 150ms on release.
|
|
69
|
+
'inline-flex items-center justify-center gap-2 rounded-md border font-sans font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow,filter] duration-150 ease-brand-out active:shadow-none active:brightness-95 active:duration-75 disabled:opacity-50 disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:pointer-events-none';
|
|
67
70
|
|
|
68
71
|
const sizes: Record<ButtonSize, string> = {
|
|
69
72
|
sm: 'h-8 px-3 text-[13px]',
|
|
@@ -107,6 +110,10 @@
|
|
|
107
110
|
const iconClass = $derived(size === 'sm' ? iconSm : iconMd);
|
|
108
111
|
|
|
109
112
|
async function handleClick(event: MouseEvent) {
|
|
113
|
+
if (inert) {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
110
117
|
const result = onclick?.(event);
|
|
111
118
|
// Anything else is a plain handler: nothing to wait for, nothing to lock.
|
|
112
119
|
if (!(result instanceof Promise)) return;
|
|
@@ -125,7 +132,7 @@
|
|
|
125
132
|
disabled={href ? undefined : inert}
|
|
126
133
|
aria-disabled={href && inert ? 'true' : undefined}
|
|
127
134
|
aria-busy={busy ? 'true' : undefined}
|
|
128
|
-
|
|
135
|
+
tabindex={href && inert ? -1 : undefined}
|
|
129
136
|
class="{base} {focusRing} {toneRing[tone]} {variantClass} {isLink
|
|
130
137
|
? ''
|
|
131
138
|
: iconOnly
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
<div
|
|
50
50
|
class="flex flex-col rounded-lg {variants[variant]}
|
|
51
|
-
{accent ? `border-l-
|
|
51
|
+
{accent ? `border-l-2 ${toneBorderLeft[tone]}` : ''}
|
|
52
52
|
{interactive
|
|
53
53
|
? `transition-shadow duration-150 ease-brand-out hover:shadow-md ${toneHoverBorder[tone]}`
|
|
54
54
|
: ''}
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
...rest
|
|
26
26
|
}: Props = $props();
|
|
27
27
|
|
|
28
|
+
const uid = $props.id();
|
|
29
|
+
|
|
28
30
|
let el: HTMLInputElement | null = $state(null);
|
|
29
31
|
|
|
30
32
|
$effect(() => {
|
|
@@ -32,44 +34,61 @@
|
|
|
32
34
|
});
|
|
33
35
|
</script>
|
|
34
36
|
|
|
35
|
-
<
|
|
36
|
-
class="
|
|
37
|
-
|
|
37
|
+
<div
|
|
38
|
+
class="inline-block font-sans text-sm {disabled
|
|
39
|
+
? 'pointer-events-none opacity-50'
|
|
40
|
+
: ''} {className}"
|
|
38
41
|
>
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
<label class="group inline-flex items-start gap-2.5 {disabled ? '' : 'cursor-pointer'}">
|
|
43
|
+
<input
|
|
44
|
+
bind:this={el}
|
|
45
|
+
type="checkbox"
|
|
46
|
+
bind:checked
|
|
47
|
+
{disabled}
|
|
48
|
+
aria-describedby={description ? `${uid}-description` : undefined}
|
|
49
|
+
class="peer sr-only"
|
|
50
|
+
{...rest}
|
|
51
|
+
/>
|
|
52
|
+
<span
|
|
53
|
+
class="mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded border border-hairline-strong bg-bg shadow-xs transition-colors duration-150 ease-brand-out
|
|
42
54
|
{tonePeerChecked[tone]} {peerFocusRing} {tonePeerFocus[tone]}"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{/if}
|
|
62
|
-
</span>
|
|
63
|
-
{#if label || description || children}
|
|
64
|
-
<span class="flex flex-col gap-0.5">
|
|
65
|
-
{#if children}
|
|
66
|
-
{@render children()}
|
|
67
|
-
{:else if label}
|
|
68
|
-
<span class="leading-snug text-text">{label}</span>
|
|
69
|
-
{/if}
|
|
70
|
-
{#if description}
|
|
71
|
-
<span class="text-[13px] leading-snug text-text-muted">{description}</span>
|
|
55
|
+
aria-hidden="true"
|
|
56
|
+
>
|
|
57
|
+
{#if indeterminate}
|
|
58
|
+
<span class="h-0.5 w-2.5 rounded-full bg-text-muted"></span>
|
|
59
|
+
{:else}
|
|
60
|
+
<svg
|
|
61
|
+
class="size-3 text-text-inverse opacity-0 transition-opacity duration-150 group-has-[:checked]:opacity-100"
|
|
62
|
+
viewBox="0 0 12 12"
|
|
63
|
+
fill="none"
|
|
64
|
+
>
|
|
65
|
+
<path
|
|
66
|
+
d="m2 6 2.6 2.6L10 3.2"
|
|
67
|
+
stroke="currentColor"
|
|
68
|
+
stroke-width="2"
|
|
69
|
+
stroke-linecap="round"
|
|
70
|
+
stroke-linejoin="round"
|
|
71
|
+
/>
|
|
72
|
+
</svg>
|
|
72
73
|
{/if}
|
|
73
74
|
</span>
|
|
75
|
+
{#if label || children}
|
|
76
|
+
<span class="flex flex-col gap-0.5">
|
|
77
|
+
{#if children}
|
|
78
|
+
{@render children()}
|
|
79
|
+
{:else if label}
|
|
80
|
+
<span class="leading-snug text-text">{label}</span>
|
|
81
|
+
{/if}
|
|
82
|
+
</span>
|
|
83
|
+
{/if}
|
|
84
|
+
</label>
|
|
85
|
+
{#if description}
|
|
86
|
+
<!-- ponytail: the indent tracks the 18px box plus the label's gap; move one, move both. -->
|
|
87
|
+
<span
|
|
88
|
+
id="{uid}-description"
|
|
89
|
+
class="mt-0.5 block pl-[28px] text-[13px] leading-snug text-text-muted"
|
|
90
|
+
>
|
|
91
|
+
{description}
|
|
92
|
+
</span>
|
|
74
93
|
{/if}
|
|
75
|
-
</
|
|
94
|
+
</div>
|
|
@@ -50,6 +50,11 @@
|
|
|
50
50
|
...rest
|
|
51
51
|
}: Props = $props();
|
|
52
52
|
|
|
53
|
+
const uid = $props.id();
|
|
54
|
+
// The group's own hint and error sit outside every control in it, so the
|
|
55
|
+
// fieldset is what has to point at them.
|
|
56
|
+
const messageId = $derived(error ? `${uid}-error` : hint ? `${uid}-hint` : undefined);
|
|
57
|
+
|
|
53
58
|
const full = $derived(max > 0 && value.length >= max);
|
|
54
59
|
|
|
55
60
|
function toggle(option: CheckboxOption) {
|
|
@@ -60,11 +65,17 @@
|
|
|
60
65
|
}
|
|
61
66
|
</script>
|
|
62
67
|
|
|
63
|
-
<fieldset
|
|
68
|
+
<fieldset
|
|
69
|
+
aria-describedby={messageId}
|
|
70
|
+
class="flex flex-col gap-3 border-0 p-0 {className}"
|
|
71
|
+
{...rest}
|
|
72
|
+
>
|
|
64
73
|
{#if legend}
|
|
65
74
|
<legend class="mb-1 font-sans text-sm font-medium text-text">
|
|
66
75
|
{legend}
|
|
67
|
-
{#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span
|
|
76
|
+
{#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span><span
|
|
77
|
+
class="sr-only">(required)</span
|
|
78
|
+
>{/if}
|
|
68
79
|
</legend>
|
|
69
80
|
{/if}
|
|
70
81
|
|
|
@@ -110,8 +121,8 @@
|
|
|
110
121
|
</div>
|
|
111
122
|
|
|
112
123
|
{#if error}
|
|
113
|
-
<p class="font-sans text-[13px] text-danger">{error}</p>
|
|
124
|
+
<p id="{uid}-error" role="alert" class="font-sans text-[13px] text-danger">{error}</p>
|
|
114
125
|
{:else if hint}
|
|
115
|
-
<p class="font-sans text-[13px] text-text-muted">{hint}</p>
|
|
126
|
+
<p id="{uid}-hint" class="font-sans text-[13px] text-text-muted">{hint}</p>
|
|
116
127
|
{/if}
|
|
117
128
|
</fieldset>
|
|
@@ -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
|
|
|
9
16
|
export interface ComboboxOption {
|
|
@@ -143,7 +150,9 @@
|
|
|
143
150
|
|
|
144
151
|
<div
|
|
145
152
|
bind:this={wrapper}
|
|
146
|
-
class="relative inline-flex w-full items-center gap-2 rounded-md border bg-bg pr-1.5 pl-3 shadow-xs transition-colors duration-150 ease-brand-out
|
|
153
|
+
class="relative inline-flex w-full items-center gap-2 rounded-md border bg-bg pr-1.5 pl-3 shadow-xs transition-colors duration-150 ease-brand-out {focusWithinRing} {toneFocusWithinRing[
|
|
154
|
+
tone
|
|
155
|
+
]}
|
|
147
156
|
{invalid ? 'border-danger' : `border-hairline-strong ${toneFocusWithinBorder[tone]}`}
|
|
148
157
|
{disabled ? 'pointer-events-none opacity-50' : ''} {className}"
|
|
149
158
|
>
|
|
@@ -210,7 +219,7 @@
|
|
|
210
219
|
use:clickOutside={{ onoutside: () => (open = false), ignore: [wrapper] }}
|
|
211
220
|
id="{id ?? 'combobox'}-listbox"
|
|
212
221
|
role="listbox"
|
|
213
|
-
class="z-50 max-h-64 overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg"
|
|
222
|
+
class="z-50 max-h-64 animate-reveal overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg"
|
|
214
223
|
>
|
|
215
224
|
{#if flat.length === 0}
|
|
216
225
|
<p class="px-3 py-6 text-center text-sm text-text-muted">
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
onclick={(event) => {
|
|
161
161
|
if (event.target === dialog) open = false;
|
|
162
162
|
}}
|
|
163
|
-
class="mx-auto mt-[12vh] mb-auto w-[min(36rem,calc(100vw-2rem))] max-w-none rounded-xl border border-hairline bg-bg p-0 text-text shadow-2xl backdrop:bg-text/30 backdrop:backdrop-blur-[1px] {className}"
|
|
163
|
+
class="mx-auto mt-[12vh] mb-auto w-[min(36rem,calc(100vw-2rem))] max-w-none animate-enter rounded-xl border border-hairline bg-bg p-0 text-text shadow-2xl backdrop:bg-text/30 backdrop:backdrop-blur-[1px] {className}"
|
|
164
164
|
>
|
|
165
165
|
<div use:focusTrap={{ autofocus: false }} onkeydown={onKeydown} role="none" class="flex flex-col">
|
|
166
166
|
<div class="flex items-center gap-3 border-b border-hairline px-4">
|
|
@@ -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()}
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
role="menu"
|
|
79
79
|
aria-label={label ?? 'Menu'}
|
|
80
80
|
tabindex="-1"
|
|
81
|
-
class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 flex-col overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg {menuClass}"
|
|
81
|
+
class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 animate-reveal flex-col overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg {menuClass}"
|
|
82
82
|
>
|
|
83
83
|
{@render menu()}
|
|
84
84
|
</div>
|
|
@@ -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
|
|
|
9
14
|
interface Props {
|
|
@@ -94,7 +99,9 @@
|
|
|
94
99
|
|
|
95
100
|
<div
|
|
96
101
|
bind:this={wrapper}
|
|
97
|
-
class="inline-flex w-full items-center gap-1 rounded-md border bg-bg pr-1 pl-3 shadow-xs transition-colors duration-150 ease-brand-out
|
|
102
|
+
class="inline-flex w-full items-center gap-1 rounded-md border bg-bg pr-1 pl-3 shadow-xs transition-colors duration-150 ease-brand-out {focusWithinRing} {toneFocusWithinRing[
|
|
103
|
+
tone
|
|
104
|
+
]}
|
|
98
105
|
{invalid ? 'border-danger' : `border-hairline-strong ${toneFocusWithinBorder[tone]}`}
|
|
99
106
|
{disabled ? 'pointer-events-none opacity-50' : ''} {className}"
|
|
100
107
|
>
|
|
@@ -172,7 +179,7 @@
|
|
|
172
179
|
use:anchored={{ anchor: wrapper, placement: 'bottom-start', offset: 4 }}
|
|
173
180
|
use:clickOutside={{ onoutside: () => (open = false), ignore: [wrapper] }}
|
|
174
181
|
id="{id ?? 'datepicker'}-calendar"
|
|
175
|
-
class="z-50 rounded-lg border border-hairline bg-bg shadow-lg"
|
|
182
|
+
class="z-50 animate-reveal rounded-lg border border-hairline bg-bg shadow-lg"
|
|
176
183
|
>
|
|
177
184
|
<Calendar
|
|
178
185
|
bind:value
|
|
@@ -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>
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
...rest
|
|
39
39
|
}: Props = $props();
|
|
40
40
|
|
|
41
|
+
const uid = $props.id();
|
|
42
|
+
|
|
41
43
|
let dialog: HTMLDialogElement | null = $state(null);
|
|
42
44
|
|
|
43
45
|
const horizontal = $derived(side === 'left' || side === 'right');
|
|
@@ -80,6 +82,8 @@
|
|
|
80
82
|
|
|
81
83
|
<dialog
|
|
82
84
|
bind:this={dialog}
|
|
85
|
+
aria-labelledby={title ? `${uid}-title` : undefined}
|
|
86
|
+
aria-describedby={description ? `${uid}-description` : undefined}
|
|
83
87
|
onclose={close}
|
|
84
88
|
onclick={(event) => {
|
|
85
89
|
if (dismissible && isBackdropClick(event, dialog)) close();
|
|
@@ -97,10 +101,12 @@
|
|
|
97
101
|
{#if icon}<span class={iconLg}>{@render icon()}</span>{/if}
|
|
98
102
|
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
99
103
|
{#if title}
|
|
100
|
-
<h2 class="font-heading text-base font-semibold text-text">{title}</h2>
|
|
104
|
+
<h2 id="{uid}-title" class="font-heading text-base font-semibold text-text">{title}</h2>
|
|
101
105
|
{/if}
|
|
102
106
|
{#if description}
|
|
103
|
-
<p class="font-sans text-[13px] text-text-muted">
|
|
107
|
+
<p id="{uid}-description" class="font-sans text-[13px] text-text-muted">
|
|
108
|
+
{description}
|
|
109
|
+
</p>
|
|
104
110
|
{/if}
|
|
105
111
|
</div>
|
|
106
112
|
{#if dismissible}
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
role="menu"
|
|
85
85
|
aria-label={label ?? 'Menu'}
|
|
86
86
|
tabindex="-1"
|
|
87
|
-
class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 flex-col overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg {className}"
|
|
87
|
+
class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 animate-reveal flex-col overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg {className}"
|
|
88
88
|
>
|
|
89
89
|
{@render children()}
|
|
90
90
|
</div>
|
|
@@ -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>
|
|
@@ -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
|
|
|
6
11
|
export type InputSize = 'sm' | 'md' | 'lg';
|
|
7
12
|
|
|
@@ -37,7 +42,9 @@
|
|
|
37
42
|
</script>
|
|
38
43
|
|
|
39
44
|
<span
|
|
40
|
-
class="inline-flex w-full items-center gap-2 rounded-md border bg-bg px-3 shadow-xs transition-colors duration-150 ease-brand-out
|
|
45
|
+
class="inline-flex w-full items-center gap-2 rounded-md border bg-bg px-3 shadow-xs transition-colors duration-150 ease-brand-out {focusWithinRing} {toneFocusWithinRing[
|
|
46
|
+
tone
|
|
47
|
+
]}
|
|
41
48
|
{sizes[size]}
|
|
42
49
|
{invalid
|
|
43
50
|
? '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-sm 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>
|
|
@@ -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-md 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
|
>
|