@blinkk/root-cms 3.0.1-alpha.0 → 3.0.1-beta.2

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/dist/richtext.js CHANGED
@@ -1,70 +1,99 @@
1
1
  import "./chunk-MLKGABMK.js";
2
2
 
3
3
  // core/richtext.tsx
4
- import { useTranslations } from "@blinkk/root";
5
- import { createContext } from "preact";
4
+ import { StringParamsProvider, useTranslations } from "@blinkk/root";
5
+ import { Component, createContext } from "preact";
6
6
  import { useContext } from "preact/hooks";
7
- import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
7
+ import { renderToString } from "preact-render-to-string";
8
+ import { jsx, jsxs } from "preact/jsx-runtime";
8
9
  var RichTextContext = createContext({});
9
10
  function useRichTextContext() {
10
11
  return useContext(RichTextContext);
11
12
  }
13
+ var RichTextComponentMapContext = createContext({});
14
+ function useRichTextContextComponentMap() {
15
+ return useContext(RichTextComponentMapContext);
16
+ }
17
+ function useRichTextTranslations() {
18
+ const ctx = useRichTextContext();
19
+ if (ctx.t) {
20
+ return ctx.t;
21
+ }
22
+ return useTranslations();
23
+ }
12
24
  function RichText(props) {
25
+ const blocks = props.data?.blocks || [];
26
+ if (blocks.length === 0) {
27
+ return null;
28
+ }
13
29
  const richTextContext = useRichTextContext();
14
- const components = {
15
- delimiter: RichText.DelimiterBlock,
30
+ const componentMap = {
16
31
  heading: RichText.HeadingBlock,
17
32
  html: RichText.HtmlBlock,
18
33
  image: RichText.ImageBlock,
19
34
  orderedList: RichText.ListBlock,
20
35
  paragraph: RichText.ParagraphBlock,
21
- quote: RichText.QuoteBlock,
22
36
  table: RichText.TableBlock,
23
37
  unorderedList: RichText.ListBlock,
24
38
  ...richTextContext.components,
25
39
  ...props.components
26
40
  };
27
- const blocks = (props.data?.blocks || []).filter((block) => {
28
- const blockType = block?.type;
29
- if (!blockType) {
30
- return false;
31
- }
32
- if (!(blockType in components)) {
33
- console.warn(`ignoring unknown richtext type: "${blockType}"`);
34
- return false;
35
- }
36
- return true;
37
- });
38
- return /* @__PURE__ */ jsx(Fragment, { children: blocks.map((block) => {
39
- const Block = components[block.type];
40
- return /* @__PURE__ */ jsx(Block, { ...block });
41
- }) });
41
+ return /* @__PURE__ */ jsx(RichTextComponentMapContext.Provider, { value: componentMap, children: blocks.map((block) => {
42
+ return /* @__PURE__ */ jsx(RichText.Block, { ...block });
43
+ }).filter((value) => !!value) });
42
44
  }
43
- RichText.ParagraphBlock = (props) => {
44
- if (!props.data?.text) {
45
+ RichText.Block = (props) => {
46
+ const block = props;
47
+ const blockType = block?.type;
48
+ if (!blockType) {
45
49
  return null;
46
50
  }
47
- const t = useTranslations();
48
- return /* @__PURE__ */ jsx("p", { dangerouslySetInnerHTML: { __html: t(props.data.text) } });
51
+ const componentMap = useRichTextContextComponentMap();
52
+ const BlockComponent = componentMap[blockType];
53
+ if (!BlockComponent) {
54
+ console.warn(`[RichText] ignoring unknown richtext type: "${blockType}"`);
55
+ return null;
56
+ }
57
+ return /* @__PURE__ */ jsx(
58
+ InlineComponentRenderer,
59
+ {
60
+ block,
61
+ componentMap,
62
+ BlockComponent
63
+ }
64
+ );
49
65
  };
50
- RichText.DelimiterBlock = () => {
51
- return /* @__PURE__ */ jsx("hr", {});
66
+ var InlineComponentRenderer = class extends Component {
67
+ render() {
68
+ const { block, componentMap, BlockComponent } = this.props;
69
+ const stringParams = collectInlineComponentParams(
70
+ block,
71
+ componentMap,
72
+ this.context
73
+ );
74
+ if (stringParams) {
75
+ return /* @__PURE__ */ jsx(StringParamsProvider, { value: stringParams, children: /* @__PURE__ */ jsx(BlockComponent, { ...block }) });
76
+ }
77
+ return /* @__PURE__ */ jsx(BlockComponent, { ...block });
78
+ }
52
79
  };
53
- RichText.HeadingBlock = (props) => {
80
+ RichText.ParagraphBlock = (props) => {
54
81
  if (!props.data?.text) {
55
82
  return null;
56
83
  }
57
- const t = useTranslations();
58
- const level = props.data.level || 2;
59
- const Component = `h${level}`;
60
- return /* @__PURE__ */ jsx(Component, { dangerouslySetInnerHTML: { __html: t(props.data.text) } });
84
+ const t = useRichTextTranslations();
85
+ const html = t(props.data.text);
86
+ return /* @__PURE__ */ jsx("p", { dangerouslySetInnerHTML: { __html: html } });
61
87
  };
62
- RichText.QuoteBlock = (props) => {
88
+ RichText.HeadingBlock = (props) => {
63
89
  if (!props.data?.text) {
64
90
  return null;
65
91
  }
66
- const t = useTranslations();
67
- return /* @__PURE__ */ jsx("blockquote", { dangerouslySetInnerHTML: { __html: t(props.data.text) } });
92
+ const t = useRichTextTranslations();
93
+ const level = props.data.level || 2;
94
+ const Component2 = `h${level}`;
95
+ const html = t(props.data.text);
96
+ return /* @__PURE__ */ jsx(Component2, { dangerouslySetInnerHTML: { __html: html } });
68
97
  };
69
98
  RichText.ListBlock = (props) => {
70
99
  if (!props.data?.items?.length) {
@@ -74,20 +103,20 @@ RichText.ListBlock = (props) => {
74
103
  if (!style) {
75
104
  style = props.type === "orderedList" ? "ordered" : "unordered";
76
105
  }
77
- const Component = style === "ordered" ? "ol" : "ul";
106
+ const Component2 = style === "ordered" ? "ol" : "ul";
78
107
  const items = props.data.items;
79
- return /* @__PURE__ */ jsx(Component, { children: items.map((item) => {
108
+ return /* @__PURE__ */ jsx(Component2, { children: items.map((item) => {
80
109
  if (item.content || item.items?.length) {
81
110
  return /* @__PURE__ */ jsxs("li", { children: [
82
111
  item.content && /* @__PURE__ */ jsx(
83
- RichText.ParagraphBlock,
112
+ RichText.Block,
84
113
  {
85
114
  type: "paragraph",
86
- data: { text: item.content }
115
+ data: { text: item.content, components: item.components }
87
116
  }
88
117
  ),
89
118
  item.items && item.items.length > 0 && /* @__PURE__ */ jsx(
90
- RichText.ListBlock,
119
+ RichText.Block,
91
120
  {
92
121
  type: props.type,
93
122
  data: { style, items: item.items }
@@ -109,7 +138,8 @@ RichText.ImageBlock = (props) => {
109
138
  return /* @__PURE__ */ jsx("img", { src: imageUrl, width, height, alt });
110
139
  };
111
140
  RichText.HtmlBlock = (props) => {
112
- const html = props.data?.html || "";
141
+ const t = useRichTextTranslations();
142
+ const html = t(props.data?.html || "");
113
143
  if (!html) {
114
144
  return null;
115
145
  }
@@ -122,8 +152,7 @@ RichText.TableBlock = (props) => {
122
152
  return null;
123
153
  }
124
154
  return /* @__PURE__ */ jsx("table", { children: /* @__PURE__ */ jsx("tbody", { children: rows.map((row, rowIndex) => /* @__PURE__ */ jsx("tr", { children: row.cells.map((cell, cellIndex) => {
125
- const isHeader = cell.type === "header";
126
- const Cell = isHeader ? "th" : "td";
155
+ const Cell = cell.type === "header" ? "th" : "td";
127
156
  return /* @__PURE__ */ jsx(Cell, { children: /* @__PURE__ */ jsx(
128
157
  RichText,
129
158
  {
@@ -146,8 +175,34 @@ function toNumber(input) {
146
175
  }
147
176
  return parsedNumber;
148
177
  }
178
+ function testContent(data) {
179
+ return data?.blocks?.length > 0;
180
+ }
181
+ function collectInlineComponentParams(block, componentMap, renderContext) {
182
+ const components = block?.data?.components || {};
183
+ if (Object.keys(components).length === 0) {
184
+ return null;
185
+ }
186
+ const params = {};
187
+ for (const [componentId, component] of Object.entries(components)) {
188
+ const Component2 = componentMap[component.type];
189
+ if (Component2) {
190
+ const key = `${component.type}:${componentId}`;
191
+ params[key] = renderToString(
192
+ /* @__PURE__ */ jsx(Component2, { ...component.data }),
193
+ renderContext
194
+ );
195
+ } else {
196
+ console.warn(
197
+ `[RichText] could not find inline component for type: "${component.type}"`
198
+ );
199
+ }
200
+ }
201
+ return params;
202
+ }
149
203
  export {
150
204
  RichText,
151
205
  RichTextContext,
206
+ testContent,
152
207
  useRichTextContext
153
208
  };
@@ -155,6 +155,11 @@ type ArrayField = CommonFieldProps & {
155
155
  * Label to use for the "add item" button. Defaults to `Add`.
156
156
  */
157
157
  buttonLabel?: string;
158
+ /**
159
+ * Whether array items should be open (expanded) by default. Defaults to
160
+ * `false`, meaning items start collapsed.
161
+ */
162
+ defaultOpen?: boolean;
158
163
  };
159
164
  declare function array(field: Omit<ArrayField, 'type'>): ArrayField;
160
165
  /**
@@ -174,6 +179,13 @@ interface SchemaPattern {
174
179
  }
175
180
  type OneOfField = CommonFieldProps & {
176
181
  type: 'oneof';
182
+ /**
183
+ * UI variant for the oneOf field.
184
+ * - `dropdown` (default): A `Select` dropdown for choosing the type.
185
+ * - `picker`: A `Button` that opens a modal with searchable cards. Use this
186
+ * variant when types declare presets or thumbnail images.
187
+ */
188
+ variant?: 'dropdown' | 'picker';
177
189
  /**
178
190
  * Schema types to include in the oneOf field. Can be:
179
191
  * - An array of Schema objects
@@ -226,6 +238,25 @@ type Field = StringField | NumberField | DateField | DateTimeField | BooleanFiel
226
238
  */
227
239
  type FieldWithId = Field;
228
240
  type ObjectLikeField = ImageField | FileField | ObjectField | OneOfField | ReferenceField;
241
+ interface SchemaPreset<T = Record<string, any>> {
242
+ /** Unique identifier for the preset within the schema. */
243
+ id: string;
244
+ /** Display title for the preset. Defaults to the preset id. */
245
+ label?: string;
246
+ /** Description shown below the title in the picker. */
247
+ description?: string;
248
+ /** Thumbnail image (URL or absolute /-path) for the picker. */
249
+ image?: string;
250
+ /**
251
+ * Field values to prefill. Deep-merged into the schema's default value
252
+ * (preset values override defaults). `_type` is set automatically. The
253
+ * preset id is NOT persisted — presets are purely a prefill mechanism.
254
+ *
255
+ * Pass a generated fields type as the generic parameter `T` to
256
+ * type-check the keys against the schema's fields.
257
+ */
258
+ data?: T;
259
+ }
229
260
  interface Schema {
230
261
  /** The name of the content type. Used as the field key. */
231
262
  name: string;
@@ -233,6 +264,19 @@ interface Schema {
233
264
  label?: string;
234
265
  /** The description of the content type. Appears in CMS menus. */
235
266
  description?: string;
267
+ /**
268
+ * Optional thumbnail image (URL or absolute /-path) used to represent this
269
+ * schema in the picker UI for `oneof` fields with `variant: 'picker'`.
270
+ */
271
+ image?: string;
272
+ /**
273
+ * Optional preset prefill configurations for use in the picker UI.
274
+ *
275
+ * Each preset appears in the picker as a separate selectable card alongside
276
+ * the schema's "blank" card. Selecting a preset sets `_type` to the schema's
277
+ * `name` and merges `data` over the schema's default field values.
278
+ */
279
+ presets?: SchemaPreset<any>[];
236
280
  /** Fields describe the structure of the content. */
237
281
  fields: FieldWithId[];
238
282
  /** Defines the preview displayed within the CMS UI. Overrides the `preview` definition for the `array` field. */
@@ -267,6 +311,24 @@ type SchemaWithTypes = Schema & {
267
311
  declare function defineSchema(schema: Schema): Schema;
268
312
  /** Defines the schema for a collection or reusable component. */
269
313
  declare const define: typeof defineSchema;
314
+ /**
315
+ * Defines a preset for use within `schema.define({presets: [...]})`. The
316
+ * optional generic parameter `T` constrains the shape of the preset's `data`
317
+ * field. Pass an auto-generated fields type to enable type-safety:
318
+ *
319
+ * ```ts
320
+ * import type {HeroFields} from './generated/types.d.ts';
321
+ *
322
+ * schema.definePreset<HeroFields>({
323
+ * id: 'big',
324
+ * label: 'Big hero',
325
+ * data: {title: 'Welcome'},
326
+ * });
327
+ * ```
328
+ */
329
+ declare function definePreset<T = Record<string, any>>(preset: SchemaPreset<T>): SchemaPreset<T>;
330
+ /** Alias for {@link definePreset}. */
331
+ declare const preset: typeof definePreset;
270
332
  type Collection = SchemaWithTypes & {
271
333
  /**
272
334
  * The ID of the collection. This comes from the schema filename, e.g
@@ -410,6 +472,7 @@ type schema_ReferencesField = ReferencesField;
410
472
  type schema_RichTextField = RichTextField;
411
473
  type schema_Schema = Schema;
412
474
  type schema_SchemaPattern = SchemaPattern;
475
+ type schema_SchemaPreset<T = Record<string, any>> = SchemaPreset<T>;
413
476
  type schema_SchemaWithTypes = SchemaWithTypes;
414
477
  type schema_SelectField = SelectField;
415
478
  type schema_StringField = StringField;
@@ -420,6 +483,7 @@ declare const schema_date: typeof date;
420
483
  declare const schema_datetime: typeof datetime;
421
484
  declare const schema_define: typeof define;
422
485
  declare const schema_defineCollection: typeof defineCollection;
486
+ declare const schema_definePreset: typeof definePreset;
423
487
  declare const schema_defineSchema: typeof defineSchema;
424
488
  declare const schema_file: typeof file;
425
489
  declare const schema_glob: typeof glob;
@@ -428,13 +492,14 @@ declare const schema_multiselect: typeof multiselect;
428
492
  declare const schema_number: typeof number;
429
493
  declare const schema_object: typeof object;
430
494
  declare const schema_oneOf: typeof oneOf;
495
+ declare const schema_preset: typeof preset;
431
496
  declare const schema_reference: typeof reference;
432
497
  declare const schema_references: typeof references;
433
498
  declare const schema_richtext: typeof richtext;
434
499
  declare const schema_select: typeof select;
435
500
  declare const schema_string: typeof string;
436
501
  declare namespace schema {
437
- export { type schema_ArrayField as ArrayField, type schema_BooleanField as BooleanField, type schema_Collection as Collection, type schema_CommonFieldProps as CommonFieldProps, type schema_DateField as DateField, type schema_DateTimeField as DateTimeField, type schema_Field as Field, type schema_FieldWithId as FieldWithId, type schema_FileField as FileField, type schema_GlobOptions as GlobOptions, type schema_ImageField as ImageField, type schema_MultiSelectField as MultiSelectField, type schema_NumberField as NumberField, type schema_ObjectField as ObjectField, type schema_ObjectLikeField as ObjectLikeField, type schema_OneOfField as OneOfField, type schema_ReferenceField as ReferenceField, type schema_ReferencesField as ReferencesField, type schema_RichTextField as RichTextField, type schema_Schema as Schema, type schema_SchemaPattern as SchemaPattern, type schema_SchemaWithTypes as SchemaWithTypes, type schema_SelectField as SelectField, type schema_StringField as StringField, schema_array as array, schema_boolean as boolean, schema_collection as collection, schema_date as date, schema_datetime as datetime, schema_define as define, schema_defineCollection as defineCollection, schema_defineSchema as defineSchema, schema_file as file, schema_glob as glob, schema_image as image, schema_multiselect as multiselect, schema_number as number, schema_object as object, schema_oneOf as oneOf, schema_reference as reference, schema_references as references, schema_richtext as richtext, schema_select as select, schema_string as string };
502
+ export { type schema_ArrayField as ArrayField, type schema_BooleanField as BooleanField, type schema_Collection as Collection, type schema_CommonFieldProps as CommonFieldProps, type schema_DateField as DateField, type schema_DateTimeField as DateTimeField, type schema_Field as Field, type schema_FieldWithId as FieldWithId, type schema_FileField as FileField, type schema_GlobOptions as GlobOptions, type schema_ImageField as ImageField, type schema_MultiSelectField as MultiSelectField, type schema_NumberField as NumberField, type schema_ObjectField as ObjectField, type schema_ObjectLikeField as ObjectLikeField, type schema_OneOfField as OneOfField, type schema_ReferenceField as ReferenceField, type schema_ReferencesField as ReferencesField, type schema_RichTextField as RichTextField, type schema_Schema as Schema, type schema_SchemaPattern as SchemaPattern, type schema_SchemaPreset as SchemaPreset, type schema_SchemaWithTypes as SchemaWithTypes, type schema_SelectField as SelectField, type schema_StringField as StringField, schema_array as array, schema_boolean as boolean, schema_collection as collection, schema_date as date, schema_datetime as datetime, schema_define as define, schema_defineCollection as defineCollection, schema_definePreset as definePreset, schema_defineSchema as defineSchema, schema_file as file, schema_glob as glob, schema_image as image, schema_multiselect as multiselect, schema_number as number, schema_object as object, schema_oneOf as oneOf, schema_preset as preset, schema_reference as reference, schema_references as references, schema_richtext as richtext, schema_select as select, schema_string as string };
438
503
  }
439
504
 
440
505
  export { type Collection as C, type Schema as S, schema as s };
@@ -1 +1 @@
1
- :root{--font-family-default: "Inter", sans-serif;--font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;--color-text-default: #333333;--color-border: #EFEFEF;--button-background-hover: #F5F5F5;--button-background-active: #E5E5E5}html{box-sizing:border-box;overflow-x:hidden;font-size:16px;line-height:1.5}*,*:before,*:after{box-sizing:inherit}html,body{font-family:var(--font-family-default);color:var(--color-text-default);height:100%;margin:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;scroll-behavior:smooth}img,picture,figure,video,canvas,svg,iframe{display:block;max-width:100%;height:auto;margin:0}a{color:inherit}select{color-scheme:light}h1,h2,h3,h4,h5,p{margin:0}body.menu\:open{overflow:hidden}#root{margin:0 auto;min-height:100vh;position:relative}.bootstrap{font-size:48px;font-weight:900;width:100%;height:100vh;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:40px;gap:36px;position:relative}.bootstrap-error{font-size:14px;font-weight:400;line-height:20px;position:absolute;bottom:100px;left:0;right:0;text-align:center;animation:.5s forwards bootstrap-loading-error;animation-delay:1s;opacity:0;padding:0 24px}@keyframes bootstrap-loading-error{0%{opacity:0}to{opacity:1}}.signin{font-family:Google Sans,arial,sans-serif;text-align:center;padding:48px 20px;color:#3c4043}.signin__headline{margin-bottom:40px}.signin__headline__title{font-size:36px;line-height:1.3;font-weight:500;margin-bottom:20px}.signin__headline__body{font-size:18px;line-height:1.5;font-weight:500}.signin__button{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:inline-flex;align-items:center;text-align:center;gap:12px;background:#fff;border:1px solid #dadce0;border-radius:4px;cursor:pointer;font-family:inherit;padding:0 12px;height:40px;box-sizing:border-box;transition:all .218s ease}.signin__warning{max-width:520px;margin:0 auto 40px;padding:12px;border:1px solid #fbbc04;border-radius:8px;background:#fff9db;color:#5f370e;font-size:14px;line-height:1.5}.signin__button:hover{border-color:#d2e3fc;background-color:#4285f40a}.signin__button__icon{width:18px;height:18px;background-color:#fff}.signin__button__label{font-size:14px;line-height:1;letter-spacing:.25px;font-weight:500;color:#3c4043}.signin__error{color:red;font-weight:700;text-align:center;max-width:60ch;margin:20px auto 0}
1
+ :root{--font-family-default: "Inter", sans-serif;--font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;--color-text-default: #333333;--color-border: #EFEFEF;--button-background-hover: #F5F5F5;--button-background-active: #E5E5E5}html{box-sizing:border-box;overflow-x:hidden;font-size:16px;line-height:1.5}*,*:before,*:after{box-sizing:inherit}html,body{font-family:var(--font-family-default);color:var(--color-text-default);height:100%;margin:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;scroll-behavior:smooth}img,picture,figure,video,canvas,svg,iframe{display:block;max-width:100%;height:auto;margin:0}a{color:inherit}select{color-scheme:light}h1,h2,h3,h4,h5,p{margin:0}body.menu\:open{overflow:hidden}#root{margin:0 auto;min-height:100vh;position:relative}.bootstrap{font-size:48px;font-weight:900;width:100%;height:100vh;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:40px;gap:36px;position:relative}.bootstrap-error{font-size:14px;font-weight:400;line-height:20px;position:absolute;bottom:100px;left:0;right:0;text-align:center;animation:.5s forwards bootstrap-loading-error;animation-delay:1s;opacity:0;padding:0 24px}.bootstrap--error{gap:16px}.bootstrap__error-title{font-size:48px;font-weight:900;line-height:1.1;margin:0}.bootstrap__error-message{color:#b00020;font-size:16px;font-weight:500;line-height:1.5;margin:0;max-width:640px}@keyframes bootstrap-loading-error{0%{opacity:0}to{opacity:1}}.signin{font-family:Google Sans,arial,sans-serif;text-align:center;padding:48px 20px;color:#3c4043}.signin__headline{margin-bottom:40px}.signin__headline__title{font-size:36px;line-height:1.3;font-weight:500;margin-bottom:20px}.signin__headline__body{font-size:18px;line-height:1.5;font-weight:500}.signin__button{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:inline-flex;align-items:center;text-align:center;gap:12px;background:#fff;border:1px solid #dadce0;border-radius:4px;cursor:pointer;font-family:inherit;padding:0 12px;height:40px;box-sizing:border-box;transition:all .218s ease}.signin__warning{max-width:520px;margin:0 auto 40px;padding:12px;border:1px solid #fbbc04;border-radius:8px;background:#fff9db;color:#5f370e;font-size:14px;line-height:1.5}.signin__button:hover{border-color:#d2e3fc;background-color:#4285f40a}.signin__button__icon{width:18px;height:18px;background-color:#fff}.signin__button__label{font-size:14px;line-height:1;letter-spacing:.25px;font-weight:500;color:#3c4043}.signin__button--busy{cursor:default;opacity:.85;border-color:#d2e3fc;background-color:#4285f40a}.signin__button--busy:hover{background-color:#4285f40a}.signin__spinner{width:18px;height:18px;color:#4285f4;animation:signin-spin .7s linear infinite}@keyframes signin-spin{to{transform:rotate(360deg)}}.signin__error{color:red;font-weight:700;text-align:center;max-width:60ch;margin:20px auto 0}