@happyvertical/smrt-svelte 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/dist/Provider.svelte +12 -2
- package/dist/Provider.svelte.d.ts.map +1 -1
- package/dist/components/admin/AgentSettingsShell.svelte +72 -4
- package/dist/components/admin/AgentSettingsShell.svelte.d.ts.map +1 -1
- package/dist/components/admin/__tests__/AgentSettingsShell.tablist.test.js +93 -0
- package/dist/components/forms/DateRangeInput.svelte +29 -5
- package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/DateTimeInput.svelte +34 -7
- package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/FileUpload.svelte +3 -1
- package/dist/components/forms/FileUpload.svelte.d.ts.map +1 -1
- package/dist/components/forms/Form.svelte +72 -36
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/FormMicButton.svelte +14 -11
- package/dist/components/forms/FormMicButton.svelte.d.ts.map +1 -1
- package/dist/components/forms/PhoneInput.svelte +29 -5
- package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/TextInput.svelte +35 -7
- package/dist/components/forms/TextInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/TextareaInput.svelte +29 -5
- package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/FileUpload.error-alert.test.js +37 -0
- package/dist/components/forms/__tests__/Form.stt-error.test.js +57 -0
- package/dist/components/forms/__tests__/FormMicButton.test.js +7 -0
- package/dist/components/forms/__tests__/mic-keyboard-a11y.test.js +74 -0
- package/dist/hooks/__tests__/stt-consumer.fixture.svelte +25 -0
- package/dist/hooks/__tests__/stt-consumer.fixture.svelte.d.ts +15 -0
- package/dist/hooks/__tests__/stt-consumer.fixture.svelte.d.ts.map +1 -0
- package/dist/hooks/__tests__/stt-ownership-harness.svelte +42 -0
- package/dist/hooks/__tests__/stt-ownership-harness.svelte.d.ts +20 -0
- package/dist/hooks/__tests__/stt-ownership-harness.svelte.d.ts.map +1 -0
- package/dist/hooks/__tests__/useSTT-ownership.test.js +102 -0
- package/dist/hooks/useSTT.svelte.d.ts.map +1 -1
- package/dist/hooks/useSTT.svelte.js +20 -6
- package/dist/hooks/useTTS.svelte.d.ts.map +1 -1
- package/dist/hooks/useTTS.svelte.js +20 -6
- package/dist/i18n/server.d.ts +5 -5
- package/dist/i18n/server.js +5 -5
- package/dist/internal/logger.d.ts +13 -0
- package/dist/internal/logger.d.ts.map +1 -0
- package/dist/internal/logger.js +12 -0
- package/dist/state/__tests__/app-state-ai-lifecycle.test.js +240 -0
- package/dist/state/app-state.svelte.d.ts +40 -8
- package/dist/state/app-state.svelte.d.ts.map +1 -1
- package/dist/state/app-state.svelte.js +224 -54
- package/package.json +6 -5
package/AGENTS.md
CHANGED
|
@@ -198,7 +198,10 @@ await expectNoA11yViolations(container); // axe; color-contrast off (jsdom has n
|
|
|
198
198
|
## Dependencies
|
|
199
199
|
|
|
200
200
|
- `@happyvertical/smrt-types` (shared types) — includes the identity data contracts (`User`, `Role`, `Membership`, `Tenant`) the role/membership components type against, so no dependency on `smrt-users` / `smrt-profiles` is needed
|
|
201
|
-
-
|
|
201
|
+
- `@happyvertical/smrt-ui` (UI runtime: primitives, theme system, i18n client, module registry) and `@happyvertical/smrt-agents` (admin shells type against its `/ui` contracts) are hard `dependencies`.
|
|
202
|
+
- `@happyvertical/smrt-languages` is a hard `dependency` (not an optional peer): the Node-only `/i18n/server` subpath imports its resolver. The browser bundle still excludes it — the client `/i18n` layer never imports the languages root, so it tree-shakes out.
|
|
203
|
+
- `@happyvertical/logger` (SDK) is a `dependency` — the browser-safe console logger used for voice/AI error reporting in the form components.
|
|
204
|
+
- Peer (all optional): `svelte` >=5.18.2, plus the browser-AI engines (`@huggingface/transformers`, `@mlc-ai/web-llm`, `@remotion/whisper-web`, `@xenova/transformers`) and `chrono-node`.
|
|
202
205
|
|
|
203
206
|
## Workspace shell primitives
|
|
204
207
|
|
package/dist/Provider.svelte
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import type { Snippet } from 'svelte';
|
|
8
8
|
import { onDestroy, untrack } from 'svelte';
|
|
9
9
|
import AILoadingOverlay from './browser-ai/svelte/components/AILoadingOverlay.svelte';
|
|
10
|
+
import { logger } from './internal/logger.js';
|
|
10
11
|
import type {
|
|
11
12
|
AIConfig,
|
|
12
13
|
AILoadingState,
|
|
@@ -188,9 +189,18 @@ $effect(() => {
|
|
|
188
189
|
}
|
|
189
190
|
});
|
|
190
191
|
|
|
191
|
-
// Cleanup on destroy
|
|
192
|
+
// Cleanup on destroy. Dispose the whole manager (not just the socket) so it
|
|
193
|
+
// unsubscribes its listeners from the module-surviving warm AI adapters; left
|
|
194
|
+
// attached, each destroyed Provider would keep pinning its `_state` proxy via
|
|
195
|
+
// those adapters' listener `Set`s and leak one set per navigation (R1). The
|
|
196
|
+
// warm cache itself survives — dispose() leaves cached adapters intact.
|
|
192
197
|
onDestroy(() => {
|
|
193
|
-
|
|
198
|
+
// dispose() is async; isolate + log its rejection so a failing adapter
|
|
199
|
+
// teardown surfaces as a logged error instead of an unhandled promise
|
|
200
|
+
// rejection during Provider teardown.
|
|
201
|
+
void appState.dispose().catch((error) => {
|
|
202
|
+
logger.error('AppState dispose failed during Provider teardown', { error });
|
|
203
|
+
});
|
|
194
204
|
});
|
|
195
205
|
</script>
|
|
196
206
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Provider.svelte.d.ts","sourceRoot":"","sources":["../src/Provider.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,YAAY,EAElB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"Provider.svelte.d.ts","sourceRoot":"","sources":["../src/Provider.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,YAAY,EAElB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAItC,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EACd,OAAO,EACP,YAAY,EACZ,IAAI,EACL,MAAM,sBAAsB,CAAC;AAK9B,UAAU,KAAK;IACb;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;;;;;;;;;;;;OAeG;IACH,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC;;OAEG;IACH,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACpD;;;;;OAKG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AA2ID,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -49,7 +49,11 @@ const {
|
|
|
49
49
|
dbConfigs = {},
|
|
50
50
|
}: Props = $props();
|
|
51
51
|
|
|
52
|
+
// Stable per-instance id base so multiple shells don't collide on tab/panel ids.
|
|
53
|
+
const idBase = $props.id();
|
|
54
|
+
|
|
52
55
|
let activeAgentId = $state<string | null>(null);
|
|
56
|
+
let tablistEl: HTMLElement | null = $state(null);
|
|
53
57
|
|
|
54
58
|
// Initialize to first agent
|
|
55
59
|
$effect(() => {
|
|
@@ -64,6 +68,50 @@ function handleAgentClick(agentId: string) {
|
|
|
64
68
|
activeAgentId = agentId;
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Roving-tabindex keyboard navigation for the agent switcher (C8). The list is
|
|
73
|
+
* a vertical tablist, so Up/Down move between agents (plus Home/End); mirrors
|
|
74
|
+
* the horizontal pattern in AgentAdminTabs.
|
|
75
|
+
*/
|
|
76
|
+
function handleAgentKeydown(event: KeyboardEvent, currentAgentId: string) {
|
|
77
|
+
const currentIndex = agents.findIndex((a) => a.id === currentAgentId);
|
|
78
|
+
if (currentIndex === -1) return;
|
|
79
|
+
|
|
80
|
+
let nextIndex: number | null = null;
|
|
81
|
+
switch (event.key) {
|
|
82
|
+
case 'ArrowDown':
|
|
83
|
+
case 'ArrowRight':
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
nextIndex = (currentIndex + 1) % agents.length;
|
|
86
|
+
break;
|
|
87
|
+
case 'ArrowUp':
|
|
88
|
+
case 'ArrowLeft':
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
nextIndex = (currentIndex - 1 + agents.length) % agents.length;
|
|
91
|
+
break;
|
|
92
|
+
case 'Home':
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
nextIndex = 0;
|
|
95
|
+
break;
|
|
96
|
+
case 'End':
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
nextIndex = agents.length - 1;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (nextIndex !== null && nextIndex !== currentIndex) {
|
|
103
|
+
const nextAgentId = agents[nextIndex].id;
|
|
104
|
+
activeAgentId = nextAgentId;
|
|
105
|
+
// Focus the newly-selected tab after the DOM updates.
|
|
106
|
+
requestAnimationFrame(() => {
|
|
107
|
+
const tabButton = tablistEl?.querySelector(
|
|
108
|
+
`[data-agent-id="${CSS.escape(nextAgentId)}"]`,
|
|
109
|
+
) as HTMLElement | null;
|
|
110
|
+
tabButton?.focus();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
67
115
|
async function handleSave(slotId: string, config: unknown) {
|
|
68
116
|
if (activeAgentId && onSave) {
|
|
69
117
|
await onSave(activeAgentId, slotId, config);
|
|
@@ -74,13 +122,26 @@ async function handleSave(slotId: string, config: unknown) {
|
|
|
74
122
|
<div class="agent-settings-shell" class:single-agent={agents.length === 1}>
|
|
75
123
|
{#if agents.length > 1}
|
|
76
124
|
<aside class="agents-sidebar">
|
|
77
|
-
<h3 class="sidebar-title">Agents</h3>
|
|
78
|
-
<
|
|
125
|
+
<h3 class="sidebar-title" id="{idBase}-agents-title">Agents</h3>
|
|
126
|
+
<div
|
|
127
|
+
class="agents-list"
|
|
128
|
+
role="tablist"
|
|
129
|
+
aria-orientation="vertical"
|
|
130
|
+
aria-labelledby="{idBase}-agents-title"
|
|
131
|
+
bind:this={tablistEl}
|
|
132
|
+
>
|
|
79
133
|
{#each agents as agent}
|
|
80
134
|
<button
|
|
81
135
|
class="agent-button"
|
|
82
136
|
class:active={activeAgentId === agent.id}
|
|
137
|
+
role="tab"
|
|
138
|
+
aria-selected={activeAgentId === agent.id}
|
|
139
|
+
aria-controls="{idBase}-panel"
|
|
140
|
+
id="{idBase}-tab-{agent.id}"
|
|
141
|
+
data-agent-id={agent.id}
|
|
142
|
+
tabindex={activeAgentId === agent.id ? 0 : -1}
|
|
83
143
|
onclick={() => handleAgentClick(agent.id)}
|
|
144
|
+
onkeydown={(e) => handleAgentKeydown(e, agent.id)}
|
|
84
145
|
>
|
|
85
146
|
<span class="agent-class">{agent.agentClass}</span>
|
|
86
147
|
{#if agent.name}
|
|
@@ -88,11 +149,18 @@ async function handleSave(slotId: string, config: unknown) {
|
|
|
88
149
|
{/if}
|
|
89
150
|
</button>
|
|
90
151
|
{/each}
|
|
91
|
-
</
|
|
152
|
+
</div>
|
|
92
153
|
</aside>
|
|
93
154
|
{/if}
|
|
94
155
|
|
|
95
|
-
<main
|
|
156
|
+
<main
|
|
157
|
+
class="agent-content"
|
|
158
|
+
id="{idBase}-panel"
|
|
159
|
+
role={agents.length > 1 ? 'tabpanel' : undefined}
|
|
160
|
+
aria-labelledby={agents.length > 1 && activeAgentId
|
|
161
|
+
? `${idBase}-tab-${activeAgentId}`
|
|
162
|
+
: undefined}
|
|
163
|
+
>
|
|
96
164
|
{#if activeAgent}
|
|
97
165
|
<header class="agent-header">
|
|
98
166
|
<h2 class="agent-title">{activeAgent.agentClass}</h2>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentSettingsShell.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AgentSettingsShell.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,wBAAwB,EACxB,YAAY,EACb,MAAM,+BAA+B,CAAC;AAMvC;;;GAGG;AACH,UAAU,SAAS;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;IACnB,8BAA8B;IAC9B,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,KAAK;IACpB,8CAA8C;IAC9C,QAAQ,EAAE,wBAAwB,CAAC;IACnC,iEAAiE;IACjE,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,sCAAsC;IACtC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACrD;
|
|
1
|
+
{"version":3,"file":"AgentSettingsShell.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AgentSettingsShell.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,wBAAwB,EACxB,YAAY,EACb,MAAM,+BAA+B,CAAC;AAMvC;;;GAGG;AACH,UAAU,SAAS;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;IACnB,8BAA8B;IAC9B,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,KAAK;IACpB,8CAA8C;IAC9C,QAAQ,EAAE,wBAAwB,CAAC;IACnC,iEAAiE;IACjE,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,sCAAsC;IACtC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACrD;AAwID,QAAA,MAAM,kBAAkB,2CAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression: the agent switcher is a proper ARIA tablist (C8).
|
|
3
|
+
*
|
|
4
|
+
* It was a <nav> of <button>s acting as single-select tabs, with no
|
|
5
|
+
* role/aria-selected/roving-tabindex/arrow-key nav — unlike sibling
|
|
6
|
+
* AgentAdminTabs which does the full tablist. The fix adds role="tablist" +
|
|
7
|
+
* role="tab" + aria-selected + roving tabindex + Up/Down/Home/End navigation,
|
|
8
|
+
* and marks the content panel role="tabpanel".
|
|
9
|
+
*/
|
|
10
|
+
import { createUIRegistry } from '@happyvertical/smrt-agents/ui';
|
|
11
|
+
import { render, screen, waitFor, within } from '@testing-library/svelte';
|
|
12
|
+
import userEvent from '@testing-library/user-event';
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import AgentSettingsShell from '../AgentSettingsShell.svelte';
|
|
15
|
+
/**
|
|
16
|
+
* Roving focus moves via requestAnimationFrame, so wait for the expected tab to
|
|
17
|
+
* actually receive focus before dispatching the next key (otherwise the next
|
|
18
|
+
* keydown fires on the previously-focused tab).
|
|
19
|
+
*/
|
|
20
|
+
async function expectFocused(el) {
|
|
21
|
+
await waitFor(() => expect(el).toHaveFocus());
|
|
22
|
+
}
|
|
23
|
+
function agents() {
|
|
24
|
+
return [
|
|
25
|
+
{ id: 'a1', agentClass: 'Praeco', name: 'Alpha', slots: {} },
|
|
26
|
+
{ id: 'a2', agentClass: 'Caelus', name: 'Beta', slots: {} },
|
|
27
|
+
{ id: 'a3', agentClass: 'Nimbus', name: 'Gamma', slots: {} },
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
function renderShell() {
|
|
31
|
+
return render(AgentSettingsShell, {
|
|
32
|
+
props: {
|
|
33
|
+
registry: createUIRegistry(),
|
|
34
|
+
agents: agents(),
|
|
35
|
+
configs: {},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The shell embeds AgentAdminTabs (its own slot tablist), so scope queries to
|
|
41
|
+
* the agent-switcher tablist — identified by its "Agents" accessible name.
|
|
42
|
+
*/
|
|
43
|
+
function switcher() {
|
|
44
|
+
return screen.getByRole('tablist', { name: 'Agents' });
|
|
45
|
+
}
|
|
46
|
+
function switcherTabs() {
|
|
47
|
+
return within(switcher()).getAllByRole('tab');
|
|
48
|
+
}
|
|
49
|
+
describe('AgentSettingsShell — agent switcher tablist (C8)', () => {
|
|
50
|
+
it('renders a tablist of tabs with the first selected', () => {
|
|
51
|
+
renderShell();
|
|
52
|
+
expect(switcher()).toBeInTheDocument();
|
|
53
|
+
const tabs = switcherTabs();
|
|
54
|
+
expect(tabs).toHaveLength(3);
|
|
55
|
+
expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
|
|
56
|
+
expect(tabs[1]).toHaveAttribute('aria-selected', 'false');
|
|
57
|
+
});
|
|
58
|
+
it('uses a roving tabindex (only the selected tab is tabbable)', () => {
|
|
59
|
+
renderShell();
|
|
60
|
+
const tabs = switcherTabs();
|
|
61
|
+
expect(tabs[0]).toHaveAttribute('tabindex', '0');
|
|
62
|
+
expect(tabs[1]).toHaveAttribute('tabindex', '-1');
|
|
63
|
+
expect(tabs[2]).toHaveAttribute('tabindex', '-1');
|
|
64
|
+
});
|
|
65
|
+
it('moves selection with ArrowDown / ArrowUp', async () => {
|
|
66
|
+
renderShell();
|
|
67
|
+
switcherTabs()[0].focus();
|
|
68
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
69
|
+
expect(switcherTabs()[1]).toHaveAttribute('aria-selected', 'true');
|
|
70
|
+
await expectFocused(switcherTabs()[1]);
|
|
71
|
+
await userEvent.keyboard('{ArrowUp}');
|
|
72
|
+
expect(switcherTabs()[0]).toHaveAttribute('aria-selected', 'true');
|
|
73
|
+
await expectFocused(switcherTabs()[0]);
|
|
74
|
+
});
|
|
75
|
+
it('jumps to last/first with End / Home', async () => {
|
|
76
|
+
renderShell();
|
|
77
|
+
switcherTabs()[0].focus();
|
|
78
|
+
await userEvent.keyboard('{End}');
|
|
79
|
+
expect(switcherTabs()[2]).toHaveAttribute('aria-selected', 'true');
|
|
80
|
+
await expectFocused(switcherTabs()[2]);
|
|
81
|
+
await userEvent.keyboard('{Home}');
|
|
82
|
+
expect(switcherTabs()[0]).toHaveAttribute('aria-selected', 'true');
|
|
83
|
+
await expectFocused(switcherTabs()[0]);
|
|
84
|
+
});
|
|
85
|
+
it('exposes a tabpanel wired to the selected tab', () => {
|
|
86
|
+
renderShell();
|
|
87
|
+
const panel = screen.getByRole('tabpanel');
|
|
88
|
+
const selectedTab = switcherTabs().find((t) => t.getAttribute('aria-selected') === 'true');
|
|
89
|
+
expect(selectedTab).toBeTruthy();
|
|
90
|
+
expect(panel).toHaveAttribute('aria-labelledby', selectedTab?.id);
|
|
91
|
+
expect(selectedTab).toHaveAttribute('aria-controls', panel.id);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -4,6 +4,7 @@ import { onDestroy, onMount } from 'svelte';
|
|
|
4
4
|
import { useAppState } from '../../hooks/useAppState.svelte.js';
|
|
5
5
|
import { useSTT } from '../../hooks/useSTT.svelte.js';
|
|
6
6
|
import { M } from '../../i18n/strings.forms.js';
|
|
7
|
+
import { logger } from '../../internal/logger.js';
|
|
7
8
|
import {
|
|
8
9
|
type FieldDefinition,
|
|
9
10
|
tryGetFormContext,
|
|
@@ -219,15 +220,26 @@ onDestroy(() => {
|
|
|
219
220
|
async function startRecording() {
|
|
220
221
|
if (!isSmrt || disabled || isParsing) return;
|
|
221
222
|
|
|
222
|
-
if (!stt.isReady || stt.adapterType !== 'whisper-wasm') {
|
|
223
|
-
await stt.initialize({ type: 'whisper-wasm' });
|
|
224
|
-
}
|
|
225
|
-
|
|
226
223
|
parseError = null;
|
|
227
224
|
recordingStartTime = Date.now();
|
|
228
225
|
isRecording = true;
|
|
229
226
|
|
|
230
|
-
|
|
227
|
+
// Guard STT init / mic acquisition so a rejection can't leave the field
|
|
228
|
+
// wedged in a permanent "Recording..." state with no visible error (C2).
|
|
229
|
+
try {
|
|
230
|
+
if (!stt.isReady || stt.adapterType !== 'whisper-wasm') {
|
|
231
|
+
await stt.initialize({ type: 'whisper-wasm' });
|
|
232
|
+
}
|
|
233
|
+
await stt.start({ continuous: true, interimResults: false });
|
|
234
|
+
} catch (err) {
|
|
235
|
+
isRecording = false;
|
|
236
|
+
parseError =
|
|
237
|
+
err instanceof Error ? err.message : 'Could not start voice input';
|
|
238
|
+
logger.error('DateRangeInput: failed to start voice recording', {
|
|
239
|
+
field: name,
|
|
240
|
+
error: err,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
231
243
|
}
|
|
232
244
|
|
|
233
245
|
async function stopRecording() {
|
|
@@ -307,6 +319,17 @@ function handleTouchEnd() {
|
|
|
307
319
|
stopRecording();
|
|
308
320
|
}
|
|
309
321
|
|
|
322
|
+
// Keyboard activation for the mic (WCAG 2.1.1): Enter/Space toggles recording.
|
|
323
|
+
function handleMicKeydown(e: KeyboardEvent) {
|
|
324
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
325
|
+
e.preventDefault();
|
|
326
|
+
if (isRecording) {
|
|
327
|
+
stopRecording();
|
|
328
|
+
} else {
|
|
329
|
+
startRecording();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
310
333
|
const primaryControlId = $derived(isSmrt ? `${name}_voice` : `${name}_start`);
|
|
311
334
|
</script>
|
|
312
335
|
|
|
@@ -342,6 +365,7 @@ const primaryControlId = $derived(isSmrt ? `${name}_voice` : `${name}_start`);
|
|
|
342
365
|
class="mic-btn"
|
|
343
366
|
class:active={isRecording}
|
|
344
367
|
disabled={disabled || isParsing}
|
|
368
|
+
onkeydown={handleMicKeydown}
|
|
345
369
|
onmousedown={handleMouseDown}
|
|
346
370
|
onmouseup={handleMouseUp}
|
|
347
371
|
onmouseleave={handleMouseLeave}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DateRangeInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/DateRangeInput.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DateRangeInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/DateRangeInput.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjD,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6BAA6B;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2BAA2B;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CAC5C;AAkYD,QAAA,MAAM,cAAc,gEAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -5,6 +5,7 @@ import { onDestroy, onMount } from 'svelte';
|
|
|
5
5
|
import { useAppState } from '../../hooks/useAppState.svelte.js';
|
|
6
6
|
import { useSTT } from '../../hooks/useSTT.svelte.js';
|
|
7
7
|
import { M } from '../../i18n/strings.forms.js';
|
|
8
|
+
import { logger } from '../../internal/logger.js';
|
|
8
9
|
import {
|
|
9
10
|
type FieldDefinition,
|
|
10
11
|
tryGetFormContext,
|
|
@@ -177,17 +178,28 @@ async function parseNaturalLanguage(text: string): Promise<string> {
|
|
|
177
178
|
async function startHoldRecording() {
|
|
178
179
|
if (!isSmrt || disabled || isParsing) return;
|
|
179
180
|
|
|
180
|
-
// Initialize STT with Whisper v2 for speed + accuracy
|
|
181
|
-
if (!stt.isReady || stt.adapterType !== 'whisper-wasm') {
|
|
182
|
-
await stt.initialize({ type: 'whisper-wasm' });
|
|
183
|
-
}
|
|
184
|
-
|
|
185
181
|
parseError = null;
|
|
186
182
|
recordingStartTime = Date.now();
|
|
187
183
|
isHolding = true;
|
|
188
184
|
|
|
189
|
-
//
|
|
190
|
-
|
|
185
|
+
// Guard STT init / mic acquisition so a rejection can't leave the field
|
|
186
|
+
// wedged in a permanent "Recording..." state with no visible error (C2).
|
|
187
|
+
try {
|
|
188
|
+
// Initialize STT with Whisper v2 for speed + accuracy
|
|
189
|
+
if (!stt.isReady || stt.adapterType !== 'whisper-wasm') {
|
|
190
|
+
await stt.initialize({ type: 'whisper-wasm' });
|
|
191
|
+
}
|
|
192
|
+
// Use continuous mode to capture all speech while holding
|
|
193
|
+
await stt.start({ continuous: true, interimResults: false });
|
|
194
|
+
} catch (err) {
|
|
195
|
+
isHolding = false;
|
|
196
|
+
parseError =
|
|
197
|
+
err instanceof Error ? err.message : 'Could not start voice input';
|
|
198
|
+
logger.error('DateTimeInput: failed to start voice recording', {
|
|
199
|
+
field: name,
|
|
200
|
+
error: err,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
191
203
|
}
|
|
192
204
|
|
|
193
205
|
async function stopHoldRecording() {
|
|
@@ -276,6 +288,20 @@ function handleMicTouchStart(e: TouchEvent) {
|
|
|
276
288
|
startHoldRecording();
|
|
277
289
|
}
|
|
278
290
|
|
|
291
|
+
// Keyboard activation for the mic (WCAG 2.1.1): Enter/Space toggles recording.
|
|
292
|
+
// In SMRT mode the text field is read-only, so this is the only keyboard path
|
|
293
|
+
// to enter a value — it must not be removed.
|
|
294
|
+
function handleMicKeydown(e: KeyboardEvent) {
|
|
295
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
296
|
+
e.preventDefault();
|
|
297
|
+
e.stopPropagation();
|
|
298
|
+
if (isHolding) {
|
|
299
|
+
stopHoldRecording();
|
|
300
|
+
} else {
|
|
301
|
+
startHoldRecording();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
279
305
|
function handleNativeChange(e: Event) {
|
|
280
306
|
const target = e.target as HTMLInputElement;
|
|
281
307
|
updateValue(target.value);
|
|
@@ -319,6 +345,7 @@ function handleNativeChange(e: Event) {
|
|
|
319
345
|
class:active={isHolding}
|
|
320
346
|
disabled={disabled || isParsing}
|
|
321
347
|
onclick={handleMicClick}
|
|
348
|
+
onkeydown={handleMicKeydown}
|
|
322
349
|
onmousedown={handleMicMouseDown}
|
|
323
350
|
onmouseup={handleMouseUp}
|
|
324
351
|
onmouseleave={handleMouseLeave}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DateTimeInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/DateTimeInput.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DateTimeInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/DateTimeInput.svelte.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AA6UD,QAAA,MAAM,aAAa,gDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -204,7 +204,9 @@ function formatFileSize(bytes: number): string {
|
|
|
204
204
|
</div>
|
|
205
205
|
|
|
206
206
|
{#if error}
|
|
207
|
-
|
|
207
|
+
<!-- Live region so async validation/reject errors are announced (C7),
|
|
208
|
+
matching the Form/TextInput announcing pattern. -->
|
|
209
|
+
<p class="file-upload__error" role="alert" aria-live="assertive">{error}</p>
|
|
208
210
|
{/if}
|
|
209
211
|
|
|
210
212
|
{#if files.length > 0}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileUpload.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/FileUpload.svelte.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,KAAK;IACpB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gCAAgC;IAChC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,iCAAiC;IACjC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACnC,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;
|
|
1
|
+
{"version":3,"file":"FileUpload.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/FileUpload.svelte.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,KAAK;IACpB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gCAAgC;IAChC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,iCAAiC;IACjC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACnC,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA0LD,QAAA,MAAM,UAAU,gDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -5,6 +5,7 @@ import { onDestroy } from 'svelte';
|
|
|
5
5
|
import { useAppState } from '../../hooks/useAppState.svelte.js';
|
|
6
6
|
import { useSTT } from '../../hooks/useSTT.svelte.js';
|
|
7
7
|
import { M } from '../../i18n/strings.forms.js';
|
|
8
|
+
import { logger } from '../../internal/logger.js';
|
|
8
9
|
import {
|
|
9
10
|
type FieldDefinition,
|
|
10
11
|
type SMRTFormContext,
|
|
@@ -253,7 +254,6 @@ async function startAudioLevelMonitoring(stream: MediaStream) {
|
|
|
253
254
|
|
|
254
255
|
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
255
256
|
const SPEECH_THRESHOLD = 20; // Lowered from 30 for better sensitivity
|
|
256
|
-
let checksWithSpeech = 0;
|
|
257
257
|
|
|
258
258
|
audioLevelInterval = setInterval(() => {
|
|
259
259
|
if (!analyser || !isFormListening) return;
|
|
@@ -261,17 +261,22 @@ async function startAudioLevelMonitoring(stream: MediaStream) {
|
|
|
261
261
|
analyser.getByteFrequencyData(dataArray);
|
|
262
262
|
const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
|
|
263
263
|
|
|
264
|
-
// Log periodically to debug
|
|
265
|
-
checksWithSpeech++;
|
|
266
|
-
if (checksWithSpeech % 10 === 0) {
|
|
267
|
-
}
|
|
268
|
-
|
|
269
264
|
if (average > SPEECH_THRESHOLD) {
|
|
270
265
|
// Speech detected - reset silence timer
|
|
271
266
|
resetSilenceTimer();
|
|
272
267
|
}
|
|
273
268
|
}, 200); // Check every 200ms
|
|
274
|
-
} catch (err) {
|
|
269
|
+
} catch (err) {
|
|
270
|
+
// Audio-level monitoring is a silence-detection enhancement, not the
|
|
271
|
+
// recording path itself — surface the failure but don't abort listening.
|
|
272
|
+
// A getUserMedia/AudioContext denial here usually means mic-permission
|
|
273
|
+
// issues the user needs to see (C3).
|
|
274
|
+
extractError =
|
|
275
|
+
err instanceof Error
|
|
276
|
+
? `Microphone monitoring unavailable: ${err.message}`
|
|
277
|
+
: 'Microphone monitoring unavailable';
|
|
278
|
+
logger.warn('Form: audio-level monitoring failed to start', { error: err });
|
|
279
|
+
}
|
|
275
280
|
}
|
|
276
281
|
|
|
277
282
|
// Stop audio level monitoring
|
|
@@ -369,37 +374,68 @@ async function startFormListening() {
|
|
|
369
374
|
// Set starting flag to prevent premature stop detection
|
|
370
375
|
isStarting = true;
|
|
371
376
|
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
isStopping = false;
|
|
383
|
-
lastSpeechTime = Date.now();
|
|
377
|
+
// STT init / start can reject (model fetch failure, mic-permission denial).
|
|
378
|
+
// Without this guard `isStarting`/`isFormListening` would stay set, the
|
|
379
|
+
// auto-stop $effect (gated on `!isStarting`) would stay suppressed, and the
|
|
380
|
+
// form would wedge in a permanent "listening" state with no surfaced error
|
|
381
|
+
// (C2). On failure: tear down, reset flags, and show the error.
|
|
382
|
+
try {
|
|
383
|
+
// Initialize STT with selected adapter
|
|
384
|
+
if (!stt.isReady || stt.adapterType !== sttAdapter) {
|
|
385
|
+
await stt.initialize({ type: sttAdapter });
|
|
386
|
+
}
|
|
384
387
|
|
|
385
|
-
|
|
386
|
-
|
|
388
|
+
extractError = null;
|
|
389
|
+
spokenText = '';
|
|
390
|
+
// Set to current stale result so the effect skips it, but new results will be processed
|
|
391
|
+
lastProcessedResult = stt.lastResult || '';
|
|
392
|
+
isFormListening = true;
|
|
393
|
+
isStopping = false;
|
|
394
|
+
lastSpeechTime = Date.now();
|
|
395
|
+
|
|
396
|
+
// Start silence timer
|
|
397
|
+
resetSilenceTimer();
|
|
398
|
+
|
|
399
|
+
// Start audio level monitoring for Whisper (no interim results)
|
|
400
|
+
// Browser STT has interim results so doesn't need this
|
|
401
|
+
if (sttAdapter === 'whisper-wasm') {
|
|
402
|
+
try {
|
|
403
|
+
const levelStream = await navigator.mediaDevices.getUserMedia({
|
|
404
|
+
audio: true,
|
|
405
|
+
});
|
|
406
|
+
startAudioLevelMonitoring(levelStream);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
// Mic-permission denial / no device — surface it; the user must see
|
|
409
|
+
// why dictation isn't working (C3).
|
|
410
|
+
extractError =
|
|
411
|
+
err instanceof Error
|
|
412
|
+
? `Microphone access failed: ${err.message}`
|
|
413
|
+
: 'Microphone access failed';
|
|
414
|
+
logger.warn('Form: getUserMedia failed for audio-level monitoring', {
|
|
415
|
+
error: err,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
387
419
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
420
|
+
await stt.start({ continuous: true, interimResults: true });
|
|
421
|
+
} catch (err) {
|
|
422
|
+
// Reset listening state so the form isn't wedged, and stop any monitoring
|
|
423
|
+
// that may have started before the failure.
|
|
424
|
+
isFormListening = false;
|
|
425
|
+
stopAudioLevelMonitoring();
|
|
426
|
+
if (silenceTimer) {
|
|
427
|
+
clearTimeout(silenceTimer);
|
|
428
|
+
silenceTimer = null;
|
|
429
|
+
}
|
|
430
|
+
extractError =
|
|
431
|
+
err instanceof Error
|
|
432
|
+
? err.message
|
|
433
|
+
: 'Could not start voice input. Check microphone permissions.';
|
|
434
|
+
logger.error('Form: failed to start form listening', { error: err });
|
|
435
|
+
} finally {
|
|
436
|
+
// Clear starting flag now that STT is actually listening (or has failed).
|
|
437
|
+
isStarting = false;
|
|
397
438
|
}
|
|
398
|
-
|
|
399
|
-
await stt.start({ continuous: true, interimResults: true });
|
|
400
|
-
|
|
401
|
-
// Clear starting flag now that STT is actually listening
|
|
402
|
-
isStarting = false;
|
|
403
439
|
}
|
|
404
440
|
|
|
405
441
|
async function stopFormListening() {
|
|
@@ -730,7 +766,7 @@ function getFormData(): Record<string, unknown> {
|
|
|
730
766
|
was never emitted (issue #1431); color-mix derives the alpha from the
|
|
731
767
|
emitted `--smrt-color-primary` token instead. */
|
|
732
768
|
background: color-mix(in srgb, var(--smrt-color-primary, #166534) 90%, transparent);
|
|
733
|
-
color:
|
|
769
|
+
color: var(--smrt-color-on-primary, #fff);
|
|
734
770
|
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
735
771
|
box-shadow: 0 -2px 12px color-mix(in srgb, var(--smrt-color-shadow) 15%, transparent);
|
|
736
772
|
z-index: var(--smrt-z-index-toast, 1500);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAWtC,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG7D,MAAM,WAAW,KAAK;IACpB,oBAAoB;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,8BAA8B;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oCAAoC;IACpC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iDAAiD;IACjD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB;IACvB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACnD,4DAA4D;IAC5D,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA8jBD,QAAA,MAAM,IAAI,2CAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
|
|
@@ -29,13 +29,8 @@ let isExtracting = $state(false);
|
|
|
29
29
|
$effect(() => {
|
|
30
30
|
const checkState = () => {
|
|
31
31
|
if (formContext) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Only log on change to reduce noise
|
|
35
|
-
if (newListening !== isListening || newExtracting !== isExtracting) {
|
|
36
|
-
}
|
|
37
|
-
isListening = newListening;
|
|
38
|
-
isExtracting = newExtracting;
|
|
32
|
+
isListening = formContext.isFormListening;
|
|
33
|
+
isExtracting = formContext.isExtracting;
|
|
39
34
|
}
|
|
40
35
|
};
|
|
41
36
|
|
|
@@ -51,6 +46,14 @@ $effect(() => {
|
|
|
51
46
|
function handleClick() {
|
|
52
47
|
formContext?.toggleListening();
|
|
53
48
|
}
|
|
49
|
+
|
|
50
|
+
// Keyboard activation for the role="button" span. A native <button> activates
|
|
51
|
+
// on both Enter and Space; this custom control must replicate both (CS2).
|
|
52
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
53
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
handleClick();
|
|
56
|
+
}
|
|
54
57
|
</script>
|
|
55
58
|
|
|
56
59
|
{#if isSmrt && formContext}
|
|
@@ -59,7 +62,7 @@ function handleClick() {
|
|
|
59
62
|
class:listening={isListening}
|
|
60
63
|
class:extracting={isExtracting}
|
|
61
64
|
onclick={handleClick}
|
|
62
|
-
onkeydown={
|
|
65
|
+
onkeydown={handleKeydown}
|
|
63
66
|
role="button"
|
|
64
67
|
tabindex="0"
|
|
65
68
|
aria-label={isListening ? 'Stop listening' : 'Click to speak'}
|
|
@@ -131,8 +134,8 @@ function handleClick() {
|
|
|
131
134
|
transform: translateX(-50%);
|
|
132
135
|
margin-top: 0.5rem;
|
|
133
136
|
padding: 0.5rem 0.75rem;
|
|
134
|
-
background: var(--smrt-color-on-surface, #1f2937);
|
|
135
|
-
color:
|
|
137
|
+
background: var(--smrt-color-inverse-surface, var(--smrt-color-on-surface, #1f2937));
|
|
138
|
+
color: var(--smrt-color-inverse-on-surface, #fff);
|
|
136
139
|
font-size: var(--smrt-typography-label-medium-size, 0.75rem);
|
|
137
140
|
font-weight: var(--smrt-typography-weight-normal, 400);
|
|
138
141
|
border-radius: 0.375rem;
|
|
@@ -148,7 +151,7 @@ function handleClick() {
|
|
|
148
151
|
left: 50%;
|
|
149
152
|
transform: translateX(-50%);
|
|
150
153
|
border: 6px solid transparent;
|
|
151
|
-
border-bottom-color: var(--smrt-color-on-surface);
|
|
154
|
+
border-bottom-color: var(--smrt-color-inverse-surface, var(--smrt-color-on-surface));
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
@keyframes pulse {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormMicButton.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/FormMicButton.svelte.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,KAAK;IACpB,2BAA2B;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"FormMicButton.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/FormMicButton.svelte.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,KAAK;IACpB,2BAA2B;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA4ED,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|