@cancia/astro 0.6.0 → 0.8.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.
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  export { z } from 'zod';
3
- export { P as PT_DECORATORS, a as PT_LIST_ITEMS, b as PT_STYLES, c as PortableTextValue, d as PtBlock, e as PtDecorator, f as PtLinkMarkDef, g as PtListItem, h as PtSpan, i as PtStyle, j as isSafeHref, p as portableTextSubsetSchema, k as ptBlockSchema } from '../portable-text-BikSqS9T.js';
3
+ export { P as PT_DECORATORS, a as PT_LIST_ITEMS, b as PT_STYLES, c as PortableTextValue, d as PtBlock, e as PtDecorator, f as PtLinkMarkDef, g as PtListItem, h as PtSpan, i as PtStyle, j as isSafeHref, p as parseRichValue, k as portableTextSubsetSchema, l as ptBlockSchema, s as serializeRichValue } from '../portable-text-D74aIuHn.js';
4
4
 
5
5
  /**
6
6
  * Widget hint the modal form uses to pick an input element. The Zod type
@@ -16,7 +16,7 @@ export { P as PT_DECORATORS, a as PT_LIST_ITEMS, b as PT_STYLES, c as PortableTe
16
16
  * z.boolean() → "checkbox"
17
17
  * z.enum([...]) → "select"
18
18
  */
19
- type FieldWidget = "text" | "textarea" | "url" | "email" | "datetime" | "number" | "checkbox" | "select" | "image" | "slug" | "array" | "object" | "reference" | "richtext";
19
+ type FieldWidget = "text" | "textarea" | "url" | "email" | "datetime" | "number" | "checkbox" | "select" | "image" | "slug" | "array" | "object" | "reference" | "richtext" | "link";
20
20
  /**
21
21
  * Fields common to every widget's metadata. All optional.
22
22
  */
@@ -61,6 +61,8 @@ type FieldMeta = (FieldMetaBase & {
61
61
  list: string;
62
62
  }) | (FieldMetaBase & {
63
63
  widget: "richtext";
64
+ }) | (FieldMetaBase & {
65
+ widget: "link";
64
66
  });
65
67
  interface ListSchemaOptions<TFields extends Record<string, z.ZodTypeAny>> {
66
68
  /** Display label for the list (e.g. "Blog Posts"). */
@@ -80,6 +82,21 @@ interface ListSchemaOptions<TFields extends Record<string, z.ZodTypeAny>> {
80
82
  * Defaults to the entry ID. The server validates slug uniqueness on save.
81
83
  */
82
84
  slugField?: keyof TFields & string;
85
+ /**
86
+ * Field name holding the entry's draft flag. When set, `canciaLoader`
87
+ * omits entries whose value is truthy from the content collection, so a
88
+ * draft never reaches the built site.
89
+ *
90
+ * The field MUST be a boolean with a default (`defineField.draft()` gives
91
+ * you exactly that) — an `undefined` draft flag reads as published, which
92
+ * is the safe direction for the pre-existing entries of a list that adds
93
+ * `draftField` later.
94
+ *
95
+ * Draft is deliberately per-list opt-in rather than an implicit field on
96
+ * every list: a list of office locations or nav links has no meaningful
97
+ * draft state, and a phantom "Draft" checkbox on every form is noise.
98
+ */
99
+ draftField?: keyof TFields & string;
83
100
  /** Field definitions. */
84
101
  fields: TFields;
85
102
  }
@@ -89,10 +106,24 @@ interface ListSchema<TFields extends Record<string, z.ZodTypeAny> = Record<strin
89
106
  titleField: string;
90
107
  bodyField: string | undefined;
91
108
  slugField: string | undefined;
109
+ draftField: string | undefined;
92
110
  fields: TFields;
93
111
  /** Zod object schema combining all fields, for one-shot validation. */
94
112
  validator: z.ZodObject<TFields>;
95
113
  }
114
+ /**
115
+ * True when `entry.data` is a draft under `schema.draftField`.
116
+ *
117
+ * The single source of truth for "is this entry a draft" — the loader, the
118
+ * toolbar preview rows and any user-side filter all call THIS, so a draft is
119
+ * decided identically everywhere. Vite-free and dependency-free by design.
120
+ *
121
+ * Reads as published (`false`) when the list declares no draftField, when the
122
+ * field is absent, or when it is `undefined` — every ambiguous case resolves
123
+ * to "published", matching the field's own default and keeping entries that
124
+ * predate the draftField visible.
125
+ */
126
+ declare function isDraft(schema: Pick<ListSchema, "draftField">, data: Record<string, unknown>): boolean;
96
127
  declare function defineList<TFields extends Record<string, z.ZodTypeAny>>(opts: ListSchemaOptions<TFields>): ListSchema<TFields>;
97
128
  /**
98
129
  * One field as seen by the modal form. Plain JSON, no Zod refs.
@@ -152,6 +183,15 @@ declare const defineField: {
152
183
  email: (o?: PlainFieldOptions) => z.ZodString;
153
184
  datetime: (o?: PlainFieldOptions) => z.ZodString;
154
185
  checkbox: (o?: PlainFieldOptions) => z.ZodBoolean;
186
+ /**
187
+ * The draft flag for a list's `draftField`. A checkbox that DEFAULTS to
188
+ * false, which is what makes it safe to add to an existing list: every
189
+ * stored entry that predates the field validates and reads as published
190
+ * rather than vanishing from the built site.
191
+ *
192
+ * Label defaults to "Draft" so the affordance reads the same on every list.
193
+ */
194
+ draft: (o?: PlainFieldOptions) => z.ZodDefault<z.ZodBoolean>;
155
195
  number: (o?: PlainFieldOptions & {
156
196
  min?: number;
157
197
  max?: number;
@@ -194,6 +234,30 @@ declare const defineField: {
194
234
  reference: (o: FieldMetaBase & {
195
235
  list: string;
196
236
  }) => z.ZodString;
237
+ /**
238
+ * A link: label + href as ONE unit.
239
+ *
240
+ * A button/CTA is two values that must stay together — saving a new label
241
+ * against a stale href produces a broken call-to-action, so they are stored
242
+ * as a single object rather than two flat keys that could drift.
243
+ *
244
+ * cta: f.link({ label: "Primary CTA" }),
245
+ *
246
+ * `href` is NOT z.url(): a relative href ("/start", "#programmes") and a
247
+ * mailto:/tel: are all legitimate and z.url() rejects the relative forms.
248
+ * It IS constrained by `isSafeHref` — the SAME guard richtext link marks
249
+ * use — so a stored value can never carry a javascript:/data: XSS payload.
250
+ * Without this, the link widget would be a hole around a protection
251
+ * richtext already enforces.
252
+ *
253
+ * Whether to open in a new tab is derived from the href at render time
254
+ * (see `isExternalHref`) instead of being stored — one less thing that can
255
+ * fall out of sync with the URL it describes.
256
+ */
257
+ link: (o?: FieldMetaBase) => z.ZodObject<{
258
+ label: z.ZodString;
259
+ href: z.ZodString;
260
+ }, z.core.$strip>;
197
261
  /**
198
262
  * A constrained rich-text body. Stored as a Portable-Text SUBSET array (D4):
199
263
  * block styles normal/h2/h3/blockquote, bullet/number lists, strong/em marks,
@@ -240,6 +304,11 @@ interface ListDescription {
240
304
  titleField: string;
241
305
  bodyField?: string;
242
306
  slugField?: string;
307
+ /**
308
+ * Field holding the draft flag, when the list opts in. The toolbar uses it
309
+ * to badge unpublished rows; the loader uses it to omit them from the build.
310
+ */
311
+ draftField?: string;
243
312
  fields: FieldDescription[];
244
313
  }
245
314
  /**
@@ -257,7 +326,38 @@ interface ListDescription {
257
326
  */
258
327
  declare function slugify(input: string): string;
259
328
  declare function describeList(name: string, schema: ListSchema): ListDescription;
329
+ /** The value behind a `link` widget: a label and where it points. */
330
+ interface LinkValue {
331
+ label: string;
332
+ href: string;
333
+ }
334
+ /**
335
+ * Parse a stored link value. NEVER throws.
336
+ *
337
+ * The KV surface stores every value as a string, so a link round-trips as
338
+ * JSON. Three cases must all degrade gracefully rather than break a render:
339
+ *
340
+ * - proper JSON object → used as-is (missing halves coerced to "")
341
+ * - a bare string → treated as the LABEL with an empty href. This is
342
+ * the migration path for a field that used to be
343
+ * `text`, and for a fallback authored in markup.
344
+ * - empty / malformed → `{ label: "", href: "" }`
345
+ *
346
+ * A content read must never throw (same rule as createContentReader).
347
+ */
348
+ declare function parseLinkValue(raw: unknown): LinkValue;
349
+ /** Serialize a link value for storage. The inverse of `parseLinkValue`. */
350
+ declare function serializeLinkValue(value: LinkValue): string;
351
+ /**
352
+ * Should this href open in a new tab? Derived, never stored.
353
+ *
354
+ * External = has a scheme+host that isn't the current origin. Relative hrefs
355
+ * ("/start", "#programmes") and mailto:/tel: are NOT external — mailto/tel
356
+ * hand off to another app, and forcing target=_blank on them leaves a blank
357
+ * tab behind.
358
+ */
359
+ declare function isExternalHref(href: string): boolean;
260
360
  /** Map of list name → schema. What users export from src/cms/schemas.ts. */
261
361
  type SchemasModule = Record<string, ListSchema>;
262
362
 
263
- export { type FieldDescription, type FieldMeta, type FieldMetaBase, type FieldWidget, type ListDescription, type ListSchema, type ListSchemaOptions, type SchemasModule, defineField, defineList, describeList, slugify };
363
+ export { type FieldDescription, type FieldMeta, type FieldMetaBase, type FieldWidget, type LinkValue, type ListDescription, type ListSchema, type ListSchemaOptions, type SchemasModule, defineField, defineList, describeList, isDraft, isExternalHref, parseLinkValue, serializeLinkValue, slugify };
@@ -2,17 +2,23 @@ import {
2
2
  defineField,
3
3
  defineList,
4
4
  describeList,
5
+ isDraft,
6
+ isExternalHref,
7
+ parseLinkValue,
8
+ serializeLinkValue,
5
9
  slugify,
6
10
  z
7
- } from "../chunk-MCHQV6Y7.js";
11
+ } from "../chunk-WVHTCFE6.js";
8
12
  import {
9
13
  PT_DECORATORS,
10
14
  PT_LIST_ITEMS,
11
15
  PT_STYLES,
12
16
  isSafeHref,
17
+ parseRichValue,
13
18
  portableTextSubsetSchema,
14
- ptBlockSchema
15
- } from "../chunk-BOIQNZAO.js";
19
+ ptBlockSchema,
20
+ serializeRichValue
21
+ } from "../chunk-KHCFVVYV.js";
16
22
  export {
17
23
  PT_DECORATORS,
18
24
  PT_LIST_ITEMS,
@@ -20,9 +26,15 @@ export {
20
26
  defineField,
21
27
  defineList,
22
28
  describeList,
29
+ isDraft,
30
+ isExternalHref,
23
31
  isSafeHref,
32
+ parseLinkValue,
33
+ parseRichValue,
24
34
  portableTextSubsetSchema,
25
35
  ptBlockSchema,
36
+ serializeLinkValue,
37
+ serializeRichValue,
26
38
  slugify,
27
39
  z
28
40
  };
@@ -0,0 +1,23 @@
1
+ import { ListSchema, SchemasModule } from './index.js';
2
+ import 'zod';
3
+ import '../portable-text-D74aIuHn.js';
4
+
5
+ /**
6
+ * Resolve the absolute path to the user's schemas.ts. Returns null if no
7
+ * file exists at the expected location.
8
+ */
9
+ declare function resolveSchemasPath(projectRoot: string, overridePath?: string): string | null;
10
+ /**
11
+ * Load (and cache) the user's schemas module. Returns an empty map if no
12
+ * schemas.ts exists — the inline editor still works for KV edits without
13
+ * any lists defined.
14
+ */
15
+ declare function loadSchemas(projectRoot: string, overridePath?: string): Promise<SchemasModule>;
16
+ /**
17
+ * Get a single list schema, or null if it doesn't exist.
18
+ */
19
+ declare function loadListSchema(projectRoot: string, listName: string, overridePath?: string): Promise<ListSchema | null>;
20
+ /** For tests — drop the in-memory cache. */
21
+ declare function clearSchemaCache(): void;
22
+
23
+ export { clearSchemaCache, loadListSchema, loadSchemas, resolveSchemasPath };
@@ -0,0 +1,12 @@
1
+ import {
2
+ clearSchemaCache,
3
+ loadListSchema,
4
+ loadSchemas,
5
+ resolveSchemasPath
6
+ } from "../chunk-VL6FO446.js";
7
+ export {
8
+ clearSchemaCache,
9
+ loadListSchema,
10
+ loadSchemas,
11
+ resolveSchemasPath
12
+ };
@@ -0,0 +1,47 @@
1
+ import { ContentMap } from '../content.js';
2
+
3
+ /** Test/dev hook — a content change must not be served from a stale cache. */
4
+ declare function clearTextCache(): void;
5
+ interface TextHelperConfig {
6
+ site: string;
7
+ lang: string;
8
+ /** Reads the whole content map. Injected so this module imports no adapter. */
9
+ read: () => Promise<ContentMap>;
10
+ }
11
+ declare function configureTextHelper(next: TextHelperConfig): void;
12
+ /**
13
+ * Load the content map. Awaited once per page, in frontmatter.
14
+ *
15
+ * NEVER throws or rejects. A content read must not break a build — the same rule
16
+ * createContentReader holds — so every failure resolves to an empty map, and
17
+ * every call site then falls back to the text authored in the page. A site whose
18
+ * database is unreachable renders exactly as its source says.
19
+ */
20
+ declare function __canciaContent(): Promise<ContentMap>;
21
+ /**
22
+ * Resolve one annotated element's text.
23
+ *
24
+ * `??` is load-bearing, not style: a stored "" means DELIBERATELY EMPTY (the
25
+ * client removed this text on purpose) and must NOT fall through to the authored
26
+ * fallback, or deleted content reappears on every build.
27
+ */
28
+ declare function __canciaText(cms: ContentMap, key: string, fallback: string): string;
29
+ interface LinkValue {
30
+ label: string;
31
+ href: string;
32
+ }
33
+ /**
34
+ * Resolve a link field — label AND href from one stored value.
35
+ *
36
+ * A link stores `{label, href}` as JSON so a new label can never be saved
37
+ * against a stale href. Both halves have an authored fallback (the text, and the
38
+ * `href=` already in the markup), which is why a link needs no t() call either.
39
+ *
40
+ * An unsafe href is DROPPED on read, falling back to the authored one — the same
41
+ * rule parseLinkValue holds, and for the same reason: validation guards the
42
+ * write path, but a value can reach a renderer from an older row or a
43
+ * hand-edited DB, so the read is where the guarantee has to hold.
44
+ */
45
+ declare function __canciaLink(cms: ContentMap, key: string, labelFallback: string, hrefFallback: string): LinkValue;
46
+
47
+ export { type LinkValue, type TextHelperConfig, __canciaContent, __canciaLink, __canciaText, clearTextCache, configureTextHelper };
@@ -0,0 +1,64 @@
1
+ // src/transform/text-helper.ts
2
+ var contentPromise = null;
3
+ function clearTextCache() {
4
+ contentPromise = null;
5
+ }
6
+ var config = null;
7
+ function configureTextHelper(next) {
8
+ config = next;
9
+ contentPromise = null;
10
+ }
11
+ async function __canciaContent() {
12
+ if (!config) return {};
13
+ try {
14
+ if (!contentPromise) contentPromise = config.read();
15
+ return await contentPromise;
16
+ } catch {
17
+ contentPromise = null;
18
+ return {};
19
+ }
20
+ }
21
+ function lang() {
22
+ return config?.lang ?? "en";
23
+ }
24
+ function __canciaText(cms, key, fallback) {
25
+ return cms[`${key}.${lang()}`] ?? fallback;
26
+ }
27
+ function isSafeHref(href) {
28
+ const trimmed = href.trim();
29
+ if (trimmed === "") return true;
30
+ if (/^(https?:|mailto:|tel:)/i.test(trimmed)) return true;
31
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed);
32
+ if (scheme) {
33
+ const firstSep = trimmed.search(/[/?#]/);
34
+ if (firstSep === -1 || scheme[1].length < firstSep) return false;
35
+ }
36
+ return true;
37
+ }
38
+ function __canciaLink(cms, key, labelFallback, hrefFallback) {
39
+ const fallback = { label: labelFallback, href: hrefFallback };
40
+ const raw = cms[`${key}.${lang()}`];
41
+ if (raw === void 0) return fallback;
42
+ if (raw === "") return { label: "", href: "" };
43
+ const trimmed = raw.trim();
44
+ if (trimmed.startsWith("{")) {
45
+ try {
46
+ const parsed = JSON.parse(trimmed);
47
+ const href = String(parsed.href ?? "");
48
+ return {
49
+ label: String(parsed.label ?? "") || labelFallback,
50
+ href: href && isSafeHref(href) ? href : hrefFallback
51
+ };
52
+ } catch {
53
+ return fallback;
54
+ }
55
+ }
56
+ return { label: raw, href: hrefFallback };
57
+ }
58
+ export {
59
+ __canciaContent,
60
+ __canciaLink,
61
+ __canciaText,
62
+ clearTextCache,
63
+ configureTextHelper
64
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cancia/astro",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Astro integration for Cancia CMS — inline editing with zero separate server",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,6 +20,10 @@
20
20
  "types": "./dist/schema/index.d.ts",
21
21
  "import": "./dist/schema/index.js"
22
22
  },
23
+ "./schema-loader": {
24
+ "types": "./dist/schema/loader.d.ts",
25
+ "import": "./dist/schema/loader.js"
26
+ },
23
27
  "./loader": {
24
28
  "types": "./dist/loader/index.d.ts",
25
29
  "import": "./dist/loader/index.js"
@@ -40,7 +44,8 @@
40
44
  "types": "./dist/richtext/index.d.ts",
41
45
  "import": "./dist/richtext/index.js"
42
46
  },
43
- "./richtext/CanciaRichText.astro": "./dist/richtext/CanciaRichText.astro"
47
+ "./richtext/CanciaRichText.astro": "./dist/richtext/CanciaRichText.astro",
48
+ "./richtext/CanciaRichRegion.astro": "./dist/richtext/CanciaRichRegion.astro"
44
49
  },
45
50
  "files": [
46
51
  "dist"
@@ -68,7 +73,8 @@
68
73
  },
69
74
  "dependencies": {
70
75
  "astro-portabletext": "^0.13.0",
71
- "zod": "^4.4.3"
76
+ "zod": "^4.4.3",
77
+ "@astrojs/compiler": "^2.10.0"
72
78
  },
73
79
  "scripts": {
74
80
  "build": "tsup",