@happyvertical/smrt-ui 0.34.5 → 0.34.6
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/AGENTS.md +15 -5
- package/dist/components/forms/Form.svelte +51 -0
- package/dist/components/forms/Form.svelte.d.ts +29 -0
- package/dist/components/forms/Form.svelte.d.ts.map +1 -0
- package/dist/components/forms/FormGroup.svelte +86 -0
- package/dist/components/forms/FormGroup.svelte.d.ts +13 -0
- package/dist/components/forms/FormGroup.svelte.d.ts.map +1 -0
- package/dist/components/forms/Input.svelte +83 -0
- package/dist/components/forms/Input.svelte.d.ts +9 -0
- package/dist/components/forms/Input.svelte.d.ts.map +1 -0
- package/dist/components/forms/Select.svelte +83 -0
- package/dist/components/forms/Select.svelte.d.ts +11 -0
- package/dist/components/forms/Select.svelte.d.ts.map +1 -0
- package/dist/components/forms/Textarea.svelte +85 -0
- package/dist/components/forms/Textarea.svelte.d.ts +10 -0
- package/dist/components/forms/Textarea.svelte.d.ts.map +1 -0
- package/dist/components/forms/Toggle.svelte +217 -0
- package/dist/components/forms/Toggle.svelte.d.ts +37 -0
- package/dist/components/forms/Toggle.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/Form.test.js +49 -0
- package/dist/components/forms/__tests__/FormGroup.test.js +48 -0
- package/dist/components/forms/__tests__/Input.test.js +49 -0
- package/dist/components/forms/__tests__/Select.test.js +37 -0
- package/dist/components/forms/__tests__/Textarea.test.js +39 -0
- package/dist/components/forms/__tests__/Toggle.test.js +87 -0
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte +16 -0
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/form-group-context.d.ts +13 -0
- package/dist/components/forms/form-group-context.d.ts.map +1 -0
- package/dist/components/forms/form-group-context.js +28 -0
- package/dist/components/forms/index.d.ts +21 -0
- package/dist/components/forms/index.d.ts.map +1 -0
- package/dist/components/forms/index.js +20 -0
- package/dist/components/ui/Button.svelte +16 -0
- package/package.json +8 -2
package/AGENTS.md
CHANGED
|
@@ -14,11 +14,20 @@ SMRT has one shared set of UI primitives, split across two packages by concern:
|
|
|
14
14
|
|
|
15
15
|
- **`smrt-ui` (here) owns the domain-agnostic VISUAL primitives** — `Button`,
|
|
16
16
|
`Card`, `Modal`/`ConfirmDialog`, `Badge`, `Avatar`, `Chip`, `Dropdown`,
|
|
17
|
-
`Tooltip`, `Skeleton`, `Tree`, `Pagination`, `DataTable`, …
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
`Tooltip`, `Skeleton`, `Tree`, `Pagination`, `DataTable`, … — **plus the
|
|
18
|
+
Provider-free base FORM primitives** under `./forms` (`Form`, `Input`,
|
|
19
|
+
`Select`, `Textarea`, `Toggle`, `FormGroup`), relocated here in #1589's
|
|
20
|
+
deferred-forms phase so domain packages can adopt them without pulling in the
|
|
21
|
+
smrt-svelte Provider or closing a build-graph cycle. These are dependency-free:
|
|
22
|
+
no Provider, no i18n, no spoken-input logic.
|
|
23
|
+
- **`smrt-svelte` owns the Provider-REQUIRED form primitives** — `CheckboxInput`,
|
|
24
|
+
the rich `Form` (field registration + voice), `TextInput`, `MoneyInput`, and
|
|
25
|
+
the specialized date/measurement/address/file inputs (they call `useAppState`
|
|
26
|
+
/ the AI hooks and carry i18n + spoken-input logic). It re-exports the base
|
|
27
|
+
**input** primitives (`Input`, `Select`, `Textarea`, `Toggle`, `FormGroup`)
|
|
28
|
+
from here so `@happyvertical/smrt-svelte/forms` stays the full barrel — but
|
|
29
|
+
**not** `Form`: that barrel's `Form` is the rich Provider-backed one, so the
|
|
30
|
+
Provider-free `Form` is only importable from `@happyvertical/smrt-ui/forms`.
|
|
22
31
|
|
|
23
32
|
**Domain packages import visual primitives from `smrt-ui` and form primitives
|
|
24
33
|
from `smrt-svelte`, and must not hand-roll raw `<button>` / `<input>` /
|
|
@@ -38,6 +47,7 @@ components are exempt — they *are* the primitives.
|
|
|
38
47
|
| `./layout` | `Container`, `Grid`, `Header`, `Footer`, `PageHeader`, `EmptyState`, … |
|
|
39
48
|
| `./calendar` | `Calendar`, `DayView` |
|
|
40
49
|
| `./chat` | `MessageBubble`, `ReactionPicker`, `TypingIndicator` |
|
|
50
|
+
| `./forms` | Provider-free base form primitives: `Form`, `Input`, `Select`, `Textarea`, `Toggle`, `FormGroup` (+ the FormGroup a11y context helpers) |
|
|
41
51
|
| `./i18n` | i18n **client**: `useI18n`, `<Trans>`, `defineMessages`, `renderTemplate` (no `smrt-languages` import — the server resolver stays in `smrt-svelte/i18n/server`) |
|
|
42
52
|
| `./registry` | `ModuleUIRegistry` for cross-package component discovery |
|
|
43
53
|
| `./theme` | simple `ThemeProvider` + context (`useTheme` consumes this from `smrt-svelte`) |
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Form — the Provider-free base `<form>` primitive.
|
|
4
|
+
*
|
|
5
|
+
* A thin, dependency-free wrapper around the native `<form>` element so domain
|
|
6
|
+
* components have a primitive to adopt instead of hand-rolling raw `<form>`
|
|
7
|
+
* markup (issue #1589). It forwards every native form attribute (including
|
|
8
|
+
* `onsubmit`) and renders its children — no Provider, no i18n, no spoken-input
|
|
9
|
+
* logic.
|
|
10
|
+
*
|
|
11
|
+
* For the rich, Provider-backed form with field registration and voice input,
|
|
12
|
+
* use `Form` from `@happyvertical/smrt-svelte/forms` instead. This one is the
|
|
13
|
+
* leaf-level building block; that one composes app state on top.
|
|
14
|
+
*
|
|
15
|
+
* `preventDefault` (default `true`) calls `event.preventDefault()` before
|
|
16
|
+
* invoking the consumer's `onsubmit`, so a click/Enter submit runs the handler
|
|
17
|
+
* without a full-page navigation — the near-universal SPA pattern. Pass
|
|
18
|
+
* `preventDefault={false}` to keep native submission (e.g. a GET/POST `action`).
|
|
19
|
+
*/
|
|
20
|
+
import type { Snippet } from 'svelte';
|
|
21
|
+
import type { HTMLFormAttributes } from 'svelte/elements';
|
|
22
|
+
|
|
23
|
+
export interface Props extends Omit<HTMLFormAttributes, 'class'> {
|
|
24
|
+
class?: string;
|
|
25
|
+
preventDefault?: boolean;
|
|
26
|
+
children: Snippet;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
class: className = '',
|
|
31
|
+
preventDefault = true,
|
|
32
|
+
onsubmit,
|
|
33
|
+
children,
|
|
34
|
+
...rest
|
|
35
|
+
}: Props = $props();
|
|
36
|
+
|
|
37
|
+
function handleSubmit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
|
|
38
|
+
if (preventDefault) event.preventDefault();
|
|
39
|
+
onsubmit?.(event);
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<form class="form {className}" onsubmit={handleSubmit} {...rest}>
|
|
44
|
+
{@render children()}
|
|
45
|
+
</form>
|
|
46
|
+
|
|
47
|
+
<style>
|
|
48
|
+
.form {
|
|
49
|
+
display: block;
|
|
50
|
+
}
|
|
51
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form — the Provider-free base `<form>` primitive.
|
|
3
|
+
*
|
|
4
|
+
* A thin, dependency-free wrapper around the native `<form>` element so domain
|
|
5
|
+
* components have a primitive to adopt instead of hand-rolling raw `<form>`
|
|
6
|
+
* markup (issue #1589). It forwards every native form attribute (including
|
|
7
|
+
* `onsubmit`) and renders its children — no Provider, no i18n, no spoken-input
|
|
8
|
+
* logic.
|
|
9
|
+
*
|
|
10
|
+
* For the rich, Provider-backed form with field registration and voice input,
|
|
11
|
+
* use `Form` from `@happyvertical/smrt-svelte/forms` instead. This one is the
|
|
12
|
+
* leaf-level building block; that one composes app state on top.
|
|
13
|
+
*
|
|
14
|
+
* `preventDefault` (default `true`) calls `event.preventDefault()` before
|
|
15
|
+
* invoking the consumer's `onsubmit`, so a click/Enter submit runs the handler
|
|
16
|
+
* without a full-page navigation — the near-universal SPA pattern. Pass
|
|
17
|
+
* `preventDefault={false}` to keep native submission (e.g. a GET/POST `action`).
|
|
18
|
+
*/
|
|
19
|
+
import type { Snippet } from 'svelte';
|
|
20
|
+
import type { HTMLFormAttributes } from 'svelte/elements';
|
|
21
|
+
export interface Props extends Omit<HTMLFormAttributes, 'class'> {
|
|
22
|
+
class?: string;
|
|
23
|
+
preventDefault?: boolean;
|
|
24
|
+
children: Snippet;
|
|
25
|
+
}
|
|
26
|
+
declare const Form: import("svelte").Component<Props, {}, "">;
|
|
27
|
+
type Form = ReturnType<typeof Form>;
|
|
28
|
+
export default Form;
|
|
29
|
+
//# sourceMappingURL=Form.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG1D,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,kBAAkB,EAAE,OAAO,CAAC;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;CACnB;AA6BD,QAAA,MAAM,IAAI,2CAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
type FormGroupContextValue,
|
|
5
|
+
nextFieldId,
|
|
6
|
+
setFormGroupContext,
|
|
7
|
+
} from './form-group-context.js';
|
|
8
|
+
|
|
9
|
+
export interface Props {
|
|
10
|
+
label: string;
|
|
11
|
+
id?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
hint?: string;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
children: Snippet;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { label, id, error, hint, required = false, children }: Props = $props();
|
|
19
|
+
|
|
20
|
+
// Stable id so the label's `for` and the wrapped input's `id` agree even when
|
|
21
|
+
// the consumer doesn't pass one.
|
|
22
|
+
const fallbackId = nextFieldId();
|
|
23
|
+
const fieldId = $derived(id ?? fallbackId);
|
|
24
|
+
const hintId = $derived(hint && !error ? `${fieldId}-hint` : undefined);
|
|
25
|
+
const errorId = $derived(error ? `${fieldId}-error` : undefined);
|
|
26
|
+
const describedBy = $derived(
|
|
27
|
+
[hintId, errorId].filter(Boolean).join(' ') || undefined,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Publish the wiring a base input auto-applies (getter stays reactive as
|
|
31
|
+
// hint/error change).
|
|
32
|
+
setFormGroupContext(
|
|
33
|
+
(): FormGroupContextValue => ({
|
|
34
|
+
inputId: fieldId,
|
|
35
|
+
describedBy,
|
|
36
|
+
invalid: !!error,
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<div class="form-group">
|
|
42
|
+
<label for={fieldId} class="form-label">
|
|
43
|
+
{label}
|
|
44
|
+
{#if required}
|
|
45
|
+
<span class="required" aria-hidden="true">*</span>
|
|
46
|
+
{/if}
|
|
47
|
+
</label>
|
|
48
|
+
{@render children()}
|
|
49
|
+
{#if hintId}
|
|
50
|
+
<p id={hintId} class="form-hint">{hint}</p>
|
|
51
|
+
{/if}
|
|
52
|
+
{#if errorId}
|
|
53
|
+
<p id={errorId} class="form-error" role="alert">{error}</p>
|
|
54
|
+
{/if}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<style>
|
|
58
|
+
.form-group {
|
|
59
|
+
margin-bottom: 1rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.form-label {
|
|
63
|
+
display: block;
|
|
64
|
+
font-size: var(--smrt-typography-label-large-size, 0.875rem);
|
|
65
|
+
font-weight: var(--smrt-typography-weight-medium, 500);
|
|
66
|
+
color: var(--smrt-color-on-surface, #374151);
|
|
67
|
+
margin-bottom: 0.375rem;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.required {
|
|
71
|
+
color: var(--smrt-color-error, #ba1a1a);
|
|
72
|
+
margin-left: 0.125rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.form-hint {
|
|
76
|
+
margin: 0.25rem 0 0;
|
|
77
|
+
font-size: var(--smrt-typography-body-small-size, 0.75rem);
|
|
78
|
+
color: var(--smrt-color-on-surface-variant, #6b7280);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.form-error {
|
|
82
|
+
margin: 0.25rem 0 0;
|
|
83
|
+
font-size: var(--smrt-typography-body-small-size, 0.75rem);
|
|
84
|
+
color: var(--smrt-color-error, #ba1a1a);
|
|
85
|
+
}
|
|
86
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export interface Props {
|
|
3
|
+
label: string;
|
|
4
|
+
id?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
hint?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
children: Snippet;
|
|
9
|
+
}
|
|
10
|
+
declare const FormGroup: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type FormGroup = ReturnType<typeof FormGroup>;
|
|
12
|
+
export default FormGroup;
|
|
13
|
+
//# sourceMappingURL=FormGroup.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FormGroup.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/FormGroup.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAQtC,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAiDD,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import { tryGetFormGroupContext } from './form-group-context.js';
|
|
4
|
+
|
|
5
|
+
export interface Props extends Omit<HTMLInputAttributes, 'class' | 'value'> {
|
|
6
|
+
value?: string | number;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
id,
|
|
12
|
+
type = 'text',
|
|
13
|
+
value = $bindable(''),
|
|
14
|
+
placeholder,
|
|
15
|
+
disabled = false,
|
|
16
|
+
readonly = false,
|
|
17
|
+
required = false,
|
|
18
|
+
name,
|
|
19
|
+
class: className = '',
|
|
20
|
+
'aria-describedby': ariaDescribedby,
|
|
21
|
+
'aria-invalid': ariaInvalid,
|
|
22
|
+
...rest
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
// When wrapped in a <FormGroup>, inherit the id (so its <label for> resolves),
|
|
26
|
+
// the hint/error association, and the error state — unless set explicitly.
|
|
27
|
+
const formGroup = tryGetFormGroupContext();
|
|
28
|
+
const resolvedId = $derived(id ?? formGroup?.().inputId);
|
|
29
|
+
const resolvedDescribedBy = $derived(
|
|
30
|
+
ariaDescribedby ?? formGroup?.().describedBy,
|
|
31
|
+
);
|
|
32
|
+
const resolvedInvalid = $derived(
|
|
33
|
+
ariaInvalid ?? (formGroup?.().invalid ? 'true' : undefined),
|
|
34
|
+
);
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<input
|
|
38
|
+
id={resolvedId}
|
|
39
|
+
{type}
|
|
40
|
+
bind:value
|
|
41
|
+
{placeholder}
|
|
42
|
+
{disabled}
|
|
43
|
+
{readonly}
|
|
44
|
+
{required}
|
|
45
|
+
{name}
|
|
46
|
+
aria-describedby={resolvedDescribedBy}
|
|
47
|
+
aria-invalid={resolvedInvalid}
|
|
48
|
+
class="input {className}"
|
|
49
|
+
{...rest}
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<style>
|
|
53
|
+
.input {
|
|
54
|
+
display: block;
|
|
55
|
+
width: 100%;
|
|
56
|
+
padding: 0.5rem 0.75rem;
|
|
57
|
+
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
58
|
+
line-height: var(--smrt-typography-body-medium-line-height, 1.5);
|
|
59
|
+
color: var(--smrt-color-on-surface, #1f2937);
|
|
60
|
+
background-color: var(--smrt-color-surface, #fff);
|
|
61
|
+
border: 1px solid var(--smrt-color-outline-variant, #d1d5db);
|
|
62
|
+
border-radius: var(--smrt-radius-small, 0.375rem);
|
|
63
|
+
transition:
|
|
64
|
+
border-color var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease-in-out),
|
|
65
|
+
box-shadow var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease-in-out);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.input:focus {
|
|
69
|
+
outline: none;
|
|
70
|
+
border-color: var(--smrt-color-primary, #005ac1);
|
|
71
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--smrt-color-primary, #005ac1) 10%, transparent);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.input:disabled {
|
|
75
|
+
background-color: var(--smrt-color-surface-container-high, #f3f4f6);
|
|
76
|
+
cursor: not-allowed;
|
|
77
|
+
opacity: 0.7;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.input::placeholder {
|
|
81
|
+
color: var(--smrt-color-on-surface-variant, #9ca3af);
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
export interface Props extends Omit<HTMLInputAttributes, 'class' | 'value'> {
|
|
3
|
+
value?: string | number;
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
declare const Input: import("svelte").Component<Props, {}, "value">;
|
|
7
|
+
type Input = ReturnType<typeof Input>;
|
|
8
|
+
export default Input;
|
|
9
|
+
//# sourceMappingURL=Input.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Input.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Input.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAI3D,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,mBAAmB,EAAE,OAAO,GAAG,OAAO,CAAC;IACzE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuCD,QAAA,MAAM,KAAK,gDAAwC,CAAC;AACpD,KAAK,KAAK,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AACtC,eAAe,KAAK,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLSelectAttributes } from 'svelte/elements';
|
|
4
|
+
import { tryGetFormGroupContext } from './form-group-context.js';
|
|
5
|
+
|
|
6
|
+
export interface Props extends Omit<HTMLSelectAttributes, 'class' | 'value'> {
|
|
7
|
+
value?: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
children: Snippet;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
id,
|
|
14
|
+
value = $bindable(''),
|
|
15
|
+
disabled = false,
|
|
16
|
+
required = false,
|
|
17
|
+
name,
|
|
18
|
+
class: className = '',
|
|
19
|
+
children,
|
|
20
|
+
'aria-describedby': ariaDescribedby,
|
|
21
|
+
'aria-invalid': ariaInvalid,
|
|
22
|
+
...rest
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
// Inherit id / hint+error association / error state from a wrapping FormGroup.
|
|
26
|
+
const formGroup = tryGetFormGroupContext();
|
|
27
|
+
const resolvedId = $derived(id ?? formGroup?.().inputId);
|
|
28
|
+
const resolvedDescribedBy = $derived(
|
|
29
|
+
ariaDescribedby ?? formGroup?.().describedBy,
|
|
30
|
+
);
|
|
31
|
+
const resolvedInvalid = $derived(
|
|
32
|
+
ariaInvalid ?? (formGroup?.().invalid ? 'true' : undefined),
|
|
33
|
+
);
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<select
|
|
37
|
+
id={resolvedId}
|
|
38
|
+
bind:value
|
|
39
|
+
{disabled}
|
|
40
|
+
{required}
|
|
41
|
+
{name}
|
|
42
|
+
aria-describedby={resolvedDescribedBy}
|
|
43
|
+
aria-invalid={resolvedInvalid}
|
|
44
|
+
class="select {className}"
|
|
45
|
+
{...rest}
|
|
46
|
+
>
|
|
47
|
+
{@render children()}
|
|
48
|
+
</select>
|
|
49
|
+
|
|
50
|
+
<style>
|
|
51
|
+
.select {
|
|
52
|
+
display: block;
|
|
53
|
+
width: 100%;
|
|
54
|
+
padding: 0.5rem 2rem 0.5rem 0.75rem;
|
|
55
|
+
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
56
|
+
line-height: var(--smrt-typography-body-medium-line-height, 1.5);
|
|
57
|
+
color: var(--smrt-color-on-surface, #1f2937);
|
|
58
|
+
background-color: var(--smrt-color-surface, #fff);
|
|
59
|
+
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2379747e' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
|
60
|
+
background-position: right 0.5rem center;
|
|
61
|
+
background-repeat: no-repeat;
|
|
62
|
+
background-size: 1.5em 1.5em;
|
|
63
|
+
border: 1px solid var(--smrt-color-outline-variant, #d1d5db);
|
|
64
|
+
border-radius: 0.375rem;
|
|
65
|
+
appearance: none;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
transition:
|
|
68
|
+
border-color 0.15s ease-in-out,
|
|
69
|
+
box-shadow 0.15s ease-in-out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.select:focus {
|
|
73
|
+
outline: none;
|
|
74
|
+
border-color: var(--smrt-color-primary, #005ac1);
|
|
75
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--smrt-color-primary, #005ac1) 10%, transparent);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.select:disabled {
|
|
79
|
+
background-color: var(--smrt-color-surface-container-high, #f3f4f6);
|
|
80
|
+
cursor: not-allowed;
|
|
81
|
+
opacity: 0.7;
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLSelectAttributes } from 'svelte/elements';
|
|
3
|
+
export interface Props extends Omit<HTMLSelectAttributes, 'class' | 'value'> {
|
|
4
|
+
value?: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
children: Snippet;
|
|
7
|
+
}
|
|
8
|
+
declare const Select: import("svelte").Component<Props, {}, "value">;
|
|
9
|
+
type Select = ReturnType<typeof Select>;
|
|
10
|
+
export default Select;
|
|
11
|
+
//# sourceMappingURL=Select.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Select.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Select.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAI5D,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,oBAAoB,EAAE,OAAO,GAAG,OAAO,CAAC;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CACnB;AAuCD,QAAA,MAAM,MAAM,gDAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
3
|
+
import { tryGetFormGroupContext } from './form-group-context.js';
|
|
4
|
+
|
|
5
|
+
export interface Props extends Omit<HTMLTextareaAttributes, 'class' | 'value'> {
|
|
6
|
+
value?: string;
|
|
7
|
+
rows?: number;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
id,
|
|
13
|
+
value = $bindable(''),
|
|
14
|
+
placeholder,
|
|
15
|
+
disabled = false,
|
|
16
|
+
readonly = false,
|
|
17
|
+
required = false,
|
|
18
|
+
name,
|
|
19
|
+
rows = 4,
|
|
20
|
+
class: className = '',
|
|
21
|
+
'aria-describedby': ariaDescribedby,
|
|
22
|
+
'aria-invalid': ariaInvalid,
|
|
23
|
+
...rest
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
// Inherit id / hint+error association / error state from a wrapping FormGroup.
|
|
27
|
+
const formGroup = tryGetFormGroupContext();
|
|
28
|
+
const resolvedId = $derived(id ?? formGroup?.().inputId);
|
|
29
|
+
const resolvedDescribedBy = $derived(
|
|
30
|
+
ariaDescribedby ?? formGroup?.().describedBy,
|
|
31
|
+
);
|
|
32
|
+
const resolvedInvalid = $derived(
|
|
33
|
+
ariaInvalid ?? (formGroup?.().invalid ? 'true' : undefined),
|
|
34
|
+
);
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<textarea
|
|
38
|
+
id={resolvedId}
|
|
39
|
+
bind:value
|
|
40
|
+
{placeholder}
|
|
41
|
+
{disabled}
|
|
42
|
+
{readonly}
|
|
43
|
+
{required}
|
|
44
|
+
{name}
|
|
45
|
+
{rows}
|
|
46
|
+
aria-describedby={resolvedDescribedBy}
|
|
47
|
+
aria-invalid={resolvedInvalid}
|
|
48
|
+
class="textarea {className}"
|
|
49
|
+
{...rest}
|
|
50
|
+
></textarea>
|
|
51
|
+
|
|
52
|
+
<style>
|
|
53
|
+
.textarea {
|
|
54
|
+
display: block;
|
|
55
|
+
width: 100%;
|
|
56
|
+
padding: 0.5rem 0.75rem;
|
|
57
|
+
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
58
|
+
line-height: var(--smrt-typography-body-medium-line-height, 1.5);
|
|
59
|
+
color: var(--smrt-color-on-surface, #1f2937);
|
|
60
|
+
background-color: var(--smrt-color-surface, #fff);
|
|
61
|
+
border: 1px solid var(--smrt-color-outline-variant, #d1d5db);
|
|
62
|
+
border-radius: 0.375rem;
|
|
63
|
+
resize: vertical;
|
|
64
|
+
min-height: 80px;
|
|
65
|
+
transition:
|
|
66
|
+
border-color 0.15s ease-in-out,
|
|
67
|
+
box-shadow 0.15s ease-in-out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.textarea:focus {
|
|
71
|
+
outline: none;
|
|
72
|
+
border-color: var(--smrt-color-primary, #005ac1);
|
|
73
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--smrt-color-primary, #005ac1) 10%, transparent);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.textarea:disabled {
|
|
77
|
+
background-color: var(--smrt-color-surface-container-high, #f3f4f6);
|
|
78
|
+
cursor: not-allowed;
|
|
79
|
+
opacity: 0.7;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.textarea::placeholder {
|
|
83
|
+
color: var(--smrt-color-on-surface-variant, #9ca3af);
|
|
84
|
+
}
|
|
85
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
2
|
+
export interface Props extends Omit<HTMLTextareaAttributes, 'class' | 'value'> {
|
|
3
|
+
value?: string;
|
|
4
|
+
rows?: number;
|
|
5
|
+
class?: string;
|
|
6
|
+
}
|
|
7
|
+
declare const Textarea: import("svelte").Component<Props, {}, "value">;
|
|
8
|
+
type Textarea = ReturnType<typeof Textarea>;
|
|
9
|
+
export default Textarea;
|
|
10
|
+
//# sourceMappingURL=Textarea.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Textarea.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Textarea.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAI9D,MAAM,WAAW,KAAM,SAAQ,IAAI,CAAC,sBAAsB,EAAE,OAAO,GAAG,OAAO,CAAC;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAsCD,QAAA,MAAM,QAAQ,gDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SMRTToggle - An accessible toggle/switch component
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Native checkbox semantics for accessibility
|
|
7
|
+
* - Bindable checked state
|
|
8
|
+
* - Disabled state
|
|
9
|
+
* - Labels on either side
|
|
10
|
+
* - Size variants
|
|
11
|
+
* - Material 3 styling
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface Props {
|
|
15
|
+
/** Whether the toggle is checked */
|
|
16
|
+
checked?: boolean;
|
|
17
|
+
/** Whether the toggle is disabled */
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/** Name attribute for form submission */
|
|
20
|
+
name?: string;
|
|
21
|
+
/** Value attribute for form submission */
|
|
22
|
+
value?: string;
|
|
23
|
+
/** Label text */
|
|
24
|
+
label?: string;
|
|
25
|
+
/** Position of the label */
|
|
26
|
+
labelPosition?: 'left' | 'right';
|
|
27
|
+
/** Size variant */
|
|
28
|
+
size?: 'sm' | 'md' | 'lg';
|
|
29
|
+
/** ID for the input element */
|
|
30
|
+
id?: string;
|
|
31
|
+
/** ARIA label for accessibility */
|
|
32
|
+
ariaLabel?: string;
|
|
33
|
+
/** Change callback */
|
|
34
|
+
onchange?: (checked: boolean) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
checked = $bindable(false),
|
|
39
|
+
disabled = false,
|
|
40
|
+
name,
|
|
41
|
+
value,
|
|
42
|
+
label,
|
|
43
|
+
labelPosition = 'right',
|
|
44
|
+
size = 'md',
|
|
45
|
+
id,
|
|
46
|
+
ariaLabel,
|
|
47
|
+
onchange,
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
|
|
50
|
+
function handleChange(event: Event) {
|
|
51
|
+
const target = event.target as HTMLInputElement;
|
|
52
|
+
checked = target.checked;
|
|
53
|
+
onchange?.(checked);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const sizeClasses = {
|
|
57
|
+
sm: 'toggle--sm',
|
|
58
|
+
md: 'toggle--md',
|
|
59
|
+
lg: 'toggle--lg',
|
|
60
|
+
};
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<label class="toggle {sizeClasses[size]}" class:toggle--disabled={disabled}>
|
|
64
|
+
{#if label && labelPosition === 'left'}
|
|
65
|
+
<span class="toggle__label toggle__label--left">{label}</span>
|
|
66
|
+
{/if}
|
|
67
|
+
|
|
68
|
+
<span class="toggle__track">
|
|
69
|
+
<input
|
|
70
|
+
type="checkbox"
|
|
71
|
+
class="toggle__input"
|
|
72
|
+
{id}
|
|
73
|
+
{name}
|
|
74
|
+
{value}
|
|
75
|
+
{disabled}
|
|
76
|
+
{checked}
|
|
77
|
+
onchange={handleChange}
|
|
78
|
+
aria-label={ariaLabel ?? label ?? ''}
|
|
79
|
+
/>
|
|
80
|
+
<span class="toggle__thumb"></span>
|
|
81
|
+
</span>
|
|
82
|
+
|
|
83
|
+
{#if label && labelPosition === 'right'}
|
|
84
|
+
<span class="toggle__label toggle__label--right">{label}</span>
|
|
85
|
+
{/if}
|
|
86
|
+
</label>
|
|
87
|
+
|
|
88
|
+
<style>
|
|
89
|
+
.toggle {
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: var(--smrt-spacing-2, 0.5rem);
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
user-select: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.toggle--disabled {
|
|
98
|
+
cursor: not-allowed;
|
|
99
|
+
opacity: 0.5;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.toggle__label {
|
|
103
|
+
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
104
|
+
color: var(--smrt-color-on-surface, #111827);
|
|
105
|
+
line-height: 1.5;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.toggle__track {
|
|
109
|
+
position: relative;
|
|
110
|
+
display: inline-flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
flex-shrink: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.toggle__input {
|
|
116
|
+
position: absolute;
|
|
117
|
+
width: 1px;
|
|
118
|
+
height: 1px;
|
|
119
|
+
padding: 0;
|
|
120
|
+
margin: -1px;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
clip: rect(0, 0, 0, 0);
|
|
123
|
+
white-space: nowrap;
|
|
124
|
+
border: 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.toggle__thumb {
|
|
128
|
+
position: relative;
|
|
129
|
+
background: var(--smrt-color-surface-container-highest, #e5e7eb);
|
|
130
|
+
border-radius: var(--smrt-radius-full, 9999px);
|
|
131
|
+
transition: background-color var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.toggle__thumb::after {
|
|
135
|
+
content: '';
|
|
136
|
+
position: absolute;
|
|
137
|
+
background: var(--smrt-color-surface, #ffffff);
|
|
138
|
+
border-radius: var(--smrt-radius-full, 9999px);
|
|
139
|
+
box-shadow: var(--smrt-elevation-1, 0 1px 3px color-mix(in srgb, var(--smrt-color-shadow) 20%, transparent));
|
|
140
|
+
transition: transform var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Checked state */
|
|
144
|
+
.toggle__input:checked + .toggle__thumb {
|
|
145
|
+
background: var(--smrt-color-primary, #005ac1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Focus state */
|
|
149
|
+
.toggle__input:focus-visible + .toggle__thumb {
|
|
150
|
+
outline: 2px solid var(--smrt-color-primary, #005ac1);
|
|
151
|
+
outline-offset: 2px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Hover state */
|
|
155
|
+
.toggle:not(.toggle--disabled):hover .toggle__thumb {
|
|
156
|
+
background: var(--smrt-color-surface-container-high, #d1d5db);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.toggle:not(.toggle--disabled):hover .toggle__input:checked + .toggle__thumb {
|
|
160
|
+
background: var(--smrt-color-primary, #2563eb);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Size: Small */
|
|
164
|
+
.toggle--sm .toggle__thumb {
|
|
165
|
+
width: 32px;
|
|
166
|
+
height: 18px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.toggle--sm .toggle__thumb::after {
|
|
170
|
+
width: 14px;
|
|
171
|
+
height: 14px;
|
|
172
|
+
top: 2px;
|
|
173
|
+
left: 2px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.toggle--sm .toggle__input:checked + .toggle__thumb::after {
|
|
177
|
+
transform: translateX(14px);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Size: Medium (default) */
|
|
181
|
+
.toggle--md .toggle__thumb {
|
|
182
|
+
width: 44px;
|
|
183
|
+
height: 24px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.toggle--md .toggle__thumb::after {
|
|
187
|
+
width: 20px;
|
|
188
|
+
height: 20px;
|
|
189
|
+
top: 2px;
|
|
190
|
+
left: 2px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.toggle--md .toggle__input:checked + .toggle__thumb::after {
|
|
194
|
+
transform: translateX(20px);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Size: Large */
|
|
198
|
+
.toggle--lg .toggle__thumb {
|
|
199
|
+
width: 56px;
|
|
200
|
+
height: 30px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.toggle--lg .toggle__thumb::after {
|
|
204
|
+
width: 26px;
|
|
205
|
+
height: 26px;
|
|
206
|
+
top: 2px;
|
|
207
|
+
left: 2px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.toggle--lg .toggle__input:checked + .toggle__thumb::after {
|
|
211
|
+
transform: translateX(26px);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.toggle--lg .toggle__label {
|
|
215
|
+
font-size: var(--smrt-typography-body-large-size, 1rem);
|
|
216
|
+
}
|
|
217
|
+
</style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMRTToggle - An accessible toggle/switch component
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Native checkbox semantics for accessibility
|
|
6
|
+
* - Bindable checked state
|
|
7
|
+
* - Disabled state
|
|
8
|
+
* - Labels on either side
|
|
9
|
+
* - Size variants
|
|
10
|
+
* - Material 3 styling
|
|
11
|
+
*/
|
|
12
|
+
export interface Props {
|
|
13
|
+
/** Whether the toggle is checked */
|
|
14
|
+
checked?: boolean;
|
|
15
|
+
/** Whether the toggle is disabled */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Name attribute for form submission */
|
|
18
|
+
name?: string;
|
|
19
|
+
/** Value attribute for form submission */
|
|
20
|
+
value?: string;
|
|
21
|
+
/** Label text */
|
|
22
|
+
label?: string;
|
|
23
|
+
/** Position of the label */
|
|
24
|
+
labelPosition?: 'left' | 'right';
|
|
25
|
+
/** Size variant */
|
|
26
|
+
size?: 'sm' | 'md' | 'lg';
|
|
27
|
+
/** ID for the input element */
|
|
28
|
+
id?: string;
|
|
29
|
+
/** ARIA label for accessibility */
|
|
30
|
+
ariaLabel?: string;
|
|
31
|
+
/** Change callback */
|
|
32
|
+
onchange?: (checked: boolean) => void;
|
|
33
|
+
}
|
|
34
|
+
declare const Toggle: import("svelte").Component<Props, {}, "checked">;
|
|
35
|
+
type Toggle = ReturnType<typeof Toggle>;
|
|
36
|
+
export default Toggle;
|
|
37
|
+
//# sourceMappingURL=Toggle.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Toggle.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Toggle.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,KAAK;IACpB,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iBAAiB;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACjC,mBAAmB;IACnB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,+BAA+B;IAC/B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sBAAsB;IACtB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC;AAiDD,QAAA,MAAM,MAAM,kDAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden test for the Provider-free Form primitive (#1589 deferred-forms phase).
|
|
3
|
+
*
|
|
4
|
+
* Form is the leaf `<form>` wrapper domain packages adopt instead of raw markup:
|
|
5
|
+
* it forwards native attributes, renders children, and (by default) prevents the
|
|
6
|
+
* native submit so the consumer's `onsubmit` runs without a page navigation.
|
|
7
|
+
*/
|
|
8
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
9
|
+
import { render, screen } from '@testing-library/svelte';
|
|
10
|
+
import userEvent from '@testing-library/user-event';
|
|
11
|
+
import { createRawSnippet } from 'svelte';
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import Form from '../Form.svelte';
|
|
14
|
+
const children = createRawSnippet(() => ({
|
|
15
|
+
render: () => '<button type="submit">Save</button>',
|
|
16
|
+
}));
|
|
17
|
+
describe('Form', () => {
|
|
18
|
+
it('renders a <form> with forwarded attributes and its children', () => {
|
|
19
|
+
const { container } = render(Form, {
|
|
20
|
+
props: { name: 'profile', class: 'profile-form', children },
|
|
21
|
+
});
|
|
22
|
+
const form = container.querySelector('form');
|
|
23
|
+
expect(form).not.toBeNull();
|
|
24
|
+
expect(form).toHaveAttribute('name', 'profile');
|
|
25
|
+
expect(form).toHaveClass('form', 'profile-form');
|
|
26
|
+
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
it('prevents the native submit and calls onsubmit by default', async () => {
|
|
29
|
+
const onsubmit = vi.fn();
|
|
30
|
+
render(Form, { props: { onsubmit, children } });
|
|
31
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
|
32
|
+
expect(onsubmit).toHaveBeenCalledTimes(1);
|
|
33
|
+
const event = onsubmit.mock.calls[0][0];
|
|
34
|
+
expect(event.defaultPrevented).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
it('leaves native submission intact when preventDefault is false', async () => {
|
|
37
|
+
const onsubmit = vi.fn();
|
|
38
|
+
render(Form, { props: { onsubmit, preventDefault: false, children } });
|
|
39
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
|
40
|
+
expect(onsubmit).toHaveBeenCalledTimes(1);
|
|
41
|
+
expect(onsubmit.mock.calls[0][0].defaultPrevented).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it('is axe-clean', async () => {
|
|
44
|
+
const { container } = render(Form, {
|
|
45
|
+
props: { 'aria-label': 'Profile form', children },
|
|
46
|
+
});
|
|
47
|
+
await expectNoA11yViolations(container);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden test for FormGroup accessibility wiring (Sweep L1, #1420).
|
|
3
|
+
*
|
|
4
|
+
* FormGroup must make a wrapped base input programmatically accessible with no
|
|
5
|
+
* extra wiring: a real <label> association, hint/error linked via
|
|
6
|
+
* aria-describedby, and aria-invalid in the error state.
|
|
7
|
+
*/
|
|
8
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
9
|
+
import { render, screen } from '@testing-library/svelte';
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import Fixture from './form-group-input.fixture.svelte';
|
|
12
|
+
describe('FormGroup a11y wiring', () => {
|
|
13
|
+
it('associates its label with the wrapped input', () => {
|
|
14
|
+
render(Fixture, {});
|
|
15
|
+
// getByLabelText only resolves if the label is programmatically associated.
|
|
16
|
+
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
it('links a hint via aria-describedby', () => {
|
|
19
|
+
render(Fixture, { props: { hint: 'Work address preferred' } });
|
|
20
|
+
const input = screen.getByLabelText('Email');
|
|
21
|
+
const describedBy = input.getAttribute('aria-describedby');
|
|
22
|
+
expect(describedBy).toBeTruthy();
|
|
23
|
+
const hint = document.getElementById(describedBy);
|
|
24
|
+
expect(hint).toHaveTextContent('Work address preferred');
|
|
25
|
+
});
|
|
26
|
+
it('sets aria-invalid and links the error in the error state', () => {
|
|
27
|
+
render(Fixture, { props: { error: 'Email is required' } });
|
|
28
|
+
const input = screen.getByLabelText('Email');
|
|
29
|
+
expect(input).toHaveAttribute('aria-invalid', 'true');
|
|
30
|
+
const describedBy = input.getAttribute('aria-describedby');
|
|
31
|
+
const error = document.getElementById(describedBy);
|
|
32
|
+
expect(error).toHaveTextContent('Email is required');
|
|
33
|
+
// error is announced
|
|
34
|
+
expect(error).toHaveAttribute('role', 'alert');
|
|
35
|
+
});
|
|
36
|
+
it('is axe-clean (labelled control, associated hint)', async () => {
|
|
37
|
+
const { container } = render(Fixture, {
|
|
38
|
+
props: { hint: 'Work address preferred' },
|
|
39
|
+
});
|
|
40
|
+
await expectNoA11yViolations(container);
|
|
41
|
+
});
|
|
42
|
+
it('is axe-clean in the error state', async () => {
|
|
43
|
+
const { container } = render(Fixture, {
|
|
44
|
+
props: { error: 'Email is required' },
|
|
45
|
+
});
|
|
46
|
+
await expectNoA11yViolations(container);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden test for Input (Sweep L4, #1423).
|
|
3
|
+
*
|
|
4
|
+
* Input is the low-level form primitive: a bare <input> whose accessible name
|
|
5
|
+
* comes from a wrapping label/FormGroup, not from itself. Programmatic-label +
|
|
6
|
+
* axe-clean coverage for the form primitives is L1's deliverable (#1420, "with
|
|
7
|
+
* L4"); here we cover render + interaction + state behavior.
|
|
8
|
+
*/
|
|
9
|
+
import { render, screen } from '@testing-library/svelte';
|
|
10
|
+
import userEvent from '@testing-library/user-event';
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
import Input from '../Input.svelte';
|
|
13
|
+
describe('Input', () => {
|
|
14
|
+
it('renders an <input> with the given id/name/type', () => {
|
|
15
|
+
render(Input, { props: { id: 'first', name: 'first', type: 'text' } });
|
|
16
|
+
const input = screen.getByRole('textbox');
|
|
17
|
+
expect(input).toHaveAttribute('id', 'first');
|
|
18
|
+
expect(input).toHaveAttribute('name', 'first');
|
|
19
|
+
expect(input).toHaveAttribute('type', 'text');
|
|
20
|
+
});
|
|
21
|
+
it('accepts typed input', async () => {
|
|
22
|
+
render(Input, { props: { id: 'q', name: 'q' } });
|
|
23
|
+
const input = screen.getByRole('textbox');
|
|
24
|
+
await userEvent.type(input, 'hello');
|
|
25
|
+
expect(input).toHaveValue('hello');
|
|
26
|
+
});
|
|
27
|
+
it('renders the placeholder', () => {
|
|
28
|
+
render(Input, { props: { id: 'q', placeholder: 'Search…' } });
|
|
29
|
+
expect(screen.getByPlaceholderText('Search…')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
it('does not accept input when disabled', async () => {
|
|
32
|
+
render(Input, { props: { id: 'q', disabled: true } });
|
|
33
|
+
const input = screen.getByRole('textbox');
|
|
34
|
+
expect(input).toBeDisabled();
|
|
35
|
+
await userEvent.type(input, 'nope');
|
|
36
|
+
expect(input).toHaveValue('');
|
|
37
|
+
});
|
|
38
|
+
it('is read-only when readonly is set', async () => {
|
|
39
|
+
render(Input, { props: { id: 'q', readonly: true, value: 'fixed' } });
|
|
40
|
+
const input = screen.getByRole('textbox');
|
|
41
|
+
expect(input).toHaveAttribute('readonly');
|
|
42
|
+
await userEvent.type(input, 'x');
|
|
43
|
+
expect(input).toHaveValue('fixed');
|
|
44
|
+
});
|
|
45
|
+
it('marks required inputs', () => {
|
|
46
|
+
render(Input, { props: { id: 'q', required: true } });
|
|
47
|
+
expect(screen.getByRole('textbox')).toBeRequired();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden test for Select (Sweep L1, #1420).
|
|
3
|
+
*
|
|
4
|
+
* Select forwards aria attributes (so it can be labelled / error-associated)
|
|
5
|
+
* and inherits id / describedby / invalid from a wrapping FormGroup via context.
|
|
6
|
+
*/
|
|
7
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
8
|
+
import { render, screen } from '@testing-library/svelte';
|
|
9
|
+
import { createRawSnippet } from 'svelte';
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import Select from '../Select.svelte';
|
|
12
|
+
function options() {
|
|
13
|
+
return createRawSnippet(() => ({
|
|
14
|
+
render: () => `<option value="a">A</option><option value="b">B</option>`,
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
describe('Select', () => {
|
|
18
|
+
it('forwards aria-label / aria-describedby / aria-invalid', () => {
|
|
19
|
+
render(Select, {
|
|
20
|
+
props: {
|
|
21
|
+
'aria-label': 'Country',
|
|
22
|
+
'aria-describedby': 'country-hint',
|
|
23
|
+
'aria-invalid': 'true',
|
|
24
|
+
children: options(),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const select = screen.getByRole('combobox', { name: 'Country' });
|
|
28
|
+
expect(select).toHaveAttribute('aria-describedby', 'country-hint');
|
|
29
|
+
expect(select).toHaveAttribute('aria-invalid', 'true');
|
|
30
|
+
});
|
|
31
|
+
it('is axe-clean when labelled', async () => {
|
|
32
|
+
const { container } = render(Select, {
|
|
33
|
+
props: { 'aria-label': 'Country', children: options() },
|
|
34
|
+
});
|
|
35
|
+
await expectNoA11yViolations(container);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden test for Textarea (Sweep L1, #1420).
|
|
3
|
+
*
|
|
4
|
+
* Textarea forwards aria attributes and inherits id / describedby / invalid from
|
|
5
|
+
* a wrapping FormGroup via context.
|
|
6
|
+
*/
|
|
7
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
8
|
+
import { render, screen } from '@testing-library/svelte';
|
|
9
|
+
import userEvent from '@testing-library/user-event';
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import Textarea from '../Textarea.svelte';
|
|
12
|
+
describe('Textarea', () => {
|
|
13
|
+
it('forwards aria-label / aria-describedby / aria-invalid', () => {
|
|
14
|
+
render(Textarea, {
|
|
15
|
+
props: {
|
|
16
|
+
'aria-label': 'Notes',
|
|
17
|
+
'aria-describedby': 'notes-hint',
|
|
18
|
+
'aria-invalid': 'true',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const textarea = screen.getByRole('textbox', { name: 'Notes' });
|
|
22
|
+
expect(textarea).toHaveAttribute('aria-describedby', 'notes-hint');
|
|
23
|
+
expect(textarea).toHaveAttribute('aria-invalid', 'true');
|
|
24
|
+
});
|
|
25
|
+
it('accepts typed input', async () => {
|
|
26
|
+
render(Textarea, { props: { 'aria-label': 'Notes' } });
|
|
27
|
+
const textarea = screen.getByRole('textbox', {
|
|
28
|
+
name: 'Notes',
|
|
29
|
+
});
|
|
30
|
+
await userEvent.type(textarea, 'hello');
|
|
31
|
+
expect(textarea).toHaveValue('hello');
|
|
32
|
+
});
|
|
33
|
+
it('is axe-clean when labelled', async () => {
|
|
34
|
+
const { container } = render(Textarea, {
|
|
35
|
+
props: { 'aria-label': 'Notes' },
|
|
36
|
+
});
|
|
37
|
+
await expectNoA11yViolations(container);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component test for Toggle (Sweep S11, #1416).
|
|
3
|
+
*
|
|
4
|
+
* Follows the golden pattern (src/components/ui/__tests__/Button.test.ts):
|
|
5
|
+
* render → assert role/name/state → drive with user-event → prove axe-clean.
|
|
6
|
+
* Toggle renders native checkbox semantics, so it surfaces as a `checkbox` role.
|
|
7
|
+
*/
|
|
8
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
9
|
+
import { render, screen } from '@testing-library/svelte';
|
|
10
|
+
import userEvent from '@testing-library/user-event';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import Toggle from '../Toggle.svelte';
|
|
13
|
+
describe('Toggle', () => {
|
|
14
|
+
it('renders a checkbox with its label as the accessible name', () => {
|
|
15
|
+
render(Toggle, { props: { label: 'Email notifications' } });
|
|
16
|
+
const toggle = screen.getByRole('checkbox', {
|
|
17
|
+
name: 'Email notifications',
|
|
18
|
+
});
|
|
19
|
+
expect(toggle).toBeInTheDocument();
|
|
20
|
+
expect(toggle).not.toBeChecked();
|
|
21
|
+
});
|
|
22
|
+
it('prefers ariaLabel over label for the accessible name', () => {
|
|
23
|
+
render(Toggle, { props: { label: 'Visible', ariaLabel: 'Wifi' } });
|
|
24
|
+
expect(screen.getByRole('checkbox', { name: 'Wifi' })).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
it('reflects the checked prop', () => {
|
|
27
|
+
render(Toggle, { props: { ariaLabel: 'Active', checked: true } });
|
|
28
|
+
expect(screen.getByRole('checkbox', { name: 'Active' })).toBeChecked();
|
|
29
|
+
});
|
|
30
|
+
it('reflects the disabled prop', () => {
|
|
31
|
+
render(Toggle, { props: { ariaLabel: 'Active', disabled: true } });
|
|
32
|
+
expect(screen.getByRole('checkbox', { name: 'Active' })).toBeDisabled();
|
|
33
|
+
});
|
|
34
|
+
it('forwards name, value, and id to the input for form submission', () => {
|
|
35
|
+
render(Toggle, {
|
|
36
|
+
props: { ariaLabel: 'Sub', name: 'sub', value: 'on', id: 'sub-toggle' },
|
|
37
|
+
});
|
|
38
|
+
const toggle = screen.getByRole('checkbox', {
|
|
39
|
+
name: 'Sub',
|
|
40
|
+
});
|
|
41
|
+
expect(toggle).toHaveAttribute('name', 'sub');
|
|
42
|
+
expect(toggle).toHaveAttribute('id', 'sub-toggle');
|
|
43
|
+
// Svelte binds `value` as the DOM property on checkboxes (not a reflected attribute).
|
|
44
|
+
expect(toggle.value).toBe('on');
|
|
45
|
+
});
|
|
46
|
+
it('toggles checked and fires onchange when clicked', async () => {
|
|
47
|
+
const onchange = vi.fn();
|
|
48
|
+
render(Toggle, { props: { ariaLabel: 'Active', onchange } });
|
|
49
|
+
const toggle = screen.getByRole('checkbox', { name: 'Active' });
|
|
50
|
+
await userEvent.click(toggle);
|
|
51
|
+
expect(toggle).toBeChecked();
|
|
52
|
+
expect(onchange).toHaveBeenCalledWith(true);
|
|
53
|
+
await userEvent.click(toggle);
|
|
54
|
+
expect(toggle).not.toBeChecked();
|
|
55
|
+
expect(onchange).toHaveBeenLastCalledWith(false);
|
|
56
|
+
});
|
|
57
|
+
it('is keyboard-activatable with Space when focused', async () => {
|
|
58
|
+
const onchange = vi.fn();
|
|
59
|
+
render(Toggle, { props: { ariaLabel: 'Active', onchange } });
|
|
60
|
+
const toggle = screen.getByRole('checkbox', { name: 'Active' });
|
|
61
|
+
toggle.focus();
|
|
62
|
+
expect(toggle).toHaveFocus();
|
|
63
|
+
await userEvent.keyboard(' ');
|
|
64
|
+
expect(toggle).toBeChecked();
|
|
65
|
+
expect(onchange).toHaveBeenCalledWith(true);
|
|
66
|
+
});
|
|
67
|
+
it('does not fire onchange when disabled', async () => {
|
|
68
|
+
const onchange = vi.fn();
|
|
69
|
+
render(Toggle, {
|
|
70
|
+
props: { ariaLabel: 'Active', disabled: true, onchange },
|
|
71
|
+
});
|
|
72
|
+
await userEvent.click(screen.getByRole('checkbox', { name: 'Active' }));
|
|
73
|
+
expect(onchange).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
it('is axe-clean with a label', async () => {
|
|
76
|
+
const { container } = render(Toggle, {
|
|
77
|
+
props: { label: 'Accessible toggle' },
|
|
78
|
+
});
|
|
79
|
+
await expectNoA11yViolations(container);
|
|
80
|
+
});
|
|
81
|
+
it('is axe-clean in the checked + disabled state', async () => {
|
|
82
|
+
const { container } = render(Toggle, {
|
|
83
|
+
props: { label: 'Locked on', checked: true, disabled: true },
|
|
84
|
+
});
|
|
85
|
+
await expectNoA11yViolations(container);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/** Test fixture (L1 #1420): an Input wrapped in a FormGroup, to exercise the
|
|
3
|
+
* FormGroup → base-input accessibility auto-wiring. */
|
|
4
|
+
import FormGroup from '../FormGroup.svelte';
|
|
5
|
+
import Input from '../Input.svelte';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
error = undefined,
|
|
9
|
+
hint = undefined,
|
|
10
|
+
required = false,
|
|
11
|
+
}: { error?: string; hint?: string; required?: boolean } = $props();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<FormGroup label="Email" {error} {hint} {required}>
|
|
15
|
+
<Input type="email" name="email" />
|
|
16
|
+
</FormGroup>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
error?: string;
|
|
3
|
+
hint?: string;
|
|
4
|
+
required?: boolean;
|
|
5
|
+
};
|
|
6
|
+
declare const FormGroupInput: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type FormGroupInput = ReturnType<typeof FormGroupInput>;
|
|
8
|
+
export default FormGroupInput;
|
|
9
|
+
//# sourceMappingURL=form-group-input.fixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-group-input.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/form-group-input.fixture.svelte.ts"],"names":[],"mappings":"AAQC,KAAK,gBAAgB,GAAI;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAmBhF,QAAA,MAAM,cAAc,sDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface FormGroupContextValue {
|
|
2
|
+
/** Stable id for the field control; matches the FormGroup label's `for`. */
|
|
3
|
+
inputId: string;
|
|
4
|
+
/** Space-joined ids of the visible hint/error text, or undefined if none. */
|
|
5
|
+
describedBy: string | undefined;
|
|
6
|
+
/** Whether the field is currently showing an error. */
|
|
7
|
+
invalid: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function setFormGroupContext(get: () => FormGroupContextValue): void;
|
|
10
|
+
export declare function tryGetFormGroupContext(): (() => FormGroupContextValue) | undefined;
|
|
11
|
+
/** Deterministic, collision-resistant id for a FormGroup that wasn't given one. */
|
|
12
|
+
export declare function nextFieldId(): string;
|
|
13
|
+
//# sourceMappingURL=form-group-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-group-context.d.ts","sourceRoot":"","sources":["../../../src/components/forms/form-group-context.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,qBAAqB;IACpC,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,uDAAuD;IACvD,OAAO,EAAE,OAAO,CAAC;CAClB;AAID,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,qBAAqB,GAAG,IAAI,CAE1E;AAED,wBAAgB,sBAAsB,IAClC,CAAC,MAAM,qBAAqB,CAAC,GAC7B,SAAS,CAEZ;AAID,mFAAmF;AACnF,wBAAgB,WAAW,IAAI,MAAM,CAGpC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormGroup accessibility context (Sweep L1, #1420).
|
|
3
|
+
*
|
|
4
|
+
* `FormGroup` publishes the wiring a wrapped input needs to be programmatically
|
|
5
|
+
* accessible — the input's `id` (so the `<label for>` resolves), the
|
|
6
|
+
* space-joined ids of the hint/error text (for `aria-describedby`), and whether
|
|
7
|
+
* the field is currently in an error state (`aria-invalid`). The base form
|
|
8
|
+
* primitives (`Input`, `Select`, `Textarea`) read this and auto-apply those
|
|
9
|
+
* attributes when the consumer hasn't set them explicitly, so an input dropped
|
|
10
|
+
* inside a `<FormGroup>` is accessible with no extra wiring.
|
|
11
|
+
*
|
|
12
|
+
* The value is a getter so the consuming input always reads the *current*
|
|
13
|
+
* reactive state (id is stable; describedBy/invalid change as hint/error do).
|
|
14
|
+
*/
|
|
15
|
+
import { getContext, setContext } from 'svelte';
|
|
16
|
+
const FORM_GROUP_KEY = Symbol('smrt-form-group');
|
|
17
|
+
export function setFormGroupContext(get) {
|
|
18
|
+
setContext(FORM_GROUP_KEY, get);
|
|
19
|
+
}
|
|
20
|
+
export function tryGetFormGroupContext() {
|
|
21
|
+
return getContext(FORM_GROUP_KEY);
|
|
22
|
+
}
|
|
23
|
+
let autoIdCounter = 0;
|
|
24
|
+
/** Deterministic, collision-resistant id for a FormGroup that wasn't given one. */
|
|
25
|
+
export function nextFieldId() {
|
|
26
|
+
autoIdCounter += 1;
|
|
27
|
+
return `smrt-field-${autoIdCounter}`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form primitives — subpath `@happyvertical/smrt-ui/forms`.
|
|
3
|
+
*
|
|
4
|
+
* The Provider-FREE base form primitives, relocated here from
|
|
5
|
+
* `@happyvertical/smrt-svelte/forms` (issue #1589 deferred-forms phase) so domain
|
|
6
|
+
* packages can adopt them without pulling in the smrt-svelte Provider or closing
|
|
7
|
+
* a build-graph cycle. smrt-ui is the leaf everyone may depend on.
|
|
8
|
+
*
|
|
9
|
+
* `Form`, `Input`, `Select`, `Textarea`, `Toggle`, `FormGroup` are generic,
|
|
10
|
+
* tokenised, a11y-checked building blocks with no Provider/i18n/spoken-input
|
|
11
|
+
* dependency. The Provider-REQUIRED inputs (`CheckboxInput`, `TextInput`,
|
|
12
|
+
* `MoneyInput`, the rich `Form`, …) stay in `@happyvertical/smrt-svelte/forms`.
|
|
13
|
+
*/
|
|
14
|
+
export { default as Form } from './Form.svelte';
|
|
15
|
+
export { default as FormGroup } from './FormGroup.svelte';
|
|
16
|
+
export { default as Input } from './Input.svelte';
|
|
17
|
+
export { default as Select } from './Select.svelte';
|
|
18
|
+
export { default as Textarea } from './Textarea.svelte';
|
|
19
|
+
export { default as Toggle } from './Toggle.svelte';
|
|
20
|
+
export { type FormGroupContextValue, nextFieldId, setFormGroupContext, tryGetFormGroupContext, } from './form-group-context.js';
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/forms/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,EACL,KAAK,qBAAqB,EAC1B,WAAW,EACX,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form primitives — subpath `@happyvertical/smrt-ui/forms`.
|
|
3
|
+
*
|
|
4
|
+
* The Provider-FREE base form primitives, relocated here from
|
|
5
|
+
* `@happyvertical/smrt-svelte/forms` (issue #1589 deferred-forms phase) so domain
|
|
6
|
+
* packages can adopt them without pulling in the smrt-svelte Provider or closing
|
|
7
|
+
* a build-graph cycle. smrt-ui is the leaf everyone may depend on.
|
|
8
|
+
*
|
|
9
|
+
* `Form`, `Input`, `Select`, `Textarea`, `Toggle`, `FormGroup` are generic,
|
|
10
|
+
* tokenised, a11y-checked building blocks with no Provider/i18n/spoken-input
|
|
11
|
+
* dependency. The Provider-REQUIRED inputs (`CheckboxInput`, `TextInput`,
|
|
12
|
+
* `MoneyInput`, the rich `Form`, …) stay in `@happyvertical/smrt-svelte/forms`.
|
|
13
|
+
*/
|
|
14
|
+
export { default as Form } from './Form.svelte';
|
|
15
|
+
export { default as FormGroup } from './FormGroup.svelte';
|
|
16
|
+
export { default as Input } from './Input.svelte';
|
|
17
|
+
export { default as Select } from './Select.svelte';
|
|
18
|
+
export { default as Textarea } from './Textarea.svelte';
|
|
19
|
+
export { default as Toggle } from './Toggle.svelte';
|
|
20
|
+
export { nextFieldId, setFormGroupContext, tryGetFormGroupContext, } from './form-group-context.js';
|
|
@@ -247,6 +247,22 @@ const linkProps = $derived(() => {
|
|
|
247
247
|
to { transform: translate(-50%, -50%) rotate(360deg); }
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/* Lay out the button's own children. The children render inside this wrapper
|
|
251
|
+
span, so the button-level `gap` never reaches them — without making the
|
|
252
|
+
wrapper itself a flex row, an icon + label (`<Button><svg/>Save</Button>`)
|
|
253
|
+
would render with no gap. Making `.content` a centered flex row lays out
|
|
254
|
+
multi-child buttons (icon+text, label+count) correctly after the #1589
|
|
255
|
+
migration, instead of needing per-button CSS to reach into `.content`. The
|
|
256
|
+
wrapper is shrink-to-fit, so a caller wanting a spread layout must also give
|
|
257
|
+
it width — e.g. a full-width button with
|
|
258
|
+
`.x :global(.content) { width: 100%; justify-content: space-between }`. */
|
|
259
|
+
.content {
|
|
260
|
+
display: inline-flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
justify-content: center;
|
|
263
|
+
gap: var(--smrt-spacing-2);
|
|
264
|
+
}
|
|
265
|
+
|
|
250
266
|
.content.loading {
|
|
251
267
|
opacity: 0;
|
|
252
268
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-ui",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.6",
|
|
4
4
|
"description": "Domain-agnostic Svelte 5 UI runtime for SMRT: primitives, i18n client, theme system, and module UI registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -43,6 +43,12 @@
|
|
|
43
43
|
"import": "./dist/components/chat/index.js",
|
|
44
44
|
"default": "./dist/components/chat/index.js"
|
|
45
45
|
},
|
|
46
|
+
"./forms": {
|
|
47
|
+
"types": "./dist/components/forms/index.d.ts",
|
|
48
|
+
"svelte": "./dist/components/forms/index.js",
|
|
49
|
+
"import": "./dist/components/forms/index.js",
|
|
50
|
+
"default": "./dist/components/forms/index.js"
|
|
51
|
+
},
|
|
46
52
|
"./i18n": {
|
|
47
53
|
"types": "./dist/i18n/index.d.ts",
|
|
48
54
|
"svelte": "./dist/i18n/index.js",
|
|
@@ -108,7 +114,7 @@
|
|
|
108
114
|
},
|
|
109
115
|
"dependencies": {
|
|
110
116
|
"esm-env": "^1.2.2",
|
|
111
|
-
"@happyvertical/smrt-types": "0.34.
|
|
117
|
+
"@happyvertical/smrt-types": "0.34.6"
|
|
112
118
|
},
|
|
113
119
|
"peerDependencies": {
|
|
114
120
|
"svelte": "^5.18.2"
|