@unitninety/site-kit 1.6.4 → 1.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.
- package/README.md +69 -1
- package/package.json +2 -2
- package/src/elements/declaration.ts +1 -1
- package/src/elements/define.ts +1 -1
- package/src/elements/ops/expand.ts +4 -1
- package/src/legacy/cli.mjs +2 -2
- package/src/legacy/content.ts +1 -1
- package/src/site/defineSite.ts +6 -3
- package/src/site/documents.ts +2 -2
- package/src/site/forms.ts +32 -0
- package/src/site/index.ts +2 -0
- package/src/site/types.ts +4 -1
- package/src/site/version.ts +1 -1
package/README.md
CHANGED
|
@@ -5,13 +5,19 @@ The library a UnitNinety site is built on. It reads the site's content from Unit
|
|
|
5
5
|
You do not usually add it by hand. The `u90` tool adds it when it converts a Next.js App Router site, at the one version that release of the tool works with:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx --yes @unitninety/u90@1.0.
|
|
8
|
+
npx --yes @unitninety/u90@1.0.5 --version
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
UnitNinety imports packages made for that version only, so keep the version the tool names.
|
|
12
12
|
|
|
13
13
|
Documentation: [converting a site](https://unitninety.com/docs/convert), [importing it](https://unitninety.com/docs/import) and [the U90 package](https://unitninety.com/docs/package).
|
|
14
14
|
|
|
15
|
+
### Layout reordering in Canvas
|
|
16
|
+
|
|
17
|
+
Site Kit 1.6.5 gives every draft layout Slot its own preview address. Moving the Slot or placing a block before it updates immediately through the shared renderer, including nested layouts. The optimistic order stays visible while saving, and older responses cannot undo a newer move. Published output is unchanged.
|
|
18
|
+
|
|
19
|
+
Use Site Kit 1.6.5 with `@unitninety/u90@1.0.5` for new sites. Existing sites must update their Site Kit dependency and redeploy to receive the fix; updating the platform alone does not update a site's bundled renderer.
|
|
20
|
+
|
|
15
21
|
### Fixed field values
|
|
16
22
|
|
|
17
23
|
Use `lock: ['$']` on a leaf such as `number({ lock: ['$'] })` to retain a site-owned value without offering a content editor for it. Updates cannot change, clear, or remove that field, including when its previous value was absent. The stored value remains available to site reads and exports. Existing locks such as `lock: ['to']` on a link continue to protect only that subpath.
|
|
@@ -37,6 +43,24 @@ The form uses each field's label, default and text constraints. The list must co
|
|
|
37
43
|
|
|
38
44
|
Creation saves a draft with the normal schema defaults and approved template. Routed entries open Canvas for images and sections; collections without pages keep their content editor in the CMS. The matching platform/Site Kit implementation must be deployed together before using this new metadata in an uploaded package.
|
|
39
45
|
|
|
46
|
+
### Immediate authored-list previews
|
|
47
|
+
|
|
48
|
+
Client components inside a draft block can opt in with `useDraftContent`:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
'use client'
|
|
52
|
+
import { useDraftContent } from '@unitninety/site-kit/bridge'
|
|
53
|
+
|
|
54
|
+
export function Header({ navigation }) {
|
|
55
|
+
const draft = useDraftContent(navigation, 'navigation')
|
|
56
|
+
return <nav>{draft.menuItems.map(item => <a key={item._id} href={item.href.href ?? '#'}>{item.label.text}</a>)}</nav>
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Call the hook on native reads before converting them for a presentation component. `renderBlocks` supplies the draft block path automatically; custom `DraftBlocks` renderers must supply each item's native `path`. Key rows by `_id`, not their editable label, URL or position. Existing server-only published content is unchanged.
|
|
61
|
+
|
|
62
|
+
Canvas prepares locally resolvable list defaults with a stable random item ID, previews the same list in both inspector and site, then saves through the normal validated queue. Nested lists, empty lists and duplicate responsive renderings work without DOM cloning. Later edits survive older refresh responses; rejected or ambiguous structural saves reconcile with an authoritative read. Draft IDs cannot collide with an existing item. Media/reference defaults requiring resolution still use the server path, and references continue using their existing entry previews. This opt-in requires the updated Site Kit; older bridges retain their ordinary DOM previews and refresh behavior.
|
|
63
|
+
|
|
40
64
|
### Immediate collection-list previews
|
|
41
65
|
|
|
42
66
|
Render a query's `EntriesRead` with `Entries` from `@unitninety/site-kit/components` when its
|
|
@@ -54,3 +78,47 @@ pending order/removal through slow refreshes; rejected saves reconcile to the se
|
|
|
54
78
|
Published output is an ordinary div and server-rendered cards. Newly added entries whose markup
|
|
55
79
|
is not already present appear when the save's server refresh arrives. Query results rendered
|
|
56
80
|
with a plain `.map()` still work, but wait for that refresh to change their order or membership.
|
|
81
|
+
|
|
82
|
+
### Shared blocks with page-only copies
|
|
83
|
+
|
|
84
|
+
A shared block uses one content group whose declaration matches a Single:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
const enquiryContent = group({ heading: text(), contact: link() })
|
|
88
|
+
const singles = defineSingles({ enquiry: enquiryContent })
|
|
89
|
+
const enquiry = block('enquiry', {
|
|
90
|
+
label: 'Enquiry',
|
|
91
|
+
fields: { content: enquiryContent },
|
|
92
|
+
shared: { single: 'enquiry', field: 'content' },
|
|
93
|
+
component: Enquiry,
|
|
94
|
+
})
|
|
95
|
+
// Include enquiry in a normal zone's allow/default lists.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A shared block may retain additional design fields only when every leaf is locked with `lock: ['$']`. Optional `shared.previewFields` lists the content keys this placement actually renders (1–30 unique existing keys); it filters both linked and page-specific trees without deleting stored data. Shared content uses the same accordion and selected-field editor as ordinary blocks.
|
|
99
|
+
|
|
100
|
+
The default `content: null` follows the Single. A content object is an independent copy owned by that block instance. Both render through the same component and retain the same block ID and controls. The Inspector labels the scope and offers shared editing or “Customise for this page”; customising copies the latest saved shared value. Templates seed the initial blocks, while an authored empty zone remains empty. Converting an existing hardcoded placement requires migrating its documents and renderer together.
|
|
101
|
+
|
|
102
|
+
### Inspector-only canvas editing
|
|
103
|
+
|
|
104
|
+
Pass `inlineEditing={false}` to `U90Bridge` in the root layout when clients should type only in the inspector. Canvas clicks still select and highlight fields; inspector text, rich-text and link-label previews remain immediate. The bridge keeps the `data-u90-edit` metadata for previews but does not add `contenteditable` or send canvas input edits. The option defaults to `true` for existing sites.
|
|
105
|
+
|
|
106
|
+
### Featured-entry fields in the block tree
|
|
107
|
+
|
|
108
|
+
Selections, relations and queries can declare the collection fields that their component displays:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
selection('projects', {
|
|
112
|
+
many: true,
|
|
113
|
+
depth: 2,
|
|
114
|
+
previewFields: ['title', 'heroMedia', 'industry', 'client', 'date', 'location'],
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`previewFields` is optional editor metadata: unique top-level field keys (1–20). It does not change stored references, query rules, ordering or read shapes. The same expandable rows work on pages, collection entries and layouts. A referenced entry expands inside its block; choosing a leaf opens its primitive editor above the tree. A referenced field can itself declare `previewFields` to expose a nested list. Cycles and deep chains fall back to the ordinary reference editor. Derived relations retain their existing read-only editor.
|
|
119
|
+
|
|
120
|
+
For example, KAW's Home declares `query('industries', { previewFields: ['name', 'description', 'capabilities'] })`. The Industries collection declares `relation('capabilities', { many: true, previewFields: ['name', 'heroMedia'] })`. Industry names, introductions, capability names and images still write to their original entry paths and update everywhere. Membership edits only change the owning reference. Queries preserve their rules and use pin/exclude/order controls; the plus control uses the existing tenant-scoped picker.
|
|
121
|
+
|
|
122
|
+
The site component must reveal the selected entry in its own tabs/carousels, including when its data arrives after selection. `useEditing()` exposes `selected` and `selectedVia`: the latter contains outer-to-inner reference field paths, disambiguating an entry reused in several parents. Mark a displayed list container with `data-u90-reference={reference.$path}` in draft mode to preserve this context when selecting its content on the canvas. Keep `data-u90-field` on each entry's own fields and a root marker on the revealed entry for row highlighting. A relation's target entry path is not its parent relation field path. These attributes do not introduce visitor-facing editing controls.
|
|
123
|
+
|
|
124
|
+
Deploy matching Site Kit/platform code before importing this metadata, and restart a running dev server after changing registry options so Payload validation hooks reload.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unitninety/site-kit",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.5",
|
|
4
4
|
"description": "The client-site half of the UnitNinety platform contract: schema authoring, content fetching, the editing bridge and the pairing CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"access": "public"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"zod": "4.
|
|
63
|
+
"zod": "4.6.6"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
66
|
"react": ">=19",
|
|
@@ -84,7 +84,7 @@ export const CollectionDeclSchema = z.strictObject({
|
|
|
84
84
|
seen.add(name)
|
|
85
85
|
})
|
|
86
86
|
})
|
|
87
|
-
export const SingleDeclSchema = z.strictObject({ key: z.string().regex(/^[a-z][a-zA-Z0-9_-]*$/), field: FieldDeclSchema })
|
|
87
|
+
export const SingleDeclSchema = z.strictObject({ key: z.string().regex(/^[a-z][a-zA-Z0-9_-]*$/), field: FieldDeclSchema, editor: z.enum(['cms', 'canvas']).optional() })
|
|
88
88
|
export const SchemaSchema = z.strictObject({ blocks: z.record(z.string(), BlockDeclSchema), collections: z.record(z.string(), CollectionDeclSchema), singles: z.record(z.string(), SingleDeclSchema) })
|
|
89
89
|
|
|
90
90
|
type Where = 'block' | 'collection' | 'single'
|
package/src/elements/define.ts
CHANGED
|
@@ -140,7 +140,7 @@ export type CollectionDecl = {
|
|
|
140
140
|
defaultSort?: { field: string; dir: 'asc' | 'desc' } | 'manual'
|
|
141
141
|
order?: string[]
|
|
142
142
|
}
|
|
143
|
-
export type SingleDecl = { key: string; field: FieldDecl }
|
|
143
|
+
export type SingleDecl = { key: string; field: FieldDecl; editor?: 'cms' | 'canvas' }
|
|
144
144
|
export type Schema = {
|
|
145
145
|
blocks: Record<string, BlockDecl>
|
|
146
146
|
collections: Record<string, CollectionDecl>
|
|
@@ -62,7 +62,10 @@ export function expandField(schema: Schema, decl: FieldDecl, value: unknown, ctx
|
|
|
62
62
|
const item = items[i]
|
|
63
63
|
const block = record(item) ? String(item._block) : ''
|
|
64
64
|
const metadata = record(item) && typeof item._hidden === 'boolean' ? { _hidden: item._hidden } : {}
|
|
65
|
-
|
|
65
|
+
// The slot has no fields, but it is still a movable layout item. DraftBlocks needs
|
|
66
|
+
// its address both to move the page content and to place other blocks before it.
|
|
67
|
+
if (block === SLOT) return { _id: c.key, _block: block, ...metadata, ...(ctx.draft ? { $path: path.child(ctx.at, c.key) } : {}) }
|
|
68
|
+
if (!Object.hasOwn(schema.blocks, block)) return { _id: c.key, _block: block, ...metadata }
|
|
66
69
|
return { _id: c.key, _block: block, ...metadata, ...(ctx.draft ? { $path: path.child(ctx.at, c.key) } : {}), ...(inner as object) }
|
|
67
70
|
})
|
|
68
71
|
}
|
package/src/legacy/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* `npx --yes @unitninety/u90@1.0.
|
|
5
|
+
* `npx --yes @unitninety/u90@1.0.5 pair <code>` — the one command an operator hands over. The u90 tool
|
|
6
6
|
* runs this file for `u90 pair`. The command is scoped and pinned because an unscoped name, run in a
|
|
7
7
|
* repository with no local bin, fetches whatever package npm holds under that name.
|
|
8
8
|
*
|
|
@@ -115,7 +115,7 @@ const run = async () => {
|
|
|
115
115
|
const args = process.argv.slice(2)
|
|
116
116
|
|
|
117
117
|
if (args[0] !== 'pair' || !args[1]) {
|
|
118
|
-
fail('Usage: npx --yes @unitninety/u90@1.0.
|
|
118
|
+
fail('Usage: npx --yes @unitninety/u90@1.0.5 pair <code> --site https://example.com [--platform https://cms…]')
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
const code = args[1]
|
package/src/legacy/content.ts
CHANGED
|
@@ -360,7 +360,7 @@ export const createContentClient = ({
|
|
|
360
360
|
: !platformUrl
|
|
361
361
|
? 'PLATFORM_URL is unset'
|
|
362
362
|
: 'PAYLOAD_API_KEY is unset'
|
|
363
|
-
}. Every page will render its committed fallback copy, and every collection route will 404. Set it in the site's environment — \`npx --yes @unitninety/u90@1.0.
|
|
363
|
+
}. Every page will render its committed fallback copy, and every collection route will 404. Set it in the site's environment — \`npx --yes @unitninety/u90@1.0.5 pair\` writes both.`,
|
|
364
364
|
)
|
|
365
365
|
}
|
|
366
366
|
return true
|
package/src/site/defineSite.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FormsSchema, type FormDeclaration } from './forms'
|
|
1
2
|
import { seedIssues, validateSchema, type BlockDecl, type CollectionDecl, type FieldDecl, type Issue, type Schema, type SeedKnown, type ZoneDecl } from '../elements'
|
|
2
3
|
import { SLOT } from './documents'
|
|
3
4
|
import type { BlockDef, CollectionDef, LayoutDef, PageDef, PageTypeDef, Seo, Singles, Site, SiteManifest } from './types'
|
|
@@ -35,7 +36,8 @@ const misplacedZones = (decl: FieldDecl, at: string, direct: boolean, into: Issu
|
|
|
35
36
|
const PAGE_KEY = /^[a-z][a-zA-Z0-9_-]*$/
|
|
36
37
|
const strip = (fields: Record<string, unknown>) => Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, JSON.parse(JSON.stringify(v))])) as Record<string, FieldDecl>
|
|
37
38
|
|
|
38
|
-
export function defineSite(input: { singles: Singles; collections: Record<string, CollectionDef>; layouts: Record<string, LayoutDef>; pages: Record<string, PageDef>; pageTypes: Record<string, PageTypeDef>; seo?: Seo; knowledge?: KnowledgePolicyV1 }): Site {
|
|
39
|
+
export function defineSite(input: { singles: Singles; collections: Record<string, CollectionDef>; layouts: Record<string, LayoutDef>; pages: Record<string, PageDef>; pageTypes: Record<string, PageTypeDef>; seo?: Seo; knowledge?: KnowledgePolicyV1; forms?: FormDeclaration[] }): Site {
|
|
40
|
+
const forms = input.forms === undefined ? undefined : FormsSchema.parse(input.forms)
|
|
39
41
|
const defaults = Object.entries(input.layouts).filter(([, l]) => l.default)
|
|
40
42
|
if (defaults.length !== 1) throw new Error(`A site needs exactly one default layout; found ${defaults.length}.`)
|
|
41
43
|
const defaultLayout = defaults[0][0]
|
|
@@ -96,7 +98,7 @@ export function defineSite(input: { singles: Singles; collections: Record<string
|
|
|
96
98
|
const schema: Schema = {
|
|
97
99
|
blocks: Object.fromEntries([...blocks.values()].map((b) => [b.name, { name: b.name, label: b.label, fields: strip(b.fields), ...(b.shared ? { shared: b.shared } : {}), ...(b.entry ? { entry: b.entry.name } : {}), order: Object.keys(b.fields) } satisfies BlockDecl])),
|
|
98
100
|
collections: Object.fromEntries(Object.values(input.collections).map((c) => [c.name, { name: c.name, label: c.label, fields: strip(c.fields), ...(c.authoring ? { authoring: JSON.parse(JSON.stringify(c.authoring)) } : {}), ...(c.template ? { template: JSON.parse(JSON.stringify(c.template.zone)) } : {}), ...(c.defaultSort ? { defaultSort: c.defaultSort } : {}), order: Object.keys(c.fields) } satisfies CollectionDecl])),
|
|
99
|
-
singles: Object.fromEntries(Object.values(input.singles).map((s) => [s.key, { key: s.key, field: JSON.parse(JSON.stringify(s.decl)) }])),
|
|
101
|
+
singles: Object.fromEntries(Object.values(input.singles).map((s) => [s.key, { key: s.key, ...(s.editor ? { editor: s.editor } : {}), field: JSON.parse(JSON.stringify(s.decl)) }])),
|
|
100
102
|
}
|
|
101
103
|
// Finding 1: seed entries were checked only when seeded, at the platform or in the defaults store.
|
|
102
104
|
const known: SeedKnown = {
|
|
@@ -106,6 +108,7 @@ export function defineSite(input: { singles: Singles; collections: Record<string
|
|
|
106
108
|
const badSeeds = Object.values(input.collections).flatMap((c) => (c.entries ?? []).flatMap((e) => seedIssues(schema, { kind: 'entry', collection: c.name }, { fields: e.fields, blocks: e.blocks }, `collections.${c.name}.entries.${e.slug}`, known)))
|
|
107
109
|
if (badSeeds.length) throw new SiteDeclarationError(badSeeds)
|
|
108
110
|
return {
|
|
111
|
+
...(forms !== undefined ? { forms } : {}),
|
|
109
112
|
...(input.knowledge !== undefined ? { knowledge: input.knowledge } : {}),
|
|
110
113
|
singles: input.singles, collections: input.collections, blocks: Object.fromEntries(blocks), schema, seo: input.seo ?? {}, defaultLayout,
|
|
111
114
|
layouts: Object.fromEntries(Object.entries(input.layouts).map(([k, l]) => [k, { default: !!l.default, parent: l.parent, blocks: l.entries.map((e) => (e === SLOT ? 'SLOT' : e.name)) }])),
|
|
@@ -128,5 +131,5 @@ export function manifestFrom(site: Site): SiteManifest {
|
|
|
128
131
|
pageTypes: Object.keys(site.pageTypes),
|
|
129
132
|
layouts: Object.keys(site.layouts),
|
|
130
133
|
}
|
|
131
|
-
return { ...(site.knowledge !== undefined ? { knowledge: site.knowledge } : {}), schema: r.schema, layouts: site.layouts, pages: site.pages, pageTypes: site.pageTypes, entries, collections, routes, seo: site.seo, bridgeVersion: 4, siteKitVersion: SITE_KIT_VERSION, order }
|
|
134
|
+
return { ...(site.forms !== undefined ? { forms: FormsSchema.parse(site.forms) } : {}), ...(site.knowledge !== undefined ? { knowledge: site.knowledge } : {}), schema: r.schema, layouts: site.layouts, pages: site.pages, pageTypes: site.pageTypes, entries, collections, routes, seo: site.seo, bridgeVersion: 4, siteKitVersion: SITE_KIT_VERSION, order }
|
|
132
135
|
}
|
package/src/site/documents.ts
CHANGED
|
@@ -4,8 +4,8 @@ import type { BlockDef, CollectionDef, Decl, EntrySeed, Fields, LayoutDef, PageD
|
|
|
4
4
|
export const SLOT = 'SLOT' as const
|
|
5
5
|
export const humanise = (name: string) => name.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase())
|
|
6
6
|
|
|
7
|
-
export const defineSingles = <M extends Record<string, Decl<unknown>>>(map: M): { [K in keyof M]: SingleDef<M[K] extends Decl<infer R> ? R : never> } =>
|
|
8
|
-
Object.fromEntries(Object.entries(map).map(([key, decl]) => [key, { key, decl }])) as never
|
|
7
|
+
export const defineSingles = <M extends Record<string, Decl<unknown>>>(map: M, authoring: Partial<Record<keyof M, { editor: 'cms' | 'canvas' }>> = {}): { [K in keyof M]: SingleDef<M[K] extends Decl<infer R> ? R : never> } =>
|
|
8
|
+
Object.fromEntries(Object.entries(map).map(([key, decl]) => [key, { key, decl, ...authoring[key] }])) as never
|
|
9
9
|
export const collection = <F extends Fields>(name: string, o: { label?: string; fields: F; route?: string; defaultSort?: CollectionDef['defaultSort']; entries?: EntrySeed[]; authoring?: CollectionDef['authoring'] }): CollectionDef<F> =>
|
|
10
10
|
({ kind: 'collection', name, label: o.label ?? humanise(name), fields: o.fields, route: o.route, defaultSort: o.defaultSort, ...(o.authoring ? { authoring: o.authoring } : {}), ...(o.entries ? { entries: o.entries } : {}) })
|
|
11
11
|
export const withTemplate = <C extends CollectionDef>(c: C, zone: Decl<unknown>, o: { layout?: string } = {}): C =>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
const identifier = z.string().regex(/^[a-zA-Z][a-zA-Z0-9_]*$/).max(100)
|
|
4
|
+
const label = z.string().trim().min(1).max(200)
|
|
5
|
+
export const FormFieldSchema = z.strictObject({
|
|
6
|
+
name: identifier, label,
|
|
7
|
+
type: z.enum(['checkbox', 'date', 'email', 'number', 'select', 'tel', 'text', 'textarea']),
|
|
8
|
+
required: z.boolean().optional(), role: z.enum(['email', 'name', 'phone']).optional(),
|
|
9
|
+
options: z.array(label).max(200).optional(),
|
|
10
|
+
}).superRefine((field, ctx) => {
|
|
11
|
+
if (field.type === 'select' && !field.options?.length) ctx.addIssue({ code: 'custom', path: ['options'], message: 'Select fields need options.' })
|
|
12
|
+
if (field.options && new Set(field.options).size !== field.options.length) ctx.addIssue({ code: 'custom', path: ['options'], message: 'Options must be unique.' })
|
|
13
|
+
})
|
|
14
|
+
export const FormDeclarationSchema = z.strictObject({
|
|
15
|
+
slug: identifier, label, fields: z.array(FormFieldSchema).min(1).max(100),
|
|
16
|
+
recipients: z.array(z.email()).max(50).optional(),
|
|
17
|
+
}).superRefine((form, ctx) => {
|
|
18
|
+
const names = new Set<string>()
|
|
19
|
+
form.fields.forEach((field, i) => {
|
|
20
|
+
if (names.has(field.name)) ctx.addIssue({ code: 'custom', path: ['fields', i, 'name'], message: 'Field names must be unique.' })
|
|
21
|
+
names.add(field.name)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
export const FormsSchema = z.array(FormDeclarationSchema).max(100).superRefine((forms, ctx) => {
|
|
25
|
+
const slugs = new Set<string>()
|
|
26
|
+
forms.forEach((form, i) => {
|
|
27
|
+
if (slugs.has(form.slug)) ctx.addIssue({ code: 'custom', path: [i, 'slug'], message: 'Form slugs must be unique.' })
|
|
28
|
+
slugs.add(form.slug)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
export type FormDeclaration = z.infer<typeof FormDeclarationSchema>
|
|
32
|
+
export type FormFieldDeclaration = z.infer<typeof FormFieldSchema>
|
package/src/site/index.ts
CHANGED
|
@@ -15,3 +15,5 @@ export { sitemapFor } from '../seo/sitemap'
|
|
|
15
15
|
|
|
16
16
|
export { SeoSeedsSchema, DocumentSeoSeedSchema, SiteSeoSeedSchema, seoSeedIssues, seoSeedAssets, type SeoSeeds, type DocumentSeoSeed, type SiteSeoSeed } from '../seo/seeds'
|
|
17
17
|
export { CollectionViewSchema, CollectionViewsSchema, collectionViewFields, defaultCollectionView, reconcileCollectionView, validateCollectionView, collectionViewsIssues, type CollectionView, type CollectionViews, type CollectionViewField } from './formView'
|
|
18
|
+
|
|
19
|
+
export { FormsSchema, FormDeclarationSchema, FormFieldSchema, type FormDeclaration, type FormFieldDeclaration } from './forms'
|
package/src/site/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { KnowledgePolicyV1 } from '../features/contracts/knowledgePolicy'
|
|
2
|
+
import type { FormDeclaration } from './forms'
|
|
2
3
|
import type { ReactNode } from 'react'
|
|
3
4
|
import type { BlockDecl, CollectionDecl, FieldDecl, GroupDecl, LeafDecl, ListDecl, Schema, SingleDecl, ZoneDecl } from '../elements'
|
|
4
5
|
|
|
@@ -20,7 +21,7 @@ export type Simplify<T> = { [K in keyof T]: T[K] } & {}
|
|
|
20
21
|
*/
|
|
21
22
|
export type ZoneNode = ReactNode
|
|
22
23
|
|
|
23
|
-
export type SingleDef<R = unknown> = { key: string; decl: Decl<R
|
|
24
|
+
export type SingleDef<R = unknown> = { key: string; decl: Decl<R>; editor?: 'cms' | 'canvas' }
|
|
24
25
|
export type Singles = Record<string, SingleDef>
|
|
25
26
|
/**
|
|
26
27
|
* An entry a collection starts with: its slug (also the key a seed's `{ $ref: { key } }` names it
|
|
@@ -45,6 +46,7 @@ export type Seo = { titleTemplate?: string; defaultImage?: string; organisation?
|
|
|
45
46
|
|
|
46
47
|
export type Site = {
|
|
47
48
|
knowledge?: KnowledgePolicyV1
|
|
49
|
+
forms?: FormDeclaration[]
|
|
48
50
|
singles: Singles; collections: Record<string, CollectionDef>
|
|
49
51
|
layouts: Record<string, { default: boolean; parent?: string; blocks: string[] }>
|
|
50
52
|
pages: Record<string, { route: string; layout: string; blocks: ZoneDecl }>
|
|
@@ -53,6 +55,7 @@ export type Site = {
|
|
|
53
55
|
}
|
|
54
56
|
export type SiteManifest = {
|
|
55
57
|
knowledge?: KnowledgePolicyV1
|
|
58
|
+
forms?: FormDeclaration[]
|
|
56
59
|
schema: Schema
|
|
57
60
|
layouts: Site['layouts']; pages: Site['pages']; pageTypes: Site['pageTypes']
|
|
58
61
|
/** Each collection's declared seed entries, by collection name; collections that declare none are absent. */
|
package/src/site/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SITE_KIT_VERSION = '1.6.
|
|
1
|
+
export const SITE_KIT_VERSION = '1.6.5'
|