@lyeve-labs/ui-kit 0.11.2 → 0.13.0
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/components/AccordionItem.svelte +1 -1
- package/dist/components/Autocomplete.svelte +191 -125
- package/dist/components/Autocomplete.svelte.d.ts +29 -8
- package/dist/components/Button.svelte +26 -4
- package/dist/components/Card.svelte +61 -3
- package/dist/components/Card.svelte.d.ts +24 -2
- package/dist/components/Checkbox.svelte +174 -59
- package/dist/components/Checkbox.svelte.d.ts +20 -3
- package/dist/components/CheckboxGroup.svelte +162 -0
- package/dist/components/CheckboxGroup.svelte.d.ts +51 -0
- package/dist/components/Collapsible.svelte +142 -0
- package/dist/components/Collapsible.svelte.d.ts +32 -0
- package/dist/components/CopyButton.svelte +126 -0
- package/dist/components/CopyButton.svelte.d.ts +14 -0
- package/dist/components/DatePicker.svelte +48 -6
- package/dist/components/DateTimePicker.svelte +337 -0
- package/dist/components/DateTimePicker.svelte.d.ts +26 -0
- package/dist/components/DescriptionList.svelte +78 -0
- package/dist/components/DescriptionList.svelte.d.ts +34 -0
- package/dist/components/Drawer.svelte +15 -4
- package/dist/components/Field.svelte +104 -0
- package/dist/components/Field.svelte.d.ts +46 -0
- package/dist/components/FileInput.svelte +5 -2
- package/dist/components/FormMessage.svelte +85 -0
- package/dist/components/FormMessage.svelte.d.ts +11 -0
- package/dist/components/Input.svelte +1 -1
- package/dist/components/Label.svelte +7 -1
- package/dist/components/Label.svelte.d.ts +6 -0
- package/dist/components/Modal.svelte +25 -8
- package/dist/components/MultiSelect.svelte +199 -109
- package/dist/components/MultiSelect.svelte.d.ts +22 -9
- package/dist/components/NumberInput.svelte +8 -4
- package/dist/components/PageHeader.svelte +37 -4
- package/dist/components/PageHeader.svelte.d.ts +15 -0
- package/dist/components/PageShell.svelte +85 -0
- package/dist/components/PageShell.svelte.d.ts +38 -0
- package/dist/components/Pagination.svelte +58 -17
- package/dist/components/Panel.svelte +101 -0
- package/dist/components/Panel.svelte.d.ts +39 -0
- package/dist/components/PasswordInput.svelte +139 -0
- package/dist/components/PasswordInput.svelte.d.ts +29 -0
- package/dist/components/Radio.svelte +152 -32
- package/dist/components/Radio.svelte.d.ts +16 -1
- package/dist/components/RadioGroup.svelte +118 -71
- package/dist/components/RadioGroup.svelte.d.ts +39 -9
- package/dist/components/SectionHeading.svelte +39 -0
- package/dist/components/SectionHeading.svelte.d.ts +21 -0
- package/dist/components/SegmentedControl.svelte +194 -0
- package/dist/components/SegmentedControl.svelte.d.ts +55 -0
- package/dist/components/Select.svelte +471 -46
- package/dist/components/Select.svelte.d.ts +95 -6
- package/dist/components/SidebarNav.svelte +259 -0
- package/dist/components/SidebarNav.svelte.d.ts +17 -0
- package/dist/components/Stat.svelte +53 -2
- package/dist/components/Stat.svelte.d.ts +31 -0
- package/dist/components/Textarea.svelte +1 -1
- package/dist/components/TimePicker.svelte +480 -0
- package/dist/components/TimePicker.svelte.d.ts +23 -0
- package/dist/components/Toaster.svelte +9 -2
- package/dist/components/Toggle.svelte +5 -1
- package/dist/components/Toggle.svelte.d.ts +2 -0
- package/dist/components/Toolbar.svelte +39 -0
- package/dist/components/Toolbar.svelte.d.ts +26 -0
- package/dist/components/Tooltip.svelte +48 -12
- package/dist/components/TreeView.svelte +339 -0
- package/dist/components/TreeView.svelte.d.ts +37 -0
- package/dist/components/dialog/Dialog.svelte +15 -58
- package/dist/components/dialog/dialog-manager.svelte.d.ts +2 -2
- package/dist/components/dialog/dialog-manager.svelte.js +21 -21
- package/dist/index.d.ts +25 -1
- package/dist/index.js +20 -1
- package/dist/internal/calendar.d.ts +119 -0
- package/dist/internal/calendar.js +225 -0
- package/dist/internal/choice.d.ts +136 -0
- package/dist/internal/choice.js +179 -0
- package/dist/internal/field.d.ts +31 -0
- package/dist/internal/field.js +42 -1
- package/dist/internal/filter.d.ts +80 -0
- package/dist/internal/filter.js +80 -0
- package/dist/internal/layout.d.ts +119 -0
- package/dist/internal/layout.js +132 -0
- package/dist/internal/listbox.svelte.d.ts +77 -0
- package/dist/internal/listbox.svelte.js +438 -0
- package/dist/internal/nav-expansion.svelte.d.ts +36 -0
- package/dist/internal/nav-expansion.svelte.js +144 -0
- package/dist/internal/nav-tree.d.ts +68 -0
- package/dist/internal/nav-tree.js +102 -0
- package/dist/internal/overlay.d.ts +25 -0
- package/dist/internal/overlay.js +92 -0
- package/dist/internal/panel.d.ts +100 -0
- package/dist/internal/panel.js +109 -0
- package/dist/internal/rollup.d.ts +52 -0
- package/dist/internal/rollup.js +67 -0
- package/dist/internal/time.d.ts +103 -0
- package/dist/internal/time.js +166 -0
- package/dist/internal/tree.d.ts +86 -0
- package/dist/internal/tree.js +111 -0
- package/dist/styles/theme.css +66 -25
- package/package.json +4 -2
- package/src/lib/styles/theme.css +66 -25
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key and value pairs, as a real description list.
|
|
3
|
+
*
|
|
4
|
+
* Detail pages laid these out with ad hoc grids of divs, which broke
|
|
5
|
+
* alignment between two pages showing the same record and, more importantly,
|
|
6
|
+
* conveyed nothing: a grid of divs has no relationship between a label and
|
|
7
|
+
* the value under it, so a screen reader reads a run of unattached strings
|
|
8
|
+
* and the listener has to infer which value belongs to which term. `dl`,
|
|
9
|
+
* `dt` and `dd` state that relationship in the markup, and it is the whole
|
|
10
|
+
* reason this component exists.
|
|
11
|
+
*
|
|
12
|
+
* A long value wraps. It is never truncated, because a clipped identifier
|
|
13
|
+
* cannot be read out, cannot be selected and cannot be copied, and an id or
|
|
14
|
+
* a key is exactly the kind of value a detail page is opened for. `break-words`
|
|
15
|
+
* covers the unbroken ones, so a token or a hash wraps instead of pushing the
|
|
16
|
+
* page sideways.
|
|
17
|
+
*/
|
|
18
|
+
import type { Snippet } from 'svelte';
|
|
19
|
+
interface Item {
|
|
20
|
+
term: string;
|
|
21
|
+
value: string;
|
|
22
|
+
/** Rendered instead of value when present. */
|
|
23
|
+
detail?: Snippet;
|
|
24
|
+
}
|
|
25
|
+
type Layout = 'inline' | 'stacked';
|
|
26
|
+
interface Props {
|
|
27
|
+
items: Item[];
|
|
28
|
+
/** 'inline' puts the term and value on one row, 'stacked' puts the value under the term. */
|
|
29
|
+
layout?: Layout;
|
|
30
|
+
class?: string;
|
|
31
|
+
}
|
|
32
|
+
declare const DescriptionList: import("svelte").Component<Props, {}, "">;
|
|
33
|
+
type DescriptionList = ReturnType<typeof DescriptionList>;
|
|
34
|
+
export default DescriptionList;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { overlay } from '../internal/overlay.js';
|
|
3
4
|
|
|
4
5
|
type Side = 'left' | 'right';
|
|
5
6
|
type Size = 'sm' | 'md' | 'lg' | 'xl';
|
|
@@ -33,6 +34,9 @@
|
|
|
33
34
|
xl: 'w-[480px]',
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
const headingId = $props.id();
|
|
38
|
+
const descriptionId = `${headingId}-description`;
|
|
39
|
+
|
|
36
40
|
function close() {
|
|
37
41
|
open = false;
|
|
38
42
|
onclose?.();
|
|
@@ -52,25 +56,32 @@
|
|
|
52
56
|
<div class="fixed inset-0 z-50 flex {side === 'right' ? 'justify-end' : 'justify-start'}">
|
|
53
57
|
<button
|
|
54
58
|
type="button"
|
|
59
|
+
tabindex="-1"
|
|
60
|
+
aria-hidden="true"
|
|
55
61
|
class="absolute inset-0 bg-black/60 backdrop-blur-sm cursor-default"
|
|
56
|
-
aria-label="Close"
|
|
57
62
|
onclick={close}
|
|
58
63
|
></button>
|
|
59
64
|
|
|
60
65
|
<div
|
|
61
|
-
|
|
66
|
+
use:overlay
|
|
67
|
+
class="relative flex h-full max-w-full flex-col {widths[size]} bg-surface shadow-2xl
|
|
62
68
|
{side === 'right' ? 'border-l' : 'border-r'} border-line
|
|
63
69
|
{side === 'right'
|
|
64
70
|
? 'animate-[drawer-in-right_150ms_ease-out]'
|
|
65
71
|
: 'animate-[drawer-in-left_150ms_ease-out]'}"
|
|
66
72
|
role="dialog"
|
|
67
73
|
aria-modal="true"
|
|
74
|
+
aria-labelledby={title ? headingId : undefined}
|
|
75
|
+
aria-describedby={description ? descriptionId : undefined}
|
|
76
|
+
aria-label={title ? undefined : 'Panel'}
|
|
68
77
|
>
|
|
69
78
|
{#if title}
|
|
70
79
|
<div class="flex shrink-0 items-start justify-between gap-4 border-b border-line px-5 py-4">
|
|
71
80
|
<div>
|
|
72
|
-
<h2 class="font-semibold text-fg">{title}</h2>
|
|
73
|
-
{#if description}
|
|
81
|
+
<h2 id={headingId} class="font-semibold text-fg">{title}</h2>
|
|
82
|
+
{#if description}
|
|
83
|
+
<p id={descriptionId} class="mt-0.5 text-sm text-muted">{description}</p>
|
|
84
|
+
{/if}
|
|
74
85
|
</div>
|
|
75
86
|
<button
|
|
76
87
|
type="button"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* What Field hands the control it wraps.
|
|
4
|
+
*
|
|
5
|
+
* The wiring is the whole point of the component. A consumer building a
|
|
6
|
+
* control the kit does not ship - a colour well, a code editor, a third
|
|
7
|
+
* party map picker - cannot reach internal/field.ts, so it hand-copies the
|
|
8
|
+
* class strings and stops there: a form in a consuming app spelled
|
|
9
|
+
* `class="flex flex-col gap-1.5"` inline, which is the literal value of
|
|
10
|
+
* FIELD_WRAP, and then wired nothing. The control had no id, the label had
|
|
11
|
+
* no `for`, and the error paragraph under it was never announced.
|
|
12
|
+
*/
|
|
13
|
+
export interface FieldWiring {
|
|
14
|
+
/** The id the control must carry. The label's `for` already points at it. */
|
|
15
|
+
id: string;
|
|
16
|
+
/**
|
|
17
|
+
* The id of whichever message is on screen, for the control's
|
|
18
|
+
* `aria-describedby`. Undefined when there is neither a hint nor an error,
|
|
19
|
+
* so the attribute is left off rather than pointing at nothing.
|
|
20
|
+
*/
|
|
21
|
+
describedBy: string | undefined;
|
|
22
|
+
/** True while an error is showing, for the control's `aria-invalid`. */
|
|
23
|
+
invalid: boolean;
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<script lang="ts">
|
|
28
|
+
import type { Snippet } from 'svelte';
|
|
29
|
+
import {
|
|
30
|
+
FIELD_ERROR,
|
|
31
|
+
FIELD_HINT,
|
|
32
|
+
FIELD_LABEL,
|
|
33
|
+
FIELD_WRAP,
|
|
34
|
+
describedBy,
|
|
35
|
+
} from '../internal/field.js';
|
|
36
|
+
|
|
37
|
+
interface Props {
|
|
38
|
+
/** Overrides the generated id, for a control the caller has to address by name. */
|
|
39
|
+
id?: string;
|
|
40
|
+
label?: string;
|
|
41
|
+
/** Screen-reader-only label, for a control whose visible name is a table column header. */
|
|
42
|
+
labelHidden?: boolean;
|
|
43
|
+
hint?: string;
|
|
44
|
+
/** Replaces the hint while it is set, and marks the wiring invalid. */
|
|
45
|
+
error?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Draws the marker beside the label. Set `required` on the control as well:
|
|
48
|
+
* the marker is decorative and the control's own attribute is what a screen
|
|
49
|
+
* reader reports.
|
|
50
|
+
*/
|
|
51
|
+
required?: boolean;
|
|
52
|
+
class?: string;
|
|
53
|
+
/** Receives what the control must carry so the message row actually reaches a screen reader. */
|
|
54
|
+
children: Snippet<[FieldWiring]>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let {
|
|
58
|
+
id = undefined,
|
|
59
|
+
label = undefined,
|
|
60
|
+
labelHidden = false,
|
|
61
|
+
hint = undefined,
|
|
62
|
+
error = undefined,
|
|
63
|
+
required = false,
|
|
64
|
+
class: klass = '',
|
|
65
|
+
children,
|
|
66
|
+
}: Props = $props();
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* $props.id(), never Math.random(). A random id is one value in the server
|
|
70
|
+
* render and a different one on hydration, so the label's `for` and the id
|
|
71
|
+
* the control was given stop matching the moment the client takes over, and
|
|
72
|
+
* clicking the label focuses nothing.
|
|
73
|
+
*/
|
|
74
|
+
const uid = $props.id();
|
|
75
|
+
const fieldId = $derived(id ?? uid);
|
|
76
|
+
|
|
77
|
+
const wiring: FieldWiring = $derived({
|
|
78
|
+
id: fieldId,
|
|
79
|
+
describedBy: describedBy(fieldId, error, hint),
|
|
80
|
+
invalid: !!error,
|
|
81
|
+
});
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<div class="{FIELD_WRAP} {klass}">
|
|
85
|
+
{#if label}
|
|
86
|
+
<!--
|
|
87
|
+
A hidden label stays a real label with a real `for`. Swapping it for an
|
|
88
|
+
aria-label on the control would name the control and drop the name out of
|
|
89
|
+
the reading order, so a reader moving through a row of them would meet
|
|
90
|
+
the controls with nothing ahead of them saying what each one is.
|
|
91
|
+
-->
|
|
92
|
+
<label for={fieldId} class="{FIELD_LABEL} {labelHidden ? 'sr-only' : ''}">
|
|
93
|
+
{label}{#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span>{/if}
|
|
94
|
+
</label>
|
|
95
|
+
{/if}
|
|
96
|
+
|
|
97
|
+
{@render children(wiring)}
|
|
98
|
+
|
|
99
|
+
{#if error}
|
|
100
|
+
<p id="{fieldId}-error" class={FIELD_ERROR}>{error}</p>
|
|
101
|
+
{:else if hint}
|
|
102
|
+
<p id="{fieldId}-hint" class={FIELD_HINT}>{hint}</p>
|
|
103
|
+
{/if}
|
|
104
|
+
</div>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Field hands the control it wraps.
|
|
3
|
+
*
|
|
4
|
+
* The wiring is the whole point of the component. A consumer building a
|
|
5
|
+
* control the kit does not ship - a colour well, a code editor, a third
|
|
6
|
+
* party map picker - cannot reach internal/field.ts, so it hand-copies the
|
|
7
|
+
* class strings and stops there: a form in a consuming app spelled
|
|
8
|
+
* `class="flex flex-col gap-1.5"` inline, which is the literal value of
|
|
9
|
+
* FIELD_WRAP, and then wired nothing. The control had no id, the label had
|
|
10
|
+
* no `for`, and the error paragraph under it was never announced.
|
|
11
|
+
*/
|
|
12
|
+
export interface FieldWiring {
|
|
13
|
+
/** The id the control must carry. The label's `for` already points at it. */
|
|
14
|
+
id: string;
|
|
15
|
+
/**
|
|
16
|
+
* The id of whichever message is on screen, for the control's
|
|
17
|
+
* `aria-describedby`. Undefined when there is neither a hint nor an error,
|
|
18
|
+
* so the attribute is left off rather than pointing at nothing.
|
|
19
|
+
*/
|
|
20
|
+
describedBy: string | undefined;
|
|
21
|
+
/** True while an error is showing, for the control's `aria-invalid`. */
|
|
22
|
+
invalid: boolean;
|
|
23
|
+
}
|
|
24
|
+
import type { Snippet } from 'svelte';
|
|
25
|
+
interface Props {
|
|
26
|
+
/** Overrides the generated id, for a control the caller has to address by name. */
|
|
27
|
+
id?: string;
|
|
28
|
+
label?: string;
|
|
29
|
+
/** Screen-reader-only label, for a control whose visible name is a table column header. */
|
|
30
|
+
labelHidden?: boolean;
|
|
31
|
+
hint?: string;
|
|
32
|
+
/** Replaces the hint while it is set, and marks the wiring invalid. */
|
|
33
|
+
error?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Draws the marker beside the label. Set `required` on the control as well:
|
|
36
|
+
* the marker is decorative and the control's own attribute is what a screen
|
|
37
|
+
* reader reports.
|
|
38
|
+
*/
|
|
39
|
+
required?: boolean;
|
|
40
|
+
class?: string;
|
|
41
|
+
/** Receives what the control must carry so the message row actually reaches a screen reader. */
|
|
42
|
+
children: Snippet<[FieldWiring]>;
|
|
43
|
+
}
|
|
44
|
+
declare const Field: import("svelte").Component<Props, {}, "">;
|
|
45
|
+
type Field = ReturnType<typeof Field>;
|
|
46
|
+
export default Field;
|
|
@@ -51,7 +51,10 @@
|
|
|
51
51
|
<label for={id} class={FIELD_LABEL}>{label}</label>
|
|
52
52
|
{/if}
|
|
53
53
|
|
|
54
|
-
<!--
|
|
54
|
+
<!-- The dashed border is the only thing marking this region as a control, so
|
|
55
|
+
it rests on line-strong: line reads 1.25:1 and failed SC 1.4.11. The
|
|
56
|
+
hover is full-strength brand, because a focus-adjacent colour dropped to
|
|
57
|
+
half alpha reads as a weaker affordance for no reason a user can infer. -->
|
|
55
58
|
<label
|
|
56
59
|
class="flex w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-xl
|
|
57
60
|
border-2 border-dashed px-4 py-8 text-center transition-colors duration-150
|
|
@@ -59,7 +62,7 @@
|
|
|
59
62
|
? 'border-danger bg-danger/5 hover:bg-danger/8'
|
|
60
63
|
: dragOver
|
|
61
64
|
? 'border-brand bg-brand/8'
|
|
62
|
-
: 'border-line bg-surface-2/40 hover:border-brand
|
|
65
|
+
: 'border-line-strong bg-surface-2/40 hover:border-brand hover:bg-brand/5'}
|
|
63
66
|
{disabled ? 'cursor-not-allowed opacity-50' : ''}"
|
|
64
67
|
ondragover={(e) => {
|
|
65
68
|
e.preventDefault();
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { TONE_GLYPH } from '../internal/tone.js';
|
|
4
|
+
|
|
5
|
+
/** The two outcomes a submit has. Anything else is a hint, not a result. */
|
|
6
|
+
type Tone = 'danger' | 'success';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
tone?: Tone;
|
|
10
|
+
class?: string;
|
|
11
|
+
children: Snippet;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { tone = 'danger', class: klass = '', children }: Props = $props();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The message colour, stated once.
|
|
18
|
+
*
|
|
19
|
+
* Eleven pages rendered a submit outcome into a bare paragraph and picked
|
|
20
|
+
* their own size and colour for it, so the same failure read as three
|
|
21
|
+
* different things depending on which settings page the user was on.
|
|
22
|
+
*/
|
|
23
|
+
const tones: Record<Tone, string> = {
|
|
24
|
+
danger: 'text-danger',
|
|
25
|
+
success: 'text-success',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A live region reports a CHANGE to what it holds. A region that arrives in
|
|
30
|
+
* the document with its text already inside it is not a change, and nothing
|
|
31
|
+
* is read out: that is why every toast was silent while each toast carried
|
|
32
|
+
* its own role="status". Toaster fixed it by keeping one region mounted for
|
|
33
|
+
* the life of the app and moving each toast into it.
|
|
34
|
+
*
|
|
35
|
+
* A submit outcome has no such permanent host, because the page mounts this
|
|
36
|
+
* component at the moment the submit resolves. So the region goes in empty
|
|
37
|
+
* and the message is placed inside it on the next frame, which is the same
|
|
38
|
+
* shape of change a toast makes.
|
|
39
|
+
*
|
|
40
|
+
* The server runs no effect and has no frames, so it renders the message
|
|
41
|
+
* straight away: a page delivered without its client bundle still carries
|
|
42
|
+
* the outcome of the submit, which matters more than announcing it.
|
|
43
|
+
*/
|
|
44
|
+
let filled = $state(typeof window === 'undefined');
|
|
45
|
+
|
|
46
|
+
$effect(() => {
|
|
47
|
+
const frame = requestAnimationFrame(() => (filled = true));
|
|
48
|
+
return () => cancelAnimationFrame(frame);
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<!--
|
|
53
|
+
The tones differ in more than colour. A failure takes role="alert", which
|
|
54
|
+
interrupts whatever the reader is doing, because the submit did not go
|
|
55
|
+
through and the user has to act on it. A confirmation takes aria-live
|
|
56
|
+
"polite" and waits for a pause, because the work is done and nothing is being
|
|
57
|
+
asked of anyone.
|
|
58
|
+
-->
|
|
59
|
+
<div
|
|
60
|
+
class="flex items-start gap-2 text-sm {tones[tone]} {klass}"
|
|
61
|
+
role={tone === 'danger' ? 'alert' : 'status'}
|
|
62
|
+
aria-live={tone === 'danger' ? 'assertive' : 'polite'}
|
|
63
|
+
>
|
|
64
|
+
{#if filled}
|
|
65
|
+
<!-- Stroked SVG, not a literal cross or tick: a font glyph lands at
|
|
66
|
+
whatever weight the reader's font gives it, which sat visibly lighter
|
|
67
|
+
than every other icon in the library. Colour alone would also be the
|
|
68
|
+
only thing telling a failure from a confirmation. -->
|
|
69
|
+
<svg
|
|
70
|
+
class="mt-0.5 shrink-0"
|
|
71
|
+
width="15"
|
|
72
|
+
height="15"
|
|
73
|
+
viewBox="0 0 24 24"
|
|
74
|
+
fill="none"
|
|
75
|
+
stroke="currentColor"
|
|
76
|
+
stroke-width="2.5"
|
|
77
|
+
stroke-linecap="round"
|
|
78
|
+
stroke-linejoin="round"
|
|
79
|
+
aria-hidden="true"
|
|
80
|
+
>
|
|
81
|
+
<path d={TONE_GLYPH[tone]} />
|
|
82
|
+
</svg>
|
|
83
|
+
<p class="min-w-0 flex-1">{@render children()}</p>
|
|
84
|
+
{/if}
|
|
85
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
/** The two outcomes a submit has. Anything else is a hint, not a result. */
|
|
3
|
+
type Tone = 'danger' | 'success';
|
|
4
|
+
interface Props {
|
|
5
|
+
tone?: Tone;
|
|
6
|
+
class?: string;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
}
|
|
9
|
+
declare const FormMessage: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type FormMessage = ReturnType<typeof FormMessage>;
|
|
11
|
+
export default FormMessage;
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
<div class="{FIELD_WRAP} {klass}">
|
|
55
55
|
{#if label}
|
|
56
56
|
<label for={fieldId} class={FIELD_LABEL}>
|
|
57
|
-
{label}{#if required}<span class="text-danger ml-0.5" aria-
|
|
57
|
+
{label}{#if required}<span class="text-danger ml-0.5" aria-hidden="true">*</span>{/if}
|
|
58
58
|
</label>
|
|
59
59
|
{/if}
|
|
60
60
|
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
children,
|
|
10
10
|
}: {
|
|
11
11
|
for?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Draws the marker beside the text. Set `required` on the control this
|
|
14
|
+
* label points at as well: the marker is decorative, and the control's own
|
|
15
|
+
* attribute is what a screen reader reports. A label is read in full to
|
|
16
|
+
* name what it points at, so anything said here lands in that name.
|
|
17
|
+
*/
|
|
12
18
|
required?: boolean;
|
|
13
19
|
hint?: string;
|
|
14
20
|
class?: string;
|
|
@@ -22,7 +28,7 @@
|
|
|
22
28
|
>
|
|
23
29
|
{@render children()}
|
|
24
30
|
{#if required}
|
|
25
|
-
<span class="text-danger font-bold leading-none" aria-
|
|
31
|
+
<span class="text-danger font-bold leading-none" aria-hidden="true">*</span>
|
|
26
32
|
{/if}
|
|
27
33
|
{#if hint}
|
|
28
34
|
<span class="text-faint normal-case font-normal ml-0.5">({hint})</span>
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
for?: string;
|
|
4
|
+
/**
|
|
5
|
+
* Draws the marker beside the text. Set `required` on the control this
|
|
6
|
+
* label points at as well: the marker is decorative, and the control's own
|
|
7
|
+
* attribute is what a screen reader reports. A label is read in full to
|
|
8
|
+
* name what it points at, so anything said here lands in that name.
|
|
9
|
+
*/
|
|
4
10
|
required?: boolean;
|
|
5
11
|
hint?: string;
|
|
6
12
|
class?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { overlay } from '../internal/overlay.js';
|
|
3
4
|
|
|
4
5
|
type Size = 'sm' | 'md' | 'lg';
|
|
5
6
|
|
|
@@ -29,6 +30,11 @@
|
|
|
29
30
|
lg: 'max-w-2xl',
|
|
30
31
|
};
|
|
31
32
|
|
|
33
|
+
// aria-labelledby needs an id that is unique per instance, because two modals
|
|
34
|
+
// can be mounted at once while one animates out.
|
|
35
|
+
const headingId = $props.id();
|
|
36
|
+
const descriptionId = `${headingId}-description`;
|
|
37
|
+
|
|
32
38
|
function close() {
|
|
33
39
|
open = false;
|
|
34
40
|
onclose?.();
|
|
@@ -48,27 +54,38 @@
|
|
|
48
54
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
49
55
|
<button
|
|
50
56
|
type="button"
|
|
57
|
+
tabindex="-1"
|
|
58
|
+
aria-hidden="true"
|
|
51
59
|
class="absolute inset-0 bg-black/60 backdrop-blur-sm cursor-default"
|
|
52
|
-
aria-label="Close"
|
|
53
60
|
onclick={close}
|
|
54
61
|
></button>
|
|
55
62
|
|
|
56
63
|
<div
|
|
57
|
-
|
|
64
|
+
use:overlay
|
|
65
|
+
class="relative flex max-h-[calc(100dvh-2rem)] w-full {widths[size]} flex-col
|
|
66
|
+
overflow-hidden rounded-xl border border-line bg-surface shadow-2xl
|
|
58
67
|
animate-[modal-in_120ms_ease-out]"
|
|
59
68
|
role="dialog"
|
|
60
69
|
aria-modal="true"
|
|
70
|
+
aria-labelledby={title ? headingId : undefined}
|
|
71
|
+
aria-describedby={description ? descriptionId : undefined}
|
|
72
|
+
aria-label={title ? undefined : 'Dialog'}
|
|
61
73
|
>
|
|
62
74
|
{#if title}
|
|
63
|
-
<div
|
|
75
|
+
<div
|
|
76
|
+
class="flex shrink-0 items-start justify-between gap-4 px-5 py-4 border-b border-line"
|
|
77
|
+
>
|
|
64
78
|
<div>
|
|
65
|
-
<h2 class="font-semibold text-fg">{title}</h2>
|
|
66
|
-
{#if description}
|
|
79
|
+
<h2 id={headingId} class="font-semibold text-fg">{title}</h2>
|
|
80
|
+
{#if description}
|
|
81
|
+
<p id={descriptionId} class="text-sm text-muted mt-0.5">{description}</p>
|
|
82
|
+
{/if}
|
|
67
83
|
</div>
|
|
68
84
|
<button
|
|
69
85
|
type="button"
|
|
70
86
|
onclick={close}
|
|
71
|
-
class="shrink-0 text-faint transition-colors duration-150 hover:text-fg
|
|
87
|
+
class="shrink-0 rounded text-faint transition-colors duration-150 hover:text-fg
|
|
88
|
+
outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand"
|
|
72
89
|
aria-label="Close"
|
|
73
90
|
>
|
|
74
91
|
<svg
|
|
@@ -85,13 +102,13 @@
|
|
|
85
102
|
</div>
|
|
86
103
|
{/if}
|
|
87
104
|
|
|
88
|
-
<div class="px-5 py-4">
|
|
105
|
+
<div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
|
89
106
|
{@render children()}
|
|
90
107
|
</div>
|
|
91
108
|
|
|
92
109
|
{#if footer}
|
|
93
110
|
<div
|
|
94
|
-
class="flex items-center justify-end gap-2 px-5 py-3 border-t border-line bg-surface-2/40"
|
|
111
|
+
class="flex shrink-0 items-center justify-end gap-2 px-5 py-3 border-t border-line bg-surface-2/40"
|
|
95
112
|
>
|
|
96
113
|
{@render footer()}
|
|
97
114
|
</div>
|