@happyvertical/smrt-svelte 0.43.4 → 0.43.5
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 +23 -0
- package/README.md +9 -0
- package/dist/components/forms/AddressInput.svelte +155 -42
- package/dist/components/forms/AddressInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/CheckboxInput.svelte +40 -13
- package/dist/components/forms/CheckboxInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/DateRangeInput.svelte +132 -16
- package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/DateTimeInput.svelte +48 -14
- package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/Form.svelte +599 -70
- package/dist/components/forms/Form.svelte.d.ts +6 -2
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/MeasurementInput.svelte +137 -31
- package/dist/components/forms/MeasurementInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/MoneyInput.svelte +42 -23
- package/dist/components/forms/MoneyInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/NumberInput.svelte +59 -27
- package/dist/components/forms/NumberInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/PhoneInput.svelte +51 -22
- package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/SelectInput.svelte +38 -22
- package/dist/components/forms/SelectInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/TextInput.svelte +32 -18
- package/dist/components/forms/TextInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/TextareaInput.svelte +25 -18
- package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/AddressInput.behavior.test.js +94 -0
- package/dist/components/forms/__tests__/Form.behavior.test.js +1 -1
- package/dist/components/forms/__tests__/Form.extraction.test.js +68 -0
- package/dist/components/forms/__tests__/Form.test.js +28 -0
- package/dist/components/forms/__tests__/Form.webmcp.test.js +2123 -114
- package/dist/components/forms/__tests__/FormMicButton.test.js +0 -1
- package/dist/components/forms/__tests__/async-invalid-field.fixture.svelte +30 -0
- package/dist/components/forms/__tests__/async-invalid-field.fixture.svelte.d.ts +7 -0
- package/dist/components/forms/__tests__/async-invalid-field.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/async-validation-form.fixture.svelte +19 -0
- package/dist/components/forms/__tests__/async-validation-form.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/async-validation-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/async-validation-snapshot-form.fixture.svelte +21 -0
- package/dist/components/forms/__tests__/async-validation-snapshot-form.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/async-validation-snapshot-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/compound-registration-wrapper.fixture.svelte +27 -0
- package/dist/components/forms/__tests__/compound-registration-wrapper.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/compound-registration-wrapper.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/custom-registration-field.fixture.svelte +48 -0
- package/dist/components/forms/__tests__/custom-registration-field.fixture.svelte.d.ts +10 -0
- package/dist/components/forms/__tests__/custom-registration-field.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-extraction-values.fixture.svelte +26 -0
- package/dist/components/forms/__tests__/form-extraction-values.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/form-extraction-values.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-registration-lifecycle.fixture.svelte +52 -0
- package/dist/components/forms/__tests__/form-registration-lifecycle.fixture.svelte.d.ts +16 -0
- package/dist/components/forms/__tests__/form-registration-lifecycle.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-with-fields.fixture.svelte +108 -5
- package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts +34 -0
- package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/form-with-policy-field.fixture.svelte +24 -0
- package/dist/components/forms/__tests__/form-with-policy-field.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/form-with-policy-field.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte +69 -4
- package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte.d.ts +32 -0
- package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/legacy-form-context.fixture.svelte +44 -0
- package/dist/components/forms/__tests__/legacy-form-context.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/legacy-form-context.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/policy-field.fixture.svelte +35 -0
- package/dist/components/forms/__tests__/policy-field.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/policy-field.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/prepare-field-value.d.ts +5 -0
- package/dist/components/forms/prepare-field-value.d.ts.map +1 -0
- package/dist/components/forms/prepare-field-value.js +15 -0
- package/dist/state/form-context.d.ts +28 -6
- package/dist/state/form-context.d.ts.map +1 -1
- package/dist/state/form-context.js +75 -7
- package/dist/web/__tests__/webmcp-composed.child.fixture.svelte +62 -0
- package/dist/web/__tests__/webmcp-composed.child.fixture.svelte.d.ts +8 -0
- package/dist/web/__tests__/webmcp-composed.child.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/webmcp-composed.fixture.svelte +52 -0
- package/dist/web/__tests__/webmcp-composed.fixture.svelte.d.ts +12 -0
- package/dist/web/__tests__/webmcp-composed.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/webmcp-composed.integration.svelte.test.js +247 -0
- package/dist/web/__tests__/webmcp-ssr.fixture.svelte +13 -0
- package/dist/web/__tests__/webmcp-ssr.fixture.svelte.d.ts +19 -0
- package/dist/web/__tests__/webmcp-ssr.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/webmcp-ui.test.js +209 -33
- package/dist/web/__tests__/webmcp.test.js +20 -0
- package/dist/web/webmcp-ui.d.ts.map +1 -1
- package/dist/web/webmcp-ui.js +50 -31
- package/dist/web/webmcp.svelte.d.ts.map +1 -1
- package/dist/web/webmcp.svelte.js +6 -1
- package/package.json +8 -6
package/AGENTS.md
CHANGED
|
@@ -19,6 +19,13 @@ subpath you are editing. This file keeps what holds in every module.
|
|
|
19
19
|
| `src/web/webmcp-provider.ts` | Provider config for generated data/model WebMCP tools: definitions, effect policy, namespace, budget, legacy/canonical filters, and fetcher seams (#2520) | — |
|
|
20
20
|
| `src/web/webmcp-ui.ts` | Fixed, low-cardinality WebMCP adapter over the Provider's mounted form-control and data-surface registries (#2521) | — |
|
|
21
21
|
|
|
22
|
+
The composed WebMCP fixture in
|
|
23
|
+
`src/web/__tests__/webmcp-composed.integration.svelte.test.ts` mounts a real
|
|
24
|
+
Provider, rich Form, DataTable/DataSurface, and a bespoke `useWebMcpTool`
|
|
25
|
+
component intent. Preserve its lifecycle assertions: SSR and missing
|
|
26
|
+
`document.modelContext` are no-ops, mounted tools are fixed-cardinality, and
|
|
27
|
+
all registrations abort when their owning component unmounts.
|
|
28
|
+
|
|
22
29
|
## The UI split — primitive-adoption contract (#1589)
|
|
23
30
|
|
|
24
31
|
SMRT's shared UI primitives are split by concern: **`smrt-ui` owns the
|
|
@@ -55,6 +62,17 @@ migration; flips strict per package as it adopts the primitives).
|
|
|
55
62
|
|
|
56
63
|
Wraps app in `+layout.svelte`. Provides auth state, permissions, WebSocket, and AI capabilities.
|
|
57
64
|
|
|
65
|
+
The rich `Form` uses smrt-ui's shared staged-edit review contract. Agent
|
|
66
|
+
proposals never mutate field bindings directly: they remain staged until a
|
|
67
|
+
human applies or discards them in the form's local review surface. Keep this
|
|
68
|
+
state machine in smrt-ui; smrt-svelte contributes Provider-backed field
|
|
69
|
+
registration and consumes the shared component rather than duplicating it.
|
|
70
|
+
The generated WebMCP form tool proposes registry stages; it never writes or
|
|
71
|
+
submits field values directly.
|
|
72
|
+
Rich `FieldDefinition` registrations may carry a `subject` for record-qualified
|
|
73
|
+
identity. The Form bridge also folds live DOM disabled/read-only state into the
|
|
74
|
+
registry and omits those fields from WebMCP schemas and staging.
|
|
75
|
+
|
|
58
76
|
```svelte
|
|
59
77
|
<script>
|
|
60
78
|
let { data, children } = $props();
|
|
@@ -100,6 +118,11 @@ adapter for a prefix; configure distinct prefixes for intentional coexistence.
|
|
|
100
118
|
|
|
101
119
|
## AI System
|
|
102
120
|
|
|
121
|
+
- Rich Form WebMCP tools only stage review proposals. Structured payloads are
|
|
122
|
+
limited to published schema properties, money uses safe integer minor units,
|
|
123
|
+
and injected legacy registries without `executeBatch` use ordered `execute`
|
|
124
|
+
calls.
|
|
125
|
+
|
|
103
126
|
- **Preload strategies**: `none`, `eager`, `idle` (recommended), `on-visible`
|
|
104
127
|
- **Warm client cache**: module-level Map survives navigation/remounts -- avoids re-downloading WASM/models
|
|
105
128
|
- **Adapters**: STT (browser-speech, whisper-cpp, whisper-wasm), TTS (browser-synthesis), LLM (webllm, transformers-llm)
|
package/README.md
CHANGED
|
@@ -156,6 +156,15 @@ hidden data-surface columns are not serialized. Read responses are marked as
|
|
|
156
156
|
untrusted content. Bespoke `useWebMcpTool` and `<Form webmcp>` tools retain their
|
|
157
157
|
existing lifecycle and submit behavior.
|
|
158
158
|
|
|
159
|
+
Custom rich fields may continue to call `registerField(field)` and later
|
|
160
|
+
`unregisterField(name)`. New code should retain and invoke the disposer returned
|
|
161
|
+
by `registerField`: it is bound to that exact registration, so cleanup cannot
|
|
162
|
+
remove a same-name replacement. The return value is additive; legacy form
|
|
163
|
+
contexts whose `registerField` returns `void` remain supported. Context accessors
|
|
164
|
+
bind legacy name-based cleanup to registrations made by that caller, so
|
|
165
|
+
overlapping same-name fields can unmount in either order without retaining a
|
|
166
|
+
detached control.
|
|
167
|
+
|
|
159
168
|
### Form Components
|
|
160
169
|
|
|
161
170
|
```svelte
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
3
|
-
import {
|
|
3
|
+
import { untrack } from 'svelte';
|
|
4
4
|
import { useAppState } from '../../hooks/useAppState.svelte.js';
|
|
5
5
|
import { M } from '../../i18n/strings.forms.js';
|
|
6
6
|
import {
|
|
7
7
|
type FieldDefinition,
|
|
8
8
|
tryGetFormContext,
|
|
9
9
|
} from '../../state/form-context.js';
|
|
10
|
+
import {
|
|
11
|
+
invalidStagedValue,
|
|
12
|
+
prepareTextFieldValue,
|
|
13
|
+
} from './prepare-field-value.js';
|
|
10
14
|
import type { AddressValue } from './types.js';
|
|
11
15
|
|
|
12
16
|
const { t } = useI18n();
|
|
@@ -80,8 +84,66 @@ let {
|
|
|
80
84
|
onchange,
|
|
81
85
|
}: Props = $props();
|
|
82
86
|
|
|
87
|
+
function countryValues(): string[] {
|
|
88
|
+
return [...new Set(countries.map((option) => option.value))];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function provinceValues(): string[] {
|
|
92
|
+
return [
|
|
93
|
+
...new Set([
|
|
94
|
+
...(!required ? [''] : []),
|
|
95
|
+
...provinces.map((option) => option.value),
|
|
96
|
+
]),
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function schemaForField(field: AddressField): Record<string, unknown> {
|
|
101
|
+
if (field === 'country') return { type: 'string', enum: countryValues() };
|
|
102
|
+
if (field === 'province') return { type: 'string', enum: provinceValues() };
|
|
103
|
+
return { type: 'string' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validatesAddressMembers(candidate: unknown): boolean {
|
|
107
|
+
if (typeof candidate !== 'object' || Array.isArray(candidate)) return false;
|
|
108
|
+
const address = candidate as Record<string, unknown>;
|
|
109
|
+
if (
|
|
110
|
+
![Object.prototype, null].includes(Object.getPrototypeOf(address)) ||
|
|
111
|
+
!Object.keys(address).every((key) =>
|
|
112
|
+
fields.includes(key as AddressField),
|
|
113
|
+
) ||
|
|
114
|
+
!fields.every(
|
|
115
|
+
(field) =>
|
|
116
|
+
!Object.hasOwn(address, field) || typeof address[field] === 'string',
|
|
117
|
+
)
|
|
118
|
+
) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
(Object.hasOwn(address, 'country') &&
|
|
123
|
+
!countryValues().includes(address.country as string)) ||
|
|
124
|
+
(Object.hasOwn(address, 'province') &&
|
|
125
|
+
!provinceValues().includes(address.province as string))
|
|
126
|
+
) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function validatesAddressCandidate(candidate: unknown): boolean {
|
|
133
|
+
if (candidate === null || candidate === undefined) return !required;
|
|
134
|
+
if (!validatesAddressMembers(candidate)) return false;
|
|
135
|
+
if (!required) return true;
|
|
136
|
+
const address = candidate as Record<string, unknown>;
|
|
137
|
+
return fields.every(
|
|
138
|
+
(field) =>
|
|
139
|
+
typeof address[field] === 'string' && address[field].trim().length > 0,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
83
143
|
const app = useAppState();
|
|
84
144
|
const formContext = tryGetFormContext();
|
|
145
|
+
const fieldOwnerId = $props.id();
|
|
146
|
+
const fieldOwnerToken = `smrt-rich-field-${fieldOwnerId}`;
|
|
85
147
|
|
|
86
148
|
const isSmrt = $derived(app.state.mode === 'smrt');
|
|
87
149
|
|
|
@@ -90,7 +152,7 @@ let street = $state(value.street ?? '');
|
|
|
90
152
|
let city = $state(value.city ?? '');
|
|
91
153
|
let province = $state(value.province ?? '');
|
|
92
154
|
let postalCode = $state(value.postalCode ?? '');
|
|
93
|
-
let country = $state(value.country ?? '
|
|
155
|
+
let country = $state(value.country ?? untrack(() => countries[0]?.value ?? ''));
|
|
94
156
|
|
|
95
157
|
// Keep in sync with external value changes
|
|
96
158
|
$effect(() => {
|
|
@@ -105,13 +167,18 @@ $effect(() => {
|
|
|
105
167
|
country = value.country;
|
|
106
168
|
});
|
|
107
169
|
|
|
108
|
-
function
|
|
170
|
+
function currentValue(): Partial<AddressValue> {
|
|
109
171
|
const newValue: Partial<AddressValue> = {};
|
|
110
172
|
if (fields.includes('street')) newValue.street = street;
|
|
111
173
|
if (fields.includes('city')) newValue.city = city;
|
|
112
174
|
if (fields.includes('province')) newValue.province = province;
|
|
113
175
|
if (fields.includes('postalCode')) newValue.postalCode = postalCode;
|
|
114
176
|
if (fields.includes('country')) newValue.country = country;
|
|
177
|
+
return newValue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function updateValue() {
|
|
181
|
+
const newValue = currentValue();
|
|
115
182
|
value = newValue;
|
|
116
183
|
onchange?.(newValue);
|
|
117
184
|
}
|
|
@@ -141,11 +208,16 @@ function parseSpokenAddress(text: string): Partial<AddressValue> {
|
|
|
141
208
|
}
|
|
142
209
|
}
|
|
143
210
|
|
|
144
|
-
// Try to extract country
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
211
|
+
// Try to extract a currently configured country by its label or value.
|
|
212
|
+
for (const option of countries) {
|
|
213
|
+
const matches = [option.label, option.value].some((candidate) => {
|
|
214
|
+
const escaped = candidate.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
215
|
+
return new RegExp(`(^|\\W)${escaped}(?=$|\\W)`, 'i').test(normalized);
|
|
216
|
+
});
|
|
217
|
+
if (matches) {
|
|
218
|
+
result.country = option.value;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
149
221
|
}
|
|
150
222
|
|
|
151
223
|
// Common city extraction patterns
|
|
@@ -192,26 +264,33 @@ const primaryFieldId = $derived.by(() => {
|
|
|
192
264
|
});
|
|
193
265
|
|
|
194
266
|
// Register with form context
|
|
195
|
-
|
|
267
|
+
$effect(() => {
|
|
196
268
|
if (formContext) {
|
|
269
|
+
const registeredName = name;
|
|
197
270
|
const fieldDef: FieldDefinition = {
|
|
198
|
-
name,
|
|
271
|
+
name: registeredName,
|
|
272
|
+
ownerToken: fieldOwnerToken,
|
|
199
273
|
type: 'address',
|
|
200
|
-
label
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
274
|
+
get label() {
|
|
275
|
+
return label;
|
|
276
|
+
},
|
|
277
|
+
get description() {
|
|
278
|
+
return (
|
|
279
|
+
description ||
|
|
280
|
+
'A mailing address with street, city, province, postal code, and country'
|
|
281
|
+
);
|
|
282
|
+
},
|
|
204
283
|
setValue: (v: unknown) => {
|
|
205
284
|
if (v === null || v === undefined) {
|
|
206
285
|
street = '';
|
|
207
286
|
city = '';
|
|
208
287
|
province = '';
|
|
209
288
|
postalCode = '';
|
|
210
|
-
country = '
|
|
289
|
+
country = countries[0]?.value ?? '';
|
|
211
290
|
updateValue();
|
|
212
291
|
return;
|
|
213
292
|
}
|
|
214
|
-
if (
|
|
293
|
+
if (validatesAddressMembers(v)) {
|
|
215
294
|
const addr = v as Partial<AddressValue>;
|
|
216
295
|
if (addr.street !== undefined) street = addr.street;
|
|
217
296
|
if (addr.city !== undefined) city = addr.city;
|
|
@@ -222,36 +301,70 @@ onMount(() => {
|
|
|
222
301
|
return;
|
|
223
302
|
}
|
|
224
303
|
// Try to parse spoken address
|
|
225
|
-
const parsed =
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (parsed.
|
|
304
|
+
const parsed = Object.fromEntries(
|
|
305
|
+
Object.entries(parseSpokenAddress(String(v))).filter(([field]) =>
|
|
306
|
+
fields.includes(field as AddressField),
|
|
307
|
+
),
|
|
308
|
+
) as Partial<AddressValue>;
|
|
309
|
+
if (Object.keys(parsed).length > 0 && validatesAddressMembers(parsed)) {
|
|
310
|
+
if (parsed.street !== undefined) street = parsed.street;
|
|
311
|
+
if (parsed.city !== undefined) city = parsed.city;
|
|
312
|
+
if (parsed.province !== undefined) province = parsed.province;
|
|
313
|
+
if (parsed.postalCode !== undefined) postalCode = parsed.postalCode;
|
|
314
|
+
if (parsed.country !== undefined) country = parsed.country;
|
|
232
315
|
updateValue();
|
|
233
316
|
}
|
|
234
317
|
},
|
|
235
|
-
getValue:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
318
|
+
getValue: currentValue,
|
|
319
|
+
prepareValue: (candidate) => {
|
|
320
|
+
if (candidate === null || candidate === undefined) {
|
|
321
|
+
return Object.fromEntries(
|
|
322
|
+
fields.map((field) => [
|
|
323
|
+
field,
|
|
324
|
+
field === 'country' ? (countries[0]?.value ?? '') : '',
|
|
325
|
+
]),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (typeof candidate === 'object' && !Array.isArray(candidate)) {
|
|
329
|
+
return candidate;
|
|
330
|
+
}
|
|
331
|
+
const parsed = parseSpokenAddress(prepareTextFieldValue(candidate));
|
|
332
|
+
const configured = Object.fromEntries(
|
|
333
|
+
Object.entries(parsed).filter(([field]) =>
|
|
334
|
+
fields.includes(field as AddressField),
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
return Object.keys(configured).length > 0
|
|
338
|
+
? { ...currentValue(), ...configured }
|
|
339
|
+
: invalidStagedValue();
|
|
340
|
+
},
|
|
341
|
+
clear: () => {
|
|
342
|
+
street = '';
|
|
343
|
+
city = '';
|
|
344
|
+
province = '';
|
|
345
|
+
postalCode = '';
|
|
346
|
+
country = countries[0]?.value ?? '';
|
|
347
|
+
updateValue();
|
|
348
|
+
return true;
|
|
349
|
+
},
|
|
350
|
+
getState: () => ({ disabled }),
|
|
351
|
+
get constraints() {
|
|
352
|
+
return { required };
|
|
353
|
+
},
|
|
354
|
+
get webMcpSchema() {
|
|
355
|
+
return {
|
|
356
|
+
type: 'object',
|
|
357
|
+
additionalProperties: false,
|
|
358
|
+
properties: Object.fromEntries(
|
|
359
|
+
fields.map((field) => [field, schemaForField(field)]),
|
|
360
|
+
),
|
|
361
|
+
...(required ? { required: [...fields] } : {}),
|
|
362
|
+
};
|
|
243
363
|
},
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
fields.every((field) => String(value[field] ?? '').trim().length > 0),
|
|
364
|
+
validateValue: validatesAddressCandidate,
|
|
365
|
+
validate: () => validatesAddressCandidate(currentValue()),
|
|
247
366
|
};
|
|
248
|
-
formContext.registerField(fieldDef);
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
onDestroy(() => {
|
|
253
|
-
if (formContext) {
|
|
254
|
-
formContext.unregisterField(name);
|
|
367
|
+
return formContext.registerField(fieldDef);
|
|
255
368
|
}
|
|
256
369
|
});
|
|
257
370
|
|
|
@@ -281,7 +394,7 @@ function handleCountryChange(e: Event) {
|
|
|
281
394
|
}
|
|
282
395
|
</script>
|
|
283
396
|
|
|
284
|
-
<div class="smrt-address">
|
|
397
|
+
<div class="smrt-address" data-smrt-field-owner={fieldOwnerToken}>
|
|
285
398
|
{#if label}
|
|
286
399
|
<label class="smrt-label" for={primaryFieldId}>
|
|
287
400
|
{label}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AddressInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/AddressInput.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AddressInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/AddressInput.svelte.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,KAAK,YAAY,GAAG,MAAM,YAAY,CAAC;AAEvC,MAAM,WAAW,KAAK;IACpB,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9B,2BAA2B;IAC3B,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IACxB,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,mCAAmC;IACnC,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,0CAA0C;IAC1C,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;CACnD;AAibD,QAAA,MAAM,YAAY,gDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { ripple } from '@happyvertical/smrt-ui';
|
|
3
3
|
import { parseSpokenBoolean } from '@happyvertical/smrt-ui/utils/forms/formatters.js';
|
|
4
|
-
import { onDestroy, onMount } from 'svelte';
|
|
5
4
|
import { useAppState } from '../../hooks/useAppState.svelte.js';
|
|
6
5
|
import {
|
|
7
6
|
type FieldDefinition,
|
|
8
7
|
tryGetFormContext,
|
|
9
8
|
} from '../../state/form-context.js';
|
|
9
|
+
import { invalidStagedValue } from './prepare-field-value.js';
|
|
10
10
|
|
|
11
11
|
export interface Props {
|
|
12
12
|
/** Field name */
|
|
@@ -46,13 +46,18 @@ function updateValue(newValue: boolean) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Register with form context
|
|
49
|
-
|
|
49
|
+
$effect(() => {
|
|
50
50
|
if (formContext) {
|
|
51
|
+
const registeredName = name;
|
|
51
52
|
const fieldDef: FieldDefinition = {
|
|
52
|
-
name,
|
|
53
|
+
name: registeredName,
|
|
53
54
|
type: 'checkbox',
|
|
54
|
-
label
|
|
55
|
-
|
|
55
|
+
get label() {
|
|
56
|
+
return label;
|
|
57
|
+
},
|
|
58
|
+
get description() {
|
|
59
|
+
return description || `Yes or no for ${label || name}`;
|
|
60
|
+
},
|
|
56
61
|
setValue: (v: unknown) => {
|
|
57
62
|
if (typeof v === 'boolean') {
|
|
58
63
|
updateValue(v);
|
|
@@ -64,16 +69,38 @@ onMount(() => {
|
|
|
64
69
|
}
|
|
65
70
|
},
|
|
66
71
|
getValue: () => checked,
|
|
67
|
-
|
|
72
|
+
prepareValue: (candidate) => {
|
|
73
|
+
if (typeof candidate === 'boolean') return candidate;
|
|
74
|
+
if (
|
|
75
|
+
candidate !== null &&
|
|
76
|
+
candidate !== undefined &&
|
|
77
|
+
typeof candidate !== 'string' &&
|
|
78
|
+
typeof candidate !== 'number'
|
|
79
|
+
) {
|
|
80
|
+
return invalidStagedValue();
|
|
81
|
+
}
|
|
82
|
+
return (
|
|
83
|
+
parseSpokenBoolean(String(candidate ?? '')) ?? invalidStagedValue()
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
clear: () => {
|
|
87
|
+
updateValue(false);
|
|
88
|
+
return true;
|
|
89
|
+
},
|
|
90
|
+
getState: () => ({ disabled }),
|
|
91
|
+
get constraints() {
|
|
92
|
+
return { required };
|
|
93
|
+
},
|
|
94
|
+
validateValue: (candidate) => {
|
|
95
|
+
const next =
|
|
96
|
+
typeof candidate === 'boolean'
|
|
97
|
+
? candidate
|
|
98
|
+
: parseSpokenBoolean(String(candidate ?? ''));
|
|
99
|
+
return next !== null && (!required || next);
|
|
100
|
+
},
|
|
68
101
|
validate: () => !required || checked,
|
|
69
102
|
};
|
|
70
|
-
formContext.registerField(fieldDef);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
onDestroy(() => {
|
|
75
|
-
if (formContext) {
|
|
76
|
-
formContext.unregisterField(name);
|
|
103
|
+
return formContext.registerField(fieldDef);
|
|
77
104
|
}
|
|
78
105
|
});
|
|
79
106
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CheckboxInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/CheckboxInput.svelte.ts"],"names":[],"mappings":"AAaA,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,+BAA+B;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC;
|
|
1
|
+
{"version":3,"file":"CheckboxInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/CheckboxInput.svelte.ts"],"names":[],"mappings":"AAaA,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,+BAA+B;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC;AAsHD,QAAA,MAAM,aAAa,kDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
3
|
-
import { onDestroy, onMount } from 'svelte';
|
|
4
3
|
import { useAppState } from '../../hooks/useAppState.svelte.js';
|
|
5
4
|
import { useSTT } from '../../hooks/useSTT.svelte.js';
|
|
6
5
|
import { M } from '../../i18n/strings.forms.js';
|
|
@@ -9,6 +8,7 @@ import {
|
|
|
9
8
|
type FieldDefinition,
|
|
10
9
|
tryGetFormContext,
|
|
11
10
|
} from '../../state/form-context.js';
|
|
11
|
+
import { invalidStagedValue } from './prepare-field-value.js';
|
|
12
12
|
import type { DateRangeValue } from './types.js';
|
|
13
13
|
|
|
14
14
|
const { t } = useI18n();
|
|
@@ -61,6 +61,8 @@ let {
|
|
|
61
61
|
const app = useAppState();
|
|
62
62
|
const stt = useSTT();
|
|
63
63
|
const formContext = tryGetFormContext();
|
|
64
|
+
const fieldOwnerId = $props.id();
|
|
65
|
+
const fieldOwnerToken = `smrt-rich-field-${fieldOwnerId}`;
|
|
64
66
|
|
|
65
67
|
const isSmrt = $derived(app.state.mode === 'smrt');
|
|
66
68
|
|
|
@@ -68,6 +70,7 @@ let isRecording = $state(false);
|
|
|
68
70
|
let isParsing = $state(false);
|
|
69
71
|
let parseError = $state<string | null>(null);
|
|
70
72
|
let recordingStartTime = 0;
|
|
73
|
+
let primaryControl = $state<HTMLElement | null>(null);
|
|
71
74
|
|
|
72
75
|
const MIN_HOLD_TIME = 500;
|
|
73
76
|
const MIN_TRANSCRIPT_LENGTH = 2;
|
|
@@ -176,16 +179,37 @@ function formatToISO(date: Date): string {
|
|
|
176
179
|
return `${year}-${month}-${day}`;
|
|
177
180
|
}
|
|
178
181
|
|
|
182
|
+
function isCanonicalCalendarDate(value: string): boolean {
|
|
183
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
184
|
+
if (!match) return false;
|
|
185
|
+
const year = Number(match[1]);
|
|
186
|
+
const month = Number(match[2]);
|
|
187
|
+
const day = Number(match[3]);
|
|
188
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
189
|
+
return (
|
|
190
|
+
date.getUTCFullYear() === year &&
|
|
191
|
+
date.getUTCMonth() === month - 1 &&
|
|
192
|
+
date.getUTCDate() === day
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
179
196
|
// Register with form context
|
|
180
|
-
|
|
197
|
+
$effect(() => {
|
|
181
198
|
if (formContext) {
|
|
199
|
+
const registeredName = name;
|
|
182
200
|
const fieldDef: FieldDefinition = {
|
|
183
|
-
name,
|
|
201
|
+
name: registeredName,
|
|
202
|
+
ownerToken: fieldOwnerToken,
|
|
184
203
|
type: 'daterange',
|
|
185
|
-
label
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
204
|
+
get label() {
|
|
205
|
+
return label;
|
|
206
|
+
},
|
|
207
|
+
get description() {
|
|
208
|
+
return (
|
|
209
|
+
description ||
|
|
210
|
+
'A date range with start and end dates (say "from [start] to [end]")'
|
|
211
|
+
);
|
|
212
|
+
},
|
|
189
213
|
setValue: (v: unknown) => {
|
|
190
214
|
if (v === null || v === undefined) {
|
|
191
215
|
updateValue('', '');
|
|
@@ -206,17 +230,102 @@ onMount(() => {
|
|
|
206
230
|
.catch(() => {});
|
|
207
231
|
},
|
|
208
232
|
getValue: () => ({ startDate, endDate }),
|
|
209
|
-
|
|
233
|
+
prepareValue: (candidate) => {
|
|
234
|
+
if (candidate === null || candidate === undefined) {
|
|
235
|
+
return { startDate: '', endDate: '' };
|
|
236
|
+
}
|
|
237
|
+
if (
|
|
238
|
+
typeof candidate !== 'object' ||
|
|
239
|
+
Array.isArray(candidate) ||
|
|
240
|
+
![Object.prototype, null].includes(Object.getPrototypeOf(candidate))
|
|
241
|
+
) {
|
|
242
|
+
return invalidStagedValue();
|
|
243
|
+
}
|
|
244
|
+
return candidate;
|
|
245
|
+
},
|
|
246
|
+
prepareExtractedValue: async (candidate) => {
|
|
247
|
+
if (typeof candidate !== 'string') {
|
|
248
|
+
return invalidStagedValue();
|
|
249
|
+
}
|
|
250
|
+
const parsed = await parseNaturalLanguageRange(candidate);
|
|
251
|
+
if (!parsed) {
|
|
252
|
+
return invalidStagedValue();
|
|
253
|
+
}
|
|
254
|
+
return { startDate: parsed.start, endDate: parsed.end };
|
|
255
|
+
},
|
|
256
|
+
clear: () => {
|
|
257
|
+
updateValue('', '');
|
|
258
|
+
return true;
|
|
259
|
+
},
|
|
260
|
+
getState: () => ({
|
|
261
|
+
disabled: disabled || primaryControl?.matches(':disabled') === true,
|
|
262
|
+
}),
|
|
263
|
+
get constraints() {
|
|
264
|
+
return { required };
|
|
265
|
+
},
|
|
266
|
+
focus: () => primaryControl?.focus(),
|
|
267
|
+
get webMcpSchema() {
|
|
268
|
+
return {
|
|
269
|
+
type: 'object',
|
|
270
|
+
additionalProperties: false,
|
|
271
|
+
properties: {
|
|
272
|
+
startDate: {
|
|
273
|
+
type: 'string',
|
|
274
|
+
format: 'date',
|
|
275
|
+
...(minDate ? { formatMinimum: minDate } : {}),
|
|
276
|
+
...(maxDate ? { formatMaximum: maxDate } : {}),
|
|
277
|
+
},
|
|
278
|
+
endDate: {
|
|
279
|
+
type: 'string',
|
|
280
|
+
format: 'date',
|
|
281
|
+
...(minDate ? { formatMinimum: minDate } : {}),
|
|
282
|
+
...(maxDate ? { formatMaximum: maxDate } : {}),
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
...(required ? { required: ['startDate', 'endDate'] } : {}),
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
validateValue: (candidate) => {
|
|
289
|
+
if (candidate === null || candidate === undefined) return !required;
|
|
290
|
+
if (
|
|
291
|
+
typeof candidate !== 'object' ||
|
|
292
|
+
Array.isArray(candidate) ||
|
|
293
|
+
![Object.prototype, null].includes(Object.getPrototypeOf(candidate))
|
|
294
|
+
) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const range = candidate as Record<string, unknown>;
|
|
298
|
+
if (
|
|
299
|
+
!Object.keys(range).every((key) =>
|
|
300
|
+
['startDate', 'endDate'].includes(key),
|
|
301
|
+
) ||
|
|
302
|
+
!Object.hasOwn(range, 'startDate') ||
|
|
303
|
+
!Object.hasOwn(range, 'endDate') ||
|
|
304
|
+
typeof range.startDate !== 'string' ||
|
|
305
|
+
typeof range.endDate !== 'string'
|
|
306
|
+
) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
const start = range.startDate;
|
|
310
|
+
const end = range.endDate;
|
|
311
|
+
if (required && (!start || !end)) return false;
|
|
312
|
+
if (
|
|
313
|
+
(start && !isCanonicalCalendarDate(start)) ||
|
|
314
|
+
(end && !isCanonicalCalendarDate(end)) ||
|
|
315
|
+
(start && minDate && start < minDate) ||
|
|
316
|
+
(start && maxDate && start > maxDate) ||
|
|
317
|
+
(end && minDate && end < minDate) ||
|
|
318
|
+
(end && maxDate && end > maxDate) ||
|
|
319
|
+
(start && end && start > end)
|
|
320
|
+
) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
},
|
|
210
325
|
validate: () =>
|
|
211
326
|
!required || (startDate.trim().length > 0 && endDate.trim().length > 0),
|
|
212
327
|
};
|
|
213
|
-
formContext.registerField(fieldDef);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
onDestroy(() => {
|
|
218
|
-
if (formContext) {
|
|
219
|
-
formContext.unregisterField(name);
|
|
328
|
+
return formContext.registerField(fieldDef);
|
|
220
329
|
}
|
|
221
330
|
});
|
|
222
331
|
|
|
@@ -336,7 +445,12 @@ function handleMicKeydown(e: KeyboardEvent) {
|
|
|
336
445
|
const primaryControlId = $derived(isSmrt ? `${name}_voice` : `${name}_start`);
|
|
337
446
|
</script>
|
|
338
447
|
|
|
339
|
-
<div
|
|
448
|
+
<div
|
|
449
|
+
class="smrt-daterange"
|
|
450
|
+
class:listening={isRecording}
|
|
451
|
+
class:parsing={isParsing}
|
|
452
|
+
data-smrt-field-owner={fieldOwnerToken}
|
|
453
|
+
>
|
|
340
454
|
{#if label}
|
|
341
455
|
<label class="smrt-label" for={primaryControlId}>
|
|
342
456
|
{label}
|
|
@@ -363,6 +477,7 @@ const primaryControlId = $derived(isSmrt ? `${name}_voice` : `${name}_start`);
|
|
|
363
477
|
</div>
|
|
364
478
|
|
|
365
479
|
<button
|
|
480
|
+
bind:this={primaryControl}
|
|
366
481
|
id={primaryControlId}
|
|
367
482
|
type="button"
|
|
368
483
|
class="mic-btn"
|
|
@@ -395,6 +510,7 @@ const primaryControlId = $derived(isSmrt ? `${name}_voice` : `${name}_start`);
|
|
|
395
510
|
<div class="date-field">
|
|
396
511
|
<label for="{name}_start" class="field-label">Start</label>
|
|
397
512
|
<input
|
|
513
|
+
bind:this={primaryControl}
|
|
398
514
|
id="{name}_start"
|
|
399
515
|
name="{name}[startDate]"
|
|
400
516
|
type="date"
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AAkfD,QAAA,MAAM,cAAc,gEAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|