@unitninety/site-kit 1.6.2 → 1.6.4

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 (44) hide show
  1. package/README.md +26 -1
  2. package/package.json +1 -1
  3. package/src/bridge/DraftBlocks.tsx +14 -7
  4. package/src/bridge/U90Bridge.tsx +7 -4
  5. package/src/bridge/client.tsx +3 -2
  6. package/src/bridge/createBridge.ts +47 -12
  7. package/src/bridge/hooks.tsx +9 -8
  8. package/src/bridge/index.ts +6 -0
  9. package/src/bridge/nativeDraft.ts +177 -0
  10. package/src/bridge/protocol.ts +45 -7
  11. package/src/bridge/registry.ts +6 -1
  12. package/src/bridge/selection.ts +134 -0
  13. package/src/bridge/styles.ts +10 -1
  14. package/src/bridge/useDraftContent.tsx +151 -0
  15. package/src/components/Link.tsx +1 -1
  16. package/src/elements/containers/group.ts +1 -1
  17. package/src/elements/containers/zone.ts +13 -2
  18. package/src/elements/declaration.ts +89 -2
  19. package/src/elements/define.ts +22 -3
  20. package/src/elements/jsonSchema.ts +4 -3
  21. package/src/elements/leaves/link.ts +17 -2
  22. package/src/elements/leaves/query.ts +6 -1
  23. package/src/elements/leaves/relation.ts +3 -0
  24. package/src/elements/leaves/rich-text.ts +32 -16
  25. package/src/elements/leaves/selection.ts +5 -4
  26. package/src/elements/ops/expand.ts +5 -4
  27. package/src/elements/ops/seed.ts +25 -6
  28. package/src/elements/ops/validate.ts +31 -2
  29. package/src/legacy/cli.mjs +2 -2
  30. package/src/legacy/content.ts +1 -1
  31. package/src/seo/robots.ts +8 -8
  32. package/src/seo/seeds.ts +61 -0
  33. package/src/server/render.tsx +3 -3
  34. package/src/site/assemble.ts +1 -1
  35. package/src/site/defineSite.ts +2 -2
  36. package/src/site/documents.ts +4 -4
  37. package/src/site/fields.ts +6 -6
  38. package/src/site/formView.ts +175 -0
  39. package/src/site/index.ts +3 -0
  40. package/src/site/replies.ts +5 -4
  41. package/src/site/types.ts +3 -1
  42. package/src/site/version.ts +1 -1
  43. package/src/site/wire.ts +1 -1
  44. package/src/testing/fake.ts +4 -4
@@ -48,7 +48,7 @@ const declOf = (site: Site, d: ReplyDocument): DocumentDecl | null => {
48
48
  export function assemble(site: Site, reply: Reply, opts: AssembleOptions): RouteRead {
49
49
  if (reply.status === 'missing') {
50
50
  if (opts.stale && opts.defaults && declaredCodePage(site, opts.target))
51
- return { ...assemble(site, replyFrom(site, opts.defaults, opts.target, reply.manifest), { ...opts, stale: false, defaults: null }), unavailable: true }
51
+ return { ...assemble(site, replyFrom(site, opts.defaults, opts.target, reply.manifest, opts.draft), { ...opts, stale: false, defaults: null }), unavailable: true }
52
52
  return { status: 'missing', tags: reply.tags }
53
53
  }
54
54
  const d = reply.document
@@ -94,8 +94,8 @@ export function defineSite(input: { singles: Singles; collections: Record<string
94
94
  for (const [k, c] of Object.entries(input.collections)) if (c.route) claim(c.route, `collection ${k}`)
95
95
  for (const [k, t] of Object.entries(input.pageTypes)) claim(t.route, `page type ${k}`)
96
96
  const schema: Schema = {
97
- blocks: Object.fromEntries([...blocks.values()].map((b) => [b.name, { name: b.name, label: b.label, fields: strip(b.fields), ...(b.entry ? { entry: b.entry.name } : {}), order: Object.keys(b.fields) } satisfies BlockDecl])),
98
- collections: Object.fromEntries(Object.values(input.collections).map((c) => [c.name, { name: c.name, label: c.label, fields: strip(c.fields), ...(c.template ? { template: JSON.parse(JSON.stringify(c.template.zone)) } : {}), ...(c.defaultSort ? { defaultSort: c.defaultSort } : {}), order: Object.keys(c.fields) } satisfies CollectionDecl])),
97
+ 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
+ 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
99
  singles: Object.fromEntries(Object.values(input.singles).map((s) => [s.key, { key: s.key, field: JSON.parse(JSON.stringify(s.decl)) }])),
100
100
  }
101
101
  // Finding 1: seed entries were checked only when seeded, at the platform or in the defaults store.
@@ -6,12 +6,12 @@ export const humanise = (name: string) => name.replace(/([a-z])([A-Z])/g, '$1 $2
6
6
 
7
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
8
  Object.fromEntries(Object.entries(map).map(([key, decl]) => [key, { key, decl }])) as never
9
- export const collection = <F extends Fields>(name: string, o: { label?: string; fields: F; route?: string; defaultSort?: CollectionDef['defaultSort']; entries?: EntrySeed[] }): CollectionDef<F> =>
10
- ({ kind: 'collection', name, label: o.label ?? humanise(name), fields: o.fields, route: o.route, defaultSort: o.defaultSort, ...(o.entries ? { entries: o.entries } : {}) })
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
+ ({ 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 =>
12
12
  ({ ...c, template: { zone: zone as ZoneDecl, layout: o.layout } })
13
- export const block = <F extends Fields, C extends CollectionDef | undefined = undefined>(name: string, o: { label?: string; fields: F; component: unknown; entry?: C }): BlockDef<F, C> =>
14
- ({ kind: 'block', name, label: o.label ?? humanise(name), fields: o.fields, component: o.component, entry: o.entry })
13
+ export const block = <F extends Fields, C extends CollectionDef | undefined = undefined>(name: string, o: { label?: string; fields: F; component: unknown; entry?: C; shared?: BlockDef['shared'] }): BlockDef<F, C> =>
14
+ ({ kind: 'block', name, label: o.label ?? humanise(name), fields: o.fields, component: o.component, entry: o.entry, ...(o.shared ? { shared: o.shared } : {}) })
15
15
  export const layout = (o: { default?: boolean; parent?: string }, entries: Array<BlockDef | typeof SLOT>): LayoutDef => ({ kind: 'layout', ...o, entries })
16
16
  export const page = (o: { route: string; layout?: string; blocks: Decl<unknown> }): PageDef => ({ kind: 'page', route: o.route, layout: o.layout, blocks: o.blocks as ZoneDecl })
17
17
  export const pageType = (o: { label: string; route: string; layout?: string; blocks: Decl<unknown> }): PageTypeDef => ({ kind: 'pageType', ...o, blocks: o.blocks as ZoneDecl })
@@ -19,20 +19,20 @@ export const image = (o: Common & { default?: unknown; constraints?: unknown; va
19
19
  leaf<ImageRead>('u90/image', o, { constraints: o.constraints, variants: o.variants })
20
20
  export const video = (o: Common & { default?: unknown; poster?: unknown } = {}) => leaf<VideoRead>('u90/video', o, { poster: o.poster })
21
21
  export const file = (o: Common & { default?: unknown; mimeTypes?: string[]; maxBytes?: number } = {}) => leaf<FileRead>('u90/file', o, { mimeTypes: o.mimeTypes, maxBytes: o.maxBytes })
22
- export const link = (o: Common & { default?: unknown; allow?: Array<'page' | 'entry' | 'url' | 'email' | 'tel'> } = {}) => leaf<LinkRead>('u90/link', o, { allow: o.allow })
22
+ export const link = (o: Common & { default?: unknown; allow?: Array<'page' | 'entry' | 'file' | 'url' | 'email' | 'tel'> } = {}) => leaf<LinkRead>('u90/link', o, { allow: o.allow })
23
23
  export const date = (o: Common & { default?: string; min?: string; max?: string } = {}) => leaf<DateRead>('u90/date', o, { min: o.min, max: o.max })
24
24
  export const number = (o: Common & { default?: number; integer?: boolean; min?: number; max?: number; step?: number } = {}) => leaf<NumberRead>('u90/number', o, { integer: o.integer, min: o.min, max: o.max, step: o.step })
25
25
  export const boolean = (o: Common & { default?: boolean } = {}) => leaf<BooleanRead>('u90/boolean', o, {})
26
26
  export const choice = (values: Array<string | { value: string; label: string }>, o: Common & { default?: string | string[]; multiple?: boolean } = {}) =>
27
27
  leaf<ChoiceReadValue>('u90/choice', o, { values: values.map((v) => (typeof v === 'string' ? { value: v, label: v[0].toUpperCase() + v.slice(1) } : v)), multiple: o.multiple })
28
- export const relation = (to: string, o: Common & { default?: unknown; many?: boolean; unique?: boolean; inverseOf?: string; depth?: number } = {}) =>
28
+ export const relation = (to: string, o: Common & { default?: unknown; many?: boolean; unique?: boolean; inverseOf?: string; depth?: number; previewFields?: string[] } = {}) =>
29
29
  leaf<EntryRead | EntryRead[] | null>('u90/relation', o, {
30
- to, inverseOf: o.inverseOf, depth: o.depth,
30
+ to, inverseOf: o.inverseOf, depth: o.depth, previewFields: o.previewFields,
31
31
  cardinality: o.inverseOf ? (o.many ? 'many-to-many' : o.unique ? 'one-to-one' : 'one-to-many') : o.many ? 'many-to-many' : o.unique ? 'one-to-one' : 'many-to-one',
32
32
  })
33
- export const selection = (to: string, o: Common & { default?: unknown; many?: boolean; max?: number; depth?: number } = {}) =>
34
- leaf<EntriesRead>('u90/selection', o, { to, many: o.many, max: o.max, depth: o.depth })
35
- export const query = (to: string, o: Common & { default?: unknown } = {}) => leaf<EntriesRead>('u90/query', o, { to })
33
+ export const selection = (to: string, o: Common & { default?: unknown; many?: boolean; max?: number; depth?: number; previewFields?: string[] } = {}) =>
34
+ leaf<EntriesRead>('u90/selection', o, { to, many: o.many, max: o.max, depth: o.depth, previewFields: o.previewFields })
35
+ export const query = (to: string, o: Common & { default?: unknown; previewFields?: string[] } = {}) => leaf<EntriesRead>('u90/query', o, { to, previewFields: o.previewFields })
36
36
  export const single = <R>(s: SingleDef<R>, o: Common = {}): Decl<R> => leaf<R>('u90/single', o, { key: s.key })
37
37
 
38
38
  export const group = <F extends Fields>(fields: F, o: { label?: string } = {}): Decl<{ [K in keyof F]: Read<F[K]> }> & { readonly __group: true } =>
@@ -0,0 +1,175 @@
1
+ import { z } from 'zod'
2
+ import { isLeaf, type CollectionDecl, type FieldDecl } from '../elements/define'
3
+ import { orderedEntries } from '../elements/order'
4
+
5
+ const safeKey = z
6
+ .string()
7
+ .min(1)
8
+ .max(200)
9
+ .refine((key) => !['__proto__', 'constructor', 'prototype'].includes(key), 'Invalid field key.')
10
+ const Field = z.strictObject({ key: safeKey, width: z.enum(['half', 'full']) })
11
+ const Group = z.strictObject({
12
+ id: z.string().regex(/^[a-zA-Z0-9_-]{1,80}$/),
13
+ label: z.string().trim().max(80),
14
+ fields: z.array(Field).max(500),
15
+ })
16
+ /** UI presentation only. No visibility, defaults, validation or site design settings. */
17
+ export const CollectionViewSchema = z.strictObject({
18
+ version: z.literal(1),
19
+ groups: z.array(Group).min(1).max(50),
20
+ })
21
+ export const CollectionViewsSchema = z
22
+ .custom<Record<string, unknown>>(
23
+ (value) =>
24
+ !!value &&
25
+ typeof value === 'object' &&
26
+ !Array.isArray(value) &&
27
+ Object.keys(value).length <= 200 &&
28
+ Object.keys(value).every((key) => !['__proto__', 'constructor', 'prototype'].includes(key)),
29
+ 'Invalid collection views.',
30
+ )
31
+ .pipe(z.record(safeKey, CollectionViewSchema))
32
+ export type CollectionView = z.infer<typeof CollectionViewSchema>
33
+ export type CollectionViews = z.infer<typeof CollectionViewsSchema>
34
+ export type CollectionViewField = {
35
+ key: string
36
+ label: string
37
+ width: 'half' | 'full'
38
+ required: boolean
39
+ }
40
+ const humanise = (name: string) =>
41
+ name
42
+ .replace(/([a-z\d])([A-Z])/g, '$1 $2')
43
+ .replace(/[-_]/g, ' ')
44
+ .replace(/^./, (c) => c.toUpperCase())
45
+ const editable = (field: FieldDecl): boolean =>
46
+ isLeaf(field)
47
+ ? !field.lock?.includes('$')
48
+ : field.container !== 'u90/group' || Object.values(field.fields).some(editable)
49
+
50
+ /** Complex schema containers remain single units, with their own nested editors. */
51
+ export function collectionViewFields(decl: CollectionDecl): CollectionViewField[] {
52
+ const fields = orderedEntries(decl.fields, decl.order).filter(([, field]) => editable(field))
53
+ const title = fields.find(([, field]) => isLeaf(field) && field.element === 'u90/text')?.[0]
54
+ return fields.map(([key, field]) => ({
55
+ key,
56
+ label: field.label || humanise(key),
57
+ required:
58
+ key === title ||
59
+ (isLeaf(field)
60
+ ? field.required === true
61
+ : field.container === 'u90/list' && (field.min ?? 0) > 0),
62
+ width:
63
+ key === title ||
64
+ !isLeaf(field) ||
65
+ field.options?.multiline === true ||
66
+ ['u90/image', 'u90/video', 'u90/file', 'u90/rich-text', 'u90/link', 'u90/query'].includes(
67
+ field.element,
68
+ )
69
+ ? 'full'
70
+ : 'half',
71
+ }))
72
+ }
73
+ const MAX_GROUPS = 50
74
+ const MAX_FIELDS_PER_GROUP = 500
75
+ export const defaultCollectionView = (fields: CollectionViewField[]): CollectionView => {
76
+ if (fields.length > MAX_GROUPS * MAX_FIELDS_PER_GROUP)
77
+ throw new Error('This collection has too many fields to configure.')
78
+ return {
79
+ version: 1,
80
+ groups: Array.from(
81
+ { length: Math.max(1, Math.ceil(fields.length / MAX_FIELDS_PER_GROUP)) },
82
+ (_, i) => ({
83
+ id: i ? `fields_${i}` : 'fields',
84
+ label: '',
85
+ fields: fields
86
+ .slice(i * MAX_FIELDS_PER_GROUP, (i + 1) * MAX_FIELDS_PER_GROUP)
87
+ .map(({ key, width }) => ({ key, width })),
88
+ }),
89
+ ),
90
+ }
91
+ }
92
+
93
+ /** Saved views from older schemas may not conceal newly added fields or revive locked ones. */
94
+ export function reconcileCollectionView(
95
+ input: unknown,
96
+ fields: CollectionViewField[],
97
+ ): CollectionView {
98
+ const parsed = CollectionViewSchema.safeParse(input)
99
+ if (!parsed.success) return defaultCollectionView(fields)
100
+ const available = new Set(fields.map((f) => f.key))
101
+ const seen = new Set<string>()
102
+ const ids = new Set<string>()
103
+ const groups = parsed.data.groups
104
+ .filter((group) => {
105
+ if (ids.has(group.id)) return false
106
+ ids.add(group.id)
107
+ return true
108
+ })
109
+ .map((group) => ({
110
+ ...group,
111
+ fields: group.fields.filter((field) => {
112
+ if (!available.has(field.key) || seen.has(field.key)) return false
113
+ seen.add(field.key)
114
+ return true
115
+ }),
116
+ }))
117
+ const missing = fields
118
+ .filter((field) => !seen.has(field.key))
119
+ .map(({ key, width }) => ({ key, width }))
120
+ for (const field of missing) {
121
+ let target = groups.find((group) => !group.label && group.fields.length < MAX_FIELDS_PER_GROUP)
122
+ if (!target && groups.length < MAX_GROUPS) {
123
+ let id = 'fields'
124
+ let suffix = 1
125
+ while (ids.has(id)) id = `fields_${suffix++}`
126
+ ids.add(id)
127
+ target = { id, label: '', fields: [] }
128
+ groups.push(target)
129
+ }
130
+ target ??= groups.find((group) => group.fields.length < MAX_FIELDS_PER_GROUP)
131
+ if (!target) throw new Error('This collection has too many fields to configure.')
132
+ target.fields.push(field)
133
+ }
134
+ return { version: 1, groups }
135
+ }
136
+
137
+ /** Writes must account for every field, exactly once. Reads alone reconcile schema drift. */
138
+ export function validateCollectionView(
139
+ input: unknown,
140
+ fields: CollectionViewField[],
141
+ ): CollectionView {
142
+ const view = CollectionViewSchema.parse(input)
143
+ if (new Set(view.groups.map((g) => g.id)).size !== view.groups.length)
144
+ throw new Error('Each group needs a unique ID.')
145
+ const keys = view.groups.flatMap((g) => g.fields.map((f) => f.key))
146
+ const expected = new Set(fields.map((f) => f.key))
147
+ if (
148
+ keys.length !== expected.size ||
149
+ new Set(keys).size !== keys.length ||
150
+ keys.some((key) => !expected.has(key))
151
+ )
152
+ throw new Error('Include every editable field once. Reload the view if the schema changed.')
153
+ return view
154
+ }
155
+
156
+ export function collectionViewsIssues(
157
+ input: CollectionViews | undefined,
158
+ collections: Record<string, CollectionDecl>,
159
+ ): { path: string; message: string }[] {
160
+ return Object.entries(input ?? {}).flatMap(([name, view]) => {
161
+ if (!Object.hasOwn(collections, name))
162
+ return [{ path: `collectionViews/${name}`, message: 'This collection is not declared.' }]
163
+ try {
164
+ validateCollectionView(view, collectionViewFields(collections[name]))
165
+ return []
166
+ } catch (error) {
167
+ return [
168
+ {
169
+ path: `collectionViews/${name}`,
170
+ message: error instanceof Error ? error.message : 'Invalid collection view.',
171
+ },
172
+ ]
173
+ }
174
+ })
175
+ }
package/src/site/index.ts CHANGED
@@ -12,3 +12,6 @@ export { metadataFor, type PageSeoInput } from '../seo/metadata'
12
12
  export { organisationJsonLd } from '../seo/jsonLd'
13
13
  export { robotsFor } from '../seo/robots'
14
14
  export { sitemapFor } from '../seo/sitemap'
15
+
16
+ export { SeoSeedsSchema, DocumentSeoSeedSchema, SiteSeoSeedSchema, seoSeedIssues, seoSeedAssets, type SeoSeeds, type DocumentSeoSeed, type SiteSeoSeed } from '../seo/seeds'
17
+ export { CollectionViewSchema, CollectionViewsSchema, collectionViewFields, defaultCollectionView, reconcileCollectionView, validateCollectionView, collectionViewsIssues, type CollectionView, type CollectionViews, type CollectionViewField } from './formView'
@@ -22,7 +22,7 @@ const tagOf = (r: Ref): string =>
22
22
  r.kind === 'media' ? `media:${r.id}` : r.kind === 'page' ? `page:${r.id}` : r.kind === 'single' ? `single:${r.key}` : r.kind === 'entry' ? `entry:${r.id}` : `collection:${r.collection}`
23
23
 
24
24
  /** A query over the store: `where` (eq, neq, in, not-in, is-set), a field `sort`, then the placement's picks and `limit`, which `applyQueryPicks` applies. */
25
- function runQuery(store: Store, collection: string, q: QueryStored): { ids: Id[]; total: number } {
25
+ function runQuery(store: Store, collection: string, q: QueryStored, draft: boolean): { ids: Id[]; total: number; matches?: Id[] } {
26
26
  let rows = [...store.entries.values()].filter((e) => e.collection === collection)
27
27
  for (const w of q.where ?? [])
28
28
  rows = rows.filter((e) => {
@@ -31,7 +31,8 @@ function runQuery(store: Store, collection: string, q: QueryStored): { ids: Id[]
31
31
  })
32
32
  const sort = q.sort
33
33
  if (sort && sort !== 'manual') rows.sort((a, b) => String(a.fields[sort.field]).localeCompare(String(b.fields[sort.field])) * (sort.dir === 'desc' ? -1 : 1))
34
- return applyQueryPicks(rows.map((r) => r.id), q, (id) => store.entries.has(entryKey(collection, id)))
34
+ const matches = rows.map(r => r.id)
35
+ return { ...applyQueryPicks(matches, q, id => store.entries.has(entryKey(collection, id))), ...(draft ? { matches } : {}) }
35
36
  }
36
37
 
37
38
  type Found = { doc: ReplyDocument; decl: DocumentDecl }
@@ -59,7 +60,7 @@ const entryFound = (site: Site, store: Store, collection: string, slug: string):
59
60
  * lookups they ask for, and the tags the read depends on. The fake answers with it, and a site
60
61
  * reading its own defaults goes through it too, so every read is assembled the same way.
61
62
  */
62
- export function replyFrom(site: Site, store: Store, target: ReplyTarget, manifest: string): Reply {
63
+ export function replyFrom(site: Site, store: Store, target: ReplyTarget, manifest: string, draft = false): Reply {
63
64
  let found: Found | null = null
64
65
  if ('route' in target) {
65
66
  const p = [...store.pages.values()].find((x) => x.route === target.route)
@@ -96,7 +97,7 @@ export function replyFrom(site: Site, store: Store, target: ReplyTarget, manifes
96
97
  const queries: DocumentReply['queries'] = {}
97
98
  const inverses: DocumentReply['inverses'] = {}
98
99
  for (const r of lookups) {
99
- if (r.kind === 'query') queries[queryKey(r.collection, r.query)] ??= runQuery(store, r.collection, r.query as QueryStored)
100
+ if (r.kind === 'query') queries[queryKey(r.collection, r.query)] ??= runQuery(store, r.collection, r.query as QueryStored, draft)
100
101
  if (r.kind === 'inverse')
101
102
  inverses[inverseKey(r.collection, r.field, r.id)] ??= [...store.entries.values()]
102
103
  .filter((e) => e.collection === r.collection && (Array.isArray(e.fields[r.field]) ? (e.fields[r.field] as unknown[]) : [e.fields[r.field]]).some((v) => String(v) === String(r.id)))
package/src/site/types.ts CHANGED
@@ -31,11 +31,12 @@ export type EntrySeed = { slug: string; fields?: Record<string, unknown>; blocks
31
31
  export type CollectionDef<F extends Fields = Fields> = {
32
32
  kind: 'collection'; name: string; label: string; fields: F
33
33
  route?: string; defaultSort?: CollectionDecl['defaultSort']
34
+ authoring?: CollectionDecl['authoring']
34
35
  template?: { zone: ZoneDecl; layout?: string }
35
36
  entries?: EntrySeed[]
36
37
  }
37
38
  export type BlockDef<F extends Fields = Fields, C extends CollectionDef | undefined = CollectionDef | undefined> = {
38
- kind: 'block'; name: string; label: string; fields: F; component: unknown; entry?: C
39
+ kind: 'block'; name: string; label: string; fields: F; component: unknown; entry?: C; shared?: BlockDecl['shared']
39
40
  }
40
41
  export type LayoutDef = { kind: 'layout'; default?: boolean; parent?: string; entries: Array<BlockDef | 'SLOT'> }
41
42
  export type PageDef = { kind: 'page'; route: string; layout?: string; blocks: ZoneDecl }
@@ -73,3 +74,4 @@ export type BlockProps<B> = B extends BlockDef<infer F, infer C>
73
74
  ? Simplify<{ [K in keyof F]: Read<F[K]> } & (C extends CollectionDef ? { entry: EntryReadOf<C> } : {})>
74
75
  : never
75
76
  export type { FieldDecl, LeafDecl, GroupDecl, ListDecl, ZoneDecl, BlockDecl, CollectionDecl, SingleDecl, Schema }
77
+ export type { CollectionAuthoring, CollectionAuthoringAnchor, CollectionAuthoringSections } from '../elements'
@@ -1 +1 @@
1
- export const SITE_KIT_VERSION = '1.6.2'
1
+ export const SITE_KIT_VERSION = '1.6.4'
package/src/site/wire.ts CHANGED
@@ -33,7 +33,7 @@ export type DocumentReply = {
33
33
  /** By `entryKey(collection, id)`. */
34
34
  entries: Record<string, EntryRecord>
35
35
  /** By `queryKey(collection, query)`. */
36
- queries: Record<string, { ids: Id[]; total: number }>
36
+ queries: Record<string, { ids: Id[]; total: number; matches?: Id[] }>
37
37
  /** By `inverseKey(collection, field, id)`. */
38
38
  inverses: Record<string, Id[]>
39
39
  tags: string[]
@@ -6,7 +6,7 @@ import { seedCtx, seedEntry, seedSite, type Store } from '../site/store'
6
6
 
7
7
  export type { RouteRead }
8
8
  const assembled = (site: Site, store: Store, target: ReplyTarget, draft: boolean): RouteRead =>
9
- assemble(site, replyFrom(site, store, target, ''), { draft, target, stale: false, defaults: null })
9
+ assemble(site, replyFrom(site, store, target, '', draft), { draft, target, stale: false, defaults: null })
10
10
  /** A route read against a store, assembled as the site assembles a platform's reply. */
11
11
  export const routeRead = (site: Site, store: Store, pathname: string, draft: boolean): RouteRead => assembled(site, store, { route: pathname }, draft)
12
12
  /**
@@ -83,14 +83,14 @@ export async function createFakePlatform(site: Site, store?: Store, opts: { mani
83
83
  // A site with languages hears which language answered, as the platform says it, and which languages have copy of the document (ruling R2).
84
84
  const answer = (reply: Reply) =>
85
85
  json(reply.status === 'ok' && chain?.locale ? { ...reply, locale: chain.locale, ...(languages ? { translated: translatedOf(`${reply.document.kind}:${reply.document.id}`) } : {}) } : reply)
86
- if (p === '/route') return answer(replyFrom(site, view, { route: url.searchParams.get('path') ?? '/' }, manifest))
87
- if (p.startsWith('/page/')) return answer(replyFrom(site, view, { page: decodeURIComponent(p.slice(6)) }, manifest))
86
+ if (p === '/route') return answer(replyFrom(site, view, { route: url.searchParams.get('path') ?? '/' }, manifest, draft))
87
+ if (p.startsWith('/page/')) return answer(replyFrom(site, view, { page: decodeURIComponent(p.slice(6)) }, manifest, draft))
88
88
  if (p.startsWith('/entry/')) {
89
89
  // The read client encodes both segments (and a URL's pathname keeps them encoded), so a
90
90
  // non-ASCII slug arrives as `caf%C3%A9`; decoded after the split, so an encoded `/` stays in its segment.
91
91
  let segments: string[]
92
92
  try { segments = p.split('/').slice(2).map(decodeURIComponent) } catch { return json({ error: 'malformed path' }, 400) }
93
- return answer(replyFrom(site, view, { entry: { collection: segments[0], slug: segments[1] } }, manifest))
93
+ return answer(replyFrom(site, view, { entry: { collection: segments[0], slug: segments[1] } }, manifest, draft))
94
94
  }
95
95
  if (p === '/site') return opts.failSite ? json({ error: 'site settings unavailable' }, 500) : json(languages ? { ...settings, languages } : settings)
96
96
  if (p === '/sitemap') {