@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
|
@@ -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 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="flex w-full items-center gap-2.5 px-3 py-1.5 text-left text-[14px] 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
|
>
|
|
@@ -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 closable = $derived(showClose ?? dismissible);
|
|
@@ -64,12 +66,14 @@
|
|
|
64
66
|
|
|
65
67
|
<dialog
|
|
66
68
|
bind:this={dialog}
|
|
69
|
+
aria-labelledby={title ? `${uid}-title` : undefined}
|
|
70
|
+
aria-describedby={description ? `${uid}-description` : undefined}
|
|
67
71
|
onclose={close}
|
|
68
72
|
onclick={onBackdrop}
|
|
69
73
|
oncancel={(event) => {
|
|
70
74
|
if (!dismissible) event.preventDefault();
|
|
71
75
|
}}
|
|
72
|
-
class="m-auto w-[calc(100%-2rem)] border border-hairline bg-bg p-0 text-text backdrop:bg-text/40 {sizes[
|
|
76
|
+
class="m-auto w-[calc(100%-2rem)] animate-enter border border-hairline bg-bg p-0 text-text backdrop:bg-text/40 {sizes[
|
|
73
77
|
size
|
|
74
78
|
]} {className}"
|
|
75
79
|
{...rest}
|
|
@@ -80,10 +84,14 @@
|
|
|
80
84
|
{#if icon}<span class={iconLg}>{@render icon()}</span>{/if}
|
|
81
85
|
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
82
86
|
{#if title}
|
|
83
|
-
<h2 class="font-heading text-lg font-medium tracking-tight">
|
|
87
|
+
<h2 id="{uid}-title" class="font-heading text-lg font-medium tracking-tight">
|
|
88
|
+
{title}
|
|
89
|
+
</h2>
|
|
84
90
|
{/if}
|
|
85
91
|
{#if description}
|
|
86
|
-
<p class="font-sans text-sm text-text-secondary">
|
|
92
|
+
<p id="{uid}-description" class="font-sans text-sm text-text-secondary">
|
|
93
|
+
{description}
|
|
94
|
+
</p>
|
|
87
95
|
{/if}
|
|
88
96
|
</div>
|
|
89
97
|
{#if closable}
|
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
id="{id ?? 'multiselect'}-listbox"
|
|
223
223
|
role="listbox"
|
|
224
224
|
aria-multiselectable="true"
|
|
225
|
-
class="z-50 max-h-64 overflow-y-auto border border-hairline bg-bg py-1 font-sans"
|
|
225
|
+
class="z-50 max-h-64 animate-reveal overflow-y-auto border border-hairline bg-bg py-1 font-sans"
|
|
226
226
|
>
|
|
227
227
|
{#if flat.length === 0}
|
|
228
228
|
<p class="px-3 py-6 text-center text-sm text-text-muted">
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
class="{link} {focusRing} {toneRing[tone]} {item.active
|
|
110
110
|
? toneText[tone]
|
|
111
111
|
: 'text-text-secondary hover:text-text'} {item.disabled
|
|
112
|
-
? 'pointer-events-none opacity-
|
|
112
|
+
? 'pointer-events-none opacity-40'
|
|
113
113
|
: ''}"
|
|
114
114
|
>
|
|
115
115
|
{#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
aria-current={item.active ? 'page' : undefined}
|
|
157
157
|
class="flex items-center justify-between gap-3 py-3 font-sans text-[15px] {item.active
|
|
158
158
|
? toneText[tone]
|
|
159
|
-
: 'text-text-secondary'} {item.disabled ? 'pointer-events-none opacity-
|
|
159
|
+
: 'text-text-secondary'} {item.disabled ? 'pointer-events-none opacity-40' : ''}"
|
|
160
160
|
>
|
|
161
161
|
<span class="flex min-w-0 items-center gap-2.5">
|
|
162
162
|
{#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
role="dialog"
|
|
78
78
|
aria-label={title}
|
|
79
79
|
tabindex="-1"
|
|
80
|
-
class="z-50 flex max-w-xs flex-col gap-2 border border-hairline bg-bg p-4 font-sans {className}"
|
|
80
|
+
class="z-50 flex max-w-xs animate-reveal flex-col gap-2 border border-hairline bg-bg p-4 font-sans {className}"
|
|
81
81
|
>
|
|
82
82
|
{#if title}
|
|
83
83
|
<p class="font-heading text-sm font-medium text-text">{title}</p>
|
|
@@ -24,32 +24,51 @@
|
|
|
24
24
|
children,
|
|
25
25
|
...rest
|
|
26
26
|
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
const uid = $props.id();
|
|
27
29
|
</script>
|
|
28
30
|
|
|
29
|
-
<
|
|
30
|
-
class="
|
|
31
|
-
|
|
31
|
+
<div
|
|
32
|
+
class="inline-block font-sans text-[15px] {disabled
|
|
33
|
+
? 'pointer-events-none opacity-50'
|
|
34
|
+
: ''} {className}"
|
|
32
35
|
>
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
<label class="group inline-flex items-start gap-3 {disabled ? '' : 'cursor-pointer'}">
|
|
37
|
+
<input
|
|
38
|
+
type="radio"
|
|
39
|
+
bind:group
|
|
40
|
+
{value}
|
|
41
|
+
{disabled}
|
|
42
|
+
aria-describedby={description ? `${uid}-description` : undefined}
|
|
43
|
+
class="peer sr-only"
|
|
44
|
+
{...rest}
|
|
45
|
+
/>
|
|
46
|
+
<span
|
|
47
|
+
class="mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded-full border border-hairline-strong bg-bg transition-colors duration-150 ease-brand-out
|
|
36
48
|
{tonePeerChecked[tone]} {peerFocusRing} {tonePeerFocus[tone]}"
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
>
|
|
51
|
+
<span
|
|
52
|
+
class="size-1.5 rounded-full bg-text-inverse opacity-0 transition-opacity duration-150 group-has-[:checked]:opacity-100"
|
|
53
|
+
></span>
|
|
54
|
+
</span>
|
|
55
|
+
{#if label || children}
|
|
56
|
+
<span class="flex flex-col gap-0.5">
|
|
57
|
+
{#if children}
|
|
58
|
+
{@render children()}
|
|
59
|
+
{:else if label}
|
|
60
|
+
<span class="leading-snug text-text">{label}</span>
|
|
61
|
+
{/if}
|
|
62
|
+
</span>
|
|
63
|
+
{/if}
|
|
64
|
+
</label>
|
|
65
|
+
{#if description}
|
|
66
|
+
<!-- ponytail: the indent tracks the 18px dot plus the label's gap; move one, move both. -->
|
|
39
67
|
<span
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<span class="flex flex-col gap-0.5">
|
|
45
|
-
{#if children}
|
|
46
|
-
{@render children()}
|
|
47
|
-
{:else if label}
|
|
48
|
-
<span class="leading-snug text-text">{label}</span>
|
|
49
|
-
{/if}
|
|
50
|
-
{#if description}
|
|
51
|
-
<span class="text-sm leading-snug text-text-muted">{description}</span>
|
|
52
|
-
{/if}
|
|
68
|
+
id="{uid}-description"
|
|
69
|
+
class="mt-0.5 block pl-[30px] text-sm leading-snug text-text-muted"
|
|
70
|
+
>
|
|
71
|
+
{description}
|
|
53
72
|
</span>
|
|
54
73
|
{/if}
|
|
55
|
-
</
|
|
74
|
+
</div>
|
|
@@ -44,13 +44,24 @@
|
|
|
44
44
|
children,
|
|
45
45
|
...rest
|
|
46
46
|
}: Props = $props();
|
|
47
|
+
|
|
48
|
+
const uid = $props.id();
|
|
49
|
+
// The group's own hint and error sit outside every control in it, so the
|
|
50
|
+
// fieldset is what has to point at them.
|
|
51
|
+
const messageId = $derived(error ? `${uid}-error` : hint ? `${uid}-hint` : undefined);
|
|
47
52
|
</script>
|
|
48
53
|
|
|
49
|
-
<fieldset
|
|
54
|
+
<fieldset
|
|
55
|
+
aria-describedby={messageId}
|
|
56
|
+
class="flex flex-col gap-3 border-0 p-0 {className}"
|
|
57
|
+
{...rest}
|
|
58
|
+
>
|
|
50
59
|
{#if legend}
|
|
51
60
|
<legend class="mb-1 font-sans text-sm font-medium text-text">
|
|
52
61
|
{legend}
|
|
53
|
-
{#if required}<span class="text-danger" aria-hidden="true">*</span
|
|
62
|
+
{#if required}<span class="text-danger" aria-hidden="true">*</span><span class="sr-only"
|
|
63
|
+
>(required)</span
|
|
64
|
+
>{/if}
|
|
54
65
|
</legend>
|
|
55
66
|
{/if}
|
|
56
67
|
|
|
@@ -92,8 +103,8 @@
|
|
|
92
103
|
</div>
|
|
93
104
|
|
|
94
105
|
{#if error}
|
|
95
|
-
<p class="font-sans text-sm text-danger">{error}</p>
|
|
106
|
+
<p id="{uid}-error" role="alert" class="font-sans text-sm text-danger">{error}</p>
|
|
96
107
|
{:else if hint}
|
|
97
|
-
<p class="font-sans text-sm text-text-muted">{hint}</p>
|
|
108
|
+
<p id="{uid}-hint" class="font-sans text-sm text-text-muted">{hint}</p>
|
|
98
109
|
{/if}
|
|
99
110
|
</fieldset>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLSelectAttributes } from 'svelte/elements';
|
|
4
|
-
import { toneFocusBorder, type Tone } from '../../core/tones.js';
|
|
4
|
+
import { focusRing, toneFocusBorder, toneRing, type Tone } from '../../core/tones.js';
|
|
5
5
|
|
|
6
6
|
export interface SelectOption {
|
|
7
7
|
value: string;
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
<select
|
|
36
36
|
bind:value
|
|
37
37
|
aria-invalid={invalid ? 'true' : undefined}
|
|
38
|
-
class="
|
|
38
|
+
class="{focusRing} {toneRing[
|
|
39
|
+
tone
|
|
40
|
+
]} h-10 w-full appearance-none border bg-bg py-0 pr-10 pl-3.5 font-sans text-[15px] text-text transition-colors duration-150 ease-brand-out disabled:pointer-events-none disabled:opacity-50
|
|
39
41
|
{invalid ? 'border-danger' : `border-hairline ${toneFocusBorder[tone]}`} {className}"
|
|
40
42
|
{...rest}
|
|
41
43
|
>
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
</script>
|
|
30
30
|
|
|
31
31
|
{#if variant === 'text' && lines > 1}
|
|
32
|
-
<div class="flex w-full flex-col gap-2 {className}" {...rest}>
|
|
32
|
+
<div aria-hidden="true" class="flex w-full flex-col gap-2 {className}" {...rest}>
|
|
33
33
|
{#each { length: lines }, i (i)}
|
|
34
34
|
<div
|
|
35
35
|
class="h-4 animate-pulse bg-bg-inset {i === lines - 1 ? 'w-2/3' : 'w-full'}"
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
</div>
|
|
40
40
|
{:else}
|
|
41
41
|
<div
|
|
42
|
+
aria-hidden="true"
|
|
42
43
|
class="animate-pulse bg-bg-inset {variants[variant]} {className}"
|
|
43
44
|
style="{width ? `width:${width};` : ''}{height ? `height:${height};` : ''}"
|
|
44
45
|
{...rest}
|
|
@@ -32,7 +32,15 @@
|
|
|
32
32
|
}: Props = $props();
|
|
33
33
|
|
|
34
34
|
const trendTone: Record<StatTrend, Tone> = { up: 'success', down: 'danger', flat: 'neutral' };
|
|
35
|
-
|
|
35
|
+
// Drawn rather than typed. `↑` renders in whatever face the reader's system
|
|
36
|
+
// substitutes for it and is announced as "up arrow" or nothing at all, and
|
|
37
|
+
// the colour beside it says nothing either — so the word ships with it.
|
|
38
|
+
const trendPath: Record<StatTrend, string> = {
|
|
39
|
+
up: 'M7 11V3.5M7 3l3.5 3.5M7 3 3.5 6.5',
|
|
40
|
+
down: 'M7 3v7.5M7 11l3.5-3.5M7 11 3.5 7.5',
|
|
41
|
+
flat: 'M3 7h7.5M11 7 7.5 3.5M11 7l-3.5 3.5'
|
|
42
|
+
};
|
|
43
|
+
const trendWord: Record<StatTrend, string> = { up: 'up', down: 'down', flat: 'no change' };
|
|
36
44
|
</script>
|
|
37
45
|
|
|
38
46
|
<div class="flex flex-col gap-1 font-sans {className}" {...rest}>
|
|
@@ -41,13 +49,29 @@
|
|
|
41
49
|
>
|
|
42
50
|
<div class="flex items-baseline gap-2">
|
|
43
51
|
<span
|
|
44
|
-
class="font-heading text-3xl font-medium tracking-tight {tone
|
|
52
|
+
class="font-heading text-3xl font-medium tracking-tight tabular-nums {tone
|
|
53
|
+
? toneText[tone]
|
|
54
|
+
: 'text-text'}"
|
|
45
55
|
>
|
|
46
56
|
{value}
|
|
47
57
|
</span>
|
|
48
58
|
{#if delta}
|
|
49
59
|
<span class="font-mono text-xs {toneText[trendTone[trend]]}">
|
|
50
|
-
|
|
60
|
+
<svg
|
|
61
|
+
class="inline-block size-[1em] align-[-0.115em]"
|
|
62
|
+
viewBox="0 0 14 14"
|
|
63
|
+
fill="none"
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
>
|
|
66
|
+
<path
|
|
67
|
+
d={trendPath[trend]}
|
|
68
|
+
stroke="currentColor"
|
|
69
|
+
stroke-width="1.6"
|
|
70
|
+
stroke-linecap="round"
|
|
71
|
+
stroke-linejoin="round"
|
|
72
|
+
/>
|
|
73
|
+
</svg>
|
|
74
|
+
<span class="sr-only">{trendWord[trend]}</span>
|
|
51
75
|
{delta}
|
|
52
76
|
</span>
|
|
53
77
|
{/if}
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
md: { track: 'h-6 w-11', thumb: 'size-5', travel: 'translate-x-5' }
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
const uid = $props.id();
|
|
34
|
+
|
|
33
35
|
function toggle() {
|
|
34
36
|
checked = !checked;
|
|
35
37
|
onchange?.(checked);
|
|
@@ -42,6 +44,7 @@
|
|
|
42
44
|
role="switch"
|
|
43
45
|
aria-checked={checked}
|
|
44
46
|
aria-label={label}
|
|
47
|
+
aria-describedby={description ? `${uid}-description` : undefined}
|
|
45
48
|
{disabled}
|
|
46
49
|
onclick={toggle}
|
|
47
50
|
class="relative mt-0.5 inline-flex shrink-0 items-center rounded-full p-0.5 transition-colors duration-200 ease-brand-out disabled:pointer-events-none
|
|
@@ -57,7 +60,9 @@
|
|
|
57
60
|
<span class="flex flex-col gap-0.5 font-sans text-[15px]">
|
|
58
61
|
{#if label}<span class="leading-snug text-text">{label}</span>{/if}
|
|
59
62
|
{#if description}
|
|
60
|
-
<span class="text-sm leading-snug text-text-muted"
|
|
63
|
+
<span id="{uid}-description" class="text-sm leading-snug text-text-muted"
|
|
64
|
+
>{description}</span
|
|
65
|
+
>
|
|
61
66
|
{/if}
|
|
62
67
|
</span>
|
|
63
68
|
{/if}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
3
|
-
import { toneFocusBorder, type Tone } from '../../core/tones.js';
|
|
3
|
+
import { focusRing, toneFocusBorder, toneRing, type Tone } from '../../core/tones.js';
|
|
4
4
|
|
|
5
5
|
interface Props extends HTMLTextareaAttributes {
|
|
6
6
|
value?: string;
|
|
@@ -48,7 +48,9 @@
|
|
|
48
48
|
bind:value
|
|
49
49
|
{rows}
|
|
50
50
|
aria-invalid={invalid ? 'true' : undefined}
|
|
51
|
-
class="
|
|
51
|
+
class="{focusRing} {toneRing[
|
|
52
|
+
tone
|
|
53
|
+
]} w-full border bg-bg px-3.5 py-2.5 font-sans text-[15px] text-text transition-colors duration-150 ease-brand-out placeholder:text-text-muted disabled:pointer-events-none disabled:opacity-50
|
|
52
54
|
{autoresize ? 'resize-none overflow-hidden' : 'resize-y'}
|
|
53
55
|
{invalid ? 'border-danger' : `border-hairline ${toneFocusBorder[tone]}`} {className}"
|
|
54
56
|
{...rest}></textarea>
|
|
@@ -30,8 +30,24 @@
|
|
|
30
30
|
const fromLeft = $derived(position.endsWith('left'));
|
|
31
31
|
const fromCenter = $derived(position.endsWith('center'));
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* A card sliding in from the edge of the screen, and a stack of them shuffling
|
|
35
|
+
* to make room, is exactly the motion this preference exists to stop. Under it
|
|
36
|
+
* the toast fades in place and the stack reorders instantly — the state change
|
|
37
|
+
* still reads, it just stops travelling.
|
|
38
|
+
*
|
|
39
|
+
* ponytail: read once at mount. Someone who flips the OS setting mid-session
|
|
40
|
+
* gets the new behaviour on the next load; a live listener is the upgrade.
|
|
41
|
+
*/
|
|
42
|
+
const stillness =
|
|
43
|
+
typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
44
|
+
|
|
33
45
|
const enter = $derived(
|
|
34
|
-
|
|
46
|
+
stillness
|
|
47
|
+
? { x: 0, y: 0 }
|
|
48
|
+
: fromCenter
|
|
49
|
+
? { y: fromTop ? -16 : 16, x: 0 }
|
|
50
|
+
: { x: fromLeft ? -16 : 16, y: 0 }
|
|
35
51
|
);
|
|
36
52
|
</script>
|
|
37
53
|
|
|
@@ -45,7 +61,7 @@
|
|
|
45
61
|
>
|
|
46
62
|
{#each toast.items as item (item.id)}
|
|
47
63
|
<div
|
|
48
|
-
animate:flip={{ duration: 200 }}
|
|
64
|
+
animate:flip={{ duration: stillness ? 0 : 200 }}
|
|
49
65
|
in:fly={{ ...enter, duration: 220 }}
|
|
50
66
|
out:fly={{ ...enter, duration: 160 }}
|
|
51
67
|
role={item.tone === 'danger' ? 'alert' : 'status'}
|
|
@@ -75,7 +91,9 @@
|
|
|
75
91
|
</span>
|
|
76
92
|
|
|
77
93
|
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
78
|
-
<p class="text-[15px] leading-snug font-medium {toneText[item.tone]}">
|
|
94
|
+
<p class="font-heading text-[15px] leading-snug font-medium {toneText[item.tone]}">
|
|
95
|
+
{item.title}
|
|
96
|
+
</p>
|
|
79
97
|
{#if item.description}
|
|
80
98
|
<p class="text-sm leading-relaxed text-text-secondary">{item.description}</p>
|
|
81
99
|
{/if}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { describedBy } from '../../core/trigger.js';
|
|
3
4
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
5
|
|
|
5
6
|
export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
|
|
@@ -13,6 +14,11 @@
|
|
|
13
14
|
|
|
14
15
|
let { content, placement = 'top', class: className = '', children, ...rest }: Props = $props();
|
|
15
16
|
|
|
17
|
+
const uid = $props.id();
|
|
18
|
+
let wrapper: HTMLElement | null = $state(null);
|
|
19
|
+
|
|
20
|
+
$effect(() => describedBy(wrapper, `${uid}-tip`));
|
|
21
|
+
|
|
16
22
|
const placements: Record<TooltipPlacement, string> = {
|
|
17
23
|
top: 'bottom-full left-1/2 mb-2 -translate-x-1/2',
|
|
18
24
|
bottom: 'top-full left-1/2 mt-2 -translate-x-1/2',
|
|
@@ -21,9 +27,10 @@
|
|
|
21
27
|
};
|
|
22
28
|
</script>
|
|
23
29
|
|
|
24
|
-
<span class="group/tt relative inline-flex" {...rest}>
|
|
30
|
+
<span bind:this={wrapper} class="group/tt relative inline-flex" {...rest}>
|
|
25
31
|
{@render children()}
|
|
26
32
|
<span
|
|
33
|
+
id="{uid}-tip"
|
|
27
34
|
role="tooltip"
|
|
28
35
|
class="pointer-events-none invisible absolute z-50 bg-text px-2.5 py-1.5 font-sans text-xs whitespace-nowrap text-text-inverse opacity-0 transition-opacity duration-150 ease-brand-out group-focus-within/tt:visible group-focus-within/tt:opacity-100 group-hover/tt:visible group-hover/tt:opacity-100
|
|
29
36
|
{placements[placement]} {className}"
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `@nqmcreative/ui/core`, re-exported below so this one import covers
|
|
6
6
|
* the whole style.
|
|
7
7
|
*/
|
|
8
|
-
export { TONES, focusRing, peerFocusRing, toneBorder, toneBorderLeft, toneBorderSoft, toneFill, toneFocusBorder, toneFocusWithinBorder, toneHoverBorder, toneHoverText, tonePeerChecked, tonePeerFocus, toneRing, toneSoft, toneSoftHover, toneSolid, toneSolidHover, toneSurface, toneText, anchored, clickOutside, focusTrap, focusable, navigateList, portal, toast, applyTheme, storedTheme, toISO, fromISO, today, isSameDay, addDays, addMonths, monthGrid, weekdayNames, monthLabel, isOutOfRange, formatISO, parseFormatted } from '../../core/index.js';
|
|
8
|
+
export { TONES, focusRing, peerFocusRing, toneBorder, toneBorderLeft, toneBorderSoft, toneFill, toneFocusBorder, toneFocusWithinBorder, focusWithinRing, toneFocusWithinRing, toneHoverBorder, toneHoverText, tonePeerChecked, tonePeerFocus, toneRing, toneSoft, toneSoftHover, toneSolid, toneSolidHover, toneSurface, toneText, anchored, clickOutside, focusTrap, focusable, navigateList, portal, toast, applyTheme, storedTheme, toISO, fromISO, today, isSameDay, addDays, addMonths, monthGrid, weekdayNames, monthLabel, isOutOfRange, formatISO, parseFormatted } from '../../core/index.js';
|
|
9
9
|
export type { Tone, Placement, AnchorOptions, ClickOutsideOptions, FocusTrapOptions, Toast, ToastOptions, ToastAction, Theme, DateFormat, TableSort, SortDirection, RejectedFile } from '../../core/index.js';
|
|
10
10
|
export { default as Button } from './Button.svelte';
|
|
11
11
|
export type { ButtonVariant, ButtonSize } from './Button.svelte';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the whole style.
|
|
7
7
|
*/
|
|
8
8
|
/* --- shared behaviour --- */
|
|
9
|
-
export { TONES, focusRing, peerFocusRing, toneBorder, toneBorderLeft, toneBorderSoft, toneFill, toneFocusBorder, toneFocusWithinBorder, toneHoverBorder, toneHoverText, tonePeerChecked, tonePeerFocus, toneRing, toneSoft, toneSoftHover, toneSolid, toneSolidHover, toneSurface, toneText, anchored, clickOutside, focusTrap, focusable, navigateList, portal, toast, applyTheme, storedTheme, toISO, fromISO, today, isSameDay, addDays, addMonths, monthGrid, weekdayNames, monthLabel, isOutOfRange, formatISO, parseFormatted } from '../../core/index.js';
|
|
9
|
+
export { TONES, focusRing, peerFocusRing, toneBorder, toneBorderLeft, toneBorderSoft, toneFill, toneFocusBorder, toneFocusWithinBorder, focusWithinRing, toneFocusWithinRing, toneHoverBorder, toneHoverText, tonePeerChecked, tonePeerFocus, toneRing, toneSoft, toneSoftHover, toneSolid, toneSolidHover, toneSurface, toneText, anchored, clickOutside, focusTrap, focusable, navigateList, portal, toast, applyTheme, storedTheme, toISO, fromISO, today, isSameDay, addDays, addMonths, monthGrid, weekdayNames, monthLabel, isOutOfRange, formatISO, parseFormatted } from '../../core/index.js';
|
|
10
10
|
/* --- actions --- */
|
|
11
11
|
export { default as Button } from './Button.svelte';
|
|
12
12
|
export { default as Link } from './Link.svelte';
|
|
@@ -78,9 +78,37 @@
|
|
|
78
78
|
--font-heading: 'Sora', ui-sans-serif, system-ui, sans-serif;
|
|
79
79
|
--font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
|
|
80
80
|
|
|
81
|
-
/* --- motion ---
|
|
81
|
+
/* --- motion ---
|
|
82
|
+
* matte enters by fading and revealing along an axis — never by moving or
|
|
83
|
+
* scaling (the style's first law). One token per overlay kind: `enter` for
|
|
84
|
+
* dialog panels + backdrops, `reveal` for anchored menus and popovers.
|
|
85
|
+
* paper and sprout define these same two names their own way, so an
|
|
86
|
+
* overlay's markup never changes between styles — only these values do.
|
|
87
|
+
*/
|
|
82
88
|
--ease-brand: cubic-bezier(0.16, 1, 0.3, 1);
|
|
83
89
|
--ease-brand-out: cubic-bezier(0.25, 1, 0.5, 1);
|
|
90
|
+
--animate-enter: nqm-fade 150ms var(--ease-brand-out);
|
|
91
|
+
--animate-reveal: nqm-shutter 160ms var(--ease-brand-out);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Enter-only, matching the Drawer: the native <dialog> and the {#if}-mounted
|
|
95
|
+
* popover each play these once on mount and leave instantly. */
|
|
96
|
+
@keyframes nqm-fade {
|
|
97
|
+
from {
|
|
98
|
+
opacity: 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
@keyframes nqm-shutter {
|
|
102
|
+
from {
|
|
103
|
+
opacity: 0;
|
|
104
|
+
clip-path: inset(0 0 100% 0);
|
|
105
|
+
}
|
|
106
|
+
to {
|
|
107
|
+
opacity: 1;
|
|
108
|
+
/* Both ends must be inset() — animating to `none` (the default) can't
|
|
109
|
+
* interpolate and the panel would snap open at the midpoint instead. */
|
|
110
|
+
clip-path: inset(0 0 0 0);
|
|
111
|
+
}
|
|
84
112
|
}
|
|
85
113
|
|
|
86
114
|
/*
|
|
@@ -207,3 +235,38 @@
|
|
|
207
235
|
--color-neutral-border: #334155;
|
|
208
236
|
}
|
|
209
237
|
}
|
|
238
|
+
|
|
239
|
+
/*
|
|
240
|
+
* The surfaces the browser draws for us.
|
|
241
|
+
*
|
|
242
|
+
* A caret and a highlight in the user-agent's blue belong to no design system,
|
|
243
|
+
* and they are the two the reader touches most: one sits in every field they
|
|
244
|
+
* type into, the other appears the moment they drag across a paragraph. The
|
|
245
|
+
* selection is mixed from the brand rather than pinned to a token so it stays
|
|
246
|
+
* a wash — the text underneath keeps its own colour and its own contrast.
|
|
247
|
+
*/
|
|
248
|
+
@layer base {
|
|
249
|
+
/* The one overlay surface a class can't reach: every <dialog> backdrop
|
|
250
|
+
* fades with its panel. */
|
|
251
|
+
dialog::backdrop {
|
|
252
|
+
animation: nqm-fade 150ms var(--ease-brand-out);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@media (prefers-reduced-motion: reduce) {
|
|
256
|
+
.animate-enter,
|
|
257
|
+
.animate-reveal,
|
|
258
|
+
dialog::backdrop {
|
|
259
|
+
animation: none;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
::selection {
|
|
264
|
+
background-color: color-mix(in oklab, var(--color-brand) 24%, transparent);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
input,
|
|
268
|
+
textarea,
|
|
269
|
+
[contenteditable] {
|
|
270
|
+
caret-color: var(--color-brand);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { toneText, type Tone } from '../../core/tones.js';
|
|
5
5
|
import { iconMd } from './icon.js';
|
|
6
6
|
|
|
7
|
-
interface Props extends HTMLAttributes<
|
|
7
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
8
8
|
title: string;
|
|
9
9
|
open?: boolean;
|
|
10
10
|
tone?: Tone;
|
|
@@ -25,27 +25,54 @@
|
|
|
25
25
|
children,
|
|
26
26
|
...rest
|
|
27
27
|
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
const uid = $props.id();
|
|
28
30
|
</script>
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
32
|
+
<!--
|
|
33
|
+
A disclosure, not a native <details>: the panel is a grid whose single row
|
|
34
|
+
animates from 0fr to 1fr, which is the one height animation that works in
|
|
35
|
+
every browser (auto-height keyframes don't interpolate). The <button> keeps
|
|
36
|
+
the keyboard and focus behaviour <summary> gave us for free; aria-expanded
|
|
37
|
+
and the region wiring replace what the open attribute carried.
|
|
38
|
+
-->
|
|
39
|
+
<div class="border-b border-hairline last:border-b-0 {className}" {...rest}>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
id="{uid}-trigger"
|
|
43
|
+
aria-expanded={open}
|
|
44
|
+
aria-controls="{uid}-panel"
|
|
45
|
+
onclick={() => (open = !open)}
|
|
46
|
+
class="flex w-full cursor-pointer items-center gap-4 px-4 py-3.5 text-left font-sans text-sm font-medium text-text transition-colors duration-150 ease-brand-out select-none hover:bg-bg-alt focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-current"
|
|
33
47
|
>
|
|
34
48
|
{#if icon}<span class={iconMd}>{@render icon()}</span>{/if}
|
|
35
49
|
<span class="flex-1">{title}</span>
|
|
36
50
|
{#if meta}<span class="text-[13px] font-normal text-text-muted">{meta}</span>{/if}
|
|
37
51
|
<svg
|
|
38
|
-
class="size-4 shrink-0 transition-transform duration-200 ease-brand-out
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
class="size-4 shrink-0 transition-transform duration-200 ease-brand-out {open
|
|
53
|
+
? 'rotate-180'
|
|
54
|
+
: ''} {toneText[tone]}"
|
|
41
55
|
viewBox="0 0 16 16"
|
|
42
56
|
fill="none"
|
|
43
57
|
aria-hidden="true"
|
|
44
58
|
>
|
|
45
59
|
<path d="m4 6 4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
|
|
46
60
|
</svg>
|
|
47
|
-
</
|
|
48
|
-
<div
|
|
49
|
-
{
|
|
61
|
+
</button>
|
|
62
|
+
<div
|
|
63
|
+
id="{uid}-panel"
|
|
64
|
+
role="region"
|
|
65
|
+
aria-labelledby="{uid}-trigger"
|
|
66
|
+
inert={!open}
|
|
67
|
+
class="grid transition-[grid-template-rows] duration-200 ease-brand-out"
|
|
68
|
+
style:grid-template-rows={open ? '1fr' : '0fr'}
|
|
69
|
+
>
|
|
70
|
+
<!-- overflow-hidden gives this grid row a min height of 0 so 0fr can
|
|
71
|
+
collapse it fully, and clips the content while it slides. -->
|
|
72
|
+
<div class="overflow-hidden">
|
|
73
|
+
<div class="px-4 pb-4 font-sans text-sm leading-relaxed text-text-secondary">
|
|
74
|
+
{@render children()}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
50
77
|
</div>
|
|
51
|
-
</
|
|
78
|
+
</div>
|