@happyvertical/smrt-svelte 0.51.39 → 0.53.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 +3 -0
- package/README.md +214 -1
- package/agents/components.md +8 -1
- package/agents/workspace.md +24 -0
- package/dist/Provider.svelte +19 -15
- package/dist/Provider.svelte.d.ts +2 -2
- package/dist/Provider.svelte.d.ts.map +1 -1
- package/dist/__tests__/provider-mode-harness.svelte +14 -0
- package/dist/__tests__/provider-mode-harness.svelte.d.ts +11 -0
- package/dist/__tests__/provider-mode-harness.svelte.d.ts.map +1 -0
- package/dist/__tests__/provider-mode-probe.svelte +13 -0
- package/dist/__tests__/provider-mode-probe.svelte.d.ts +8 -0
- package/dist/__tests__/provider-mode-probe.svelte.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/browser-speech.d.ts +11 -0
- package/dist/browser-ai/adapters/stt/browser-speech.d.ts.map +1 -1
- package/dist/browser-ai/adapters/stt/browser-speech.js +20 -4
- package/dist/browser-ai/adapters/stt/browser-speech.test.js +113 -0
- package/dist/browser-ai/adapters/stt/dictation-source.d.ts +3 -0
- package/dist/browser-ai/adapters/stt/dictation-source.d.ts.map +1 -0
- package/dist/browser-ai/adapters/stt/dictation-source.js +38 -0
- package/dist/browser-ai/adapters/stt/dictation-source.test.js +52 -0
- package/dist/browser-ai/adapters/stt/index.d.ts +2 -1
- package/dist/browser-ai/adapters/stt/index.d.ts.map +1 -1
- package/dist/browser-ai/adapters/stt/index.js +2 -1
- package/dist/components/forms/FileUpload.svelte +120 -16
- package/dist/components/forms/FileUpload.svelte.d.ts +7 -1
- package/dist/components/forms/FileUpload.svelte.d.ts.map +1 -1
- package/dist/components/forms/Form.svelte +118 -8
- package/dist/components/forms/Form.svelte.d.ts +18 -2
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/FormScope.svelte +202 -0
- package/dist/components/forms/FormScope.svelte.d.ts +22 -0
- package/dist/components/forms/FormScope.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/FileUpload.native-form.test.js +249 -0
- package/dist/components/forms/__tests__/FileUploadReset.fixture.svelte +24 -0
- package/dist/components/forms/__tests__/FileUploadReset.fixture.svelte.d.ts +10 -0
- package/dist/components/forms/__tests__/FileUploadReset.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/Form.ui-controls.test.js +119 -0
- package/dist/components/forms/__tests__/Form.webmcp.test.js +5 -3
- package/dist/components/forms/__tests__/FormScope.test.js +39 -0
- package/dist/components/forms/__tests__/form-scope.fixture.svelte +30 -0
- package/dist/components/forms/__tests__/form-scope.fixture.svelte.d.ts +10 -0
- package/dist/components/forms/__tests__/form-scope.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte +84 -0
- package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte.d.ts +14 -0
- package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/index.d.ts +1 -0
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +1 -0
- package/dist/components/workspace/README.md +11 -0
- package/dist/components/workspace/__tests__/AdminShell.responsive.test.js +597 -0
- package/dist/components/workspace/__tests__/AdminShell.test.js +257 -1
- package/dist/components/workspace/__tests__/TenantNav.test.js +115 -0
- package/dist/components/workspace/__tests__/WorkspaceAccountMenu.test.js +6 -0
- package/dist/components/workspace/__tests__/admin-shell-header-overflow.test.js +25 -0
- package/dist/components/workspace/__tests__/admin-shell-mobile.test.js +217 -0
- package/dist/components/workspace/__tests__/admin-shell-page-contracts.test.js +30 -0
- package/dist/components/workspace/__tests__/admin-shell-responsive-state.test.js +184 -0
- package/dist/components/workspace/__tests__/admin-shell-state.test.js +25 -0
- package/dist/components/workspace/__tests__/admin-shell-theme-hooks.test.js +36 -0
- package/dist/components/workspace/__tests__/shell-chrome-components.test.js +108 -0
- package/dist/components/workspace/admin-shell/AdminShell.svelte +1099 -59
- package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +44 -1
- package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte +170 -0
- package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte.d.ts +11 -0
- package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/PhoneTopBar.svelte +93 -0
- package/dist/components/workspace/admin-shell/PhoneTopBar.svelte.d.ts +11 -0
- package/dist/components/workspace/admin-shell/PhoneTopBar.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/ShellNavToggle.svelte +88 -0
- package/dist/components/workspace/admin-shell/ShellNavToggle.svelte.d.ts +14 -0
- package/dist/components/workspace/admin-shell/ShellNavToggle.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/ShellTitle.svelte +42 -0
- package/dist/components/workspace/admin-shell/ShellTitle.svelte.d.ts +11 -0
- package/dist/components/workspace/admin-shell/ShellTitle.svelte.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/TenantNav.svelte +116 -13
- package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts +8 -2
- package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte +16 -2
- package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte.d.ts +2 -0
- package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/mobile-shell-dom.d.ts +37 -0
- package/dist/components/workspace/admin-shell/mobile-shell-dom.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/mobile-shell-dom.js +150 -0
- package/dist/components/workspace/admin-shell/mobile-shell.d.ts +191 -0
- package/dist/components/workspace/admin-shell/mobile-shell.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/mobile-shell.js +165 -0
- package/dist/components/workspace/admin-shell/settings.d.ts +27 -2
- package/dist/components/workspace/admin-shell/settings.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/settings.js +84 -2
- package/dist/components/workspace/admin-shell/state.svelte.d.ts +57 -1
- package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/state.svelte.js +156 -6
- package/dist/components/workspace/admin-shell/types.d.ts +115 -1
- package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
- package/dist/components/workspace/admin-shell/types.js +13 -0
- package/dist/components/workspace/index.d.ts +11 -4
- package/dist/components/workspace/index.d.ts.map +1 -1
- package/dist/components/workspace/index.js +12 -3
- package/dist/i18n/strings.forms.d.ts +3 -0
- package/dist/i18n/strings.forms.d.ts.map +1 -1
- package/dist/i18n/strings.forms.js +3 -0
- package/dist/i18n/strings.workspace.d.ts +14 -0
- package/dist/i18n/strings.workspace.d.ts.map +1 -1
- package/dist/i18n/strings.workspace.js +14 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/provider-mode.test.js +86 -0
- package/dist/state/app-state.svelte.d.ts.map +1 -1
- package/dist/state/app-state.svelte.js +8 -8
- package/dist/web/__tests__/assistant-dock.integration.svelte.test.js +2 -2
- package/dist/web/__tests__/list-sort-surface.fixture.svelte +41 -0
- package/dist/web/__tests__/list-sort-surface.fixture.svelte.d.ts +15 -0
- package/dist/web/__tests__/list-sort-surface.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/list-sort-surface.svelte.test.js +59 -0
- package/dist/web/__tests__/surfaces.child.fixture.svelte +69 -0
- package/dist/web/__tests__/surfaces.child.fixture.svelte.d.ts +16 -0
- package/dist/web/__tests__/surfaces.child.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/surfaces.fixture.svelte +26 -0
- package/dist/web/__tests__/surfaces.fixture.svelte.d.ts +18 -0
- package/dist/web/__tests__/surfaces.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/surfaces.svelte.test.js +140 -0
- package/dist/web/__tests__/webmcp-ui.test.js +18 -0
- package/dist/web/index.d.ts +4 -1
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +4 -1
- package/dist/web/link-surface.svelte.d.ts +14 -0
- package/dist/web/link-surface.svelte.d.ts.map +1 -0
- package/dist/web/link-surface.svelte.js +49 -0
- package/dist/web/list-surface.svelte.d.ts +80 -0
- package/dist/web/list-surface.svelte.d.ts.map +1 -0
- package/dist/web/list-surface.svelte.js +309 -0
- package/dist/web/step-surface.svelte.d.ts +17 -0
- package/dist/web/step-surface.svelte.d.ts.map +1 -0
- package/dist/web/step-surface.svelte.js +56 -0
- package/dist/web/webmcp-ui-context.d.ts +7 -0
- package/dist/web/webmcp-ui-context.d.ts.map +1 -1
- package/dist/web/webmcp-ui-context.js +10 -0
- package/dist/web/webmcp-ui.d.ts.map +1 -1
- package/dist/web/webmcp-ui.js +16 -3
- package/package.json +17 -17
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrowserSpeechSTTAdapter errors: every Web Speech error reaches `onError`
|
|
3
|
+
* with its code (`speechError`), including an `aborted` the adapter did not
|
|
4
|
+
* cause, and through smrt-ui's `Dictation` Brave's "starts, then `network`"
|
|
5
|
+
* becomes a plain error instead of a silent stop.
|
|
6
|
+
*/
|
|
7
|
+
import { Dictation } from '@happyvertical/smrt-ui/forms';
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import { BrowserSpeechSTTAdapter, } from './browser-speech.js';
|
|
10
|
+
import { createSttDictationSource } from './dictation-source.js';
|
|
11
|
+
class FakeRecognition {
|
|
12
|
+
static last = null;
|
|
13
|
+
/** What the browser does after start(): Brave errors with `network`. */
|
|
14
|
+
static afterStart = null;
|
|
15
|
+
lang = '';
|
|
16
|
+
continuous = false;
|
|
17
|
+
interimResults = false;
|
|
18
|
+
maxAlternatives = 1;
|
|
19
|
+
onresult = null;
|
|
20
|
+
onerror = null;
|
|
21
|
+
onstart = null;
|
|
22
|
+
onend = null;
|
|
23
|
+
constructor() {
|
|
24
|
+
FakeRecognition.last = this;
|
|
25
|
+
}
|
|
26
|
+
start = vi.fn(() => {
|
|
27
|
+
this.onstart?.();
|
|
28
|
+
FakeRecognition.afterStart?.(this);
|
|
29
|
+
});
|
|
30
|
+
stop = vi.fn(() => this.onend?.());
|
|
31
|
+
abort = vi.fn(() => {
|
|
32
|
+
this.onerror?.({ error: 'aborted', message: '' });
|
|
33
|
+
this.onend?.();
|
|
34
|
+
});
|
|
35
|
+
fail(code) {
|
|
36
|
+
this.onerror?.({ error: code, message: '' });
|
|
37
|
+
this.onend?.();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function install() {
|
|
41
|
+
window.webkitSpeechRecognition =
|
|
42
|
+
FakeRecognition;
|
|
43
|
+
}
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
delete window
|
|
46
|
+
.webkitSpeechRecognition;
|
|
47
|
+
FakeRecognition.last = null;
|
|
48
|
+
FakeRecognition.afterStart = null;
|
|
49
|
+
});
|
|
50
|
+
describe('BrowserSpeechSTTAdapter errors', () => {
|
|
51
|
+
it.each([
|
|
52
|
+
'network',
|
|
53
|
+
'not-allowed',
|
|
54
|
+
'service-not-allowed',
|
|
55
|
+
'no-speech',
|
|
56
|
+
'audio-capture',
|
|
57
|
+
'aborted',
|
|
58
|
+
'language-not-supported',
|
|
59
|
+
])('reports %s with its code', async (code) => {
|
|
60
|
+
install();
|
|
61
|
+
const adapter = new BrowserSpeechSTTAdapter();
|
|
62
|
+
const errors = [];
|
|
63
|
+
adapter.onError((error) => errors.push(error));
|
|
64
|
+
await adapter.start({ continuous: true });
|
|
65
|
+
FakeRecognition.last?.fail(code);
|
|
66
|
+
expect(errors).toHaveLength(1);
|
|
67
|
+
expect(errors[0].speechError).toBe(code);
|
|
68
|
+
});
|
|
69
|
+
it('does not report the abort it caused itself', async () => {
|
|
70
|
+
install();
|
|
71
|
+
const adapter = new BrowserSpeechSTTAdapter();
|
|
72
|
+
const onError = vi.fn();
|
|
73
|
+
adapter.onError(onError);
|
|
74
|
+
await adapter.start();
|
|
75
|
+
adapter.abort();
|
|
76
|
+
expect(onError).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe('Dictation over the browser adapter', () => {
|
|
80
|
+
it("Brave: it beeps, then 'network' — shown as unavailable, not a silent stop", async () => {
|
|
81
|
+
install();
|
|
82
|
+
FakeRecognition.afterStart = (r) => queueMicrotask(() => r.fail('network'));
|
|
83
|
+
const log = vi.fn();
|
|
84
|
+
const beep = vi.fn(async () => true);
|
|
85
|
+
const dictation = new Dictation({
|
|
86
|
+
source: createSttDictationSource(),
|
|
87
|
+
onText: () => { },
|
|
88
|
+
beep,
|
|
89
|
+
log,
|
|
90
|
+
requestMicrophone: false,
|
|
91
|
+
});
|
|
92
|
+
await dictation.start();
|
|
93
|
+
await vi.waitFor(() => expect(dictation.state).toBe('error'));
|
|
94
|
+
expect(beep).toHaveBeenCalled();
|
|
95
|
+
expect(dictation.errorKind).toBe('unsupported');
|
|
96
|
+
expect(dictation.errorCode).toBe('network');
|
|
97
|
+
expect(log).toHaveBeenCalledWith(expect.objectContaining({ kind: 'unsupported', code: 'network' }));
|
|
98
|
+
});
|
|
99
|
+
it('an end straight after starting with no error is a failure too', async () => {
|
|
100
|
+
install();
|
|
101
|
+
FakeRecognition.afterStart = (r) => queueMicrotask(() => r.onend?.());
|
|
102
|
+
const dictation = new Dictation({
|
|
103
|
+
source: createSttDictationSource(),
|
|
104
|
+
onText: () => { },
|
|
105
|
+
beep: false,
|
|
106
|
+
log: () => { },
|
|
107
|
+
requestMicrophone: false,
|
|
108
|
+
});
|
|
109
|
+
await dictation.start();
|
|
110
|
+
await vi.waitFor(() => expect(dictation.state).toBe('error'));
|
|
111
|
+
expect(dictation.errorKind).toBe('interrupted');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dictation-source.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/stt/dictation-source.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE5D,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,aAA0C,GAClD,MAAM,OAAO,CAAC,UAAU,CAAC,CAc3B"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A speech source for smrt-ui's `Dictation` (the microphone button and
|
|
3
|
+
* "press and hold a field to speak" in `@happyvertical/smrt-ui/forms`).
|
|
4
|
+
*
|
|
5
|
+
* `STTAdapter` already has the shape `Dictation` needs (`start`, `stop`,
|
|
6
|
+
* `onResult`, `onError`, `onStart`, `onEnd`), so this only creates the
|
|
7
|
+
* adapter lazily, once, the first time someone starts dictating: nothing
|
|
8
|
+
* loads and no microphone prompt appears until then. Defaults to the
|
|
9
|
+
* browser's own speech recognition (`browser-speech`), which needs no
|
|
10
|
+
* download; an unsupported browser rejects with
|
|
11
|
+
* `CapabilityNotAvailableError`, which `Dictation` reports in plain words.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```svelte
|
|
15
|
+
* <script>
|
|
16
|
+
* import { createSttDictationSource } from '@happyvertical/smrt-svelte/browser-ai';
|
|
17
|
+
* import { Dictation, DictationButton } from '@happyvertical/smrt-ui/forms';
|
|
18
|
+
* const dictation = new Dictation({ source: createSttDictationSource(), onText: (t) => … });
|
|
19
|
+
* </script>
|
|
20
|
+
* <DictationButton {dictation} />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { getSTT } from './factory.js';
|
|
24
|
+
export function createSttDictationSource(options = { type: 'browser-speech' }) {
|
|
25
|
+
let adapter = null;
|
|
26
|
+
return () => {
|
|
27
|
+
adapter ??= (async () => {
|
|
28
|
+
const created = await getSTT(options);
|
|
29
|
+
await created.ensureInitialized();
|
|
30
|
+
return created;
|
|
31
|
+
})().catch((error) => {
|
|
32
|
+
// Let a later tap try again (for example after a permission change).
|
|
33
|
+
adapter = null;
|
|
34
|
+
throw error;
|
|
35
|
+
});
|
|
36
|
+
return adapter;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createSttDictationSource: the browser speech adapter, created once and
|
|
3
|
+
* lazily, and rejected plainly where the browser has no speech recognition.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { createSttDictationSource } from './dictation-source.js';
|
|
7
|
+
class FakeRecognition {
|
|
8
|
+
lang = '';
|
|
9
|
+
continuous = false;
|
|
10
|
+
interimResults = false;
|
|
11
|
+
maxAlternatives = 1;
|
|
12
|
+
onresult = null;
|
|
13
|
+
onerror = null;
|
|
14
|
+
onstart = null;
|
|
15
|
+
onend = null;
|
|
16
|
+
start = vi.fn(() => this.onstart?.());
|
|
17
|
+
stop = vi.fn(() => this.onend?.());
|
|
18
|
+
abort = vi.fn();
|
|
19
|
+
}
|
|
20
|
+
describe('createSttDictationSource', () => {
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
delete window.SpeechRecognition;
|
|
23
|
+
delete window
|
|
24
|
+
.webkitSpeechRecognition;
|
|
25
|
+
});
|
|
26
|
+
it('creates one browser speech adapter, on first use', async () => {
|
|
27
|
+
window.webkitSpeechRecognition =
|
|
28
|
+
FakeRecognition;
|
|
29
|
+
const provide = createSttDictationSource();
|
|
30
|
+
const first = await provide();
|
|
31
|
+
const second = await provide();
|
|
32
|
+
expect(first).toBe(second);
|
|
33
|
+
expect(first.type).toBe('browser-speech');
|
|
34
|
+
const onStart = vi.fn();
|
|
35
|
+
first.onStart(onStart);
|
|
36
|
+
await first.start({
|
|
37
|
+
language: 'en-CA',
|
|
38
|
+
continuous: true,
|
|
39
|
+
interimResults: true,
|
|
40
|
+
});
|
|
41
|
+
expect(onStart).toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
it('rejects with CapabilityNotAvailableError without speech recognition, then can retry', async () => {
|
|
44
|
+
const provide = createSttDictationSource();
|
|
45
|
+
await expect(provide()).rejects.toMatchObject({
|
|
46
|
+
name: 'CapabilityNotAvailableError',
|
|
47
|
+
});
|
|
48
|
+
window.SpeechRecognition =
|
|
49
|
+
FakeRecognition;
|
|
50
|
+
await expect(provide()).resolves.toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Speech-to-Text adapters
|
|
3
3
|
*/
|
|
4
|
-
export { BrowserSpeechSTTAdapter } from './browser-speech.js';
|
|
4
|
+
export { type BrowserSpeechError, BrowserSpeechSTTAdapter, } from './browser-speech.js';
|
|
5
|
+
export { createSttDictationSource } from './dictation-source.js';
|
|
5
6
|
export * from './factory.js';
|
|
6
7
|
export * from './types.js';
|
|
7
8
|
export { WhisperCppSTTAdapter } from './whisper-cpp.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/stt/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/stt/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,KAAK,kBAAkB,EACvB,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Speech-to-Text adapters
|
|
3
3
|
*/
|
|
4
|
-
export { BrowserSpeechSTTAdapter } from './browser-speech.js';
|
|
4
|
+
export { BrowserSpeechSTTAdapter, } from './browser-speech.js';
|
|
5
|
+
export { createSttDictationSource } from './dictation-source.js';
|
|
5
6
|
export * from './factory.js';
|
|
6
7
|
export * from './types.js';
|
|
7
8
|
export { WhisperCppSTTAdapter } from './whisper-cpp.js';
|
|
@@ -8,7 +8,19 @@
|
|
|
8
8
|
* - Accept filter (mime types / extensions)
|
|
9
9
|
* - Multiple file support
|
|
10
10
|
* - Max size validation
|
|
11
|
+
* - Native form posting via `name` (the accepted list is mirrored onto the
|
|
12
|
+
* real input with `DataTransfer`, so picks, drops and removals post exactly
|
|
13
|
+
* what the list shows), `required` validation, and camera `capture`
|
|
14
|
+
* - A reset of the owning form (`form.reset()`, a reset button, SvelteKit
|
|
15
|
+
* `enhance`'s `update()` after a success) empties the list, as it empties a
|
|
16
|
+
* native file input, and reports it through `onchange([])`
|
|
11
17
|
* - Material 3 styling
|
|
18
|
+
*
|
|
19
|
+
* Fallback: where the `DataTransfer` constructor is unavailable (Safari < 14.1,
|
|
20
|
+
* jsdom), the input keeps the browser's own selection only while it matches the
|
|
21
|
+
* accepted list exactly; otherwise it is cleared. A native submit then never
|
|
22
|
+
* posts a file the list does not show, but drops, accumulated picks and
|
|
23
|
+
* partial removals post nothing until the next matching pick.
|
|
12
24
|
*/
|
|
13
25
|
|
|
14
26
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
@@ -23,7 +35,7 @@ export interface Props {
|
|
|
23
35
|
multiple?: boolean;
|
|
24
36
|
/** Selected files (bindable) */
|
|
25
37
|
files?: File[];
|
|
26
|
-
/** Callback when files change */
|
|
38
|
+
/** Callback when files change, including the empty list after a form reset */
|
|
27
39
|
onchange?: (files: File[]) => void;
|
|
28
40
|
/** Maximum file size in bytes */
|
|
29
41
|
maxSize?: number;
|
|
@@ -33,6 +45,12 @@ export interface Props {
|
|
|
33
45
|
label?: string;
|
|
34
46
|
/** Hint text shown below the label */
|
|
35
47
|
hint?: string;
|
|
48
|
+
/** Form field name; the accepted files post under it in a native `multipart/form-data` submit */
|
|
49
|
+
name?: string;
|
|
50
|
+
/** Camera hint forwarded to the input: `environment` (rear) or `user` (front) */
|
|
51
|
+
capture?: 'environment' | 'user';
|
|
52
|
+
/** Require at least one file for native form validation */
|
|
53
|
+
required?: boolean;
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
let {
|
|
@@ -44,12 +62,61 @@ let {
|
|
|
44
62
|
disabled = false,
|
|
45
63
|
label = 'Drag and drop files here',
|
|
46
64
|
hint = 'or click to browse',
|
|
65
|
+
name,
|
|
66
|
+
capture,
|
|
67
|
+
required = false,
|
|
47
68
|
}: Props = $props();
|
|
48
69
|
|
|
49
70
|
let isDragging = $state(false);
|
|
50
71
|
let error = $state<string | null>(null);
|
|
51
72
|
let inputRef = $state<HTMLInputElement | null>(null);
|
|
52
73
|
|
|
74
|
+
// Mirror the accepted list onto the real input so a native form submit posts
|
|
75
|
+
// exactly what the list shows (picks, drops, removals, and external resets of
|
|
76
|
+
// the bound `files`). Assigning `input.files` fires no change event.
|
|
77
|
+
$effect(() => {
|
|
78
|
+
const input = inputRef;
|
|
79
|
+
const current = files;
|
|
80
|
+
if (!input) return;
|
|
81
|
+
if (typeof DataTransfer === 'function') {
|
|
82
|
+
try {
|
|
83
|
+
const transfer = new DataTransfer();
|
|
84
|
+
for (const file of current) transfer.items.add(file);
|
|
85
|
+
input.files = transfer.files;
|
|
86
|
+
return;
|
|
87
|
+
} catch {
|
|
88
|
+
// Constructor present but unusable; fall through to the fallback.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const native = input.files ? Array.from(input.files) : [];
|
|
92
|
+
const matches =
|
|
93
|
+
native.length === current.length &&
|
|
94
|
+
native.every((file, i) => file === current[i]);
|
|
95
|
+
if (!matches) input.value = '';
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// The owning form reset: the browser empties the native input, so the list
|
|
99
|
+
// must empty too or it keeps showing files that no longer post. `reset` fires
|
|
100
|
+
// before the browser resets the controls; clearing `files` makes the mirror
|
|
101
|
+
// effect above assign an empty list, so the two agree instead of fighting
|
|
102
|
+
// (and a cancelled reset still leaves list and input both empty). The capture
|
|
103
|
+
// phase on `document` reads the input's form at dispatch time, so moving the
|
|
104
|
+
// input between forms needs no re-wiring and no page listener can hide it.
|
|
105
|
+
$effect(() => {
|
|
106
|
+
if (typeof document === 'undefined') return;
|
|
107
|
+
const onReset = (event: Event) => {
|
|
108
|
+
const form = inputRef?.form;
|
|
109
|
+
if (!form || event.target !== form) return;
|
|
110
|
+
const hadFiles = files.length > 0;
|
|
111
|
+
error = null;
|
|
112
|
+
isDragging = false;
|
|
113
|
+
files = [];
|
|
114
|
+
if (hadFiles) onchange?.(files);
|
|
115
|
+
};
|
|
116
|
+
document.addEventListener('reset', onReset, true);
|
|
117
|
+
return () => document.removeEventListener('reset', onReset, true);
|
|
118
|
+
});
|
|
119
|
+
|
|
53
120
|
function handleDragEnter(e: DragEvent) {
|
|
54
121
|
e.preventDefault();
|
|
55
122
|
if (!disabled) isDragging = true;
|
|
@@ -79,10 +146,16 @@ function handleFileSelect(e: Event) {
|
|
|
79
146
|
const input = e.target as HTMLInputElement;
|
|
80
147
|
if (input.files) {
|
|
81
148
|
addFiles(Array.from(input.files));
|
|
82
|
-
input.value = '';
|
|
83
149
|
}
|
|
84
150
|
}
|
|
85
151
|
|
|
152
|
+
function handleInvalid(e: Event) {
|
|
153
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
154
|
+
// The input is visually hidden and aria-hidden, so announce the native
|
|
155
|
+
// validation failure through the live error region as well.
|
|
156
|
+
error = input.validationMessage || t(M['ui.file_upload.required']);
|
|
157
|
+
}
|
|
158
|
+
|
|
86
159
|
function handleZoneClick() {
|
|
87
160
|
if (!disabled) inputRef?.click();
|
|
88
161
|
}
|
|
@@ -127,18 +200,24 @@ function addFiles(newFiles: File[]) {
|
|
|
127
200
|
// Validate accepted types
|
|
128
201
|
const rejected = filtered.filter((f) => !isFileAccepted(f));
|
|
129
202
|
if (rejected.length > 0) {
|
|
130
|
-
errorMessages.push(
|
|
203
|
+
errorMessages.push(
|
|
204
|
+
t(M['ui.file_upload.rejected_type'], { count: rejected.length }),
|
|
205
|
+
);
|
|
131
206
|
filtered = filtered.filter((f) => isFileAccepted(f));
|
|
132
207
|
}
|
|
133
208
|
|
|
134
209
|
// Validate max size
|
|
135
|
-
|
|
136
|
-
|
|
210
|
+
const limit = maxSize;
|
|
211
|
+
if (limit) {
|
|
212
|
+
const oversized = filtered.filter((f) => f.size > limit);
|
|
137
213
|
if (oversized.length > 0) {
|
|
138
214
|
errorMessages.push(
|
|
139
|
-
|
|
215
|
+
t(M['ui.file_upload.exceeds_max_size'], {
|
|
216
|
+
count: oversized.length,
|
|
217
|
+
size: formatFileSize(limit),
|
|
218
|
+
}),
|
|
140
219
|
);
|
|
141
|
-
filtered = filtered.filter((f) => f.size <=
|
|
220
|
+
filtered = filtered.filter((f) => f.size <= limit);
|
|
142
221
|
}
|
|
143
222
|
}
|
|
144
223
|
|
|
@@ -192,16 +271,26 @@ function formatFileSize(bytes: number): string {
|
|
|
192
271
|
</div>
|
|
193
272
|
<p class="drop-zone__label">{label}</p>
|
|
194
273
|
<p class="drop-zone__hint">{hint}</p>
|
|
195
|
-
<input
|
|
196
|
-
bind:this={inputRef}
|
|
197
|
-
type="file"
|
|
198
|
-
{accept}
|
|
199
|
-
{multiple}
|
|
200
|
-
onchange={handleFileSelect}
|
|
201
|
-
hidden
|
|
202
|
-
{disabled}
|
|
203
|
-
/>
|
|
204
274
|
</div>
|
|
275
|
+
<!-- Visually hidden rather than `hidden`: a hidden control cannot be focused,
|
|
276
|
+
so the browser could not show its `required` validation bubble and would
|
|
277
|
+
silently block the submit. The drop zone stays the one control exposed
|
|
278
|
+
to assistive tech and the tab order. -->
|
|
279
|
+
<input
|
|
280
|
+
bind:this={inputRef}
|
|
281
|
+
class="file-upload__input"
|
|
282
|
+
type="file"
|
|
283
|
+
{name}
|
|
284
|
+
{accept}
|
|
285
|
+
{multiple}
|
|
286
|
+
{capture}
|
|
287
|
+
{required}
|
|
288
|
+
{disabled}
|
|
289
|
+
tabindex="-1"
|
|
290
|
+
aria-hidden="true"
|
|
291
|
+
onchange={handleFileSelect}
|
|
292
|
+
oninvalid={handleInvalid}
|
|
293
|
+
/>
|
|
205
294
|
|
|
206
295
|
{#if error}
|
|
207
296
|
<!-- Live region so async validation/reject errors are announced (C7),
|
|
@@ -235,11 +324,26 @@ function formatFileSize(bytes: number): string {
|
|
|
235
324
|
|
|
236
325
|
<style>
|
|
237
326
|
.file-upload {
|
|
327
|
+
position: relative;
|
|
238
328
|
display: flex;
|
|
239
329
|
flex-direction: column;
|
|
240
330
|
gap: var(--smrt-spacing-3, 0.75rem);
|
|
241
331
|
}
|
|
242
332
|
|
|
333
|
+
.file-upload__input {
|
|
334
|
+
position: absolute;
|
|
335
|
+
inset-block-start: 0;
|
|
336
|
+
inset-inline-start: 0;
|
|
337
|
+
width: 1px;
|
|
338
|
+
height: 1px;
|
|
339
|
+
margin: 0;
|
|
340
|
+
padding: 0;
|
|
341
|
+
border: 0;
|
|
342
|
+
overflow: hidden;
|
|
343
|
+
clip-path: inset(50%);
|
|
344
|
+
white-space: nowrap;
|
|
345
|
+
}
|
|
346
|
+
|
|
243
347
|
.file-upload.disabled {
|
|
244
348
|
opacity: 0.5;
|
|
245
349
|
pointer-events: none;
|
|
@@ -5,7 +5,7 @@ export interface Props {
|
|
|
5
5
|
multiple?: boolean;
|
|
6
6
|
/** Selected files (bindable) */
|
|
7
7
|
files?: File[];
|
|
8
|
-
/** Callback when files change */
|
|
8
|
+
/** Callback when files change, including the empty list after a form reset */
|
|
9
9
|
onchange?: (files: File[]) => void;
|
|
10
10
|
/** Maximum file size in bytes */
|
|
11
11
|
maxSize?: number;
|
|
@@ -15,6 +15,12 @@ export interface Props {
|
|
|
15
15
|
label?: string;
|
|
16
16
|
/** Hint text shown below the label */
|
|
17
17
|
hint?: string;
|
|
18
|
+
/** Form field name; the accepted files post under it in a native `multipart/form-data` submit */
|
|
19
|
+
name?: string;
|
|
20
|
+
/** Camera hint forwarded to the input: `environment` (rear) or `user` (front) */
|
|
21
|
+
capture?: 'environment' | 'user';
|
|
22
|
+
/** Require at least one file for native form validation */
|
|
23
|
+
required?: boolean;
|
|
18
24
|
}
|
|
19
25
|
declare const FileUpload: import("svelte").Component<Props, {}, "files">;
|
|
20
26
|
type FileUpload = ReturnType<typeof FileUpload>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileUpload.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/FileUpload.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FileUpload.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/FileUpload.svelte.ts"],"names":[],"mappings":"AA8BA,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,8EAA8E;IAC9E,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;IACd,iGAAiG;IACjG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC;IACjC,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAwPD,QAAA,MAAM,UAAU,gDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|