@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
|
@@ -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)] rounded-xl border border-hairline bg-bg p-0 text-text shadow-2xl backdrop:bg-text/30 backdrop:backdrop-blur-[1px] {sizes[
|
|
76
|
+
class="m-auto w-[calc(100%-2rem)] animate-enter rounded-xl border border-hairline bg-bg p-0 text-text shadow-2xl backdrop:bg-text/30 backdrop:backdrop-blur-[1px] {sizes[
|
|
73
77
|
size
|
|
74
78
|
]} {className}"
|
|
75
79
|
{...rest}
|
|
@@ -80,10 +84,12 @@
|
|
|
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-base font-semibold text-text">{title}</h2>
|
|
87
|
+
<h2 id="{uid}-title" class="font-heading text-base font-semibold text-text">{title}</h2>
|
|
84
88
|
{/if}
|
|
85
89
|
{#if description}
|
|
86
|
-
<p class="font-sans text-[13px] text-text-muted">
|
|
90
|
+
<p id="{uid}-description" class="font-sans text-[13px] text-text-muted">
|
|
91
|
+
{description}
|
|
92
|
+
</p>
|
|
87
93
|
{/if}
|
|
88
94
|
</div>
|
|
89
95
|
{#if closable}
|
|
@@ -232,7 +232,7 @@
|
|
|
232
232
|
id="{id ?? 'multiselect'}-listbox"
|
|
233
233
|
role="listbox"
|
|
234
234
|
aria-multiselectable="true"
|
|
235
|
-
class="z-50 max-h-64 overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg"
|
|
235
|
+
class="z-50 max-h-64 animate-reveal overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg"
|
|
236
236
|
>
|
|
237
237
|
{#if flat.length === 0}
|
|
238
238
|
<p class="px-3 py-6 text-center text-sm text-text-muted">
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
class="{link} {focusRing} {toneRing[tone]} {item.active
|
|
112
112
|
? toneSoft[tone]
|
|
113
113
|
: 'text-text-secondary hover:bg-bg-inset hover:text-text'} {item.disabled
|
|
114
|
-
? 'pointer-events-none opacity-
|
|
114
|
+
? 'pointer-events-none opacity-40'
|
|
115
115
|
: ''}"
|
|
116
116
|
>
|
|
117
117
|
{#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
class="flex items-center justify-between gap-3 rounded-md px-3 py-2.5 font-sans text-sm font-medium {item.active
|
|
165
165
|
? `${toneSoft[tone]}`
|
|
166
166
|
: 'text-text-secondary hover:bg-bg-inset'} {item.disabled
|
|
167
|
-
? 'pointer-events-none opacity-
|
|
167
|
+
? 'pointer-events-none opacity-40'
|
|
168
168
|
: ''}"
|
|
169
169
|
>
|
|
170
170
|
<span class="flex min-w-0 items-center gap-2.5">
|
|
@@ -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 rounded-lg border border-hairline bg-bg p-4 font-sans shadow-lg {className}"
|
|
80
|
+
class="z-50 flex max-w-xs animate-reveal flex-col gap-2 rounded-lg border border-hairline bg-bg p-4 font-sans shadow-lg {className}"
|
|
81
81
|
>
|
|
82
82
|
{#if title}
|
|
83
83
|
<p class="font-heading text-sm font-semibold 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-sm {disabled
|
|
33
|
+
? 'pointer-events-none opacity-50'
|
|
34
|
+
: ''} {className}"
|
|
32
35
|
>
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
<label class="group inline-flex items-start gap-2.5 {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 shadow-xs 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-[13px] leading-snug text-text-muted">{description}</span>
|
|
52
|
-
{/if}
|
|
68
|
+
id="{uid}-description"
|
|
69
|
+
class="mt-0.5 block pl-[28px] text-[13px] 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="ml-0.5 text-danger" aria-hidden="true">*</span
|
|
62
|
+
{#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span><span
|
|
63
|
+
class="sr-only">(required)</span
|
|
64
|
+
>{/if}
|
|
54
65
|
</legend>
|
|
55
66
|
{/if}
|
|
56
67
|
|
|
@@ -94,8 +105,8 @@
|
|
|
94
105
|
</div>
|
|
95
106
|
|
|
96
107
|
{#if error}
|
|
97
|
-
<p class="font-sans text-[13px] text-danger">{error}</p>
|
|
108
|
+
<p id="{uid}-error" role="alert" class="font-sans text-[13px] text-danger">{error}</p>
|
|
98
109
|
{:else if hint}
|
|
99
|
-
<p class="font-sans text-[13px] text-text-muted">{hint}</p>
|
|
110
|
+
<p id="{uid}-hint" class="font-sans text-[13px] text-text-muted">{hint}</p>
|
|
100
111
|
{/if}
|
|
101
112
|
</fieldset>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
-
import { focusRing, toneRing, type Tone } from '../../core/tones.js';
|
|
4
|
+
import { focusRing, toneRing, toneText, type Tone } from '../../core/tones.js';
|
|
5
5
|
import { iconMd } from './icon.js';
|
|
6
6
|
|
|
7
7
|
export interface SegmentOption {
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
onclick={() => select(option)}
|
|
93
93
|
class="inline-flex items-center justify-center rounded-md font-sans font-medium whitespace-nowrap transition-all duration-150 ease-brand-out disabled:pointer-events-none disabled:opacity-40
|
|
94
94
|
{iconOnly ? squares[size] : sizes[size]} {focusRing} {toneRing[tone]}
|
|
95
|
-
{value === option.value ?
|
|
95
|
+
{value === option.value ? `bg-bg ${toneText[tone]} shadow-xs` : 'text-text-muted hover:text-text'}"
|
|
96
96
|
>
|
|
97
97
|
{#if option.icon}<span class={iconMd}>{@render option.icon()}</span>{/if}
|
|
98
98
|
{#if !iconOnly}{option.label}{/if}
|
|
@@ -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 rounded-md border bg-bg py-0 pr-9 pl-3 font-sans text-sm text-text shadow-xs transition-colors duration-150 ease-brand-out disabled:pointer-events-none disabled:opacity-50
|
|
39
41
|
{invalid ? 'border-danger' : `border-hairline-strong ${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 rounded-md 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}>
|
|
@@ -49,7 +57,21 @@
|
|
|
49
57
|
</span>
|
|
50
58
|
{#if delta}
|
|
51
59
|
<span class="text-sm font-medium {toneText[trendTone[trend]]}">
|
|
52
|
-
|
|
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>
|
|
53
75
|
{delta}
|
|
54
76
|
</span>
|
|
55
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-sm">
|
|
58
61
|
{#if label}<span class="leading-snug text-text">{label}</span>{/if}
|
|
59
62
|
{#if description}
|
|
60
|
-
<span class="text-[13px] leading-snug text-text-muted"
|
|
63
|
+
<span id="{uid}-description" class="text-[13px] 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 rounded-md border bg-bg px-3 py-2 font-sans text-sm text-text shadow-xs 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-strong ${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
|
|
|
@@ -50,7 +66,7 @@
|
|
|
50
66
|
would fight the page underneath.
|
|
51
67
|
-->
|
|
52
68
|
<div
|
|
53
|
-
animate:flip={{ duration: 200 }}
|
|
69
|
+
animate:flip={{ duration: stillness ? 0 : 200 }}
|
|
54
70
|
in:fly={{ ...enter, duration: 220 }}
|
|
55
71
|
out:fly={{ ...enter, duration: 160 }}
|
|
56
72
|
role={item.tone === 'danger' ? 'alert' : 'status'}
|
|
@@ -92,7 +108,7 @@
|
|
|
92
108
|
</span>
|
|
93
109
|
|
|
94
110
|
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
95
|
-
<p class="text-sm leading-snug font-semibold text-text">{item.title}</p>
|
|
111
|
+
<p class="font-heading text-sm leading-snug font-semibold text-text">{item.title}</p>
|
|
96
112
|
{#if item.description}
|
|
97
113
|
<p class="text-[13px] leading-relaxed text-text-secondary">{item.description}</p>
|
|
98
114
|
{/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-1.5 -translate-x-1/2',
|
|
18
24
|
bottom: 'top-full left-1/2 mt-1.5 -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 rounded-md bg-text px-2 py-1 font-sans text-xs font-medium whitespace-nowrap text-text-inverse opacity-0 shadow-lg 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';
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
--color-text-inverse: #ffffff;
|
|
30
30
|
|
|
31
31
|
/* --- brand / primary (indigo) --- */
|
|
32
|
-
--color-brand: #
|
|
33
|
-
--color-brand-hover: #
|
|
34
|
-
--color-brand-light: #
|
|
35
|
-
--color-brand-border: #
|
|
32
|
+
--color-brand: #15803d;
|
|
33
|
+
--color-brand-hover: #166534;
|
|
34
|
+
--color-brand-light: #e8f6ee;
|
|
35
|
+
--color-brand-border: #a7d9b9;
|
|
36
36
|
|
|
37
37
|
/* --- accent (rose) --- */
|
|
38
38
|
--color-accent: #e11d48;
|
|
@@ -95,9 +95,46 @@
|
|
|
95
95
|
'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
96
96
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
|
97
97
|
|
|
98
|
-
/* --- motion ---
|
|
98
|
+
/* --- motion ---
|
|
99
|
+
* paper enters by rising and fading — depth is its language, so overlays
|
|
100
|
+
* settle onto the page rather than snapping or scaling in. One token per
|
|
101
|
+
* overlay kind: `enter` for dialog panels + backdrops, `reveal` for anchored
|
|
102
|
+
* menus and popovers. matte and sprout define these same two names their own
|
|
103
|
+
* way, so an overlay's markup never changes between styles — only the values.
|
|
104
|
+
*/
|
|
99
105
|
--ease-brand: cubic-bezier(0.4, 0, 0.2, 1);
|
|
100
106
|
--ease-brand-out: cubic-bezier(0, 0, 0.2, 1);
|
|
107
|
+
--animate-enter: nqm-rise 200ms var(--ease-brand-out);
|
|
108
|
+
--animate-reveal: nqm-drop 180ms var(--ease-brand-out);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Enter-only, matching the Drawer: the native <dialog> and the {#if}-mounted
|
|
112
|
+
* popover each play these once on mount and leave instantly. */
|
|
113
|
+
@keyframes nqm-fade {
|
|
114
|
+
from {
|
|
115
|
+
opacity: 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
@keyframes nqm-rise {
|
|
119
|
+
from {
|
|
120
|
+
opacity: 0;
|
|
121
|
+
/* dialogs rise and grow a hair — a gentle zoom onto the page */
|
|
122
|
+
transform: translateY(8px) scale(0.98);
|
|
123
|
+
/* one layer, matching the panel's shadow-2xl, so box-shadow interpolates
|
|
124
|
+
* to the resting shadow instead of snapping (a mismatched layer count is
|
|
125
|
+
* discrete). The shadow deepens as the panel settles. */
|
|
126
|
+
box-shadow: 0 8px 20px -12px rgb(0 0 0 / 0.18);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
@keyframes nqm-drop {
|
|
130
|
+
from {
|
|
131
|
+
opacity: 0;
|
|
132
|
+
transform: translateY(-6px);
|
|
133
|
+
/* two layers, matching the menus' shadow-lg — same reason as above */
|
|
134
|
+
box-shadow:
|
|
135
|
+
0 2px 6px -3px rgb(0 0 0 / 0.08),
|
|
136
|
+
0 1px 3px -2px rgb(0 0 0 / 0.06);
|
|
137
|
+
}
|
|
101
138
|
}
|
|
102
139
|
|
|
103
140
|
/*
|
|
@@ -126,10 +163,10 @@
|
|
|
126
163
|
--color-text-muted: #9ca3af;
|
|
127
164
|
--color-text-inverse: #0b0f19;
|
|
128
165
|
|
|
129
|
-
--color-brand: #
|
|
130
|
-
--color-brand-hover: #
|
|
131
|
-
--color-brand-light: #
|
|
132
|
-
--color-brand-border: #
|
|
166
|
+
--color-brand: #46a06a;
|
|
167
|
+
--color-brand-hover: #5cb47e;
|
|
168
|
+
--color-brand-light: #12281b;
|
|
169
|
+
--color-brand-border: #255034;
|
|
133
170
|
|
|
134
171
|
--color-accent: #fb7185;
|
|
135
172
|
--color-accent-hover: #fda4af;
|
|
@@ -182,10 +219,10 @@
|
|
|
182
219
|
--color-text-muted: #9ca3af;
|
|
183
220
|
--color-text-inverse: #0b0f19;
|
|
184
221
|
|
|
185
|
-
--color-brand: #
|
|
186
|
-
--color-brand-hover: #
|
|
187
|
-
--color-brand-light: #
|
|
188
|
-
--color-brand-border: #
|
|
222
|
+
--color-brand: #46a06a;
|
|
223
|
+
--color-brand-hover: #5cb47e;
|
|
224
|
+
--color-brand-light: #12281b;
|
|
225
|
+
--color-brand-border: #255034;
|
|
189
226
|
|
|
190
227
|
--color-accent: #fb7185;
|
|
191
228
|
--color-accent-hover: #fda4af;
|
|
@@ -223,3 +260,38 @@
|
|
|
223
260
|
--color-neutral-border: #374151;
|
|
224
261
|
}
|
|
225
262
|
}
|
|
263
|
+
|
|
264
|
+
/*
|
|
265
|
+
* The surfaces the browser draws for us.
|
|
266
|
+
*
|
|
267
|
+
* A caret and a highlight in the user-agent's blue belong to no design system,
|
|
268
|
+
* and they are the two the reader touches most: one sits in every field they
|
|
269
|
+
* type into, the other appears the moment they drag across a paragraph. The
|
|
270
|
+
* selection is mixed from the brand rather than pinned to a token so it stays
|
|
271
|
+
* a wash — the text underneath keeps its own colour and its own contrast.
|
|
272
|
+
*/
|
|
273
|
+
@layer base {
|
|
274
|
+
/* The one overlay surface a class can't reach: every <dialog> backdrop
|
|
275
|
+
* fades with its panel. */
|
|
276
|
+
dialog::backdrop {
|
|
277
|
+
animation: nqm-fade 180ms var(--ease-brand-out);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@media (prefers-reduced-motion: reduce) {
|
|
281
|
+
.animate-enter,
|
|
282
|
+
.animate-reveal,
|
|
283
|
+
dialog::backdrop {
|
|
284
|
+
animation: none;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
::selection {
|
|
289
|
+
background-color: color-mix(in oklab, var(--color-brand) 24%, transparent);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
input,
|
|
293
|
+
textarea,
|
|
294
|
+
[contenteditable] {
|
|
295
|
+
caret-color: var(--color-brand);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
const variants = $derived({
|
|
42
42
|
soft: `${toneSurface[tone]} border ${toneBorderSoft[tone]}`,
|
|
43
43
|
outline: `bg-bg ${edge} border ${toneBorderSoft[tone]}`,
|
|
44
|
-
accent: `${toneSurface[tone]} border-l-
|
|
44
|
+
accent: `${toneSurface[tone]} border-l-2 ${toneBorder[tone]}`
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
function dismiss() {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
|
|
71
71
|
<div class="flex min-w-0 flex-1 flex-col gap-1">
|
|
72
72
|
{#if title}
|
|
73
|
-
<p class="text-sm leading-snug font-semibold {toneText[tone]}">{title}</p>
|
|
73
|
+
<p class="font-heading text-sm leading-snug font-semibold {toneText[tone]}">{title}</p>
|
|
74
74
|
{/if}
|
|
75
75
|
{#if children}
|
|
76
76
|
<div class="text-[13px] leading-relaxed text-text-secondary">{@render children()}</div>
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
type="button"
|
|
83
83
|
onclick={dismiss}
|
|
84
84
|
aria-label="Dismiss"
|
|
85
|
-
class="-m-1 shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg/60 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
85
|
+
class="-m-1 shrink-0 rounded-xl p-1 text-text-muted transition-colors duration-150 hover:bg-bg/60 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
86
86
|
>
|
|
87
87
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
88
88
|
<path
|
|
@@ -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-2xl' : 'rounded-full'} {sizes[size]} {src
|
|
69
|
+
{squared ? 'rounded-2xl' : '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}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
{#if item.href && i < items.length - 1}
|
|
31
31
|
<a
|
|
32
32
|
href={item.href}
|
|
33
|
-
class="inline-flex items-center gap-1.5 rounded px-1 py-0.5 text-text-muted transition-colors duration-150 ease-brand-out hover:bg-bg-inset {toneHoverText[
|
|
33
|
+
class="inline-flex items-center gap-1.5 rounded-md px-1 py-0.5 text-text-muted transition-colors duration-150 ease-brand-out hover:bg-bg-inset {toneHoverText[
|
|
34
34
|
tone
|
|
35
35
|
]}"
|
|
36
36
|
>
|