@nqmcreative/ui 0.4.1 → 0.4.2
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 +6 -2
- package/dist/styles/matte/Checkbox.svelte +49 -30
- package/dist/styles/matte/CheckboxGroup.svelte +15 -4
- package/dist/styles/matte/Combobox.svelte +11 -2
- package/dist/styles/matte/ConfirmDialog.svelte +10 -2
- package/dist/styles/matte/DatePicker.svelte +9 -2
- package/dist/styles/matte/Divider.svelte +1 -1
- package/dist/styles/matte/Drawer.svelte +10 -2
- 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 +10 -2
- package/dist/styles/matte/Navbar.svelte +2 -2
- 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 +21 -0
- package/dist/styles/paper/Alert.svelte +2 -2
- package/dist/styles/paper/Avatar.svelte +14 -3
- package/dist/styles/paper/Button.svelte +5 -1
- 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 +11 -2
- package/dist/styles/paper/ConfirmDialog.svelte +8 -2
- package/dist/styles/paper/DatePicker.svelte +9 -2
- package/dist/styles/paper/Divider.svelte +1 -1
- package/dist/styles/paper/Drawer.svelte +8 -2
- 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 +8 -2
- package/dist/styles/paper/Navbar.svelte +2 -2
- 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 +21 -0
- 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 +50 -24
- package/package.json +1 -1
- package/registry.json +4 -2
|
@@ -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';
|
|
@@ -223,3 +223,24 @@
|
|
|
223
223
|
--color-neutral-border: #374151;
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* The surfaces the browser draws for us.
|
|
229
|
+
*
|
|
230
|
+
* A caret and a highlight in the user-agent's blue belong to no design system,
|
|
231
|
+
* and they are the two the reader touches most: one sits in every field they
|
|
232
|
+
* type into, the other appears the moment they drag across a paragraph. The
|
|
233
|
+
* selection is mixed from the brand rather than pinned to a token so it stays
|
|
234
|
+
* a wash — the text underneath keeps its own colour and its own contrast.
|
|
235
|
+
*/
|
|
236
|
+
@layer base {
|
|
237
|
+
::selection {
|
|
238
|
+
background-color: color-mix(in oklab, var(--color-brand) 24%, transparent);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
input,
|
|
242
|
+
textarea,
|
|
243
|
+
[contenteditable] {
|
|
244
|
+
caret-color: var(--color-brand);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -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
|
>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
toneText,
|
|
15
15
|
type Tone
|
|
16
16
|
} from '../../core/tones.js';
|
|
17
|
-
import {
|
|
17
|
+
import { lift, liftSoft } from './lift.js';
|
|
18
18
|
|
|
19
19
|
export type ButtonVariant = 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'primary';
|
|
20
20
|
export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
@@ -92,11 +92,11 @@
|
|
|
92
92
|
case 'solid':
|
|
93
93
|
return `border-transparent ${toneSolid[tone]} ${toneSolidHover[tone]} ${lift[tone]}`;
|
|
94
94
|
case 'soft':
|
|
95
|
-
return `border-transparent ${toneSoft[tone]} ${toneSoftHover[tone]} ${
|
|
95
|
+
return `border-transparent ${toneSoft[tone]} ${toneSoftHover[tone]} ${liftSoft[tone]}`;
|
|
96
96
|
// Outline keeps its own colours on hover — swapping to a solid fill is
|
|
97
97
|
// matte's move, and it fights sprout's soft surfaces.
|
|
98
98
|
case 'outline':
|
|
99
|
-
return `bg-bg ${
|
|
99
|
+
return `bg-bg ${liftSoft[tone]} ${toneText[tone]} ${toneBorderSoft[tone]} hover:bg-bg-alt`;
|
|
100
100
|
// Nothing raised, so nothing to sink: ghost stays flat on the page.
|
|
101
101
|
case 'ghost':
|
|
102
102
|
return `border-transparent bg-transparent ${toneText[tone]} ${toneSoftHover[tone]} active:translate-y-0`;
|
|
@@ -116,6 +116,10 @@
|
|
|
116
116
|
const iconClass = $derived(size === 'sm' ? iconSm : iconMd);
|
|
117
117
|
|
|
118
118
|
async function handleClick(event: MouseEvent) {
|
|
119
|
+
if (inert) {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
119
123
|
const result = onclick?.(event);
|
|
120
124
|
// Anything else is a plain handler: nothing to wait for, nothing to lock.
|
|
121
125
|
if (!(result instanceof Promise)) return;
|
|
@@ -134,7 +138,7 @@
|
|
|
134
138
|
disabled={href ? undefined : inert}
|
|
135
139
|
aria-disabled={href && inert ? 'true' : undefined}
|
|
136
140
|
aria-busy={busy ? 'true' : undefined}
|
|
137
|
-
|
|
141
|
+
tabindex={href && inert ? -1 : undefined}
|
|
138
142
|
class="{base} {focusRing} {toneRing[tone]} {variantClass} {isLink
|
|
139
143
|
? ''
|
|
140
144
|
: iconOnly
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
type="button"
|
|
190
190
|
onclick={() => pick(now)}
|
|
191
191
|
disabled={blocked(now)}
|
|
192
|
-
class="self-start rounded px-1 text-[13px] font-medium transition-colors duration-150 disabled:pointer-events-none disabled:opacity-40 {toneText[
|
|
192
|
+
class="self-start rounded-md px-1 text-[13px] font-medium transition-colors duration-150 disabled:pointer-events-none disabled:opacity-40 {toneText[
|
|
193
193
|
tone
|
|
194
194
|
]} hover:underline hover:underline-offset-2 {focusRing} {toneRing[tone]}"
|
|
195
195
|
>
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
...rest
|
|
41
41
|
}: Props = $props();
|
|
42
42
|
|
|
43
|
-
// The default card is the one the style is named for:
|
|
43
|
+
// The default card is the one the style is named for: white, a hairline, and
|
|
44
44
|
// the lip. `filled` and `tinted` stay flat on purpose — a tinted panel that
|
|
45
45
|
// also floats is two claims about the same box.
|
|
46
46
|
const variants = $derived({
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
<div
|
|
54
54
|
class="flex flex-col rounded-[28px] {variants[variant]}
|
|
55
|
-
{accent ? `border-l-
|
|
55
|
+
{accent ? `border-l-2 ${toneBorderLeft[tone]}` : ''}
|
|
56
56
|
{interactive
|
|
57
57
|
? `transition-[box-shadow,transform,border-color] duration-150 ease-brand-out hover:-translate-y-0.5 ${softOnHover} ${toneHoverBorder[tone]}`
|
|
58
58
|
: ''}
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
...rest
|
|
27
27
|
}: Props = $props();
|
|
28
28
|
|
|
29
|
+
const uid = $props.id();
|
|
30
|
+
|
|
29
31
|
let el: HTMLInputElement | null = $state(null);
|
|
30
32
|
|
|
31
33
|
$effect(() => {
|
|
@@ -33,44 +35,61 @@
|
|
|
33
35
|
});
|
|
34
36
|
</script>
|
|
35
37
|
|
|
36
|
-
<
|
|
37
|
-
class="
|
|
38
|
-
|
|
38
|
+
<div
|
|
39
|
+
class="inline-block font-sans text-sm {disabled
|
|
40
|
+
? 'pointer-events-none opacity-50'
|
|
41
|
+
: ''} {className}"
|
|
39
42
|
>
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
<label class="group inline-flex items-start gap-2.5 {disabled ? '' : 'cursor-pointer'}">
|
|
44
|
+
<input
|
|
45
|
+
bind:this={el}
|
|
46
|
+
type="checkbox"
|
|
47
|
+
bind:checked
|
|
48
|
+
{disabled}
|
|
49
|
+
aria-describedby={description ? `${uid}-description` : undefined}
|
|
50
|
+
class="peer sr-only"
|
|
51
|
+
{...rest}
|
|
52
|
+
/>
|
|
53
|
+
<span
|
|
54
|
+
class="mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded-md border border-hairline-strong bg-bg {edge} transition-colors duration-150 ease-brand-out
|
|
43
55
|
{tonePeerChecked[tone]} {peerFocusRing} {tonePeerFocus[tone]}"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
{/if}
|
|
63
|
-
</span>
|
|
64
|
-
{#if label || description || children}
|
|
65
|
-
<span class="flex flex-col gap-0.5">
|
|
66
|
-
{#if children}
|
|
67
|
-
{@render children()}
|
|
68
|
-
{:else if label}
|
|
69
|
-
<span class="leading-snug text-text">{label}</span>
|
|
70
|
-
{/if}
|
|
71
|
-
{#if description}
|
|
72
|
-
<span class="text-[13px] leading-snug text-text-muted">{description}</span>
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
>
|
|
58
|
+
{#if indeterminate}
|
|
59
|
+
<span class="h-0.5 w-2.5 rounded-full bg-text-muted"></span>
|
|
60
|
+
{:else}
|
|
61
|
+
<svg
|
|
62
|
+
class="size-3 text-text-inverse opacity-0 transition-opacity duration-150 group-has-[:checked]:opacity-100"
|
|
63
|
+
viewBox="0 0 12 12"
|
|
64
|
+
fill="none"
|
|
65
|
+
>
|
|
66
|
+
<path
|
|
67
|
+
d="m2 6 2.6 2.6L10 3.2"
|
|
68
|
+
stroke="currentColor"
|
|
69
|
+
stroke-width="2"
|
|
70
|
+
stroke-linecap="round"
|
|
71
|
+
stroke-linejoin="round"
|
|
72
|
+
/>
|
|
73
|
+
</svg>
|
|
73
74
|
{/if}
|
|
74
75
|
</span>
|
|
76
|
+
{#if label || children}
|
|
77
|
+
<span class="flex flex-col gap-0.5">
|
|
78
|
+
{#if children}
|
|
79
|
+
{@render children()}
|
|
80
|
+
{:else if label}
|
|
81
|
+
<span class="leading-snug text-text">{label}</span>
|
|
82
|
+
{/if}
|
|
83
|
+
</span>
|
|
84
|
+
{/if}
|
|
85
|
+
</label>
|
|
86
|
+
{#if description}
|
|
87
|
+
<!-- ponytail: the indent tracks the 18px box plus the label's gap; move one, move both. -->
|
|
88
|
+
<span
|
|
89
|
+
id="{uid}-description"
|
|
90
|
+
class="mt-0.5 block pl-[28px] text-[13px] leading-snug text-text-muted"
|
|
91
|
+
>
|
|
92
|
+
{description}
|
|
93
|
+
</span>
|
|
75
94
|
{/if}
|
|
76
|
-
</
|
|
95
|
+
</div>
|
|
@@ -51,6 +51,11 @@
|
|
|
51
51
|
...rest
|
|
52
52
|
}: Props = $props();
|
|
53
53
|
|
|
54
|
+
const uid = $props.id();
|
|
55
|
+
// The group's own hint and error sit outside every control in it, so the
|
|
56
|
+
// fieldset is what has to point at them.
|
|
57
|
+
const messageId = $derived(error ? `${uid}-error` : hint ? `${uid}-hint` : undefined);
|
|
58
|
+
|
|
54
59
|
const full = $derived(max > 0 && value.length >= max);
|
|
55
60
|
|
|
56
61
|
function toggle(option: CheckboxOption) {
|
|
@@ -61,11 +66,17 @@
|
|
|
61
66
|
}
|
|
62
67
|
</script>
|
|
63
68
|
|
|
64
|
-
<fieldset
|
|
69
|
+
<fieldset
|
|
70
|
+
aria-describedby={messageId}
|
|
71
|
+
class="flex flex-col gap-3 border-0 p-0 {className}"
|
|
72
|
+
{...rest}
|
|
73
|
+
>
|
|
65
74
|
{#if legend}
|
|
66
75
|
<legend class="mb-1 font-sans text-sm font-medium text-text">
|
|
67
76
|
{legend}
|
|
68
|
-
{#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span
|
|
77
|
+
{#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span><span
|
|
78
|
+
class="sr-only">(required)</span
|
|
79
|
+
>{/if}
|
|
69
80
|
</legend>
|
|
70
81
|
{/if}
|
|
71
82
|
|
|
@@ -111,8 +122,8 @@
|
|
|
111
122
|
</div>
|
|
112
123
|
|
|
113
124
|
{#if error}
|
|
114
|
-
<p class="font-sans text-[13px] text-danger">{error}</p>
|
|
125
|
+
<p id="{uid}-error" role="alert" class="font-sans text-[13px] text-danger">{error}</p>
|
|
115
126
|
{:else if hint}
|
|
116
|
-
<p class="font-sans text-[13px] text-text-muted">{hint}</p>
|
|
127
|
+
<p id="{uid}-hint" class="font-sans text-[13px] text-text-muted">{hint}</p>
|
|
117
128
|
{/if}
|
|
118
129
|
</fieldset>
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
>
|
|
75
75
|
{#snippet prefix()}
|
|
76
76
|
<span
|
|
77
|
-
class="relative grid size-5 place-items-center overflow-hidden rounded border border-hairline-strong"
|
|
77
|
+
class="relative grid size-5 place-items-center overflow-hidden rounded-lg border border-hairline-strong"
|
|
78
78
|
>
|
|
79
79
|
<span class="absolute inset-0" style="background-color: {resolved}"></span>
|
|
80
80
|
<input
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
import { anchored } from '../../core/actions/anchor.js';
|
|
4
4
|
import { clickOutside, portal } from '../../core/actions/dismissable.js';
|
|
5
5
|
import { ListCursor, groupItems, matchQuery, revealIndex } from '../../core/list.svelte.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
focusWithinRing,
|
|
8
|
+
toneFocusWithinBorder,
|
|
9
|
+
toneFocusWithinRing,
|
|
10
|
+
toneSoft,
|
|
11
|
+
toneText,
|
|
12
|
+
type Tone
|
|
13
|
+
} from '../../core/tones.js';
|
|
7
14
|
import { iconMd } from './icon.js';
|
|
8
15
|
import { edge, soft } from './lift.js';
|
|
9
16
|
|
|
@@ -144,7 +151,9 @@
|
|
|
144
151
|
|
|
145
152
|
<div
|
|
146
153
|
bind:this={wrapper}
|
|
147
|
-
class="relative inline-flex w-full items-center gap-2 rounded-xl border bg-bg pr-1.5 pl-3 {edge} transition-colors duration-150 ease-brand-out
|
|
154
|
+
class="relative inline-flex w-full items-center gap-2 rounded-xl border bg-bg pr-1.5 pl-3 {edge} transition-colors duration-150 ease-brand-out {focusWithinRing} {toneFocusWithinRing[
|
|
155
|
+
tone
|
|
156
|
+
]}
|
|
148
157
|
{invalid ? 'border-danger' : `border-hairline-strong ${toneFocusWithinBorder[tone]}`}
|
|
149
158
|
{disabled ? 'pointer-events-none opacity-50' : ''} {className}"
|
|
150
159
|
>
|
|
@@ -178,7 +187,7 @@
|
|
|
178
187
|
type="button"
|
|
179
188
|
onclick={clear}
|
|
180
189
|
aria-label="Clear selection"
|
|
181
|
-
class="shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
190
|
+
class="shrink-0 rounded-xl p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
|
|
182
191
|
>
|
|
183
192
|
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
184
193
|
<path
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
children
|
|
37
37
|
}: Props = $props();
|
|
38
38
|
|
|
39
|
+
const uid = $props.id();
|
|
40
|
+
|
|
39
41
|
async function confirm() {
|
|
40
42
|
try {
|
|
41
43
|
loading = true;
|
|
@@ -55,6 +57,8 @@
|
|
|
55
57
|
|
|
56
58
|
<Modal
|
|
57
59
|
bind:open
|
|
60
|
+
aria-labelledby="{uid}-title"
|
|
61
|
+
aria-describedby={description ? `${uid}-description` : undefined}
|
|
58
62
|
size="sm"
|
|
59
63
|
dismissible={!loading}
|
|
60
64
|
showClose={false}
|
|
@@ -73,9 +77,11 @@
|
|
|
73
77
|
{/if}
|
|
74
78
|
</span>
|
|
75
79
|
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
76
|
-
<h2 class="font-heading text-base font-semibold text-text">{title}</h2>
|
|
80
|
+
<h2 id="{uid}-title" class="font-heading text-base font-semibold text-text">{title}</h2>
|
|
77
81
|
{#if description}
|
|
78
|
-
<p class="text-[13px] leading-relaxed text-text-secondary">
|
|
82
|
+
<p id="{uid}-description" class="text-[13px] leading-relaxed text-text-secondary">
|
|
83
|
+
{description}
|
|
84
|
+
</p>
|
|
79
85
|
{/if}
|
|
80
86
|
{#if children}
|
|
81
87
|
{@render children()}
|