entasis 0.13.5 → 0.13.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/components/Address/Address.svelte +44 -0
  2. package/dist/components/Address/Address.svelte.d.ts +4 -0
  3. package/dist/components/Address/address.d.ts +99 -0
  4. package/dist/components/Address/address.google.d.ts +89 -0
  5. package/dist/components/Address/address.google.fixtures.json +808 -0
  6. package/dist/components/Address/address.google.js +184 -0
  7. package/dist/components/Address/address.js +109 -0
  8. package/dist/components/Address/address.mcp.d.ts +1 -0
  9. package/dist/components/Address/address.mcp.js +100 -0
  10. package/dist/components/Address/address.props.d.ts +26 -0
  11. package/dist/components/Address/address.props.js +1 -0
  12. package/dist/components/Address/address.provider.d.ts +19 -0
  13. package/dist/components/Address/address.provider.js +69 -0
  14. package/dist/components/Address/address.theme.d.ts +63 -0
  15. package/dist/components/Address/address.theme.js +42 -0
  16. package/dist/components/Address/index.d.ts +6 -0
  17. package/dist/components/Address/index.js +5 -0
  18. package/dist/components/Form/AddressInput/AddressInput.svelte +482 -0
  19. package/dist/components/Form/AddressInput/AddressInput.svelte.d.ts +4 -0
  20. package/dist/components/Form/AddressInput/addressInput.mcp.d.ts +1 -0
  21. package/dist/components/Form/AddressInput/addressInput.mcp.js +110 -0
  22. package/dist/components/Form/AddressInput/addressInput.props.d.ts +72 -0
  23. package/dist/components/Form/AddressInput/addressInput.props.js +1 -0
  24. package/dist/components/Form/AddressInput/addressInput.theme.d.ts +63 -0
  25. package/dist/components/Form/AddressInput/addressInput.theme.js +55 -0
  26. package/dist/components/Form/AddressInput/index.d.ts +3 -0
  27. package/dist/components/Form/AddressInput/index.js +2 -0
  28. package/dist/components/Form/Field/field.d.ts +5 -2
  29. package/dist/components/Form/Field/field.state.svelte.js +4 -0
  30. package/dist/components/Form/Field/index.d.ts +1 -1
  31. package/dist/components/Form/Field/schemas.js +8 -0
  32. package/dist/components/Form/Form/FormInputRenderer.svelte +6 -0
  33. package/dist/components/Form/Form/form.d.ts +6 -0
  34. package/dist/components/Form/Form/form.mcp.d.ts +1 -1
  35. package/dist/components/Form/Form/form.mcp.js +1 -1
  36. package/dist/components/Form/Form/form.state.svelte.d.ts +142 -11
  37. package/dist/components/Form/Form/form.state.svelte.js +2 -0
  38. package/dist/components/Form/Form/visibility.d.ts +186 -20
  39. package/dist/components/Form/MultiStepForm/multiStepForm.state.svelte.d.ts +136 -0
  40. package/dist/components/Form/SignatureInput/SignatureInput.svelte +327 -0
  41. package/dist/components/Form/SignatureInput/SignatureInput.svelte.d.ts +4 -0
  42. package/dist/components/Form/SignatureInput/index.d.ts +4 -0
  43. package/dist/components/Form/SignatureInput/index.js +2 -0
  44. package/dist/components/Form/SignatureInput/signatureInput.mcp.d.ts +1 -0
  45. package/dist/components/Form/SignatureInput/signatureInput.mcp.js +74 -0
  46. package/dist/components/Form/SignatureInput/signatureInput.props.d.ts +29 -0
  47. package/dist/components/Form/SignatureInput/signatureInput.props.js +1 -0
  48. package/dist/components/Form/SignatureInput/signatureInput.strokes.d.ts +77 -0
  49. package/dist/components/Form/SignatureInput/signatureInput.strokes.js +149 -0
  50. package/dist/components/Form/SignatureInput/signatureInput.theme.d.ts +99 -0
  51. package/dist/components/Form/SignatureInput/signatureInput.theme.js +70 -0
  52. package/dist/generated/componentAliases.d.ts +3 -0
  53. package/dist/generated/componentAliases.js +3 -0
  54. package/dist/generated/componentContract.d.ts +46 -1
  55. package/dist/generated/componentContract.js +113 -0
  56. package/dist/generated/componentMcpRegistry.js +6 -0
  57. package/dist/i18n/ar.js +18 -0
  58. package/dist/i18n/de.js +18 -0
  59. package/dist/i18n/en.d.ts +18 -0
  60. package/dist/i18n/en.js +18 -0
  61. package/dist/i18n/es.js +18 -0
  62. package/dist/i18n/fr.js +18 -0
  63. package/dist/i18n/ja.js +18 -0
  64. package/dist/i18n/pt.js +18 -0
  65. package/dist/i18n/zh.js +18 -0
  66. package/package.json +25 -1
@@ -0,0 +1,482 @@
1
+ <script lang="ts">
2
+ import { onDestroy, setContext, untrack } from 'svelte';
3
+ import Button from '../../Button/Button.svelte';
4
+ import { builtInAddressProvider, getAddressProvider } from '../../Address/address.provider.js';
5
+ import {
6
+ COUNTRY_CODES,
7
+ countryName,
8
+ formatAddressLine,
9
+ type Address,
10
+ type AddressField
11
+ } from '../../Address/address.js';
12
+ import Autocomplete from '../Autocomplete/Autocomplete.svelte';
13
+ import type { AutocompleteItemsRequest } from '../Autocomplete/autocomplete.props.js';
14
+ import Combobox from '../Combobox/Combobox.svelte';
15
+ import Field from '../Field/Field.svelte';
16
+ import FieldHiddenInputs from '../Field/FieldHiddenInputs.svelte';
17
+ import { createFieldState } from '../Field/field.state.svelte.js';
18
+ import TextInput from '../TextInput/TextInput.svelte';
19
+ import { useI18n } from '../../../i18n/context.svelte.js';
20
+ import type {
21
+ AddressFieldConfig,
22
+ AddressFieldRow,
23
+ AddressInputProps
24
+ } from './addressInput.props.js';
25
+ import { useAddressInputTheme } from './addressInput.theme.js';
26
+
27
+ const DEFAULT_FIELDS: readonly AddressFieldRow[] = [
28
+ 'line1',
29
+ 'line2',
30
+ [
31
+ { field: 'postalCode', width: 1 },
32
+ { field: 'city', width: 2 }
33
+ ],
34
+ 'country'
35
+ ];
36
+
37
+ let {
38
+ defaultValue = null,
39
+ value = $bindable(),
40
+ errors = $bindable([]),
41
+ focused = $bindable(false),
42
+ required = false,
43
+ disabled,
44
+ readOnly,
45
+ messages,
46
+ name,
47
+ onValidate,
48
+ onValueChange,
49
+ visible,
50
+ size = 'normal',
51
+ density = 'normal',
52
+ labelPosition,
53
+ provider: providerProp,
54
+ fields = DEFAULT_FIELDS,
55
+ editableFields,
56
+ requiredFields,
57
+ countries,
58
+ defaultCountry,
59
+ layout = 'fields',
60
+ minSearchLength = 3,
61
+ placeholder,
62
+ onResolve,
63
+ i18n,
64
+ label,
65
+ theme,
66
+ fieldAttrs,
67
+ ...rest
68
+ }: AddressInputProps = $props();
69
+ if (value === undefined) value = untrack(() => defaultValue);
70
+
71
+ const id = $props.id();
72
+ const t = $derived(useI18n(i18n));
73
+ const classes = $derived(useAddressInputTheme(theme));
74
+
75
+ const text = (part: string | null | undefined) => (part ?? '').trim();
76
+
77
+ // The layout: rows of parts, each with its settings.
78
+ type Part = AddressFieldConfig & { width: number };
79
+ const rows = $derived.by(() => {
80
+ const seen: AddressField[] = [];
81
+ return fields.map((row) =>
82
+ (Array.isArray(row) ? row : [row]).map((item: AddressField | AddressFieldConfig): Part => {
83
+ const part = typeof item === 'string' ? { field: item, width: 1 } : { width: 1, ...item };
84
+ if (seen.includes(part.field)) {
85
+ throw new TypeError(
86
+ `AddressInput fields list the "${part.field}" part twice. Each part appears once.`
87
+ );
88
+ }
89
+ seen.push(part.field);
90
+ return part;
91
+ })
92
+ );
93
+ });
94
+ const parts = $derived(rows.flat());
95
+ const shownParts = $derived(parts.map((part) => part.field));
96
+ const shown = $derived(new Set(shownParts));
97
+ const editable = $derived(new Set(editableFields ?? shownParts));
98
+ const requiredParts = $derived(
99
+ (
100
+ requiredFields ??
101
+ (required ? shownParts.filter((part) => part !== 'line2' && part !== 'region') : [])
102
+ ).filter((part) => shown.has(part))
103
+ );
104
+
105
+ /** Whether `address` holds anything a person entered (a defaulted country alone does not count). */
106
+ const hasContent = (address: Address | null | undefined) =>
107
+ !!address &&
108
+ shownParts.some((part) => {
109
+ if (part !== 'country') return text(address[part]) !== '';
110
+ const code = text(address.countryCode).toUpperCase();
111
+ if (code) return code !== defaultCountry?.toUpperCase();
112
+ return text(address.country) !== '';
113
+ });
114
+
115
+ const partLabel = (part: AddressField) =>
116
+ parts.find((entry) => entry.field === part)?.label ??
117
+ {
118
+ line1: t.addressLine1,
119
+ line2: t.addressLine2,
120
+ postalCode: t.addressPostalCode,
121
+ city: t.addressCity,
122
+ region: t.addressRegion,
123
+ country: t.addressCountry
124
+ }[part];
125
+
126
+ const missingParts = (address: Address | null | undefined) =>
127
+ hasContent(address) ? requiredParts.filter((part) => text(address?.[part]) === '') : [];
128
+
129
+ const field = createFieldState({
130
+ id,
131
+ get value() {
132
+ return value;
133
+ },
134
+ set value(nextValue) {
135
+ value = nextValue;
136
+ },
137
+ get errors() {
138
+ return errors;
139
+ },
140
+ set errors(nextErrors: string[] | boolean) {
141
+ errors = nextErrors;
142
+ },
143
+ get focused() {
144
+ return focused;
145
+ },
146
+ set focused(nextFocused: boolean) {
147
+ focused = nextFocused;
148
+ },
149
+ onValueChange: (nextValue) => onValueChange?.(nextValue),
150
+ get disabled() {
151
+ return disabled;
152
+ },
153
+ set disabled(nextDisabled: boolean | undefined) {
154
+ disabled = nextDisabled;
155
+ },
156
+ get readOnly() {
157
+ return readOnly;
158
+ },
159
+ get messages() {
160
+ return messages;
161
+ },
162
+ get required() {
163
+ return required;
164
+ },
165
+ get name() {
166
+ return name;
167
+ },
168
+ set name(nextName: string | undefined) {
169
+ name = nextName;
170
+ },
171
+ // The parts a started address still misses come first, then the app's own rule.
172
+ onValidate: (address) => {
173
+ const missing = missingParts(address).map((part) => t.addressPartRequired(partLabel(part)));
174
+ if (missing.length) return missing;
175
+ return onValidate?.(address) ?? false;
176
+ },
177
+ get visible() {
178
+ return visible;
179
+ },
180
+ type: 'address'
181
+ });
182
+
183
+ // The search: this field's own `provider`, else the one set for the app, else Google Places
184
+ // through the app's `/api/places` route. `null` at either level turns it off.
185
+ const contextProvider = getAddressProvider();
186
+ const provider = $derived(
187
+ providerProp !== undefined
188
+ ? providerProp
189
+ : contextProvider
190
+ ? contextProvider.provider
191
+ : builtInAddressProvider()
192
+ );
193
+
194
+ // The parts are inputs of their own, but only this field belongs to a surrounding Form: they
195
+ // edit its value, they are not entries of the Form.
196
+ setContext('form', null);
197
+
198
+ const address = $derived<Address>(field.value ?? {});
199
+ const countryCode = $derived(
200
+ text(address.countryCode).toUpperCase() || defaultCountry?.toUpperCase() || ''
201
+ );
202
+
203
+ const commit = (next: Address) => field.setValue(hasContent(next) ? next : null);
204
+
205
+ /** A manual edit: the provider's id, coordinates and one-line address no longer describe it. */
206
+ const edit = (part: AddressField, input: string | null) => {
207
+ const next: Address = {
208
+ ...address,
209
+ [part]: input?.trim() ? input : null,
210
+ formatted: null,
211
+ placeId: null,
212
+ location: null
213
+ };
214
+ if (part === 'line1') {
215
+ next.streetNumber = null;
216
+ next.street = null;
217
+ }
218
+ if (!next.countryCode && defaultCountry) {
219
+ next.countryCode = defaultCountry.toUpperCase();
220
+ next.country = countryName(next.countryCode, t.locale);
221
+ }
222
+ commit(next);
223
+ };
224
+
225
+ const editCountry = (code: string | null) => {
226
+ commit({
227
+ ...address,
228
+ countryCode: code,
229
+ country: code ? countryName(code, t.locale) : null,
230
+ formatted: null,
231
+ placeId: null,
232
+ location: null
233
+ });
234
+ };
235
+
236
+ const countryItems = $derived(
237
+ (countries?.length ? countries.map((code) => code.toUpperCase()) : COUNTRY_CODES)
238
+ .map((code) => ({ value: code, label: countryName(code, t.locale) }))
239
+ .sort((a, b) => a.label.localeCompare(b.label, t.locale))
240
+ );
241
+
242
+ // One session per search, from the first keystroke to the resolved address: a provider that
243
+ // bills by session (Google Places) counts the suggestions and the details as one.
244
+ const newSession = () =>
245
+ typeof crypto !== 'undefined' && 'randomUUID' in crypto
246
+ ? crypto.randomUUID()
247
+ : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
248
+ let session = newSession();
249
+ let resolving = $state(false);
250
+ let resolveController: AbortController | null = null;
251
+ onDestroy(() => resolveController?.abort());
252
+
253
+ const search = async (query: string, { signal }: AutocompleteItemsRequest) => {
254
+ if (!provider || query.trim().length < minSearchLength) return [];
255
+ const suggestions = await provider.search(query, {
256
+ signal,
257
+ session,
258
+ language: t.locale,
259
+ countries
260
+ });
261
+ return suggestions.map((suggestion) => ({
262
+ value: suggestion.id,
263
+ label: suggestion.label,
264
+ description: suggestion.description
265
+ }));
266
+ };
267
+
268
+ const choose = async (suggestionId: string) => {
269
+ if (!provider) return;
270
+ resolveController?.abort();
271
+ const controller = new AbortController();
272
+ resolveController = controller;
273
+ resolving = true;
274
+ try {
275
+ const resolved = await provider.resolve(suggestionId, {
276
+ signal: controller.signal,
277
+ session,
278
+ language: t.locale,
279
+ countries
280
+ });
281
+ if (controller.signal.aborted) return;
282
+ session = newSession();
283
+ if (!resolved) return;
284
+ commit(resolved);
285
+ searchText = formatAddressLine(resolved, { locale: t.locale });
286
+ manual = false;
287
+ onResolve?.(resolved);
288
+ } catch (error) {
289
+ if (controller.signal.aborted) return;
290
+ field.errors = [t.addressLookupFailed];
291
+ void error;
292
+ } finally {
293
+ if (resolveController === controller) {
294
+ resolving = false;
295
+ resolveController = null;
296
+ }
297
+ }
298
+ };
299
+
300
+ // The search layout's input shows the chosen address on one line; typing only searches.
301
+ let searchText = $state(untrack(() => formatAddressLine(field.value, { locale: t.locale })));
302
+ let manual = $state(false);
303
+ $effect(() => {
304
+ const line = formatAddressLine(field.value, { locale: t.locale });
305
+ untrack(() => {
306
+ if (!resolving) searchText = line;
307
+ });
308
+ });
309
+ const showFields = $derived(layout === 'fields' || manual);
310
+ const searchable = $derived(!!provider && editable.has('line1'));
311
+
312
+ // Labels on the left: every part takes its own row, its label in the column all the parts
313
+ // share, and the address's legend heads them.
314
+ const partsLabelLeft = $derived(labelPosition === 'left' && showFields);
315
+ const layoutRows = $derived(partsLabelLeft ? parts.map((part) => [part]) : rows);
316
+ const rowColumns = (row: readonly Part[]) =>
317
+ row.map((part) => `minmax(0, ${Math.max(part.width, 0)}fr)`).join(' ');
318
+
319
+ const autofill: Record<AddressField, string> = {
320
+ line1: 'address-line1',
321
+ line2: 'address-line2',
322
+ postalCode: 'postal-code',
323
+ city: 'address-level2',
324
+ region: 'address-level1',
325
+ country: 'country'
326
+ };
327
+
328
+ // After a failed validation: the required parts still empty, all of them for an empty
329
+ // required address.
330
+ const missingShown = $derived.by(() => {
331
+ if (!field.hasError) return [];
332
+ if (!hasContent(field.value)) return required ? requiredParts : [];
333
+ return missingParts(field.value);
334
+ });
335
+ // A missing part's input carries its own message, so it is described and marked invalid, but
336
+ // only the address announces it: the part's own error block stays hidden.
337
+ const partErrors = (part: AddressField) =>
338
+ missingShown.includes(part) ? [t.addressPartRequired(partLabel(part))] : [];
339
+ const partTheme = $derived({ errorsContainer: { base: classes.partErrors() } });
340
+ const partRequired = (part: AddressField) =>
341
+ requiredParts.includes(part) && (required || hasContent(field.value));
342
+ const partReadOnly = (part: AddressField) => field.readOnly || !editable.has(part);
343
+
344
+ // No part is the field's control: Field would mark it invalid and describe it by every error
345
+ // of the address. The parts a validation finds missing carry their own message instead, and a
346
+ // Form focuses the first part of an invalid address.
347
+ let frame = $state<HTMLElement | null>(null);
348
+ const onfocusin = () => (field.focused = true);
349
+ const onfocusout = (event: FocusEvent) => {
350
+ if (!frame?.contains(event.relatedTarget as Node | null)) field.focused = false;
351
+ };
352
+
353
+ // Submitted under `name[part]` (and `name[countryCode]`), only when the app named the field.
354
+ const baseName = $derived(
355
+ field.name === `${field.type}-input-${field.id}` ? undefined : field.name
356
+ );
357
+ const submitted = $derived(
358
+ baseName
359
+ ? [...shownParts, 'countryCode' as const].map((part) => ({
360
+ name: `${baseName}[${part}]`,
361
+ value: part === 'countryCode' ? address.countryCode || null : text(address[part]) || null
362
+ }))
363
+ : []
364
+ );
365
+ </script>
366
+
367
+ <Field
368
+ as="fieldset"
369
+ {field}
370
+ {label}
371
+ {size}
372
+ {density}
373
+ labelPosition={partsLabelLeft ? 'top' : labelPosition}
374
+ {theme}
375
+ fieldAttrs={{ ...fieldAttrs, ...(label ? {} : { 'aria-label': t.addressInput }) }}
376
+ {...rest}
377
+ >
378
+ <div
379
+ bind:this={frame}
380
+ data-slot="address-input"
381
+ data-layout={layout}
382
+ data-resolving={resolving ? 'true' : undefined}
383
+ aria-busy={resolving || undefined}
384
+ class={classes.parts()}
385
+ {onfocusin}
386
+ {onfocusout}
387
+ >
388
+ {#if !showFields}
389
+ <div data-slot="address-search" class={classes.search()}>
390
+ <Autocomplete
391
+ {size}
392
+ inputAttrs={{ 'aria-label': t.addressSearch }}
393
+ mode="none"
394
+ items={search}
395
+ value={searchText}
396
+ placeholder={placeholder ?? t.addressSearchPlaceholder}
397
+ disabled={field.disabled}
398
+ readOnly={field.readOnly}
399
+ validationMode="submit"
400
+ errors={field.hasError}
401
+ onValueChange={(next) => (searchText = next ?? '')}
402
+ onSelect={({ item }) => choose(item.value)}
403
+ />
404
+ {#if !field.disabled && !field.readOnly}
405
+ <Button variant="link" size="small" color="neutral" onclick={() => (manual = true)}>
406
+ {t.addressEnterManually}
407
+ </Button>
408
+ {/if}
409
+ </div>
410
+ {:else}
411
+ <div data-slot="address-parts" class={classes.grid({ density })}>
412
+ {#each layoutRows as row (row.map((part) => part.field).join())}
413
+ <div
414
+ data-slot="address-row"
415
+ class={classes.row({ density })}
416
+ style:--address-row-columns={rowColumns(row)}
417
+ >
418
+ {#each row as { field: part, placeholder: partPlaceholder } (part)}
419
+ <div data-slot="address-part" data-part={part} class={classes.part()}>
420
+ {#if part === 'line1' && searchable && !partReadOnly('line1')}
421
+ <Autocomplete
422
+ label={partLabel('line1')}
423
+ {size}
424
+ {density}
425
+ {labelPosition}
426
+ mode="none"
427
+ items={search}
428
+ value={address.line1 ?? ''}
429
+ placeholder={partPlaceholder ?? placeholder ?? t.addressSearchPlaceholder}
430
+ required={partRequired('line1')}
431
+ disabled={field.disabled}
432
+ validationMode="submit"
433
+ errors={partErrors('line1')}
434
+ theme={partTheme}
435
+ onValueChange={(next) => edit('line1', next)}
436
+ onSelect={({ item }) => choose(item.value)}
437
+ />
438
+ {:else if part === 'country'}
439
+ <Combobox
440
+ label={partLabel('country')}
441
+ {size}
442
+ {labelPosition}
443
+ items={countryItems}
444
+ value={countryCode || null}
445
+ placeholder={partPlaceholder}
446
+ required={partRequired('country')}
447
+ disabled={field.disabled}
448
+ readOnly={partReadOnly('country')}
449
+ validationMode="submit"
450
+ errors={partErrors('country')}
451
+ theme={partTheme}
452
+ onValueChange={({ value: code }) => editCountry(code)}
453
+ />
454
+ {:else}
455
+ <TextInput
456
+ label={partLabel(part)}
457
+ {size}
458
+ {density}
459
+ {labelPosition}
460
+ value={address[part] ?? ''}
461
+ placeholder={partPlaceholder}
462
+ required={partRequired(part)}
463
+ disabled={field.disabled}
464
+ readOnly={partReadOnly(part)}
465
+ validationMode="submit"
466
+ errors={partErrors(part)}
467
+ theme={partTheme}
468
+ inputAttrs={{ autocomplete: autofill[part] as never }}
469
+ onValueChange={(next) => edit(part, next)}
470
+ />
471
+ {/if}
472
+ </div>
473
+ {/each}
474
+ </div>
475
+ {/each}
476
+ </div>
477
+ {/if}
478
+ </div>
479
+ {#each submitted as entry (entry.name)}
480
+ <FieldHiddenInputs {field} name={entry.name} value={entry.value} required={false} />
481
+ {/each}
482
+ </Field>
@@ -0,0 +1,4 @@
1
+ import type { AddressInputProps } from './addressInput.props.js';
2
+ declare const AddressInput: import("svelte").Component<AddressInputProps, {}, "value" | "errors" | "focused">;
3
+ type AddressInput = ReturnType<typeof AddressInput>;
4
+ export default AddressInput;
@@ -0,0 +1 @@
1
+ export declare const addressInputDescription = "\n# AddressInput\n\nA form field for a postal address. Its value is an `AddressValue` object (see `entasis/address`). Choose which parts it shows, which a person may edit and which are required. It searches Google Places by itself, and choosing a suggestion fills every part, with its coordinates (`location: { lat, lng }`).\n\n## Basic usage\n\n```svelte\n<script lang=\"ts\">\n import { AddressInput } from './index.ts';\n import type { AddressValue } from '../../Address/index.ts';\n\n let address = $state<AddressValue | null>(null);\n</script>\n\n<AddressInput label=\"Delivery address\" required defaultCountry=\"FR\" bind:value={address} />\n```\n\n## Setup\n\nThe search goes through the app's own `/api/places` route, so the Google key stays on the server. Four steps:\n\n1. **A Google key.** In Google Cloud, enable **Places API (New)** on a project with billing, create an API key and restrict it to Places API (New). Give it no website (referrer) restriction: only your server calls Google, and a server request carries no referrer.\n2. **The key in the server environment**, as `GOOGLE_PLACE_API_KEY`: in `.env` locally, and as a secret of your host in production (for example `wrangler secret put GOOGLE_PLACE_API_KEY` on Cloudflare). SvelteKit 3 reads declared variables only:\n\n ```ts\n // src/env.ts\n import { defineEnvVars } from '@sveltejs/kit/env';\n\n export const variables = defineEnvVars({\n GOOGLE_PLACE_API_KEY: {}\n });\n ```\n\n SvelteKit 2 needs no declaration: read `env.GOOGLE_PLACE_API_KEY` from `$env/dynamic/private`.\n3. **The route**, one file:\n\n ```ts nocheck\n // src/routes/api/places/[...path]/+server.ts\n import { createGooglePlacesProxy } from '../../Address/index.ts';\n import { GOOGLE_PLACE_API_KEY } from '$app/env/private';\n\n export const { GET, POST } = createGooglePlacesProxy({ apiKey: GOOGLE_PLACE_API_KEY });\n ```\n\n It forwards only the address search and the place lookup, with the key; any other path or method, or a missing key, answers 404.\n4. **The field**: `<AddressInput label=\"Address\" />`. No provider to build.\n\nWithout the route the search turns itself off (the dev server warns once) and the parts are typed by hand. To search elsewhere: `setAddressProvider(createGooglePlacesProvider({ endpoint: '/my/route' }))` in a layout, `createGooglePlacesProvider({ apiKey })` with a browser key restricted to your sites, or any `{ search, resolve }` provider. `provider={null}` turns one field's search off.\n\nIn a Form or `ask()`, use `type: 'address'` with the same props:\n\n```ts\nconst inputs = { address: { type: 'address', label: 'Address', required: true } } as const;\n```\n\n## Props\n\nExtends the shared Field props (`label`, `description`, `helper`, `required`, `disabled`, `readOnly`, `name`, `size`, `density`, `labelPosition`, `messages`, `onValidate`, `errors`, `focused`, `visible`) plus:\n\n- **value** / **defaultValue**: `AddressValue | null` (bindable). `null` while nothing is entered.\n- **onValueChange**: `(value: AddressValue | null) => void` \u2014 every edit of a part and every resolved suggestion.\n- **provider**: `AddressProvider | null` \u2014 the search. Defaults to the one `setAddressProvider` set for the app, else to Google Places through `/api/places`. `null` turns the search off: every part is typed by hand.\n- **fields**: `AddressFieldRow[]` \u2014 the parts shown and their layout. Each entry is a row: one part, or an array of parts side by side. A part is its name (`'line1'`, `'line2'`, `'postalCode'`, `'city'`, `'region'`, `'country'`) or its settings `{ field, width?, label?, placeholder? }`: `width` is its share of the row by proportion (default 1), `label` and `placeholder` replace the catalog's. Default `['line1', 'line2', [{ field: 'postalCode', width: 1 }, { field: 'city', width: 2 }], 'country']`. Under 28rem of the field's width every part takes its own row. A part listed twice throws.\n- **editableFields**: `AddressField[]` (default: every shown part) \u2014 parts a person may type in. The others show what the search filled, read-only.\n- **requiredFields**: `AddressField[]` \u2014 parts that must be filled once the address is started, or always when `required`. Defaults to the shown parts except `line2` and `region` when `required`, and to none otherwise. A missing part reads \"Required: City\" under the field and marks its input invalid.\n- **countries**: `string[]` \u2014 ISO codes the search and the country list are limited to.\n- **defaultCountry**: `string` \u2014 country code a new address starts in. A defaulted country alone does not make the address filled.\n- **labelPosition**: `'top' | 'left'`. With `left`, every part has its label on the left, in one column the parts share, one part per row, under the address's legend. In the `search` layout the legend itself sits on the left of the search input.\n- **layout**: `'fields' | 'search'` (default `'fields'`). `fields`: one input per part; the street line searches. `search`: one search input showing the chosen address on one line, with a button to enter or edit the parts by hand.\n- **minSearchLength**: `number` (default 3) \u2014 characters typed before the search runs.\n- **placeholder**: hint in the search input (the street line, unless its settings give one, or the single input of `layout=\"search\"`).\n- **onResolve**: `(address: AddressValue) => void` \u2014 the address a chosen suggestion resolved to, after the value changed.\n- **i18n**, **theme** (parts `parts`, `grid`, `row`, `part`, `search`, `partErrors`, plus Field parts).\n\n## Layout\n\n```svelte\n<AddressInput\n label=\"Address\"\n fields={[\n [{ field: 'line1', width: 3 }, { field: 'line2', label: 'Building', placeholder: 'Bldg B, floor 3' }],\n [{ field: 'postalCode', width: 1 }, { field: 'city', width: 2 }],\n ['region', 'country']\n ]}\n/>\n```\n\n`editableFields` and `requiredFields` name the parts, whatever row they sit in. The theme's `grid` part holds the rows, `row` lays one out through `--address-row-columns`, and `part` wraps each input.\n\n## Behaviour\n\n- Typing in the street line edits `line1` and searches. Choosing a suggestion resolves it and replaces the whole value (with `line2` from the provider, usually empty): `streetNumber`, `street`, `formatted`, `placeId` and `location` come with it.\n- A manual edit clears `formatted`, `placeId` and `location`; editing `line1` also clears `streetNumber` and `street`.\n- The country is a searchable list of countries named in the active locale; its value is `countryCode`, and `country` follows in the same locale.\n- A failed resolve shows \"This address could not be loaded. Fill it in by hand.\" and leaves the parts editable.\n- One session id covers the searches and the resolve of each chosen address.\n\n## Native forms and Form\n\n- In a Form, only the address is an entry: the part inputs inside it are not Form fields.\n- With a `name`, hidden inputs submit `name[line1]`, `name[postalCode]`\u2026 for each shown part, plus `name[countryCode]`.\n\n## Accessibility\n\n- The field is a `<fieldset>` whose legend is the label (or \"Address\" without one). Each part is a labelled input with its browser autofill hint (`address-line1`, `postal-code`, `address-level2`\u2026; the street line turns the browser's off while a provider suggests).\n- Required parts show the required marker. Errors are linked to the fieldset, and the missing parts' inputs are marked invalid.\n- The search is an `Autocomplete`: a combobox with a listbox of suggestions.\n- While a suggestion resolves, the parts have `aria-busy=\"true\"`.\n";
@@ -0,0 +1,110 @@
1
+ export const addressInputDescription = `
2
+ # AddressInput
3
+
4
+ A form field for a postal address. Its value is an \`AddressValue\` object (see \`entasis/address\`). Choose which parts it shows, which a person may edit and which are required. It searches Google Places by itself, and choosing a suggestion fills every part, with its coordinates (\`location: { lat, lng }\`).
5
+
6
+ ## Basic usage
7
+
8
+ \`\`\`svelte
9
+ <script lang="ts">
10
+ import { AddressInput } from './index.ts';
11
+ import type { AddressValue } from '../../Address/index.ts';
12
+
13
+ let address = $state<AddressValue | null>(null);
14
+ </script>
15
+
16
+ <AddressInput label="Delivery address" required defaultCountry="FR" bind:value={address} />
17
+ \`\`\`
18
+
19
+ ## Setup
20
+
21
+ The search goes through the app's own \`/api/places\` route, so the Google key stays on the server. Four steps:
22
+
23
+ 1. **A Google key.** In Google Cloud, enable **Places API (New)** on a project with billing, create an API key and restrict it to Places API (New). Give it no website (referrer) restriction: only your server calls Google, and a server request carries no referrer.
24
+ 2. **The key in the server environment**, as \`GOOGLE_PLACE_API_KEY\`: in \`.env\` locally, and as a secret of your host in production (for example \`wrangler secret put GOOGLE_PLACE_API_KEY\` on Cloudflare). SvelteKit 3 reads declared variables only:
25
+
26
+ \`\`\`ts
27
+ // src/env.ts
28
+ import { defineEnvVars } from '@sveltejs/kit/env';
29
+
30
+ export const variables = defineEnvVars({
31
+ GOOGLE_PLACE_API_KEY: {}
32
+ });
33
+ \`\`\`
34
+
35
+ SvelteKit 2 needs no declaration: read \`env.GOOGLE_PLACE_API_KEY\` from \`$env/dynamic/private\`.
36
+ 3. **The route**, one file:
37
+
38
+ \`\`\`ts nocheck
39
+ // src/routes/api/places/[...path]/+server.ts
40
+ import { createGooglePlacesProxy } from '../../Address/index.ts';
41
+ import { GOOGLE_PLACE_API_KEY } from '$app/env/private';
42
+
43
+ export const { GET, POST } = createGooglePlacesProxy({ apiKey: GOOGLE_PLACE_API_KEY });
44
+ \`\`\`
45
+
46
+ It forwards only the address search and the place lookup, with the key; any other path or method, or a missing key, answers 404.
47
+ 4. **The field**: \`<AddressInput label="Address" />\`. No provider to build.
48
+
49
+ Without the route the search turns itself off (the dev server warns once) and the parts are typed by hand. To search elsewhere: \`setAddressProvider(createGooglePlacesProvider({ endpoint: '/my/route' }))\` in a layout, \`createGooglePlacesProvider({ apiKey })\` with a browser key restricted to your sites, or any \`{ search, resolve }\` provider. \`provider={null}\` turns one field's search off.
50
+
51
+ In a Form or \`ask()\`, use \`type: 'address'\` with the same props:
52
+
53
+ \`\`\`ts
54
+ const inputs = { address: { type: 'address', label: 'Address', required: true } } as const;
55
+ \`\`\`
56
+
57
+ ## Props
58
+
59
+ Extends the shared Field props (\`label\`, \`description\`, \`helper\`, \`required\`, \`disabled\`, \`readOnly\`, \`name\`, \`size\`, \`density\`, \`labelPosition\`, \`messages\`, \`onValidate\`, \`errors\`, \`focused\`, \`visible\`) plus:
60
+
61
+ - **value** / **defaultValue**: \`AddressValue | null\` (bindable). \`null\` while nothing is entered.
62
+ - **onValueChange**: \`(value: AddressValue | null) => void\` — every edit of a part and every resolved suggestion.
63
+ - **provider**: \`AddressProvider | null\` — the search. Defaults to the one \`setAddressProvider\` set for the app, else to Google Places through \`/api/places\`. \`null\` turns the search off: every part is typed by hand.
64
+ - **fields**: \`AddressFieldRow[]\` — the parts shown and their layout. Each entry is a row: one part, or an array of parts side by side. A part is its name (\`'line1'\`, \`'line2'\`, \`'postalCode'\`, \`'city'\`, \`'region'\`, \`'country'\`) or its settings \`{ field, width?, label?, placeholder? }\`: \`width\` is its share of the row by proportion (default 1), \`label\` and \`placeholder\` replace the catalog's. Default \`['line1', 'line2', [{ field: 'postalCode', width: 1 }, { field: 'city', width: 2 }], 'country']\`. Under 28rem of the field's width every part takes its own row. A part listed twice throws.
65
+ - **editableFields**: \`AddressField[]\` (default: every shown part) — parts a person may type in. The others show what the search filled, read-only.
66
+ - **requiredFields**: \`AddressField[]\` — parts that must be filled once the address is started, or always when \`required\`. Defaults to the shown parts except \`line2\` and \`region\` when \`required\`, and to none otherwise. A missing part reads "Required: City" under the field and marks its input invalid.
67
+ - **countries**: \`string[]\` — ISO codes the search and the country list are limited to.
68
+ - **defaultCountry**: \`string\` — country code a new address starts in. A defaulted country alone does not make the address filled.
69
+ - **labelPosition**: \`'top' | 'left'\`. With \`left\`, every part has its label on the left, in one column the parts share, one part per row, under the address's legend. In the \`search\` layout the legend itself sits on the left of the search input.
70
+ - **layout**: \`'fields' | 'search'\` (default \`'fields'\`). \`fields\`: one input per part; the street line searches. \`search\`: one search input showing the chosen address on one line, with a button to enter or edit the parts by hand.
71
+ - **minSearchLength**: \`number\` (default 3) — characters typed before the search runs.
72
+ - **placeholder**: hint in the search input (the street line, unless its settings give one, or the single input of \`layout="search"\`).
73
+ - **onResolve**: \`(address: AddressValue) => void\` — the address a chosen suggestion resolved to, after the value changed.
74
+ - **i18n**, **theme** (parts \`parts\`, \`grid\`, \`row\`, \`part\`, \`search\`, \`partErrors\`, plus Field parts).
75
+
76
+ ## Layout
77
+
78
+ \`\`\`svelte
79
+ <AddressInput
80
+ label="Address"
81
+ fields={[
82
+ [{ field: 'line1', width: 3 }, { field: 'line2', label: 'Building', placeholder: 'Bldg B, floor 3' }],
83
+ [{ field: 'postalCode', width: 1 }, { field: 'city', width: 2 }],
84
+ ['region', 'country']
85
+ ]}
86
+ />
87
+ \`\`\`
88
+
89
+ \`editableFields\` and \`requiredFields\` name the parts, whatever row they sit in. The theme's \`grid\` part holds the rows, \`row\` lays one out through \`--address-row-columns\`, and \`part\` wraps each input.
90
+
91
+ ## Behaviour
92
+
93
+ - Typing in the street line edits \`line1\` and searches. Choosing a suggestion resolves it and replaces the whole value (with \`line2\` from the provider, usually empty): \`streetNumber\`, \`street\`, \`formatted\`, \`placeId\` and \`location\` come with it.
94
+ - A manual edit clears \`formatted\`, \`placeId\` and \`location\`; editing \`line1\` also clears \`streetNumber\` and \`street\`.
95
+ - The country is a searchable list of countries named in the active locale; its value is \`countryCode\`, and \`country\` follows in the same locale.
96
+ - A failed resolve shows "This address could not be loaded. Fill it in by hand." and leaves the parts editable.
97
+ - One session id covers the searches and the resolve of each chosen address.
98
+
99
+ ## Native forms and Form
100
+
101
+ - In a Form, only the address is an entry: the part inputs inside it are not Form fields.
102
+ - With a \`name\`, hidden inputs submit \`name[line1]\`, \`name[postalCode]\`… for each shown part, plus \`name[countryCode]\`.
103
+
104
+ ## Accessibility
105
+
106
+ - The field is a \`<fieldset>\` whose legend is the label (or "Address" without one). Each part is a labelled input with its browser autofill hint (\`address-line1\`, \`postal-code\`, \`address-level2\`…; the street line turns the browser's off while a provider suggests).
107
+ - Required parts show the required marker. Errors are linked to the fieldset, and the missing parts' inputs are marked invalid.
108
+ - The search is an \`Autocomplete\`: a combobox with a listbox of suggestions.
109
+ - While a suggestion resolves, the parts have \`aria-busy="true"\`.
110
+ `;