@happyvertical/smrt-ui 0.55.0 → 0.55.10
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 +2 -1
- package/README.md +113 -5
- package/dist/components/forms/CodeSelect.svelte +12 -2
- package/dist/components/forms/CodeSelect.svelte.d.ts.map +1 -1
- package/dist/components/forms/Combobox.svelte +50 -7
- package/dist/components/forms/Combobox.svelte.d.ts +26 -0
- package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
- package/dist/components/forms/DictationButton.svelte +110 -3
- package/dist/components/forms/DictationButton.svelte.d.ts.map +1 -1
- package/dist/components/forms/DictationStatus.svelte +33 -5
- package/dist/components/forms/DictationStatus.svelte.d.ts +2 -0
- package/dist/components/forms/DictationStatus.svelte.d.ts.map +1 -1
- package/dist/components/forms/FieldLabel.svelte +44 -0
- package/dist/components/forms/FieldLabel.svelte.d.ts +20 -0
- package/dist/components/forms/FieldLabel.svelte.d.ts.map +1 -0
- package/dist/components/forms/FormGroup.svelte +2 -17
- package/dist/components/forms/FormGroup.svelte.d.ts.map +1 -1
- package/dist/components/forms/RelationInput.svelte +378 -0
- package/dist/components/forms/RelationInput.svelte.d.ts +75 -0
- package/dist/components/forms/RelationInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/code-select-options.d.ts +8 -1
- package/dist/components/forms/code-select-options.d.ts.map +1 -1
- package/dist/components/forms/code-select-options.js +46 -5
- package/dist/components/forms/code-select-types.d.ts +4 -0
- package/dist/components/forms/code-select-types.d.ts.map +1 -1
- package/dist/components/forms/dictation.svelte.d.ts +84 -2
- package/dist/components/forms/dictation.svelte.d.ts.map +1 -1
- package/dist/components/forms/dictation.svelte.js +236 -2
- package/dist/components/forms/hands-free-capture.d.ts +70 -0
- package/dist/components/forms/hands-free-capture.d.ts.map +1 -0
- package/dist/components/forms/hands-free-capture.js +19 -0
- package/dist/components/forms/hands-free-vad.d.ts +5 -0
- package/dist/components/forms/hands-free-vad.d.ts.map +1 -0
- package/dist/components/forms/hands-free-vad.js +80 -0
- package/dist/components/forms/index.d.ts +4 -1
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +3 -0
- package/dist/components/forms/types.d.ts +9 -0
- package/dist/components/forms/types.d.ts.map +1 -1
- package/dist/components/layout/ActionGroup.svelte +55 -0
- package/dist/components/layout/ActionGroup.svelte.d.ts +18 -0
- package/dist/components/layout/ActionGroup.svelte.d.ts.map +1 -0
- package/dist/components/layout/Container.svelte +1 -0
- package/dist/components/layout/Grid.svelte +2 -1
- package/dist/components/layout/PageHeader.svelte +3 -3
- package/dist/components/layout/PageHeader.svelte.d.ts +3 -3
- package/dist/components/layout/PageLayout.svelte +61 -0
- package/dist/components/layout/PageLayout.svelte.d.ts +18 -0
- package/dist/components/layout/PageLayout.svelte.d.ts.map +1 -0
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/components/layout/index.js +2 -0
- package/dist/components/permissions/PermissionPicker.svelte +213 -0
- package/dist/components/permissions/PermissionPicker.svelte.d.ts +27 -0
- package/dist/components/permissions/PermissionPicker.svelte.d.ts.map +1 -0
- package/dist/currency.d.ts +3 -0
- package/dist/currency.d.ts.map +1 -0
- package/dist/currency.js +2 -0
- package/dist/i18n/strings.ui.d.ts +7 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +7 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/svelte/playground/InteractiveControlsPreview.svelte +22 -0
- package/dist/svelte/playground/InteractiveControlsPreview.svelte.d.ts.map +1 -1
- package/dist/svelte/playground/PermissionPickerPreview.svelte +27 -0
- package/dist/svelte/playground/PermissionPickerPreview.svelte.d.ts +19 -0
- package/dist/svelte/playground/PermissionPickerPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts +14 -2
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +10 -0
- package/dist/themes/README.md +5 -0
- package/dist/themes/styles/base.css +31 -0
- package/dist/themes/styles/index.d.ts +2 -0
- package/dist/themes/styles/index.d.ts.map +1 -1
- package/dist/themes/styles/index.js +2 -0
- package/package.json +17 -2
package/AGENTS.md
CHANGED
|
@@ -17,7 +17,7 @@ SMRT has one shared set of UI primitives, split across two packages by concern:
|
|
|
17
17
|
`Tooltip`, `Skeleton`, `Tree`, `Pagination`, `DataTable`, `ContentList`, … — **plus the
|
|
18
18
|
Provider-free base FORM primitives** under `./forms` (`Form`, `Input`,
|
|
19
19
|
`Select`, `Textarea`, `Checkbox`, `Switch`, `RadioGroup`, `Slider`,
|
|
20
|
-
`RangeSlider`, `Combobox`, `MultiSelect`, `TagsInput`, `FilePicker`,
|
|
20
|
+
`RangeSlider`, `Combobox`, `RelationInput`, `MultiSelect`, `TagsInput`, `FilePicker`,
|
|
21
21
|
`FormGroup`), relocated here in #1589's
|
|
22
22
|
deferred-forms phase so domain packages can adopt them without pulling in the
|
|
23
23
|
smrt-svelte Provider or closing a build-graph cycle. These are dependency-free:
|
|
@@ -53,6 +53,7 @@ components are exempt — they *are* the primitives.
|
|
|
53
53
|
| `./calendar` | `CalendarView` (time-zone-aware month/week grids + phone agenda) and its Svelte-free date model (`toEntries`, `layoutMonth`, `layoutWeek`, `dateKeyInZone`, …); deprecated `Calendar`, `DayView` |
|
|
54
54
|
| `./chat` | `MessageBubble`, `ReactionPicker`, `TypingIndicator` |
|
|
55
55
|
| `./forms` | Provider-free fields, choice controls, sliders/ranges, combobox/listbox/multiselect/tags, date/time/file controls, `CameraCapture`/`SignaturePad` capture, plus the transport-neutral control interaction registry |
|
|
56
|
+
| `.` permissions | `PermissionCheck` and the domain-agnostic `PermissionPicker` for grouped, searchable native role-grant forms over host-supplied catalog records |
|
|
56
57
|
| `./form-retry` | **Svelte-free** form retry (#3291): submission key, in-flight refusal, conditional reset, opt-in restore-after-reload, browser `runOnce()` digest. Also re-exported from `./forms`. Framework-free TS typed structurally against SvelteKit's `SubmitFunction` — never import `@sveltejs/kit` here. Guide: `docs/content/form-retry.md` |
|
|
57
58
|
| `./i18n` | i18n **client**: `useI18n`, `<Trans>`, `defineMessages`, `renderTemplate` (no `smrt-languages` import — the server resolver stays in `smrt-svelte/i18n/server`) |
|
|
58
59
|
| `./registry` | `ModuleUIRegistry` for cross-package component discovery |
|
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ pnpm add @happyvertical/smrt-ui
|
|
|
14
14
|
import { ThemeProvider } from '@happyvertical/smrt-ui/themes';
|
|
15
15
|
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
16
16
|
import { Form, FormGroup, Input, Switch } from '@happyvertical/smrt-ui/forms';
|
|
17
|
+
import '@happyvertical/smrt-ui/themes/styles/base.css';
|
|
18
|
+
import '@happyvertical/smrt-ui/themes/styles/material.css';
|
|
17
19
|
</script>
|
|
18
20
|
|
|
19
21
|
<ThemeProvider preset="material" colorScheme="system">
|
|
@@ -30,7 +32,7 @@ pnpm add @happyvertical/smrt-ui
|
|
|
30
32
|
| Area | Components |
|
|
31
33
|
| --- | --- |
|
|
32
34
|
| Fields | `Form`, `Field`/`FormGroup`, `Fieldset`, `InputGroup`, `ErrorSummary`, `FormActionBar` |
|
|
33
|
-
| Text and structured input | `Input`, `Textarea`, `Select`, `Combobox`, `Listbox`, `MultiSelect`, `TagsInput`, `SearchInput` |
|
|
35
|
+
| Text and structured input | `Input`, `Textarea`, `Select`, `Combobox`, `RelationInput`, `Listbox`, `MultiSelect`, `TagsInput`, `SearchInput` |
|
|
34
36
|
| Choices | `Checkbox`, `RadioGroup`/`Radio`, `Switch`, `Toggle`, `ToggleButton`, `SegmentedControl` |
|
|
35
37
|
| Values and files | `Slider`, `RangeSlider`, `DatePicker`, `TimePicker`, `FilePicker` |
|
|
36
38
|
| Capture | `CameraCapture`, `SignaturePad` |
|
|
@@ -38,7 +40,7 @@ pnpm add @happyvertical/smrt-ui
|
|
|
38
40
|
| Disclosure and overlays | `Popover`, `Disclosure`, `Accordion`/`AccordionItem`, `Modal`, `Drawer`/`Sheet`, `PhoneSheet`, `ConfirmDialog` |
|
|
39
41
|
| Feedback | `Alert`, `ToastViewport`, `Progress`, `Meter`, `Spinner`, `LoadingOverlay`, `WorkingStrip` |
|
|
40
42
|
| Collections | `CollectionToolbar`, `CollectionList`/`ContentList`, `DataTable`, `Pagination` |
|
|
41
|
-
| Layout and navigation | `Container`, `Grid`, `Header`, `Footer`, `PageHeader`, `EmptyState`, `Tabs`, `FilterChips` |
|
|
43
|
+
| Layout and navigation | `Container`, `PageLayout`, `Grid`, `ActionGroup`, `Header`, `Footer`, `PageHeader`, `EmptyState`, `Tabs`, `FilterChips` |
|
|
42
44
|
| Calendar | `CalendarView` (deprecated: `Calendar`, `DayView`) |
|
|
43
45
|
|
|
44
46
|
### Phone surfaces
|
|
@@ -79,6 +81,36 @@ pnpm add @happyvertical/smrt-ui
|
|
|
79
81
|
body (`Content-Type` is the recording's type, `?language=&durationMs=`)
|
|
80
82
|
and answers `{ text }`; 413, 503 and 401/403 become "too long", "not set
|
|
81
83
|
up" and "not allowed" messages.
|
|
84
|
+
- Hands-free dictation: `new Dictation({ mode: 'hands-free', handsFreeCapture:
|
|
85
|
+
createHandsFreeCapture, … })` keeps the microphone on and writes each
|
|
86
|
+
sentence down when the speaker pauses; one tap on the microphone ends it.
|
|
87
|
+
`createHandsFreeCapture` comes from `@happyvertical/smrt-ui/forms/hands-free`,
|
|
88
|
+
the one entry that imports the optional peer `@happyvertical/speech`
|
|
89
|
+
(>= 0.102.5: on-device voice activity detection with an adaptive noise
|
|
90
|
+
floor, pre-roll and a 30 s split; no audio leaves the page). It needs a
|
|
91
|
+
speech source with `transcribePcm` (smrt-svelte's on-device `whisper-local`
|
|
92
|
+
and `moonshine`); with any of the three missing it is plain press-to-talk.
|
|
93
|
+
Utterances are written down one at a time in the order spoken
|
|
94
|
+
(`dictation.queued` counts those waiting); `speaking` and `level` (0 to 1)
|
|
95
|
+
drive the button: a steady ring while listening, a halo that swells with the
|
|
96
|
+
voice while speaking, a spinner while writing down, all still under
|
|
97
|
+
`prefers-reduced-motion`. `vad` tunes `silenceMs` (default 800),
|
|
98
|
+
`minSpeechMs`, `preRollMs`, `maxUtteranceMs` and `sensitivity`.
|
|
99
|
+
The help text ("Just talk; I write it down when you pause...") is the
|
|
100
|
+
microphone's tooltip and `aria-describedby`, not a line in the form;
|
|
101
|
+
`DictationStatus` keeps a visually hidden polite live region ("Listening",
|
|
102
|
+
"Hearing you...", "Writing it down...") and shows errors. Its `sending` prop
|
|
103
|
+
adds the one short visible hint, "Sending...". The chat composer's
|
|
104
|
+
`sendOnPause` / `sendOnPauseMs` (default 1200) builds send-on-pause on it.
|
|
105
|
+
Half-duplex: browsers do not reliably cancel `speechSynthesis` from the
|
|
106
|
+
microphone, so call `dictation.suspend()` while the page reads a reply aloud
|
|
107
|
+
and `dictation.resume()` after (the chat composer/dock do this from their
|
|
108
|
+
`speaking` prop). The microphone stays open but nothing is heard (an
|
|
109
|
+
utterance in progress is discarded, a ~350 ms guard follows `resume()`);
|
|
110
|
+
`dictation.suspended` drives the dimmed, dashed-outline button and the
|
|
111
|
+
"Paused while the assistant speaks" tooltip/live region. A custom
|
|
112
|
+
`HandsFreeCapture` may implement optional `suspend()` / `resume()`;
|
|
113
|
+
`Dictation` ignores utterances that arrive while suspended either way.
|
|
82
114
|
|
|
83
115
|
### Link tabs
|
|
84
116
|
|
|
@@ -93,11 +125,15 @@ under the phone top bar. Pair it with `useLinkSurface` (smrt-svelte) so
|
|
|
93
125
|
agents can switch tabs too.
|
|
94
126
|
|
|
95
127
|
Use the focused subpaths (`/forms`, `/form-retry`, `/ui`, `/feedback`, `/data`,
|
|
96
|
-
`/data-surface`, `/layout`, `/themes`) to keep imports explicit. The
|
|
128
|
+
`/data-surface`, `/currency`, `/layout`, `/themes`) to keep imports explicit. The
|
|
97
129
|
Svelte-free `/data-surface` entry exposes the registry contracts and shared
|
|
98
130
|
protocol limits for server adapters; the Svelte-free `/form-retry` entry
|
|
99
|
-
exposes the form-retry helper. The
|
|
100
|
-
|
|
131
|
+
exposes the form-retry helper. The Svelte-free `/currency` entry exposes
|
|
132
|
+
`ISO_4217_MINOR_UNITS`, the shared ISO 4217 registry used for deterministic
|
|
133
|
+
minor-unit scaling. A missing code is unsupported; a `null` value is a valid
|
|
134
|
+
ISO fund, metal, test, or no-currency code without a defined minor unit, so
|
|
135
|
+
callers handling integer minor-unit money must reject both cases rather than
|
|
136
|
+
inventing a decimal precision. The package root remains a compatibility barrel.
|
|
101
137
|
|
|
102
138
|
### Calendar dates and shop time
|
|
103
139
|
|
|
@@ -124,6 +160,50 @@ controls with no name or an empty name are omitted. Disabled controls, including
|
|
|
124
160
|
those inside disabled fieldsets, are omitted by native `FormData`. Native form
|
|
125
161
|
reset restores the initial selection and its label, unless reset is canceled.
|
|
126
162
|
|
|
163
|
+
### RelationInput
|
|
164
|
+
|
|
165
|
+
`RelationInput` (`/forms`) moved here from `@happyvertical/smrt-svelte/forms` in #3637 (originally #3600); smrt-svelte keeps a deprecated re-export. Labels are props (`createLabel`, `clearLabel`, `loadingText`, ...); there is no message catalog.
|
|
166
|
+
|
|
167
|
+
Searchable single-select for relation (foreign-key) fields, a thin wrapper over
|
|
168
|
+
`Combobox`. It is presentation only: the caller supplies the data,
|
|
169
|
+
so the same lookup an assistant uses to turn "Acme" into a record id backs the
|
|
170
|
+
picker. Option shape is `RelationOption` — `{ id, label, detail? }`.
|
|
171
|
+
|
|
172
|
+
- **Props.** `name`, `label`, `value` (bindable id, `''` = none), `required`,
|
|
173
|
+
`disabled`, `error`, `placeholder`, `description`, `search(query)`, optional
|
|
174
|
+
`resolve(id)` and `onCreate(query)`, plus `debounceMs` (250), `interaction`,
|
|
175
|
+
`onchange` and overridable text (`createLabel`, `clearLabel`, `loadingText`,
|
|
176
|
+
`emptyText`, `errorText`, `resultsText`).
|
|
177
|
+
- **`search`** runs with `''` when the list opens and then debounced per
|
|
178
|
+
keystroke; a response for a superseded query is dropped. A rejected search
|
|
179
|
+
shows `errorText` and the next keystroke retries.
|
|
180
|
+
- **`resolve`** supplies the label of a `value` that has not been searched yet
|
|
181
|
+
(initial value, parent rebind, form reset). Without it such a value shows an
|
|
182
|
+
empty field, never the raw id. A `null` result also leaves the field empty.
|
|
183
|
+
- **`onCreate`** adds a "New ..." button; it receives the text most recently
|
|
184
|
+
searched for and may return the created `RelationOption`, which is selected.
|
|
185
|
+
- **Form integration** is through `Combobox`'s control registration (`Form`
|
|
186
|
+
provides the registry), not a `FieldDefinition`: agents see a `combobox`
|
|
187
|
+
control whose value is the id. The form posts the id under `name`.
|
|
188
|
+
- **Known limit.** An agent staging a value through the registered control can
|
|
189
|
+
only choose among the options last listed (the registry matches against
|
|
190
|
+
`options`; `allowCustom` is off), so it cannot write an id it has not
|
|
191
|
+
searched. It fails closed. Writing a resolved id needs an async hook on
|
|
192
|
+
`Combobox` and is tracked separately.
|
|
193
|
+
- **Stale options.** Typing clears the listed options until the new search
|
|
194
|
+
answers, so Enter or a click can never pick a record for an older query.
|
|
195
|
+
- **Clear button** shows only when not `required`, not `disabled`, and a value
|
|
196
|
+
is set; it returns focus to the field.
|
|
197
|
+
- **Accessibility** is the combobox pattern (`role=combobox`, `aria-expanded`,
|
|
198
|
+
`aria-activedescendant`, listbox/option, Arrow/Enter/Escape) plus a polite
|
|
199
|
+
`role=status` live region announcing searching / "N results" / no matches,
|
|
200
|
+
`aria-busy` while fetching, and `aria-invalid` + `aria-describedby` for
|
|
201
|
+
`error` and `description`.
|
|
202
|
+
- Options render `detail` under the label, so the option's accessible name
|
|
203
|
+
includes it. The `Combobox` additions that make this possible (`filter`,
|
|
204
|
+
`onquery`, `status`, `busy`, `invalid`, `describedby`, `optionContent`) are
|
|
205
|
+
generic and default to the previous behaviour.
|
|
206
|
+
|
|
127
207
|
### MultiSelect form submission
|
|
128
208
|
|
|
129
209
|
A named `MultiSelect` submits one hidden native input per selected option value,
|
|
@@ -944,6 +1024,7 @@ hand-authored, and every text pairing clears WCAG AA.
|
|
|
944
1024
|
```svelte
|
|
945
1025
|
<script>
|
|
946
1026
|
import { ThemeProvider } from '@happyvertical/smrt-ui/themes';
|
|
1027
|
+
import '@happyvertical/smrt-ui/themes/styles/base.css';
|
|
947
1028
|
import '@happyvertical/smrt-ui/themes/styles/all.css';
|
|
948
1029
|
import '@happyvertical/smrt-ui/themes/styles/fonts.css';
|
|
949
1030
|
</script>
|
|
@@ -953,6 +1034,33 @@ hand-authored, and every text pairing clears WCAG AA.
|
|
|
953
1034
|
</ThemeProvider>
|
|
954
1035
|
```
|
|
955
1036
|
|
|
1037
|
+
The application baseline is intentionally separate from theme tokens. Import
|
|
1038
|
+
`base.css` once at the app entry point to remove the browser body margin, use
|
|
1039
|
+
border-box sizing, apply the active theme to the document, and make native form
|
|
1040
|
+
controls inherit the app typeface. It does not reset lists or content margins.
|
|
1041
|
+
|
|
1042
|
+
PageLayout keeps wide tables and tab rows within the available page width,
|
|
1043
|
+
including when nested inside a semantic section. Their own scroll containers
|
|
1044
|
+
remain scrollable. Place visual sections directly under PageLayout to use its
|
|
1045
|
+
vertical gap.
|
|
1046
|
+
|
|
1047
|
+
Use `PageLayout` inside an application shell to give each route responsive
|
|
1048
|
+
gutters, block padding, vertical rhythm, and the width appropriate to its
|
|
1049
|
+
content. It never creates a scroll container or claims viewport height.
|
|
1050
|
+
Use `ActionGroup` for adjacent page or card actions; it wraps with token spacing
|
|
1051
|
+
and can justify actions at the start or end without form-specific behavior.
|
|
1052
|
+
|
|
1053
|
+
```svelte
|
|
1054
|
+
<script>
|
|
1055
|
+
import { PageHeader, PageLayout } from '@happyvertical/smrt-ui/layout';
|
|
1056
|
+
</script>
|
|
1057
|
+
|
|
1058
|
+
<PageLayout maxWidth="xl" gap="md">
|
|
1059
|
+
<PageHeader title="Projects" subtitle="Plan and track active work" />
|
|
1060
|
+
<!-- collection, cards, form, or detail content -->
|
|
1061
|
+
</PageLayout>
|
|
1062
|
+
```
|
|
1063
|
+
|
|
956
1064
|
Run the shared playground to inspect the full catalog under every preset and
|
|
957
1065
|
light/dark scheme.
|
|
958
1066
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { ComponentProps } from 'svelte';
|
|
3
3
|
import type { CodeSelectProps } from './code-select-types.js';
|
|
4
|
+
import FieldLabel from './FieldLabel.svelte';
|
|
4
5
|
import Input from './Input.svelte';
|
|
5
6
|
import Select from './Select.svelte';
|
|
6
7
|
|
|
@@ -18,9 +19,17 @@ let {
|
|
|
18
19
|
locale: _locale,
|
|
19
20
|
name,
|
|
20
21
|
form,
|
|
22
|
+
label,
|
|
23
|
+
id: idProp,
|
|
24
|
+
required,
|
|
21
25
|
...rest
|
|
22
26
|
}: Props = $props();
|
|
23
27
|
const hiddenId = $props.id();
|
|
28
|
+
const labelId = `${hiddenId}-label`;
|
|
29
|
+
const controlId = $derived(idProp ?? `${hiddenId}-control`);
|
|
30
|
+
const labelProps = $derived(
|
|
31
|
+
label ? { id: controlId, 'aria-labelledby': labelId } : { id: idProp },
|
|
32
|
+
);
|
|
24
33
|
let select: Select | undefined = $state();
|
|
25
34
|
let input: Input | undefined = $state();
|
|
26
35
|
const choices = $derived(options.filter((option) => option.value !== ''));
|
|
@@ -38,10 +47,11 @@ export function getElement(): HTMLInputElement | HTMLSelectElement | null {
|
|
|
38
47
|
return (freeText ? input : select)?.getElement() ?? null;
|
|
39
48
|
}
|
|
40
49
|
</script>
|
|
50
|
+
{#if label}<FieldLabel id={labelId} for={controlId} {label} {required} />{/if}
|
|
41
51
|
{#if freeText}
|
|
42
|
-
<Input {...inputRest} bind:this={input} bind:value type="text" {name} {form} {placeholder} disabled={disabled || readOnly} />
|
|
52
|
+
<Input {...inputRest} {...labelProps} {required} bind:this={input} bind:value type="text" {name} {form} {placeholder} disabled={disabled || readOnly} />
|
|
43
53
|
{:else}
|
|
44
|
-
<Select {...rest} bind:this={select} bind:value {name} {form} disabled={disabled || readOnly}>
|
|
54
|
+
<Select {...rest} {...labelProps} {required} bind:this={select} bind:value {name} {form} disabled={disabled || readOnly}>
|
|
45
55
|
<option value="">{placeholder}</option>
|
|
46
56
|
{#if retained}<option value={value}>{value}</option>{/if}
|
|
47
57
|
{#each choices as option}<option value={option.value} disabled={option.disabled}>{option.label ?? option.value}</option>{/each}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeSelect.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/CodeSelect.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"CodeSelect.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/CodeSelect.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAM9D,UAAU,KAAM,SAAQ,eAAe;IACrC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA4DD,QAAA,MAAM,UAAU;iBAtBG,IAAI;sBAIC,gBAAgB,GAAG,iBAAiB,GAAG,IAAI;WAkBX,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { untrack } from 'svelte';
|
|
2
|
+
import { type Snippet, untrack } from 'svelte';
|
|
3
3
|
import {
|
|
4
4
|
emitControlChange,
|
|
5
5
|
highlightControl,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
matchingOption,
|
|
18
18
|
prepareTextControlValue,
|
|
19
19
|
} from './control-value-validation.js';
|
|
20
|
+
import FieldLabel from './FieldLabel.svelte';
|
|
20
21
|
import { useControlRegistration } from './use-control-registration.svelte.js';
|
|
21
22
|
export interface Props {
|
|
22
23
|
/** Array of options to select from. */
|
|
@@ -43,6 +44,31 @@ export interface Props {
|
|
|
43
44
|
* field stays empty rather than flashing the raw value (an id).
|
|
44
45
|
*/
|
|
45
46
|
valueLabel?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Whether typing filters `options` by label. Set `false` when the caller
|
|
49
|
+
* supplies the matching options itself (server-side search via `onquery`),
|
|
50
|
+
* so a result that matched on something other than its label stays listed.
|
|
51
|
+
*/
|
|
52
|
+
filter?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Called with the text to search for when the list opens (empty) and on
|
|
55
|
+
* every keystroke. Pair with `filter={false}` and fresh `options`.
|
|
56
|
+
*/
|
|
57
|
+
onquery?: (query: string) => void;
|
|
58
|
+
/**
|
|
59
|
+
* Message announced politely to assistive technology while the list is open
|
|
60
|
+
* ("Searching…", "3 results", "No results"). It is also shown inside the
|
|
61
|
+
* list while there are no options to list.
|
|
62
|
+
*/
|
|
63
|
+
status?: string;
|
|
64
|
+
/** Marks the combobox busy while options are being fetched. */
|
|
65
|
+
busy?: boolean;
|
|
66
|
+
/** Marks the field invalid (`aria-invalid`). */
|
|
67
|
+
invalid?: boolean;
|
|
68
|
+
/** Ids of elements describing the field (error text, hints). */
|
|
69
|
+
describedby?: string;
|
|
70
|
+
/** Custom content for one option row; defaults to the label. */
|
|
71
|
+
optionContent?: Snippet<[ControlOption]>;
|
|
46
72
|
/** Callback when the selected value changes. */
|
|
47
73
|
onvaluechange?: (value: string) => void;
|
|
48
74
|
/** CSS class to apply to the combobox container. */
|
|
@@ -59,6 +85,13 @@ let {
|
|
|
59
85
|
allowCustom = false,
|
|
60
86
|
interaction,
|
|
61
87
|
valueLabel,
|
|
88
|
+
filter = true,
|
|
89
|
+
onquery,
|
|
90
|
+
status,
|
|
91
|
+
busy = false,
|
|
92
|
+
invalid = false,
|
|
93
|
+
describedby,
|
|
94
|
+
optionContent,
|
|
62
95
|
onvaluechange,
|
|
63
96
|
class: className = '',
|
|
64
97
|
}: Props = $props();
|
|
@@ -83,7 +116,7 @@ let query = $state(untrack(() => labelForValue(value)));
|
|
|
83
116
|
let typed = $state(false);
|
|
84
117
|
let activeIndex = $state(0);
|
|
85
118
|
const filtered = $derived(
|
|
86
|
-
typed
|
|
119
|
+
typed && filter
|
|
87
120
|
? options.filter((option) =>
|
|
88
121
|
option.label.toLowerCase().includes(query.toLowerCase()),
|
|
89
122
|
)
|
|
@@ -136,6 +169,7 @@ function openList() {
|
|
|
136
169
|
(option) => String(option.value) === value,
|
|
137
170
|
);
|
|
138
171
|
activeIndex = selected >= 0 ? selected : 0;
|
|
172
|
+
onquery?.('');
|
|
139
173
|
}
|
|
140
174
|
function closeList() {
|
|
141
175
|
open = false;
|
|
@@ -147,6 +181,7 @@ function handleInput(event: Event & { currentTarget: HTMLInputElement }) {
|
|
|
147
181
|
typed = true;
|
|
148
182
|
open = true;
|
|
149
183
|
activeIndex = 0;
|
|
184
|
+
onquery?.(query);
|
|
150
185
|
if (allowCustom) {
|
|
151
186
|
value = query;
|
|
152
187
|
onvaluechange?.(value);
|
|
@@ -179,6 +214,10 @@ function handleKeydown(event: KeyboardEvent) {
|
|
|
179
214
|
closeList();
|
|
180
215
|
}
|
|
181
216
|
}
|
|
217
|
+
// Options that arrive late can be fewer than the highlighted row.
|
|
218
|
+
$effect(() => {
|
|
219
|
+
if (activeIndex >= filtered.length) activeIndex = 0;
|
|
220
|
+
});
|
|
182
221
|
$effect(() => {
|
|
183
222
|
const form = inputEl?.form;
|
|
184
223
|
if (!form) return;
|
|
@@ -296,19 +335,23 @@ useControlRegistration(() => {
|
|
|
296
335
|
<div bind:this={rootEl} class="combobox {className}" data-smrt-control={controlId} data-smrt-form={interactionContext?.formId}
|
|
297
336
|
data-smrt-subject-type={interaction === false ? undefined : interaction?.subject?.type}
|
|
298
337
|
data-smrt-subject-id={interaction === false ? undefined : interaction?.subject?.id}>
|
|
299
|
-
<
|
|
338
|
+
<FieldLabel for={inputId} {label} {required} />
|
|
300
339
|
<!-- The form posts the committed value (an option id), never the label the person sees. -->
|
|
301
340
|
{#if name}<input type="hidden" {name} {value} {disabled} />{/if}
|
|
302
341
|
<input bind:this={inputEl} id={inputId} role="combobox" autocomplete="off" {placeholder} {disabled} required={required && !value} aria-required={required ? 'true' : undefined} value={query}
|
|
303
|
-
aria-expanded={open} aria-controls={listId} aria-autocomplete="list" aria-activedescendant={open && filtered[activeIndex] ? `${listId}-${activeIndex}` : undefined}
|
|
342
|
+
aria-expanded={open} aria-controls={listId} aria-busy={busy ? 'true' : undefined} aria-invalid={invalid ? 'true' : undefined} aria-describedby={describedby} aria-autocomplete="list" aria-activedescendant={open && filtered[activeIndex] ? `${listId}-${activeIndex}` : undefined}
|
|
304
343
|
onfocus={openList} onclick={openList} oninput={handleInput} onkeydown={handleKeydown} />
|
|
305
344
|
{#if open && filtered.length}<div id={listId} class="options" role="listbox">{#each filtered as option, index (option.value)}<button id={`${listId}-${index}`} type="button" role="option"
|
|
306
|
-
aria-selected={String(option.value) === value} class:active={index === activeIndex} disabled={option.disabled} onpointerdown={(event) => event.preventDefault()} onclick={() => commit(option, true)}>{option.label}</button>{/each}</div>
|
|
345
|
+
aria-selected={String(option.value) === value} class:active={index === activeIndex} disabled={option.disabled} onpointerdown={(event) => event.preventDefault()} onclick={() => commit(option, true)}>{#if optionContent}{@render optionContent(option)}{:else}{option.label}{/if}</button>{/each}</div>
|
|
346
|
+
{:else if open && status}<div class="options empty" aria-hidden="true">{status}</div>{/if}
|
|
347
|
+
{#if status !== undefined}<div class="sr-status" role="status" aria-live="polite">{open ? status : ''}</div>{/if}
|
|
307
348
|
</div>
|
|
308
349
|
<style>
|
|
309
|
-
.combobox { position: relative; display: grid; gap:
|
|
310
|
-
|
|
350
|
+
.combobox { position: relative; display: grid; gap: 0; color: var(--smrt-color-on-surface); }
|
|
351
|
+
input { box-sizing: border-box; width: 100%; padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface); color: inherit; }
|
|
311
352
|
input:focus { outline: 2px solid var(--smrt-color-primary); outline-offset: 1px; } .options { position: absolute; z-index: var(--smrt-z-index-dropdown); top: 100%; left: 0; right: 0; display: grid; padding: var(--smrt-spacing-1); border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface-container); box-shadow: var(--smrt-elevation-2); }
|
|
312
353
|
.options button { padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 0; border-radius: var(--smrt-radius-extra-small); background: transparent; color: var(--smrt-color-on-surface); text-align: left; } .options button.active { background: var(--smrt-color-secondary-container); }
|
|
354
|
+
.options.empty { padding: var(--smrt-spacing-2) var(--smrt-spacing-3); color: var(--smrt-color-on-surface-variant); }
|
|
355
|
+
.sr-status { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
313
356
|
:global(.combobox[data-smrt-highlighted='true']) { outline: 3px solid var(--smrt-color-tertiary); outline-offset: 4px; }
|
|
314
357
|
</style>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
1
2
|
import type { ControlInteractionOptions, ControlOption } from './control-interaction.js';
|
|
2
3
|
export interface Props {
|
|
3
4
|
/** Array of options to select from. */
|
|
@@ -24,6 +25,31 @@ export interface Props {
|
|
|
24
25
|
* field stays empty rather than flashing the raw value (an id).
|
|
25
26
|
*/
|
|
26
27
|
valueLabel?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Whether typing filters `options` by label. Set `false` when the caller
|
|
30
|
+
* supplies the matching options itself (server-side search via `onquery`),
|
|
31
|
+
* so a result that matched on something other than its label stays listed.
|
|
32
|
+
*/
|
|
33
|
+
filter?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Called with the text to search for when the list opens (empty) and on
|
|
36
|
+
* every keystroke. Pair with `filter={false}` and fresh `options`.
|
|
37
|
+
*/
|
|
38
|
+
onquery?: (query: string) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Message announced politely to assistive technology while the list is open
|
|
41
|
+
* ("Searching…", "3 results", "No results"). It is also shown inside the
|
|
42
|
+
* list while there are no options to list.
|
|
43
|
+
*/
|
|
44
|
+
status?: string;
|
|
45
|
+
/** Marks the combobox busy while options are being fetched. */
|
|
46
|
+
busy?: boolean;
|
|
47
|
+
/** Marks the field invalid (`aria-invalid`). */
|
|
48
|
+
invalid?: boolean;
|
|
49
|
+
/** Ids of elements describing the field (error text, hints). */
|
|
50
|
+
describedby?: string;
|
|
51
|
+
/** Custom content for one option row; defaults to the label. */
|
|
52
|
+
optionContent?: Snippet<[ControlOption]>;
|
|
27
53
|
/** Callback when the selected value changes. */
|
|
28
54
|
onvaluechange?: (value: string) => void;
|
|
29
55
|
/** CSS class to apply to the combobox container. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Combobox.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Combobox.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Combobox.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Combobox.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,OAAO,EAAW,MAAM,QAAQ,CAAC;AAM/C,OAAO,KAAK,EACV,yBAAyB,EACzB,aAAa,EACd,MAAM,0BAA0B,CAAC;AAYlC,MAAM,WAAW,KAAK;IACpB,uCAAuC;IACvC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,mDAAmD;IACnD,WAAW,CAAC,EAAE,yBAAyB,GAAG,KAAK,CAAC;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gDAAgD;IAChD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,aAAa,CAAC,EAAE,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;IACzC,gDAAgD;IAChD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAyRD,QAAA,MAAM,QAAQ,gDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
* 44px target, named by `aria-label`, `aria-pressed` while listening, and a
|
|
8
8
|
* listening dot that does not rely on color alone (the name changes too).
|
|
9
9
|
* Pair it with `DictationStatus` for the words ("Listening…", errors).
|
|
10
|
+
*
|
|
11
|
+
* Hands-free (`mode: 'hands-free'`): a steady ring means it is listening,
|
|
12
|
+
* a halo that swells with the voice means someone is speaking, and a small
|
|
13
|
+
* spinner means sentences are being written down. The ring is the
|
|
14
|
+
* non-motion cue; the halo and spinner stop moving for people who ask for
|
|
15
|
+
* reduced motion. While the page speaks a reply (`Dictation.suspend()`) the
|
|
16
|
+
* button dims with a dashed outline and its tooltip reads "Paused while the
|
|
17
|
+
* assistant speaks". Tapping ends it either way. The help text ("Just talk;
|
|
18
|
+
* I write it down when you pause...") is the tooltip and the button's
|
|
19
|
+
* `aria-describedby`, never a line of text in the form.
|
|
10
20
|
*/
|
|
11
21
|
import { M } from '../../i18n/strings.ui.js';
|
|
12
22
|
import { useI18n } from '../../i18n/use-i18n.js';
|
|
@@ -40,6 +50,11 @@ let {
|
|
|
40
50
|
|
|
41
51
|
const { t } = useI18n();
|
|
42
52
|
const active = $derived(dictation.active);
|
|
53
|
+
const handsFree = $derived(dictation.handsFree);
|
|
54
|
+
// Paused while the assistant speaks (half-duplex): not hearing, still open.
|
|
55
|
+
const paused = $derived(active && handsFree && dictation.suspended);
|
|
56
|
+
const speaking = $derived(active && handsFree && dictation.speaking && !paused);
|
|
57
|
+
const writing = $derived(handsFree && dictation.queued > 0);
|
|
43
58
|
// While a recording is being written down, a tap would do nothing.
|
|
44
59
|
const busy = $derived(dictation.state === 'transcribing');
|
|
45
60
|
const name = $derived(
|
|
@@ -47,6 +62,22 @@ const name = $derived(
|
|
|
47
62
|
? (stopLabel ?? t(M['ui.dictation.stop']))
|
|
48
63
|
: (label ?? t(M['ui.dictation.start'])),
|
|
49
64
|
);
|
|
65
|
+
const uid = $props.id();
|
|
66
|
+
const helpId = `smrt-dictation-help-${uid}`;
|
|
67
|
+
const hint = $derived(
|
|
68
|
+
active
|
|
69
|
+
? handsFree
|
|
70
|
+
? paused
|
|
71
|
+
? t(M['ui.dictation.paused_speaking'])
|
|
72
|
+
: t(M['ui.dictation.listening_hands_free'])
|
|
73
|
+
: name
|
|
74
|
+
: (title ??
|
|
75
|
+
t(
|
|
76
|
+
dictation.wantsHandsFree
|
|
77
|
+
? M['ui.dictation.start_hands_free']
|
|
78
|
+
: M['ui.dictation.start_hint'],
|
|
79
|
+
)),
|
|
80
|
+
);
|
|
50
81
|
|
|
51
82
|
function handleClick() {
|
|
52
83
|
// A tap is a user gesture: wake the audio so the ready beep can play.
|
|
@@ -60,14 +91,19 @@ function handleClick() {
|
|
|
60
91
|
type="button"
|
|
61
92
|
variant="ghost"
|
|
62
93
|
size="sm"
|
|
63
|
-
class={`smrt-dictation-button${active ? ' smrt-dictation-button--listening' : ''} ${className}`.trim()}
|
|
94
|
+
class={`smrt-dictation-button${active ? ' smrt-dictation-button--listening' : ''}${active && handsFree ? ' smrt-dictation-button--hands-free' : ''}${speaking ? ' smrt-dictation-button--speaking' : ''}${paused ? ' smrt-dictation-button--paused' : ''} ${className}`.trim()}
|
|
64
95
|
aria-label={name}
|
|
65
96
|
aria-pressed={active}
|
|
66
97
|
aria-busy={busy || undefined}
|
|
67
|
-
title={
|
|
98
|
+
title={hint}
|
|
99
|
+
aria-describedby={hint !== name ? helpId : undefined}
|
|
68
100
|
disabled={disabled || busy}
|
|
69
101
|
onclick={handleClick}
|
|
70
102
|
data-dictation-state={dictation.state}
|
|
103
|
+
data-dictation-mode={handsFree ? 'hands-free' : undefined}
|
|
104
|
+
data-dictation-speaking={speaking || undefined}
|
|
105
|
+
data-dictation-paused={paused || undefined}
|
|
106
|
+
style={speaking ? `--smrt-dictation-level: ${dictation.level}` : undefined}
|
|
71
107
|
>
|
|
72
108
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
73
109
|
<rect x="9" y="2" width="6" height="12" rx="3"></rect>
|
|
@@ -77,7 +113,13 @@ function handleClick() {
|
|
|
77
113
|
{#if active}
|
|
78
114
|
<span class="smrt-dictation-dot" aria-hidden="true"></span>
|
|
79
115
|
{/if}
|
|
116
|
+
{#if writing || (handsFree && dictation.state === 'transcribing')}
|
|
117
|
+
<span class="smrt-dictation-writing" aria-hidden="true"></span>
|
|
118
|
+
{/if}
|
|
80
119
|
</Button>
|
|
120
|
+
{#if hint !== name}
|
|
121
|
+
<span id={helpId} class="smrt-dictation-help">{hint}</span>
|
|
122
|
+
{/if}
|
|
81
123
|
|
|
82
124
|
<style>
|
|
83
125
|
:global(.smrt-dictation-button) {
|
|
@@ -99,6 +141,16 @@ function handleClick() {
|
|
|
99
141
|
color: var(--smrt-color-on-error-container, #410002);
|
|
100
142
|
}
|
|
101
143
|
|
|
144
|
+
/* Read out with the button's name; never shown. */
|
|
145
|
+
.smrt-dictation-help {
|
|
146
|
+
position: absolute;
|
|
147
|
+
inline-size: 1px;
|
|
148
|
+
block-size: 1px;
|
|
149
|
+
overflow: hidden;
|
|
150
|
+
clip-path: inset(50%);
|
|
151
|
+
white-space: nowrap;
|
|
152
|
+
}
|
|
153
|
+
|
|
102
154
|
.smrt-dictation-dot {
|
|
103
155
|
position: absolute;
|
|
104
156
|
inset-block-start: 6px;
|
|
@@ -110,6 +162,54 @@ function handleClick() {
|
|
|
110
162
|
animation: smrt-dictation-pulse 1.2s ease-in-out infinite;
|
|
111
163
|
}
|
|
112
164
|
|
|
165
|
+
/* Hands-free: a steady ring while listening ... */
|
|
166
|
+
:global(.smrt-dictation-button--hands-free) {
|
|
167
|
+
box-shadow: 0 0 0 2px var(--smrt-color-error, #ba1a1a);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* ... and a halo that swells with the voice while someone is speaking. */
|
|
171
|
+
:global(.smrt-dictation-button--speaking) {
|
|
172
|
+
box-shadow:
|
|
173
|
+
0 0 0 3px var(--smrt-color-error, #ba1a1a),
|
|
174
|
+
0 0 0
|
|
175
|
+
calc(4px + 10px * var(--smrt-dictation-level, 0))
|
|
176
|
+
color-mix(in srgb, var(--smrt-color-error, #ba1a1a) 28%, transparent);
|
|
177
|
+
transition: box-shadow 80ms linear;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Paused while the assistant speaks: a dashed, dimmed ring (not color alone). */
|
|
181
|
+
:global(.smrt-dictation-button--paused) {
|
|
182
|
+
opacity: 0.6;
|
|
183
|
+
box-shadow: none;
|
|
184
|
+
outline: 2px dashed var(--smrt-color-outline, #73777f);
|
|
185
|
+
outline-offset: -2px;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
:global(.smrt-dictation-button--paused) .smrt-dictation-dot {
|
|
189
|
+
animation: none;
|
|
190
|
+
opacity: 0.4;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Sentences are being written down. */
|
|
194
|
+
.smrt-dictation-writing {
|
|
195
|
+
position: absolute;
|
|
196
|
+
inset-block-end: 6px;
|
|
197
|
+
inset-inline-end: 6px;
|
|
198
|
+
inline-size: 10px;
|
|
199
|
+
block-size: 10px;
|
|
200
|
+
border-radius: 50%;
|
|
201
|
+
border: 2px solid var(--smrt-color-outline-variant, #c4c6cf);
|
|
202
|
+
border-block-start-color: var(--smrt-color-primary, #2f5ea8);
|
|
203
|
+
background: var(--smrt-color-surface, #fff);
|
|
204
|
+
animation: smrt-dictation-spin 0.9s linear infinite;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@keyframes smrt-dictation-spin {
|
|
208
|
+
to {
|
|
209
|
+
transform: rotate(360deg);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
113
213
|
@keyframes smrt-dictation-pulse {
|
|
114
214
|
0%,
|
|
115
215
|
100% {
|
|
@@ -121,8 +221,15 @@ function handleClick() {
|
|
|
121
221
|
}
|
|
122
222
|
|
|
123
223
|
@media (prefers-reduced-motion: reduce) {
|
|
124
|
-
.smrt-dictation-dot
|
|
224
|
+
.smrt-dictation-dot,
|
|
225
|
+
.smrt-dictation-writing {
|
|
125
226
|
animation: none;
|
|
126
227
|
}
|
|
228
|
+
|
|
229
|
+
/* Still ringed, but the halo no longer animates with the voice. */
|
|
230
|
+
:global(.smrt-dictation-button--speaking) {
|
|
231
|
+
transition: none;
|
|
232
|
+
box-shadow: 0 0 0 4px var(--smrt-color-error, #ba1a1a);
|
|
233
|
+
}
|
|
127
234
|
}
|
|
128
235
|
</style>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DictationButton.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/DictationButton.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DictationButton.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/DictationButton.svelte.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAIvD,MAAM,WAAW,KAAK;IACpB,kDAAkD;IAClD,SAAS,EAAE,SAAS,CAAC;IACrB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+ED,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|