@sevenfold/setto-client 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -10
- package/dist/admin/App.d.ts +3 -2
- package/dist/admin/DeploymentsView.d.ts +5 -0
- package/dist/admin/styles.d.ts +20 -0
- package/dist/block-tree/schema.d.ts +14 -16
- package/dist/edit-mode/mount.d.ts +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/lib/api.d.ts +15 -0
- package/dist/lib/i18n-store.d.ts +38 -0
- package/dist/lib/nested-resource.d.ts +7 -1
- package/dist/lib/urls.d.ts +13 -0
- package/dist/modules/forms/FormsView.d.ts +17 -0
- package/dist/modules/forms/SettoForm.d.ts +58 -0
- package/dist/modules/forms/api.d.ts +40 -0
- package/dist/modules/forms/field-editor.d.ts +29 -0
- package/dist/modules/forms/fields.d.ts +112 -0
- package/dist/modules/forms/index.d.ts +12 -0
- package/dist/modules/index.d.ts +5 -0
- package/dist/modules/module.d.ts +30 -0
- package/dist/setto-client.js +2635 -1124
- package/dist/setto-client.js.map +1 -1
- package/package.json +1 -1
- package/dist/SettoForm.d.ts +0 -18
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Editors authenticate via Supabase, edit text inline on the live page, pick secti
|
|
|
11
11
|
Sevenfold sites pin an npm version via alias:
|
|
12
12
|
|
|
13
13
|
```json
|
|
14
|
-
"@setto/client": "npm:@sevenfold/setto-client@0.
|
|
14
|
+
"@setto/client": "npm:@sevenfold/setto-client@0.7.0"
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
`bun install` pulls the prebuilt package from npm — no GitHub token required on Vercel.
|
|
@@ -23,10 +23,10 @@ For local monorepo development the consumer's `vite.config.ts` aliases `@setto/c
|
|
|
23
23
|
### Releasing a new version
|
|
24
24
|
|
|
25
25
|
1. Bump `version` in `package.json` following semver (patch / minor / major).
|
|
26
|
-
2. Commit and push to `main`. GitHub Actions publishes `@sevenfold/setto-client` to npm automatically (
|
|
26
|
+
2. Commit and push to `main`. GitHub Actions publishes `@sevenfold/setto-client` to npm automatically via npm trusted publishing (OIDC — no registry token in this repo).
|
|
27
27
|
3. Confirm the version is on npm: `npm view @sevenfold/setto-client@X.Y.Z version`.
|
|
28
|
-
4. In each consumer (`
|
|
29
|
-
- Update the pin in `package.json` (e.g. `@0.
|
|
28
|
+
4. In each consumer (`setto-site`, the setto-agent templates, every generated site):
|
|
29
|
+
- Update the pin in `package.json` (e.g. `@0.7.0` → `@0.7.1`).
|
|
30
30
|
- Run `bun install` to refresh the lockfile.
|
|
31
31
|
- Commit and push — Vercel installs the new npm version on the next deploy.
|
|
32
32
|
|
|
@@ -136,13 +136,14 @@ Sign-in at `/setto` persists via Supabase, so subsequent visits drop straight in
|
|
|
136
136
|
|
|
137
137
|
### What editors see
|
|
138
138
|
|
|
139
|
-
A small round Setto button floats in the bottom-
|
|
139
|
+
A small round Setto button floats in the bottom-left corner. While there are no unpublished changes it stays as a circle showing only the Setto mark. As soon as you edit something it grows into a pill containing **Publiser** and a **⋯** menu with:
|
|
140
140
|
|
|
141
141
|
| Item | Effect |
|
|
142
142
|
|------|--------|
|
|
143
143
|
| **Avbryt** | Discards every unsaved draft (text, section colours, image uploads) |
|
|
144
|
-
| **Logg ut** | Signs out via Supabase and exits edit mode |
|
|
145
144
|
| **Historikk** | Opens the `/setto` dashboard with deployment history |
|
|
145
|
+
| **Innstillinger** | Opens the `/setto` dashboard on the module settings — the forms inbox and who receives form email |
|
|
146
|
+
| **Logg ut** | Signs out via Supabase and exits edit mode |
|
|
146
147
|
|
|
147
148
|
| Action | How |
|
|
148
149
|
|--------|-----|
|
|
@@ -280,6 +281,45 @@ Click a block's background to edit **that block's** colours. Click the section p
|
|
|
280
281
|
|
|
281
282
|
---
|
|
282
283
|
|
|
284
|
+
## Forms (`SettoForm`)
|
|
285
|
+
|
|
286
|
+
`<SettoForm formId="kontakt" />` renders a form whose fields live in the locale bundle, so editors can add, remove, reorder and relabel them in edit mode like any other copy. Submissions go to setto-server, which stores them and emails the site's recipients.
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
import { SettoForm } from '@setto/client';
|
|
290
|
+
|
|
291
|
+
<SettoForm formId="kontakt" className="grid gap-4" />
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The locale supplies the texts and the numbered field list:
|
|
295
|
+
|
|
296
|
+
```json
|
|
297
|
+
"form": {
|
|
298
|
+
"kontakt": {
|
|
299
|
+
"submit": "Send melding",
|
|
300
|
+
"success": "Takk! Vi tar kontakt så snart vi kan.",
|
|
301
|
+
"error": "Noe gikk galt. Prøv igjen.",
|
|
302
|
+
"fields": {
|
|
303
|
+
"1": { "name": "navn", "type": "text", "label": "Navn", "required": "true" },
|
|
304
|
+
"2": { "name": "epost", "type": "email", "label": "E-post", "required": "true" },
|
|
305
|
+
"3": { "name": "melding", "type": "textarea", "label": "Melding" }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Field types: `text`, `email`, `tel`, `number`, `url`, `date`, `textarea`, `select`, `radio`, `checkbox`, `checkboxes` (`FORM_FIELD_TYPES`). `select`, `radio` and `checkboxes` take newline-separated `options`. All values are strings — `"required": "true"`, not `true`.
|
|
312
|
+
|
|
313
|
+
- **Styling.** Every part carries a `setto-form__*` class (`__field`, `__label`, `__input`, `__textarea`, `__select`, `__button`, `__error`, `__success`, …) plus `data-setto-field="<name>"` on each field wrapper. A minimal zero-specificity baseline is injected once, so any site rule wins; `classNames` puts classes on the parts directly. A site styling its own button should set `border: 0` (the baseline draws a 1px `currentColor` border).
|
|
314
|
+
- **Success.** The form is replaced by a `role="status"` panel with the locale's `success` text and a "send another" link; focus moves to it.
|
|
315
|
+
- **Spam.** A honeypot, a time-to-submit measurement and controlled inputs on the client; Origin check, per-site and per-address rate limits and a link-count heuristic on the server. Nothing asks the visitor to prove anything, and a caught bot sees a normal success.
|
|
316
|
+
- **Own backend.** Pass `endpoint="https://…"` to POST the bare values there instead of to Setto.
|
|
317
|
+
- **Inbox and recipients.** FAB → **Innstillinger** (or `/setto?view=forms`): submission history and the recipient list. With no recipients configured, the site's owners receive the email.
|
|
318
|
+
|
|
319
|
+
`SettoForm` is the first Setto *module* — a feature with a client component, an admin view and a server route. `SETTO_MODULES` lists them; the admin app renders one tab per module.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
283
323
|
## Server setup (setto-server)
|
|
284
324
|
|
|
285
325
|
Site configuration is split in two:
|
|
@@ -373,7 +413,15 @@ Produces `dist/setto-client.js` and `.d.ts` via Vite library mode. Only needed b
|
|
|
373
413
|
| `SettoSection` | Section wrapper + edit selection |
|
|
374
414
|
| `SettoBlock` | Nested card/panel with its own colour toolbar |
|
|
375
415
|
| `useSectionTheme` | Read section colour tokens |
|
|
376
|
-
| `
|
|
416
|
+
| `SettoImage`, `SettoIcon`, `SettoVideo` | Editable image (upload), icon and video leaves |
|
|
417
|
+
| `SettoRepeater` | Editable list stored in i18n (add/remove items) |
|
|
418
|
+
| `SettoAnimation` | Entrance animation wrapper (fadeIn, slideUp, stagger, scrollReveal) |
|
|
419
|
+
| `SettoBlockTree` | Renders a `home.layout.json` block tree with the components above |
|
|
420
|
+
| `GuestEditProvider` / `useGuestEdit` | Session-only "try editing" mode for a marketing site — no auth, never published |
|
|
421
|
+
| `SettoForm`, `FORM_FIELD_TYPES` | Editable form that submits to Setto (forms module) |
|
|
422
|
+
| `SETTO_MODULES` | Registry of modules the admin app shows settings for |
|
|
423
|
+
| `SettoAdminApp` | `/setto` login + dashboard (history, module settings) |
|
|
424
|
+
| `SETTO_BASE`, `adminViewUrl` | The `/setto` path and links into a dashboard view |
|
|
377
425
|
| `AuthGate` | Standalone login wrapper |
|
|
378
426
|
| `BrandColor`, `SectionSchema`, `SettoConfig` | Types for host config |
|
|
379
427
|
|
|
@@ -381,11 +429,11 @@ Produces `dist/setto-client.js` and `.d.ts` via Vite library mode. Only needed b
|
|
|
381
429
|
|
|
382
430
|
## Limitations (v0)
|
|
383
431
|
|
|
384
|
-
- `<T>` supports text content only — not HTML attributes (`placeholder`, `alt`, `aria-label`).
|
|
385
|
-
-
|
|
432
|
+
- `<T>` supports text content only — not HTML attributes (`placeholder`, `alt`, `aria-label`); form field placeholders are the exception, edited through the field editor.
|
|
433
|
+
- Lists are `SettoRepeater` (add/remove items); repeater items cannot be reordered yet — form fields can.
|
|
386
434
|
- Drafts are in-memory — refresh discards unpublished changes.
|
|
387
435
|
- Section colour toolbar only offers `brandColors` — no custom hex/rgba input.
|
|
388
|
-
-
|
|
436
|
+
- Only `SettoSection` and `SettoBlock` backgrounds and the tokens their schemas list are themeable — no per-element colour picking.
|
|
389
437
|
- Single editor per site (no concurrent-edit conflict handling).
|
|
390
438
|
|
|
391
439
|
## Knowledge base
|
package/dist/admin/App.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Behaviour after login:
|
|
5
5
|
* - Redirects to the site home `/`. Edit mode activates automatically there
|
|
6
6
|
* via `SettoProvider` when the user has access to `config.siteId`.
|
|
7
|
-
* - Shows the dashboard only when the user lacks access to this site, or
|
|
8
|
-
* `/setto?deployment=…` for publish progress
|
|
7
|
+
* - Shows the dashboard only when the user lacks access to this site, or
|
|
8
|
+
* when a panel was asked for: `/setto?deployment=…` for publish progress
|
|
9
|
+
* and history, `/setto?view=<module>` for a module's panel (forms today).
|
|
9
10
|
*/
|
|
10
11
|
export declare function SettoAdminApp(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The dashboard's look, shared by the shell and every module view so a new
|
|
4
|
+
* module's panel reads as part of the same place.
|
|
5
|
+
*/
|
|
6
|
+
export declare const shellStyle: CSSProperties;
|
|
7
|
+
export declare const headerStyle: CSSProperties;
|
|
8
|
+
export declare const mainStyle: CSSProperties;
|
|
9
|
+
export declare const cardStyle: CSSProperties;
|
|
10
|
+
export declare const panelStyle: CSSProperties;
|
|
11
|
+
export declare const rowStyle: CSSProperties;
|
|
12
|
+
export declare const mutedStyle: CSSProperties;
|
|
13
|
+
export declare const errStyle: CSSProperties;
|
|
14
|
+
export declare const subheadingStyle: CSSProperties;
|
|
15
|
+
export declare const primaryBtnStyle: CSSProperties;
|
|
16
|
+
export declare const secondaryBtnStyle: CSSProperties;
|
|
17
|
+
export declare const dangerBtnStyle: CSSProperties;
|
|
18
|
+
export declare const inputStyle: CSSProperties;
|
|
19
|
+
export declare const tabBarStyle: CSSProperties;
|
|
20
|
+
export declare function tabStyle(active: boolean): CSSProperties;
|
|
@@ -74,29 +74,28 @@ export interface RepeaterNode extends BaseNode {
|
|
|
74
74
|
*/
|
|
75
75
|
itemTemplate: BlockNode[];
|
|
76
76
|
}
|
|
77
|
-
export type FormFieldType
|
|
78
|
-
export
|
|
79
|
-
/** Submitted form field name. */
|
|
80
|
-
name: string;
|
|
81
|
-
type: FormFieldType;
|
|
82
|
-
required?: boolean;
|
|
83
|
-
/** i18n key for the field label. */
|
|
84
|
-
labelKey: string;
|
|
85
|
-
/** i18n key for the placeholder text. */
|
|
86
|
-
placeholderKey?: string;
|
|
87
|
-
}
|
|
77
|
+
export type { FormFieldType } from '../modules/forms/fields';
|
|
78
|
+
export type { FormFieldProp as FormField } from '../modules/forms/fields';
|
|
88
79
|
export interface FormNode extends BaseNode {
|
|
89
80
|
type: 'form';
|
|
90
81
|
/**
|
|
91
82
|
* Scopes all i18n keys for this form: `form.{formId}.submit`,
|
|
92
|
-
* `form.{formId}.success`, `form.{formId}.error
|
|
83
|
+
* `form.{formId}.success`, `form.{formId}.error`, and the editable field
|
|
84
|
+
* list under `form.{formId}.fields`.
|
|
93
85
|
*/
|
|
94
86
|
formId: string;
|
|
95
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Static POST endpoint for a site with its own backend. Without it the form
|
|
89
|
+
* posts to Setto, which stores the submission and emails the site's owners.
|
|
90
|
+
*/
|
|
96
91
|
endpoint?: string;
|
|
97
|
-
/**
|
|
92
|
+
/** @deprecated Ignored. Forms post to Setto unless `endpoint` is set. */
|
|
98
93
|
endpointKey?: string;
|
|
99
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Seed fields, used only while the locale has no `form.{formId}.fields`
|
|
96
|
+
* list. Once the editor touches the field set, the locale wins.
|
|
97
|
+
*/
|
|
98
|
+
fields?: import('../modules/forms/fields').FormFieldProp[];
|
|
100
99
|
}
|
|
101
100
|
export interface VideoNode extends BaseNode {
|
|
102
101
|
type: 'video';
|
|
@@ -117,4 +116,3 @@ export interface AnimationNode extends BaseNode {
|
|
|
117
116
|
duration?: number;
|
|
118
117
|
children: BlockNode[];
|
|
119
118
|
}
|
|
120
|
-
export {};
|
|
@@ -7,7 +7,7 @@ interface EditModeShellProps {
|
|
|
7
7
|
active: boolean;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Edit mode: full-width page with a floating Setto FAB anchored bottom-
|
|
10
|
+
* Edit mode: full-width page with a floating Setto FAB anchored bottom-left.
|
|
11
11
|
* Text is edited inline via contentEditable on `<T>` — no sidebar.
|
|
12
12
|
*
|
|
13
13
|
* Always mounted around the host app so toggling edit mode does not remount
|
package/dist/index.d.ts
CHANGED
|
@@ -9,15 +9,17 @@ export { SettoImage } from './SettoImage';
|
|
|
9
9
|
export type { SettoImageProps } from './SettoImage';
|
|
10
10
|
export { SettoRepeater } from './SettoRepeater';
|
|
11
11
|
export type { SettoRepeaterProps } from './SettoRepeater';
|
|
12
|
-
export { SettoForm } from './
|
|
13
|
-
export type { SettoFormProps } from './
|
|
12
|
+
export { SettoForm, FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS, formFieldsPrefix, isFormFieldType, readLocaleFields, settoSubmissionUrl, } from './modules/forms';
|
|
13
|
+
export type { SettoFormProps, SettoFormClassNames, FormFieldProp, FormSettings, FormSubmissionRow, FormValue, FormValues, ResolvedFormField, SubmissionEnvelope, } from './modules/forms';
|
|
14
|
+
export { SETTO_MODULES } from './modules';
|
|
15
|
+
export type { SettoModule } from './modules';
|
|
14
16
|
export { SettoVideo } from './SettoVideo';
|
|
15
17
|
export type { SettoVideoProps } from './SettoVideo';
|
|
16
18
|
export { SettoAnimation } from './SettoAnimation';
|
|
17
19
|
export type { SettoAnimationProps } from './SettoAnimation';
|
|
18
20
|
export { useSectionTheme } from './use-section-theme';
|
|
19
21
|
export { SettoAdminApp } from './admin/App';
|
|
20
|
-
export { SETTO_BASE } from './lib/urls';
|
|
22
|
+
export { SETTO_BASE, adminViewUrl } from './lib/urls';
|
|
21
23
|
export { AuthGate } from './edit-mode/auth-gate';
|
|
22
24
|
export type { SettoConfig, BrandColor, DeploymentRow, SiteRow, ContentFile, PublishFile, PublishResult, } from './types';
|
|
23
25
|
export type { SectionSchema, SectionColorField } from './section-schema';
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
2
|
import type { ContentFile, PublishFile, PublishResult } from '../types';
|
|
3
3
|
import type { AssetDraft } from './asset-store';
|
|
4
|
+
/**
|
|
5
|
+
* One authenticated JSON call against setto-server. Modules build their api
|
|
6
|
+
* segment on this (see `modules/forms/api.ts`) rather than on `fetch`, so
|
|
7
|
+
* bearer handling and error shaping stay in one place.
|
|
8
|
+
*/
|
|
9
|
+
export type ApiRequest = <T>(path: string, init?: {
|
|
10
|
+
method?: string;
|
|
11
|
+
body?: unknown;
|
|
12
|
+
}, label?: string) => Promise<T>;
|
|
4
13
|
export declare function createApi(args: {
|
|
5
14
|
apiUrl: string;
|
|
6
15
|
supabase: SupabaseClient;
|
|
7
16
|
}): {
|
|
17
|
+
request: ApiRequest;
|
|
18
|
+
forms: {
|
|
19
|
+
getSettings(siteId: string): Promise<import("..").FormSettings>;
|
|
20
|
+
saveSettings(siteId: string, recipients: string[]): Promise<import("..").FormSettings>;
|
|
21
|
+
deleteSubmission(siteId: string, id: string): Promise<void>;
|
|
22
|
+
};
|
|
8
23
|
getContent(siteId: string): Promise<{
|
|
9
24
|
files: ContentFile[];
|
|
10
25
|
}>;
|
package/dist/lib/i18n-store.d.ts
CHANGED
|
@@ -19,6 +19,18 @@ export interface DraftEntry {
|
|
|
19
19
|
original: string;
|
|
20
20
|
/** Current draft value. */
|
|
21
21
|
current: string;
|
|
22
|
+
/**
|
|
23
|
+
* The key did not exist before this session (a list item the editor added).
|
|
24
|
+
* Reverting deletes it instead of writing an empty string back, which would
|
|
25
|
+
* leave a blank item on the page.
|
|
26
|
+
*/
|
|
27
|
+
created?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* The key existed and the editor deleted it (a list item they removed).
|
|
30
|
+
* Without this a removal was invisible to the draft count: an editor who
|
|
31
|
+
* only deleted a FAQ entry had nothing to publish.
|
|
32
|
+
*/
|
|
33
|
+
removed?: boolean;
|
|
22
34
|
}
|
|
23
35
|
export interface I18nStoreSnapshot {
|
|
24
36
|
/** All edits made in this session, keyed by `${lng}::${key}`. */
|
|
@@ -36,6 +48,8 @@ export declare class I18nStore {
|
|
|
36
48
|
/** Stable namespace used by react-i18next defaults. */
|
|
37
49
|
private ns;
|
|
38
50
|
private i18n;
|
|
51
|
+
/** >0 while a batch is running; consumers are notified once at the end. */
|
|
52
|
+
private batchDepth;
|
|
39
53
|
/** Cached reference — useSyncExternalStore requires referential stability. */
|
|
40
54
|
private cachedSnapshot;
|
|
41
55
|
constructor(i18n: I18nType, options?: {
|
|
@@ -55,6 +69,12 @@ export declare class I18nStore {
|
|
|
55
69
|
* If the value is set back to the original, the draft entry is cleared.
|
|
56
70
|
*/
|
|
57
71
|
set(key: string, lng: string, value: string): void;
|
|
72
|
+
/**
|
|
73
|
+
* Deletes a key from the bundle. A key that was created this session simply
|
|
74
|
+
* disappears; one that was on disk is kept as a removal draft so the
|
|
75
|
+
* deletion counts as a change and reverts cleanly.
|
|
76
|
+
*/
|
|
77
|
+
delete(key: string, lng: string): void;
|
|
58
78
|
/** Resets a single draft back to its original value. */
|
|
59
79
|
revert(key: string, lng: string): void;
|
|
60
80
|
/** Resets every draft. */
|
|
@@ -72,6 +92,14 @@ export declare class I18nStore {
|
|
|
72
92
|
addListItem(prefix: string, _lng: string, template: Record<string, string>): string;
|
|
73
93
|
/** Removes a list item and all nested keys under `prefix.itemKey` in every language. */
|
|
74
94
|
removeListItem(prefix: string, itemKey: string, _lng: string): void;
|
|
95
|
+
/**
|
|
96
|
+
* Moves a list item one step up or down by swapping its values with its
|
|
97
|
+
* neighbour's, in every language. Items keep their numeric keys and the keys
|
|
98
|
+
* keep their order; only the contents trade places. That is what makes the
|
|
99
|
+
* move a set of ordinary drafts — publish, revert and the change count all
|
|
100
|
+
* work without knowing a move happened.
|
|
101
|
+
*/
|
|
102
|
+
moveListItem(prefix: string, itemKey: string, direction: -1 | 1, _lng: string): boolean;
|
|
75
103
|
/** Re-sorts list keys after removal (optional compaction — not used by default). */
|
|
76
104
|
sortListKeys(prefix: string, lng: string): string[];
|
|
77
105
|
/**
|
|
@@ -81,6 +109,16 @@ export declare class I18nStore {
|
|
|
81
109
|
* The caller maps language → file path via `sites.content_paths`.
|
|
82
110
|
*/
|
|
83
111
|
serialiseBundles(): Record<string, unknown>;
|
|
112
|
+
/** Runs `fn` and notifies consumers once, however many edits it makes. */
|
|
113
|
+
batch(fn: () => void): void;
|
|
114
|
+
private languages;
|
|
115
|
+
private valueOrUndefined;
|
|
116
|
+
private setOrDelete;
|
|
117
|
+
/** Every string key under `prefix`, nested to any depth. */
|
|
118
|
+
private leavesUnder;
|
|
119
|
+
private deleteFromBundle;
|
|
120
|
+
private restore;
|
|
121
|
+
private notify;
|
|
84
122
|
private bump;
|
|
85
123
|
}
|
|
86
124
|
export {};
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
export declare function getNested(obj: unknown, key: string): unknown;
|
|
3
3
|
/** Sets a dotted key path on a nested object (mutates in place). */
|
|
4
4
|
export declare function setNested(obj: Record<string, unknown>, key: string, value: unknown): void;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Deletes a dotted key path from a nested object (mutates in place).
|
|
7
|
+
*
|
|
8
|
+
* Parents left empty by the deletion go too. Deleting the last leaf of
|
|
9
|
+
* `form.kontakt.fields.3` used to leave `"3": {}` behind, which published as
|
|
10
|
+
* a phantom list item and rendered as an empty field.
|
|
11
|
+
*/
|
|
6
12
|
export declare function deleteNested(obj: Record<string, unknown>, key: string): void;
|
|
7
13
|
/** Collects all dotted keys under a prefix from a nested object. */
|
|
8
14
|
export declare function collectKeysUnder(obj: unknown, prefix: string): string[];
|
package/dist/lib/urls.d.ts
CHANGED
|
@@ -13,4 +13,17 @@ export declare function siteHomeUrl(pathname?: string): string;
|
|
|
13
13
|
export declare function isAdminRoute(): boolean;
|
|
14
14
|
/** Setto deployment progress panel (`/setto?deployment=…`). */
|
|
15
15
|
export declare function isAdminDeploymentView(): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* A named dashboard view (`/setto?view=forms`). Views come from the module
|
|
18
|
+
* registry in `src/modules`; the dashboard shows them as tabs. Opening one
|
|
19
|
+
* directly is how the editor's toolbar deep-links into the "backend".
|
|
20
|
+
*/
|
|
21
|
+
export declare function adminViewUrl(view: string): string;
|
|
22
|
+
export declare function currentAdminView(): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* True when the visitor came to `/setto` for a specific panel — a view or a
|
|
25
|
+
* deployment — rather than to sign in. Sign-in normally bounces straight to
|
|
26
|
+
* the site with edit mode on; a panel request stays on the dashboard.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isAdminPanelView(): boolean;
|
|
16
29
|
export declare function adminRedirectUrl(): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FormSubmissionRow } from './api';
|
|
2
|
+
/**
|
|
3
|
+
* The forms panel of the dashboard: who gets the mail, and everything that
|
|
4
|
+
* has come in. Submissions arrive live through the same Supabase channel
|
|
5
|
+
* pattern the deployment list uses; the owner can open one, and delete it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function FormsView({ siteId }: {
|
|
8
|
+
siteId: string;
|
|
9
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
interface SubmissionField {
|
|
11
|
+
name: string;
|
|
12
|
+
label: string;
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
/** `data` as ordered, labelled rows — the dashboard's version of the email's table. */
|
|
16
|
+
export declare function submissionFields(row: FormSubmissionRow): SubmissionField[];
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type FormFieldProp } from './fields';
|
|
2
|
+
/**
|
|
3
|
+
* A form the site owner can change and a visitor can trust.
|
|
4
|
+
*
|
|
5
|
+
* Fields come from the locale (`form.<id>.fields`, see `fields.ts`) and fall
|
|
6
|
+
* back to the `fields` prop until the editor first touches them, at which
|
|
7
|
+
* point the prop fields are copied into the locale and the locale wins. From
|
|
8
|
+
* then on adding, removing, reordering and relabelling are ordinary drafts.
|
|
9
|
+
*
|
|
10
|
+
* Submissions go to setto-server, which stores them and emails the site's
|
|
11
|
+
* recipients. A site with its own backend passes `endpoint` and gets the bare
|
|
12
|
+
* values instead. Either way the visitor sees the form replaced by the
|
|
13
|
+
* success message — with focus moved to it, so it is announced as well as
|
|
14
|
+
* seen.
|
|
15
|
+
*
|
|
16
|
+
* Bots: a honeypot input a person never sees, the time from first touch to
|
|
17
|
+
* submit, and controlled inputs (a script that pokes `.value` never changes
|
|
18
|
+
* React state, so it submits an empty form). The server adds an Origin check,
|
|
19
|
+
* a per-site and per-address rate limit, and a link-count heuristic. None of
|
|
20
|
+
* it asks the visitor to prove anything.
|
|
21
|
+
*/
|
|
22
|
+
export interface SettoFormClassNames {
|
|
23
|
+
form?: string;
|
|
24
|
+
field?: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
input?: string;
|
|
27
|
+
textarea?: string;
|
|
28
|
+
select?: string;
|
|
29
|
+
/** Wrapper around a radio / checkbox group's options. */
|
|
30
|
+
options?: string;
|
|
31
|
+
/** One radio / checkbox option (the label element). */
|
|
32
|
+
option?: string;
|
|
33
|
+
/** A single checkbox field (the label element). */
|
|
34
|
+
checkbox?: string;
|
|
35
|
+
actions?: string;
|
|
36
|
+
button?: string;
|
|
37
|
+
success?: string;
|
|
38
|
+
error?: string;
|
|
39
|
+
fieldError?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface SettoFormProps {
|
|
42
|
+
/** Scopes the form's i18n keys and identifies it in the inbox. */
|
|
43
|
+
formId: string;
|
|
44
|
+
/** Seed fields, used until the locale has a `form.<id>.fields` list. */
|
|
45
|
+
fields?: FormFieldProp[];
|
|
46
|
+
/**
|
|
47
|
+
* POST target for a site with its own backend; receives the bare values as
|
|
48
|
+
* JSON. Leave unset to submit to Setto.
|
|
49
|
+
*/
|
|
50
|
+
endpoint?: string;
|
|
51
|
+
/** @deprecated Ignored — forms post to Setto unless `endpoint` is set. */
|
|
52
|
+
endpointKey?: string;
|
|
53
|
+
/** Class on the `<form>` (and the success panel that replaces it). */
|
|
54
|
+
className?: string;
|
|
55
|
+
/** Classes per part; the `setto-form__*` classes stay for CSS hooks. */
|
|
56
|
+
classNames?: SettoFormClassNames;
|
|
57
|
+
}
|
|
58
|
+
export declare function SettoForm({ formId, fields: propFields, endpoint, endpointKey, className, classNames, }: SettoFormProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ApiRequest } from '../../lib/api';
|
|
2
|
+
/** The forms row of `site_module_settings` as the server reports it, with the defaults resolved. */
|
|
3
|
+
export interface FormSettings {
|
|
4
|
+
/** Addresses the owner chose; empty means "use the defaults". */
|
|
5
|
+
recipients: string[];
|
|
6
|
+
/** The site owners' addresses — where mail goes when `recipients` is empty. */
|
|
7
|
+
defaultRecipients: string[];
|
|
8
|
+
/** What a submission would be sent to right now. */
|
|
9
|
+
effectiveRecipients: string[];
|
|
10
|
+
updatedAt: string | null;
|
|
11
|
+
}
|
|
12
|
+
/** A row of `form_submissions`, read through Supabase RLS in the dashboard. */
|
|
13
|
+
export interface FormSubmissionRow {
|
|
14
|
+
id: string;
|
|
15
|
+
site_id: string;
|
|
16
|
+
form_id: string;
|
|
17
|
+
data: Record<string, unknown>;
|
|
18
|
+
labels: Record<string, string>;
|
|
19
|
+
field_order: string[];
|
|
20
|
+
meta: {
|
|
21
|
+
page?: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
referer?: string;
|
|
24
|
+
userAgent?: string;
|
|
25
|
+
ip?: string;
|
|
26
|
+
/** Milliseconds from the visitor's first interaction to submit; absent for API clients. */
|
|
27
|
+
elapsed?: number;
|
|
28
|
+
recipientSource?: 'settings' | 'owners' | 'none';
|
|
29
|
+
};
|
|
30
|
+
email_status: 'pending' | 'sent' | 'failed' | 'skipped';
|
|
31
|
+
email_error: string | null;
|
|
32
|
+
recipients: string[];
|
|
33
|
+
created_at: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function createFormsApi(request: ApiRequest): {
|
|
36
|
+
getSettings(siteId: string): Promise<FormSettings>;
|
|
37
|
+
saveSettings(siteId: string, recipients: string[]): Promise<FormSettings>;
|
|
38
|
+
deleteSubmission(siteId: string, id: string): Promise<void>;
|
|
39
|
+
};
|
|
40
|
+
export type FormsApi = ReturnType<typeof createFormsApi>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type FormFieldType, type ResolvedFormField } from './fields';
|
|
2
|
+
/**
|
|
3
|
+
* The chrome an editor sees around each form field: move up/down, settings,
|
|
4
|
+
* remove. It exists only in edit mode; the published form has none of it.
|
|
5
|
+
*
|
|
6
|
+
* Everything here writes through `onChange`, which the form routes into the
|
|
7
|
+
* draft store. The toolbar does not know where fields live and does not need
|
|
8
|
+
* to — that is the form's business.
|
|
9
|
+
*/
|
|
10
|
+
export interface FieldPatch {
|
|
11
|
+
name?: string;
|
|
12
|
+
type?: FormFieldType;
|
|
13
|
+
required?: boolean;
|
|
14
|
+
label?: string;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
options?: string;
|
|
17
|
+
}
|
|
18
|
+
interface FormFieldToolsProps {
|
|
19
|
+
field: ResolvedFormField;
|
|
20
|
+
index: number;
|
|
21
|
+
count: number;
|
|
22
|
+
/** Open the settings popover as soon as the tools mount (a field just added). */
|
|
23
|
+
autoOpen?: boolean;
|
|
24
|
+
onMove: (direction: -1 | 1) => void;
|
|
25
|
+
onRemove: () => void;
|
|
26
|
+
onChange: (patch: FieldPatch) => void;
|
|
27
|
+
}
|
|
28
|
+
export declare function FormFieldTools({ field, index, count, autoOpen, onMove, onRemove, onChange, }: FormFieldToolsProps): import("react/jsx-runtime").JSX.Element;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { i18n as I18nType, TFunction } from 'i18next';
|
|
2
|
+
/**
|
|
3
|
+
* What a form is made of, and where that lives.
|
|
4
|
+
*
|
|
5
|
+
* A form's fields are content, not code. They sit in the locale bundle under
|
|
6
|
+
* `form.<formId>.fields.<n>` as an ordinary Setto list — the same shape a
|
|
7
|
+
* repeater uses — so the editor can add, remove, reorder and relabel them
|
|
8
|
+
* with the draft store it already has, and publishing writes them to
|
|
9
|
+
* `no.json` through the path that already exists. Nothing new has to be
|
|
10
|
+
* stored, fetched or deployed for a form to change.
|
|
11
|
+
*
|
|
12
|
+
* "form": {
|
|
13
|
+
* "kontakt": {
|
|
14
|
+
* "submit": "Send", "success": "Takk! …", "error": "Noe gikk galt.",
|
|
15
|
+
* "fields": {
|
|
16
|
+
* "1": { "name": "navn", "type": "text", "label": "Navn", "required": "true" },
|
|
17
|
+
* "2": { "name": "epost", "type": "email", "label": "E-post", "required": "true" },
|
|
18
|
+
* "3": { "name": "tjeneste", "type": "select", "label": "Hva gjelder det?",
|
|
19
|
+
* "options": "Tak\nVindu\nAnnet" },
|
|
20
|
+
* "4": { "name": "melding", "type": "textarea", "label": "Melding" }
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* Every value is a string because every locale value is: `required` is
|
|
26
|
+
* `"true"`, `options` is one per line. `label`, `placeholder` and `options`
|
|
27
|
+
* are per language; `name`, `type` and `required` are the same in all of them.
|
|
28
|
+
*/
|
|
29
|
+
export declare const FORM_FIELD_TYPES: readonly ["text", "email", "tel", "number", "url", "date", "textarea", "select", "radio", "checkbox", "checkboxes"];
|
|
30
|
+
export type FormFieldType = (typeof FORM_FIELD_TYPES)[number];
|
|
31
|
+
/** What the editor calls each type. */
|
|
32
|
+
export declare const FORM_FIELD_TYPE_LABELS: Record<FormFieldType, string>;
|
|
33
|
+
/** Field props that are the same in every language. */
|
|
34
|
+
export declare const STRUCTURAL_FIELD_PROPS: readonly ["name", "type", "required"];
|
|
35
|
+
export declare function isFormFieldType(value: unknown): value is FormFieldType;
|
|
36
|
+
export declare function fieldTypeHasOptions(type: FormFieldType): boolean;
|
|
37
|
+
export declare function fieldTypeHasPlaceholder(type: FormFieldType): boolean;
|
|
38
|
+
/** A field as the form renders it, whichever source it came from. */
|
|
39
|
+
export interface ResolvedFormField {
|
|
40
|
+
/** Item key under `form.<id>.fields` — absent when the field came from props. */
|
|
41
|
+
itemKey?: string;
|
|
42
|
+
name: string;
|
|
43
|
+
type: FormFieldType;
|
|
44
|
+
/** i18n key the label is rendered (and inline-edited) through. */
|
|
45
|
+
labelKey: string;
|
|
46
|
+
label: string;
|
|
47
|
+
placeholder: string;
|
|
48
|
+
required: boolean;
|
|
49
|
+
options: string[];
|
|
50
|
+
}
|
|
51
|
+
export declare function formFieldsPrefix(formId: string): string;
|
|
52
|
+
export declare function parseOptions(value: string | undefined): string[];
|
|
53
|
+
/**
|
|
54
|
+
* Fields as the locale bundle defines them, or null when no loaded language
|
|
55
|
+
* has a `fields` list for this form and it should fall back to whatever the
|
|
56
|
+
* host passed. The current language is read first, then i18next's fallback
|
|
57
|
+
* chain, so a second language that has not translated the form still gets
|
|
58
|
+
* its structure. Items without a name are skipped: they are the empty husks a
|
|
59
|
+
* half-finished edit can leave behind, not fields.
|
|
60
|
+
*/
|
|
61
|
+
export declare function readLocaleFields(i18n: I18nType, lng: string, formId: string, ns?: string): ResolvedFormField[] | null;
|
|
62
|
+
/** The host-supplied shape a form can still be given as a prop. */
|
|
63
|
+
export interface FormFieldProp {
|
|
64
|
+
/** Submitted form field name. */
|
|
65
|
+
name: string;
|
|
66
|
+
type: FormFieldType;
|
|
67
|
+
required?: boolean;
|
|
68
|
+
/** i18n key for the field label. */
|
|
69
|
+
labelKey: string;
|
|
70
|
+
/** i18n key for the placeholder text. */
|
|
71
|
+
placeholderKey?: string;
|
|
72
|
+
/** i18n key for the options of a select/radio/checkboxes field, one per line. */
|
|
73
|
+
optionsKey?: string;
|
|
74
|
+
}
|
|
75
|
+
export declare function resolvePropFields(fields: FormFieldProp[], t: TFunction): ResolvedFormField[];
|
|
76
|
+
/** `Hva gjelder det?` → `hva-gjelder-det`; falls back to `felt`. */
|
|
77
|
+
export declare function slugifyFieldName(label: string): string;
|
|
78
|
+
/** A name no existing field uses. */
|
|
79
|
+
export declare function uniqueFieldName(base: string, taken: Iterable<string>): string;
|
|
80
|
+
export type FormValue = string | boolean | string[];
|
|
81
|
+
export type FormValues = Record<string, FormValue>;
|
|
82
|
+
export declare function emptyValue(type: FormFieldType): FormValue;
|
|
83
|
+
export declare function emptyValues(fields: ResolvedFormField[]): FormValues;
|
|
84
|
+
export type FieldErrorCode = 'required' | 'email' | 'url' | 'number';
|
|
85
|
+
/** Norwegian fallbacks; a site overrides them via `form.validation.<code>`. */
|
|
86
|
+
export declare const FIELD_ERROR_DEFAULTS: Record<FieldErrorCode, string>;
|
|
87
|
+
export declare function validateValues(fields: ResolvedFormField[], values: FormValues): Record<string, FieldErrorCode>;
|
|
88
|
+
/**
|
|
89
|
+
* The body `SettoForm` posts to setto-server. See `form-submission.ts` there.
|
|
90
|
+
*
|
|
91
|
+
* `hp` and `elapsed` are the bot signals: the honeypot's value (a person
|
|
92
|
+
* cannot see the field, so anything in it came from a script) and the
|
|
93
|
+
* milliseconds between the visitor's first touch of the form and submit
|
|
94
|
+
* (a script fills a form in tens of milliseconds; a person cannot).
|
|
95
|
+
*/
|
|
96
|
+
export interface SubmissionEnvelope {
|
|
97
|
+
data: FormValues;
|
|
98
|
+
labels: Record<string, string>;
|
|
99
|
+
order: string[];
|
|
100
|
+
title?: string;
|
|
101
|
+
page?: string;
|
|
102
|
+
hp: string;
|
|
103
|
+
elapsed: number;
|
|
104
|
+
}
|
|
105
|
+
export declare function buildEnvelope(fields: ResolvedFormField[], values: FormValues, extra: {
|
|
106
|
+
title?: string;
|
|
107
|
+
page?: string;
|
|
108
|
+
hp: string;
|
|
109
|
+
elapsed: number;
|
|
110
|
+
}): SubmissionEnvelope;
|
|
111
|
+
/** Where a site's forms go unless the host says otherwise. */
|
|
112
|
+
export declare function settoSubmissionUrl(apiUrl: string, siteId: string, formId: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SettoModule } from '../module';
|
|
2
|
+
/**
|
|
3
|
+
* The forms module: a form primitive the site renders, an inbox in the
|
|
4
|
+
* dashboard, and the api segment between them. Server side lives in
|
|
5
|
+
* setto-server `src/modules/forms`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const formsModule: SettoModule;
|
|
8
|
+
export { SettoForm } from './SettoForm';
|
|
9
|
+
export type { SettoFormProps, SettoFormClassNames } from './SettoForm';
|
|
10
|
+
export type { FormSettings, FormSubmissionRow, FormsApi } from './api';
|
|
11
|
+
export { FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS, formFieldsPrefix, isFormFieldType, readLocaleFields, settoSubmissionUrl, } from './fields';
|
|
12
|
+
export type { FormFieldProp, FormFieldType, FormValue, FormValues, ResolvedFormField, SubmissionEnvelope, } from './fields';
|