@velastack/cms 0.6.3 → 0.6.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.
Files changed (48) hide show
  1. package/README.md +96 -4
  2. package/dist/components/admin-bar/admin-bar-internal.svelte +18 -12
  3. package/dist/components/admin-bar/admin-bar.css +1 -1
  4. package/dist/components/admin-bar/admin-bar.svelte +1 -1
  5. package/dist/components/admin-bar/field-input.svelte +1 -1
  6. package/dist/components/admin-bar/history-panel.svelte +1 -1
  7. package/dist/components/admin-bar/structured-editor-popover.svelte +21 -3
  8. package/dist/components/admin-bar/ui/badge/badge.svelte.d.ts +1 -1
  9. package/dist/components/cms/cms-form-controls.svelte +23 -0
  10. package/dist/components/cms/cms-form-controls.svelte.d.ts +13 -0
  11. package/dist/components/cms/cms-form-fields.svelte +68 -0
  12. package/dist/components/cms/cms-form-fields.svelte.d.ts +21 -0
  13. package/dist/components/cms/cms-form.svelte +99 -0
  14. package/dist/components/cms/cms-form.svelte.d.ts +35 -0
  15. package/dist/components/cms/cms-whatsapp-form-editable.svelte +43 -0
  16. package/dist/components/cms/cms-whatsapp-form-editable.svelte.d.ts +17 -0
  17. package/dist/components/cms/cms-whatsapp-form.svelte +167 -0
  18. package/dist/components/cms/cms-whatsapp-form.svelte.d.ts +43 -0
  19. package/dist/components/cms/index.d.ts +5 -0
  20. package/dist/components/cms/index.js +3 -0
  21. package/dist/components/cms/use-form-target.svelte.d.ts +18 -0
  22. package/dist/components/cms/use-form-target.svelte.js +68 -0
  23. package/dist/core/forms/target.d.ts +3 -0
  24. package/dist/core/forms/target.js +16 -0
  25. package/dist/core/forms/whatsapp.d.ts +18 -0
  26. package/dist/core/forms/whatsapp.js +29 -0
  27. package/dist/core/shapes/form-fields.d.ts +20 -0
  28. package/dist/core/shapes/form-fields.js +117 -0
  29. package/dist/core/shapes/index.d.ts +4 -0
  30. package/dist/core/shapes/index.js +2 -0
  31. package/dist/core/shapes/whatsapp.d.ts +8 -0
  32. package/dist/core/shapes/whatsapp.js +28 -0
  33. package/dist/index.d.ts +5 -0
  34. package/dist/index.js +6 -0
  35. package/dist/server/load-cms.js +10 -7
  36. package/dist/server/mock-adapter.js +4 -0
  37. package/package.json +6 -1
  38. package/plugin/dist/build.d.ts +34 -2
  39. package/plugin/dist/build.js +58 -2
  40. package/plugin/dist/build.js.map +1 -1
  41. package/plugin/dist/index.js +66 -20
  42. package/plugin/dist/index.js.map +1 -1
  43. package/plugin/dist/manifest.d.ts +22 -0
  44. package/plugin/dist/manifest.js +35 -4
  45. package/plugin/dist/manifest.js.map +1 -1
  46. package/plugin/dist/parse-svelte.d.ts +6 -0
  47. package/plugin/dist/parse-svelte.js +5 -0
  48. package/plugin/dist/parse-svelte.js.map +1 -1
@@ -0,0 +1,68 @@
1
+ <script lang="ts" module>
2
+ /** `CmsFormFields`: a form's inputs as content editors can change. */
3
+ export * from '../../core/shapes/form-fields.js';
4
+ </script>
5
+
6
+ <script lang="ts">
7
+ /**
8
+ * Goes inside a `CmsForm` or `CmsWhatsAppForm`. Renders one input per
9
+ * field through the template's `field` snippet, or plain labelled inputs
10
+ * without one. Editors add, remove, reorder and retype fields from the
11
+ * pencil; each field's submitted `name` stays put (see the shape).
12
+ */
13
+ import type { Snippet } from 'svelte';
14
+ import Structured from './cms-structured.svelte';
15
+ import {
16
+ cmsFormFields,
17
+ formFieldsView,
18
+ type CmsFormFieldItem,
19
+ type CmsFormFieldsValue
20
+ } from '../../core/shapes/form-fields.js';
21
+
22
+ type Props = {
23
+ name: string;
24
+ scope?: string;
25
+ /** The full value, or a bare field list. */
26
+ fallback?: Partial<CmsFormFieldsValue> | Array<Partial<CmsFormFieldItem>>;
27
+ value?: unknown;
28
+ field?: Snippet<[CmsFormFieldItem]>;
29
+ };
30
+ let { name, scope, fallback, value, field }: Props = $props();
31
+ </script>
32
+
33
+ {#snippet plain(f: CmsFormFieldItem)}
34
+ <label class="cms-form-field" data-type={f.type}>
35
+ {#if f.type === 'checkbox'}
36
+ <input type="checkbox" name={f.name} value="yes" required={f.required} />
37
+ <span>{f.label}</span>
38
+ {:else}
39
+ <span>{f.label}</span>
40
+ {#if f.type === 'textarea'}
41
+ <textarea name={f.name} required={f.required} placeholder={f.placeholder || undefined}
42
+ ></textarea>
43
+ {:else if f.type === 'select'}
44
+ <select name={f.name} required={f.required}>
45
+ <option value="">{f.placeholder}</option>
46
+ {#each f.options as option, i (i)}
47
+ <option>{option}</option>
48
+ {/each}
49
+ </select>
50
+ {:else}
51
+ <input
52
+ type={f.type}
53
+ name={f.name}
54
+ required={f.required}
55
+ placeholder={f.placeholder || undefined}
56
+ />
57
+ {/if}
58
+ {/if}
59
+ </label>
60
+ {/snippet}
61
+
62
+ <Structured schema={cmsFormFields} {name} {scope} {fallback} {value} view={formFieldsView}>
63
+ {#snippet children(fields)}
64
+ {#each fields as f (f.id)}
65
+ {@render (field ?? plain)(f)}
66
+ {/each}
67
+ {/snippet}
68
+ </Structured>
@@ -0,0 +1,21 @@
1
+ /** `CmsFormFields`: a form's inputs as content editors can change. */
2
+ export * from '../../core/shapes/form-fields.js';
3
+ /**
4
+ * Goes inside a `CmsForm` or `CmsWhatsAppForm`. Renders one input per
5
+ * field through the template's `field` snippet, or plain labelled inputs
6
+ * without one. Editors add, remove, reorder and retype fields from the
7
+ * pencil; each field's submitted `name` stays put (see the shape).
8
+ */
9
+ import type { Snippet } from 'svelte';
10
+ import { type CmsFormFieldItem, type CmsFormFieldsValue } from '../../core/shapes/form-fields.js';
11
+ type Props = {
12
+ name: string;
13
+ scope?: string;
14
+ /** The full value, or a bare field list. */
15
+ fallback?: Partial<CmsFormFieldsValue> | Array<Partial<CmsFormFieldItem>>;
16
+ value?: unknown;
17
+ field?: Snippet<[CmsFormFieldItem]>;
18
+ };
19
+ declare const CmsFormFields: import("svelte").Component<Props, {}, "">;
20
+ type CmsFormFields = ReturnType<typeof CmsFormFields>;
21
+ export default CmsFormFields;
@@ -0,0 +1,99 @@
1
+ <script lang="ts" module>
2
+ import type { FormStatus } from './use-form-target.svelte.js';
3
+
4
+ export type { FormStatus as CmsFormStatus };
5
+
6
+ /** What `children` gets: how the last submission went. */
7
+ export type CmsFormState = {
8
+ readonly status: FormStatus;
9
+ /** The endpoint's or action's message on `failure` / `error`. */
10
+ readonly message: string | null;
11
+ /** The action's returned data on `success` / `failure`. */
12
+ readonly data: Record<string, unknown> | null;
13
+ };
14
+ </script>
15
+
16
+ <script lang="ts">
17
+ /**
18
+ * `CmsForm`: a form that posts to a hosted form by `key`, or to the
19
+ * template's own `action`. The inputs are the template's children.
20
+ *
21
+ * It is a real `<form method="POST">`, so it works without JavaScript;
22
+ * with it, `use:enhance` posts in place and `children` sees the status.
23
+ * The hosted endpoint answers enhanced posts the way a SvelteKit action
24
+ * does, so both destinations behave the same. Results are handled here
25
+ * rather than by the default callback, which would reload the page's data
26
+ * on success and send a network failure to the error page.
27
+ */
28
+ import type { Snippet } from 'svelte';
29
+ import type { HTMLFormAttributes } from 'svelte/elements';
30
+ import { enhance, type SubmitFunction } from '$app/forms';
31
+ import { cmsStore } from './cms-store.svelte.js';
32
+ import { followRedirect, useFormTarget } from './use-form-target.svelte.js';
33
+ import FormControls from './cms-form-controls.svelte';
34
+
35
+ type Props = Omit<HTMLFormAttributes, 'action' | 'method' | 'children'> & {
36
+ /** A hosted form's key, as the template manifest declares it. */
37
+ key?: string;
38
+ /** Post here instead: a route, or a SvelteKit action like `?/contact`. */
39
+ action?: string;
40
+ /** Where to go after a successful submission. */
41
+ redirect?: string;
42
+ children: Snippet<[CmsFormState]>;
43
+ };
44
+ let { key, action, redirect, children, ...rest }: Props = $props();
45
+
46
+ const target = useFormTarget(() => ({ key, action }));
47
+
48
+ let status = $state<FormStatus>('idle');
49
+ let message = $state<string | null>(null);
50
+ let data = $state<Record<string, unknown> | null>(null);
51
+ const formState: CmsFormState = {
52
+ get status() {
53
+ return status;
54
+ },
55
+ get message() {
56
+ return message;
57
+ },
58
+ get data() {
59
+ return data;
60
+ }
61
+ };
62
+
63
+ const messageOf = (value: unknown): string | null =>
64
+ value && typeof value === 'object' && 'message' in value && typeof value.message === 'string'
65
+ ? value.message
66
+ : null;
67
+
68
+ const submit: SubmitFunction = ({ cancel }) => {
69
+ // Editors are changing the page, not filling it in.
70
+ if (cmsStore.isEditing) return cancel();
71
+ status = 'submitting';
72
+ message = null;
73
+ data = null;
74
+ return async ({ result, formElement, update }) => {
75
+ if (result.type === 'success') {
76
+ status = 'success';
77
+ data = result.data ?? null;
78
+ formElement.reset();
79
+ if (redirect) await followRedirect(redirect);
80
+ } else if (result.type === 'failure') {
81
+ status = 'failure';
82
+ data = result.data ?? null;
83
+ message = messageOf(result.data);
84
+ } else if (result.type === 'redirect') {
85
+ // A same-app action redirected; let SvelteKit follow it.
86
+ status = 'success';
87
+ await update();
88
+ } else {
89
+ status = 'error';
90
+ message = messageOf(result.error);
91
+ }
92
+ };
93
+ };
94
+ </script>
95
+
96
+ <form {...rest} method="POST" action={target.url} use:enhance={submit}>
97
+ <FormControls {redirect} token={target.token} />
98
+ {@render children(formState)}
99
+ </form>
@@ -0,0 +1,35 @@
1
+ import type { FormStatus } from './use-form-target.svelte.js';
2
+ export type { FormStatus as CmsFormStatus };
3
+ /** What `children` gets: how the last submission went. */
4
+ export type CmsFormState = {
5
+ readonly status: FormStatus;
6
+ /** The endpoint's or action's message on `failure` / `error`. */
7
+ readonly message: string | null;
8
+ /** The action's returned data on `success` / `failure`. */
9
+ readonly data: Record<string, unknown> | null;
10
+ };
11
+ /**
12
+ * `CmsForm`: a form that posts to a hosted form by `key`, or to the
13
+ * template's own `action`. The inputs are the template's children.
14
+ *
15
+ * It is a real `<form method="POST">`, so it works without JavaScript;
16
+ * with it, `use:enhance` posts in place and `children` sees the status.
17
+ * The hosted endpoint answers enhanced posts the way a SvelteKit action
18
+ * does, so both destinations behave the same. Results are handled here
19
+ * rather than by the default callback, which would reload the page's data
20
+ * on success and send a network failure to the error page.
21
+ */
22
+ import type { Snippet } from 'svelte';
23
+ import type { HTMLFormAttributes } from 'svelte/elements';
24
+ type Props = Omit<HTMLFormAttributes, 'action' | 'method' | 'children'> & {
25
+ /** A hosted form's key, as the template manifest declares it. */
26
+ key?: string;
27
+ /** Post here instead: a route, or a SvelteKit action like `?/contact`. */
28
+ action?: string;
29
+ /** Where to go after a successful submission. */
30
+ redirect?: string;
31
+ children: Snippet<[CmsFormState]>;
32
+ };
33
+ declare const CmsForm: import("svelte").Component<Props, {}, "">;
34
+ type CmsForm = ReturnType<typeof CmsForm>;
35
+ export default CmsForm;
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The editor for `CmsWhatsAppForm`: the shape's fields, plus the
4
+ * `{placeholders}` the form's inputs provide, read from the form when the
5
+ * popover opens so fields added through `CmsFormFields` show up too.
6
+ */
7
+ import type { Snippet } from 'svelte';
8
+ import type { Tree } from '../../core/path.js';
9
+ import type { FormShape } from '../../core/shapes/form.js';
10
+ import type { CmsWhatsAppValue } from '../../core/shapes/whatsapp.js';
11
+ import type { CmsField } from './use-cms-field.svelte.js';
12
+ import { getWhatsAppFieldNames } from './cms-whatsapp-form.svelte';
13
+ import StructuredEditorPopover from '../admin-bar/structured-editor-popover.svelte';
14
+ import FieldsForm from '../admin-bar/fields-form.svelte';
15
+
16
+ type Props = {
17
+ field: CmsField<CmsWhatsAppValue>;
18
+ schema: FormShape<CmsWhatsAppValue>;
19
+ preview: Snippet;
20
+ };
21
+ let { field, schema, preview }: Props = $props();
22
+
23
+ const fieldNames = getWhatsAppFieldNames();
24
+ </script>
25
+
26
+ <StructuredEditorPopover {field} {schema} label={schema.label} {preview}>
27
+ {#snippet editor(draft: CmsWhatsAppValue, update: (next: CmsWhatsAppValue) => void)}
28
+ {@const names = fieldNames()}
29
+ <FieldsForm
30
+ fields={schema.fields}
31
+ value={draft as Tree}
32
+ onChange={(next) => update(next as CmsWhatsAppValue)}
33
+ />
34
+ {#if names.length > 0}
35
+ <p class="vela:text-[11px] vela:text-bar-text-tertiary vela:m-0">
36
+ Placeholders:
37
+ {#each names as name (name)}
38
+ <code class="vela:font-mono vela:mr-1">{`{${name}}`}</code>
39
+ {/each}
40
+ </p>
41
+ {/if}
42
+ {/snippet}
43
+ </StructuredEditorPopover>
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The editor for `CmsWhatsAppForm`: the shape's fields, plus the
3
+ * `{placeholders}` the form's inputs provide, read from the form when the
4
+ * popover opens so fields added through `CmsFormFields` show up too.
5
+ */
6
+ import type { Snippet } from 'svelte';
7
+ import type { FormShape } from '../../core/shapes/form.js';
8
+ import type { CmsWhatsAppValue } from '../../core/shapes/whatsapp.js';
9
+ import type { CmsField } from './use-cms-field.svelte.js';
10
+ type Props = {
11
+ field: CmsField<CmsWhatsAppValue>;
12
+ schema: FormShape<CmsWhatsAppValue>;
13
+ preview: Snippet;
14
+ };
15
+ declare const CmsWhatsappFormEditable: import("svelte").Component<Props, {}, "">;
16
+ type CmsWhatsappFormEditable = ReturnType<typeof CmsWhatsappFormEditable>;
17
+ export default CmsWhatsappFormEditable;
@@ -0,0 +1,167 @@
1
+ <script lang="ts" module>
2
+ /** `CmsWhatsAppForm`: a form whose answers open a prefilled WhatsApp chat. */
3
+ import { getContext } from 'svelte';
4
+ import { isControlField } from '../../core/forms/target.js';
5
+ import type { CmsFormState } from './cms-form.svelte';
6
+
7
+ export * from '../../core/shapes/whatsapp.js';
8
+
9
+ export type CmsWhatsAppFormState = CmsFormState & {
10
+ /** A chat link with no message, for a `<noscript>` fallback; `null` without a number. */
11
+ readonly whatsappHref: string | null;
12
+ };
13
+
14
+ const FIELD_NAMES = Symbol('vela-cms:whatsapp-field-names');
15
+
16
+ /** For the settings editor: the `{placeholders}` this form's inputs provide. */
17
+ export const getWhatsAppFieldNames = (): (() => string[]) =>
18
+ getContext<() => string[]>(FIELD_NAMES) ?? (() => []);
19
+
20
+ const fieldNames = (form: HTMLFormElement): string[] => {
21
+ const names = new Set<string>();
22
+ for (const el of form.elements) {
23
+ const name = (el as HTMLInputElement).name;
24
+ const type = (el as HTMLInputElement).type;
25
+ if (name && !isControlField(name) && type !== 'submit' && type !== 'button') names.add(name);
26
+ }
27
+ return [...names];
28
+ };
29
+ </script>
30
+
31
+ <script lang="ts">
32
+ /**
33
+ * Like `CmsForm`, but a submission opens `wa.me` with the editor's
34
+ * message (answers substituted for `{field}` placeholders) addressed to
35
+ * the editor's number. The answers are also posted in the background, so
36
+ * the lead is kept even if the visitor never sends the chat.
37
+ *
38
+ * The chat opens inside the submit handler, before the post settles: a
39
+ * popup opened after an `await` loses the click's user activation and is
40
+ * blocked. `keepalive` lets the post finish if the page goes away. If the
41
+ * popup is blocked anyway, this tab goes to WhatsApp instead.
42
+ *
43
+ * Without JavaScript the form posts normally, so the answers are still
44
+ * stored and `redirect` applies; `whatsappHref` is there for a
45
+ * `<noscript>` link.
46
+ */
47
+ import { setContext, type Snippet } from 'svelte';
48
+ import type { HTMLFormAttributes } from 'svelte/elements';
49
+ import Structured from './cms-structured.svelte';
50
+ import FormControls from './cms-form-controls.svelte';
51
+ import { cmsStore } from './cms-store.svelte.js';
52
+ import { followRedirect, useFormTarget, type FormStatus } from './use-form-target.svelte.js';
53
+ import { cmsWhatsApp, type CmsWhatsAppValue } from '../../core/shapes/whatsapp.js';
54
+ import { fillMessage, whatsappHref } from '../../core/forms/whatsapp.js';
55
+
56
+ type Props = Omit<HTMLFormAttributes, 'action' | 'method' | 'children'> & {
57
+ /** The CMS field holding the number and message. */
58
+ name: string;
59
+ scope?: string;
60
+ fallback?: Partial<CmsWhatsAppValue>;
61
+ value?: unknown;
62
+ /** A hosted form's key, to record each submission to. */
63
+ key?: string;
64
+ /** Record to this route or SvelteKit action instead. */
65
+ action?: string;
66
+ /** Where this tab goes once the chat has opened in a new one. */
67
+ redirect?: string;
68
+ children: Snippet<[CmsWhatsAppFormState]>;
69
+ };
70
+ let {
71
+ name,
72
+ scope,
73
+ fallback,
74
+ value,
75
+ key,
76
+ action,
77
+ redirect,
78
+ children: content,
79
+ ...rest
80
+ }: Props = $props();
81
+
82
+ const target = useFormTarget(() => ({ key, action }));
83
+
84
+ let formEl = $state<HTMLFormElement>();
85
+ setContext(FIELD_NAMES, () => (formEl ? fieldNames(formEl) : []));
86
+
87
+ let status = $state<FormStatus>('idle');
88
+ let message = $state<string | null>(null);
89
+
90
+ const stateFor = (settings: CmsWhatsAppValue): CmsWhatsAppFormState => ({
91
+ get status() {
92
+ return status;
93
+ },
94
+ get message() {
95
+ return message;
96
+ },
97
+ data: null,
98
+ whatsappHref: whatsappHref(settings.number)
99
+ });
100
+
101
+ const record = (body: FormData) => {
102
+ if (!target.url) {
103
+ status = 'success';
104
+ return;
105
+ }
106
+ body.delete('_redirect');
107
+ status = 'submitting';
108
+ fetch(target.url, {
109
+ method: 'POST',
110
+ body,
111
+ keepalive: true,
112
+ headers: { accept: 'application/json' }
113
+ })
114
+ .then(async (res) => {
115
+ status = res.ok ? 'success' : 'failure';
116
+ if (!res.ok) message = ((await res.json().catch(() => null))?.message as string) ?? null;
117
+ })
118
+ .catch(() => {
119
+ status = 'error';
120
+ });
121
+ };
122
+
123
+ const submit = (event: SubmitEvent, settings: CmsWhatsAppValue) => {
124
+ event.preventDefault();
125
+ if (cmsStore.isEditing) return;
126
+ const form = event.currentTarget as HTMLFormElement;
127
+ const body = new FormData(form, event.submitter);
128
+ const href = whatsappHref(settings.number, fillMessage(settings.message, body));
129
+ message = null;
130
+ record(body);
131
+ if (!href) {
132
+ status = 'error';
133
+ message = 'No WhatsApp number is set.';
134
+ return;
135
+ }
136
+ const chat = window.open(href, '_blank');
137
+ if (!chat) {
138
+ location.assign(href);
139
+ return;
140
+ }
141
+ chat.opener = null;
142
+ if (redirect) void followRedirect(redirect);
143
+ };
144
+ </script>
145
+
146
+ <Structured
147
+ schema={cmsWhatsApp}
148
+ {name}
149
+ {scope}
150
+ {fallback}
151
+ {value}
152
+ view={(settings) => settings}
153
+ editable={() => import('./cms-whatsapp-form-editable.svelte')}
154
+ >
155
+ {#snippet children(settings)}
156
+ <form
157
+ {...rest}
158
+ method="POST"
159
+ action={target.url}
160
+ bind:this={formEl}
161
+ onsubmit={(event) => submit(event, settings)}
162
+ >
163
+ <FormControls {redirect} token={target.token} />
164
+ {@render content(stateFor(settings))}
165
+ </form>
166
+ {/snippet}
167
+ </Structured>
@@ -0,0 +1,43 @@
1
+ import type { CmsFormState } from './cms-form.svelte';
2
+ export * from '../../core/shapes/whatsapp.js';
3
+ export type CmsWhatsAppFormState = CmsFormState & {
4
+ /** A chat link with no message, for a `<noscript>` fallback; `null` without a number. */
5
+ readonly whatsappHref: string | null;
6
+ };
7
+ /** For the settings editor: the `{placeholders}` this form's inputs provide. */
8
+ export declare const getWhatsAppFieldNames: () => (() => string[]);
9
+ /**
10
+ * Like `CmsForm`, but a submission opens `wa.me` with the editor's
11
+ * message (answers substituted for `{field}` placeholders) addressed to
12
+ * the editor's number. The answers are also posted in the background, so
13
+ * the lead is kept even if the visitor never sends the chat.
14
+ *
15
+ * The chat opens inside the submit handler, before the post settles: a
16
+ * popup opened after an `await` loses the click's user activation and is
17
+ * blocked. `keepalive` lets the post finish if the page goes away. If the
18
+ * popup is blocked anyway, this tab goes to WhatsApp instead.
19
+ *
20
+ * Without JavaScript the form posts normally, so the answers are still
21
+ * stored and `redirect` applies; `whatsappHref` is there for a
22
+ * `<noscript>` link.
23
+ */
24
+ import { type Snippet } from 'svelte';
25
+ import type { HTMLFormAttributes } from 'svelte/elements';
26
+ import { type CmsWhatsAppValue } from '../../core/shapes/whatsapp.js';
27
+ type Props = Omit<HTMLFormAttributes, 'action' | 'method' | 'children'> & {
28
+ /** The CMS field holding the number and message. */
29
+ name: string;
30
+ scope?: string;
31
+ fallback?: Partial<CmsWhatsAppValue>;
32
+ value?: unknown;
33
+ /** A hosted form's key, to record each submission to. */
34
+ key?: string;
35
+ /** Record to this route or SvelteKit action instead. */
36
+ action?: string;
37
+ /** Where this tab goes once the chat has opened in a new one. */
38
+ redirect?: string;
39
+ children: Snippet<[CmsWhatsAppFormState]>;
40
+ };
41
+ declare const CmsWhatsappForm: import("svelte").Component<Props, {}, "">;
42
+ type CmsWhatsappForm = ReturnType<typeof CmsWhatsappForm>;
43
+ export default CmsWhatsappForm;
@@ -23,5 +23,10 @@ export { default as CmsGallery } from './cms-gallery.svelte';
23
23
  export { default as CmsLogos } from './cms-logos.svelte';
24
24
  export { default as CmsSchedule } from './cms-schedule.svelte';
25
25
  export { default as CmsEntries } from './cms-entries.svelte';
26
+ export { default as CmsForm } from './cms-form.svelte';
27
+ export type { CmsFormState, CmsFormStatus } from './cms-form.svelte';
28
+ export { default as CmsFormFields } from './cms-form-fields.svelte';
29
+ export { default as CmsWhatsAppForm } from './cms-whatsapp-form.svelte';
30
+ export type { CmsWhatsAppFormState } from './cms-whatsapp-form.svelte';
26
31
  export { CMS_SCOPE, getCmsScope, getCmsValue } from './scope.js';
27
32
  export type { CmsScope, CmsPayload, CmsEntry } from './scope.js';
@@ -22,4 +22,7 @@ export { default as CmsGallery } from './cms-gallery.svelte';
22
22
  export { default as CmsLogos } from './cms-logos.svelte';
23
23
  export { default as CmsSchedule } from './cms-schedule.svelte';
24
24
  export { default as CmsEntries } from './cms-entries.svelte';
25
+ export { default as CmsForm } from './cms-form.svelte';
26
+ export { default as CmsFormFields } from './cms-form-fields.svelte';
27
+ export { default as CmsWhatsAppForm } from './cms-whatsapp-form.svelte';
25
28
  export { CMS_SCOPE, getCmsScope, getCmsValue } from './scope.js';
@@ -0,0 +1,18 @@
1
+ export type FormStatus = 'idle' | 'submitting' | 'success' | 'failure' | 'error';
2
+ export type FormTargetInput = {
3
+ key?: string;
4
+ action?: string;
5
+ };
6
+ /**
7
+ * After an enhanced post, go where `_redirect` would have sent a post
8
+ * without JavaScript. The hosted endpoint never answers an enhanced post
9
+ * with a redirect (the client would hand an external URL to `goto`), so the
10
+ * form follows it itself.
11
+ */
12
+ export declare const followRedirect: (redirect: string) => Promise<void>;
13
+ export type FormTarget = {
14
+ /** The form's `action`; `undefined` posts back to the page. */
15
+ readonly url: string | undefined;
16
+ readonly token: string | null;
17
+ };
18
+ export declare const useFormTarget: (input: () => FormTargetInput) => FormTarget;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Where a `CmsForm` or `CmsWhatsAppForm` posts, and the load-time token a
3
+ * hosted form wants with it.
4
+ *
5
+ * `action` wins when given: the template's own route or SvelteKit action.
6
+ * Otherwise `key` names a hosted form beside the CMS endpoint (see
7
+ * `core/forms/target.ts`). The token only exists for hosted forms. It is
8
+ * fetched on mount because the endpoint refuses one younger than two
9
+ * seconds; a post without it is still stored, just as unverified.
10
+ */
11
+ import { dev } from '$app/env';
12
+ import { goto } from '$app/navigation';
13
+ import { cms } from './cms-store.svelte.js';
14
+ import { hostedFormUrl } from '../../core/forms/target.js';
15
+ /**
16
+ * After an enhanced post, go where `_redirect` would have sent a post
17
+ * without JavaScript. The hosted endpoint never answers an enhanced post
18
+ * with a redirect (the client would hand an external URL to `goto`), so the
19
+ * form follows it itself.
20
+ */
21
+ export const followRedirect = async (redirect) => {
22
+ const url = new URL(redirect, location.href);
23
+ if (url.origin === location.origin)
24
+ await goto(url.href);
25
+ else
26
+ location.assign(url.href);
27
+ };
28
+ export const useFormTarget = (input) => {
29
+ const hosted = $derived.by(() => {
30
+ const { key, action } = input();
31
+ return action === undefined && key ? hostedFormUrl(cms.endpoint, key) : null;
32
+ });
33
+ const url = $derived(input().action ?? hosted ?? undefined);
34
+ let token = $state(null);
35
+ $effect(() => {
36
+ const target = hosted;
37
+ token = null;
38
+ if (!target)
39
+ return;
40
+ let live = true;
41
+ fetch(target, { headers: { accept: 'application/json' } })
42
+ .then((res) => (res.ok ? res.json() : null))
43
+ .then((body) => {
44
+ if (live && typeof body?.token === 'string')
45
+ token = body.token;
46
+ })
47
+ .catch(() => { });
48
+ return () => {
49
+ live = false;
50
+ };
51
+ });
52
+ $effect(() => {
53
+ if (dev && url === undefined) {
54
+ const { key } = input();
55
+ console.warn(key
56
+ ? `[cms] Form "${key}" has no hosted endpoint (CMS endpoint "${cms.endpoint}"); pass \`action\`.`
57
+ : '[cms] Form has neither `key` nor `action`; it will post back to the page.');
58
+ }
59
+ });
60
+ return {
61
+ get url() {
62
+ return url;
63
+ },
64
+ get token() {
65
+ return token;
66
+ }
67
+ };
68
+ };
@@ -0,0 +1,3 @@
1
+ export declare const hostedFormUrl: (endpoint: string, key: string) => string | null;
2
+ /** Fields a form carries for the endpoint rather than the submission. */
3
+ export declare const isControlField: (name: string) => boolean;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Where a form posts when the template names a hosted form by `key`.
3
+ *
4
+ * velastack.dev serves a project's CMS at `/v1/projects/<id>/cms` and its
5
+ * hosted forms beside it at `/v1/projects/<id>/forms/<key>`, the forms a
6
+ * template's manifest declares. Any other endpoint shape (a same-app
7
+ * `/api/cms`, this package's own `/cms` backend) has no hosted forms, so
8
+ * there is nothing to derive and the template passes `action` instead.
9
+ */
10
+ const HOSTED_CMS = /^(.*\/v1\/projects\/[^/?#]+)\/cms\/?$/;
11
+ export const hostedFormUrl = (endpoint, key) => {
12
+ const match = HOSTED_CMS.exec(endpoint);
13
+ return match ? `${match[1]}/forms/${encodeURIComponent(key)}` : null;
14
+ };
15
+ /** Fields a form carries for the endpoint rather than the submission. */
16
+ export const isControlField = (name) => name.startsWith('_');
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Building the `wa.me` link a `CmsWhatsAppForm` opens: the editor's format
3
+ * string with the visitor's answers substituted, sent to the editor's number.
4
+ */
5
+ /**
6
+ * Substitute `{field}` placeholders with the submitted values. A field sent
7
+ * more than once (checkboxes, multi-selects) joins with commas; a placeholder
8
+ * with no matching field, and file uploads, become empty.
9
+ */
10
+ export declare const fillMessage: (template: string, data: FormData) => string;
11
+ /**
12
+ * The digits `wa.me` wants: country code and number, no `+`, spaces or
13
+ * dashes. A leading `00` international prefix is dropped, since no country
14
+ * code starts with 0.
15
+ */
16
+ export declare const whatsappNumber: (raw: string) => string;
17
+ /** `https://wa.me/<number>?text=<message>`, or `null` when no number is set. */
18
+ export declare const whatsappHref: (number: string, message?: string) => string | null;