@materializecms/core 0.0.0-stage → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Novità di Materialize
2
+
3
+ Ogni versione, dalla più recente. Le novità in arrivo si scrivono sotto «Prossima versione»: `npm run bump` le assegna alla nuova versione.
4
+
5
+ ## Prossima versione
6
+
7
+ ## 0.1.0 — 2026-10-31
8
+
9
+ La prima versione pubblica.
10
+
11
+ - **Editor visuale**: pagine disegnate su una tela, con sezioni, colonne, testi, immagini, video, icone e forme; componenti riutilizzabili con varianti; righelli, guide, allineamenti e misure; modalità Contenuti per chi modifica solo i testi.
12
+ - **Animazioni ed effetti**: animazioni allo scorrimento e con linee temporali, stati al passaggio del mouse, sfondi WebGL e oggetti 3D.
13
+ - **Contenuti**: raccolte come Progetti, Diario e Team, con pagine modello; azioni rapide e selezione multipla negli elenchi e nei Media.
14
+ - **Siti in più lingue**: traduzioni collegate tra loro, anche con l’AI, e un selettore di lingua per il sito.
15
+ - **SEO**: titoli e descrizioni, sitemap, controlli sulla pagina e reindirizzamenti automatici quando cambia un indirizzo.
16
+ - **Moduli di contatto** con invio delle email dal tuo server di posta.
17
+ - **Assistente AI e server MCP**: testi, traduzioni e modifiche alle pagine chiedendole a parole.
18
+ - **Importazione da WordPress**: pagine, articoli e immagini.
19
+ - **Statistiche e cookie**: Google Analytics, Tag Manager, Meta Pixel, Plausible, Matomo e codice personalizzato, con il banner per il consenso.
20
+ - **Installazione su VPS con un comando**: Node.js, HTTPS con Caddy, servizio che riparte da solo, firewall e link per finire nel browser.
21
+ - **Immagine Docker** con `compose.yaml` pronto per l'HTTPS: il sito sta in un volume, quindi gli aggiornamenti dal pannello restano anche ricreando il container.
22
+ - **Installazione nel browser**: controllo del server, nome e lingua del sito, account amministratore, passo per passo.
23
+ - **Aggiornamenti con un clic** da Impostazioni → Aggiornamenti, con backup prima e ritorno automatico alla versione precedente se la nuova non parte; le correzioni possono installarsi da sole.
24
+ - **Sicurezza**: verifica in due passaggi, pausa dopo i tentativi di accesso falliti, registro delle attività, backup giornalieri del database e dei media.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cristiano Casazza
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,19 @@
1
- # Temporary Holding Version
1
+ # @materializecms/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Il modello dei contenuti di Materialize: raccolte, campi, validazione, libreria di sezioni, SEO.
4
+
5
+ Qui si definisce lo schema del sito in `materialize.config.ts`, con `defineConfig`, `defineCollection` e i campi `f.*`. Contiene anche i tipi condivisi, la libreria di sezioni del costruttore di pagine, la SEO e le animazioni.
6
+
7
+ ```ts
8
+ import { defineCollection, defineConfig, f } from '@materializecms/core'
9
+ ```
10
+
11
+ ## Materialize
12
+
13
+ CMS open source con editor visuale: le pagine si disegnano sulla tela, il sito è fatto di file statici veloci. Per un nuovo sito (serve Node 24):
14
+
15
+ ```bash
16
+ npm create materialize@latest
17
+ ```
18
+
19
+ Documentazione e codice: https://github.com/materializecms/materialize · Licenza MIT.
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
2
  "name": "@materializecms/core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Il modello dei contenuti di Materialize: raccolte, campi, validazione, libreria di sezioni, SEO.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/materializecms/materialize.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "homepage": "https://github.com/materializecms/materialize#readme",
13
+ "bugs": "https://github.com/materializecms/materialize/issues",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./src/index.d.ts",
17
+ "default": "./src/index.js"
18
+ },
19
+ "./slug": {
20
+ "types": "./src/slug.d.ts",
21
+ "default": "./src/slug.js"
22
+ },
23
+ "./style": {
24
+ "types": "./src/style.d.ts",
25
+ "default": "./src/style.js"
26
+ },
27
+ "./page": {
28
+ "types": "./src/page.d.ts",
29
+ "default": "./src/page.js"
30
+ },
31
+ "./library": {
32
+ "types": "./src/library.d.ts",
33
+ "default": "./src/library.js"
34
+ },
35
+ "./motion": {
36
+ "types": "./src/motion.d.ts",
37
+ "default": "./src/motion.js"
38
+ },
39
+ "./scene": {
40
+ "types": "./src/scene.d.ts",
41
+ "default": "./src/scene.js"
42
+ },
43
+ "./icons": {
44
+ "types": "./src/icons.d.ts",
45
+ "default": "./src/icons.js"
46
+ },
47
+ "./seo": {
48
+ "types": "./src/seo.d.ts",
49
+ "default": "./src/seo.js"
50
+ },
51
+ "./i18n": {
52
+ "types": "./src/i18n.d.ts",
53
+ "default": "./src/i18n.js"
54
+ },
55
+ "./tracking": {
56
+ "types": "./src/tracking.d.ts",
57
+ "default": "./src/tracking.js"
58
+ },
59
+ "./updates": {
60
+ "types": "./src/updates.d.ts",
61
+ "default": "./src/updates.js"
62
+ }
63
+ },
64
+ "dependencies": {
65
+ "lucide": "1.64.0",
66
+ "three": "0.186.1",
67
+ "zod": "4.6.7"
68
+ },
69
+ "engines": {
70
+ "node": ">=24"
71
+ },
72
+ "publishConfig": {
73
+ "access": "public"
74
+ }
75
+ }
@@ -0,0 +1,204 @@
1
+ import { z } from 'zod';
2
+ import type { PageNode } from './page.ts';
3
+ import { type BlockStyle } from './style.ts';
4
+ export type FieldKind = 'text' | 'richText' | 'number' | 'boolean' | 'date' | 'select' | 'slug' | 'media' | 'link' | 'relation' | 'blocks' | 'page' | 'group' | 'array' | 'seo' | 'color' | 'json';
5
+ /**
6
+ * A field definition. `V` is the stored value type and `R` whether the field is required;
7
+ * both exist only at the type level so documents can be typed straight from the schema.
8
+ */
9
+ export interface Field<V = unknown, R extends boolean = boolean> {
10
+ readonly kind: FieldKind;
11
+ readonly required: R;
12
+ readonly label?: string;
13
+ readonly description?: string;
14
+ readonly localized: boolean;
15
+ readonly options: Readonly<Record<string, unknown>>;
16
+ /** Validator for a present value; optionality is applied by the collection. */
17
+ readonly schema: () => z.ZodType;
18
+ readonly children?: Fields;
19
+ readonly item?: Field;
20
+ readonly blocks?: readonly Block[];
21
+ readonly __value?: V;
22
+ }
23
+ export type Fields = Record<string, Field>;
24
+ export interface Block<N extends string = string, F extends Fields = Fields> {
25
+ readonly name: N;
26
+ readonly label: string;
27
+ readonly description?: string;
28
+ readonly fields: F;
29
+ }
30
+ interface Common<R extends boolean> {
31
+ label?: string;
32
+ description?: string;
33
+ required?: R;
34
+ /** Store a separate value per locale. Defaults to true for text-like fields in localized collections. */
35
+ localized?: boolean;
36
+ }
37
+ type Req<O> = O extends {
38
+ required: true;
39
+ } ? true : false;
40
+ export interface Media {
41
+ src: string;
42
+ alt: string;
43
+ width?: number;
44
+ height?: number;
45
+ /** Focal point in percent, kept visible whatever the crop. */
46
+ focal?: {
47
+ x: number;
48
+ y: number;
49
+ };
50
+ }
51
+ export interface Link {
52
+ href: string;
53
+ label?: string;
54
+ newTab?: boolean;
55
+ }
56
+ export interface Seo {
57
+ title?: string;
58
+ description?: string;
59
+ image?: Media;
60
+ noindex?: boolean;
61
+ /** Search engines index the page but do not follow its links. */
62
+ nofollow?: boolean;
63
+ /** Title and description when shared (Facebook, LinkedIn, WhatsApp, X), if different. */
64
+ socialTitle?: string;
65
+ socialDescription?: string;
66
+ /** The address search engines should consider the original, when this page repeats another. */
67
+ canonical?: string;
68
+ /** Structured data: what kind of page this is (see SCHEMA_TYPES in seo.ts). */
69
+ schema?: 'article' | 'project' | 'service' | 'event' | 'faq';
70
+ faq?: {
71
+ q: string;
72
+ a: string;
73
+ }[];
74
+ event?: {
75
+ start?: string;
76
+ end?: string;
77
+ place?: string;
78
+ address?: string;
79
+ online?: boolean;
80
+ price?: string;
81
+ };
82
+ }
83
+ /** Inline text with formatting marks. */
84
+ export interface RichTextRun {
85
+ text: string;
86
+ bold?: boolean;
87
+ italic?: boolean;
88
+ href?: string;
89
+ }
90
+ export interface RichTextBlock {
91
+ type: 'paragraph' | 'heading' | 'quote' | 'listItem' | 'image';
92
+ level?: 2 | 3 | 4;
93
+ children: RichTextRun[];
94
+ /** An image between paragraphs (type "image"); its children are empty. */
95
+ image?: Media;
96
+ }
97
+ export type RichText = RichTextBlock[];
98
+ interface TextOptions<R extends boolean> extends Common<R> {
99
+ min?: number;
100
+ max?: number;
101
+ multiline?: boolean;
102
+ placeholder?: string;
103
+ }
104
+ declare function text<const O extends TextOptions<boolean>>(o?: O): Field<string, Req<O>>;
105
+ declare function richText<const O extends Common<boolean>>(o?: O): Field<RichText, Req<O>>;
106
+ interface NumberOptions<R extends boolean> extends Common<R> {
107
+ min?: number;
108
+ max?: number;
109
+ integer?: boolean;
110
+ }
111
+ declare function number<const O extends NumberOptions<boolean>>(o?: O): Field<number, Req<O>>;
112
+ declare function boolean<const O extends Common<boolean>>(o?: O): Field<boolean, Req<O>>;
113
+ declare function date<const O extends Common<boolean>>(o?: O): Field<string, Req<O>>;
114
+ type SelectOption = string | {
115
+ value: string;
116
+ label: string;
117
+ };
118
+ type OptionValue<T> = T extends {
119
+ value: infer V extends string;
120
+ } ? V : T extends string ? T : never;
121
+ interface SelectOptions<R extends boolean, T extends readonly SelectOption[]> extends Common<R> {
122
+ options: T;
123
+ }
124
+ declare function select<const T extends readonly SelectOption[], const O extends SelectOptions<boolean, T>>(o: O & {
125
+ options: T;
126
+ }): Field<OptionValue<T[number]>, Req<O>>;
127
+ interface SlugOptions<R extends boolean> extends Common<R> {
128
+ /** Field to generate the slug from when it is left empty. */
129
+ from?: string;
130
+ }
131
+ declare function slug<const O extends SlugOptions<boolean>>(o?: O): Field<string, Req<O>>;
132
+ interface MediaOptions<R extends boolean> extends Common<R> {
133
+ accept?: string;
134
+ focalPoint?: boolean;
135
+ }
136
+ declare function media<const O extends MediaOptions<boolean>>(o?: O): Field<Media, Req<O>>;
137
+ declare function link<const O extends Common<boolean>>(o?: O): Field<Link, Req<O>>;
138
+ interface RelationOptions<R extends boolean, M extends boolean> extends Common<R> {
139
+ to: string;
140
+ many?: M;
141
+ }
142
+ declare function relation<const O extends RelationOptions<boolean, boolean>>(o: O): Field<O extends {
143
+ many: true;
144
+ } ? string[] : string, Req<O>>;
145
+ export type BlockValue<B extends Block> = B extends Block<infer N, infer F> ? {
146
+ _type: N;
147
+ _key: string;
148
+ _label?: string;
149
+ _style?: BlockStyle;
150
+ } & InferFields<F> : never;
151
+ interface BlocksOptions<R extends boolean> extends Common<R> {
152
+ min?: number;
153
+ max?: number;
154
+ }
155
+ declare function blocks<const B extends readonly Block[], const O extends BlocksOptions<boolean>>(defs: B, o?: O): Field<BlockValue<B[number]>[], Req<O>>;
156
+ declare function group<const F extends Fields, const O extends Common<boolean>>(fields: F, o?: O): Field<InferFields<F>, Req<O>>;
157
+ interface ArrayOptions<R extends boolean> extends Common<R> {
158
+ min?: number;
159
+ max?: number;
160
+ }
161
+ declare function array<const I extends Field, const O extends ArrayOptions<boolean>>(item: I, o?: O): Field<NonNullable<I['__value']>[], Req<O>>;
162
+ declare function seo<const O extends Common<boolean>>(o?: O): Field<Seo, Req<O>>;
163
+ declare function color<const O extends Common<boolean>>(o?: O): Field<string, Req<O>>;
164
+ /**
165
+ * A page composed on the canvas: sections of frames, text, images and buttons, with their design.
166
+ * Render it with `renderPage` from `@materializecms/core/page`, or the Astro `<Page>` component.
167
+ */
168
+ declare function page<const O extends Common<boolean>>(o?: O): Field<PageNode[], Req<O>>;
169
+ declare function json<const O extends Common<boolean>>(o?: O): Field<unknown, Req<O>>;
170
+ /** Field builders: `f.text({ required: true })`, `f.blocks([hero, gallery])`, … */
171
+ export declare const f: {
172
+ text: typeof text;
173
+ richText: typeof richText;
174
+ number: typeof number;
175
+ boolean: typeof boolean;
176
+ date: typeof date;
177
+ select: typeof select;
178
+ slug: typeof slug;
179
+ media: typeof media;
180
+ link: typeof link;
181
+ relation: typeof relation;
182
+ blocks: typeof blocks;
183
+ page: typeof page;
184
+ group: typeof group;
185
+ array: typeof array;
186
+ seo: typeof seo;
187
+ color: typeof color;
188
+ json: typeof json;
189
+ };
190
+ type ValueOf<F> = F extends Field<infer V, boolean> ? V : never;
191
+ type RequiredKeys<F extends Fields> = {
192
+ [K in keyof F]: F[K] extends Field<unknown, true> ? K : never;
193
+ }[keyof F];
194
+ type Prettify<T> = {
195
+ [K in keyof T]: T[K];
196
+ } & {};
197
+ /** The document data type described by a set of fields. */
198
+ export type InferFields<F extends Fields> = Prettify<{
199
+ [K in RequiredKeys<F>]: ValueOf<F[K]>;
200
+ } & {
201
+ [K in Exclude<keyof F, RequiredKeys<F>>]?: ValueOf<F[K]>;
202
+ }>;
203
+ export declare function objectShape(fields: Fields): Record<string, z.ZodType>;
204
+ export {};
package/src/fields.js ADDED
@@ -0,0 +1,213 @@
1
+ import { z } from 'zod';
2
+ import { SLUG_PATTERN } from './slug.js';
3
+ import { ANCHOR_PATTERN, BLOCK_STYLE } from './style.js';
4
+ const blockStyleSchema = z.object({
5
+ background: z.enum(BLOCK_STYLE.background).optional(),
6
+ spacing: z.enum(BLOCK_STYLE.spacing).optional(),
7
+ animation: z.enum(BLOCK_STYLE.animation).optional(),
8
+ anchor: z.string().max(60).regex(ANCHOR_PATTERN, 'Usa lettere minuscole, numeri e trattini').optional(),
9
+ });
10
+ // Returns Field<never, never>, which is assignable to the precise Field<V, R> each builder declares.
11
+ function field(kind, common, options, schema, extra = {}) {
12
+ return {
13
+ kind,
14
+ required: (common?.required ?? false),
15
+ label: common?.label,
16
+ description: common?.description,
17
+ localized: common?.localized ?? ['text', 'richText', 'slug', 'seo', 'link'].includes(kind),
18
+ options,
19
+ schema,
20
+ ...extra,
21
+ };
22
+ }
23
+ // ---------- shared validators ----------
24
+ const mediaSchema = z.object({
25
+ src: z.string().min(1),
26
+ alt: z.string(),
27
+ width: z.number().int().positive().optional(),
28
+ height: z.number().int().positive().optional(),
29
+ focal: z.object({ x: z.number().min(0).max(100), y: z.number().min(0).max(100) }).optional(),
30
+ });
31
+ const linkSchema = z.object({
32
+ href: z.string().min(1),
33
+ label: z.string().optional(),
34
+ newTab: z.boolean().optional(),
35
+ });
36
+ const runSchema = z.object({
37
+ text: z.string(),
38
+ bold: z.boolean().optional(),
39
+ italic: z.boolean().optional(),
40
+ href: z.string().optional(),
41
+ });
42
+ const richTextSchema = z.array(z
43
+ .object({
44
+ type: z.enum(['paragraph', 'heading', 'quote', 'listItem', 'image']),
45
+ level: z.union([z.literal(2), z.literal(3), z.literal(4)]).optional(),
46
+ children: z.array(runSchema),
47
+ image: mediaSchema.optional(),
48
+ })
49
+ .refine((b) => b.type !== 'image' || !!b.image, { message: 'Scegli l’immagine', path: ['image'] }));
50
+ function text(o) {
51
+ const { min, max, multiline, placeholder } = o ?? {};
52
+ return field('text', o, { min, max, multiline, placeholder }, () => {
53
+ let s = z.string().trim();
54
+ if (o?.required)
55
+ s = s.min(Math.max(1, min ?? 1));
56
+ else if (min)
57
+ s = s.min(min);
58
+ if (max)
59
+ s = s.max(max);
60
+ return s;
61
+ });
62
+ }
63
+ function richText(o) {
64
+ return field('richText', o, {}, () => (o?.required ? richTextSchema.min(1) : richTextSchema));
65
+ }
66
+ function number(o) {
67
+ const { min, max, integer } = o ?? {};
68
+ return field('number', o, { min, max, integer }, () => {
69
+ let s = z.number();
70
+ if (integer)
71
+ s = s.int();
72
+ if (min !== undefined)
73
+ s = s.min(min);
74
+ if (max !== undefined)
75
+ s = s.max(max);
76
+ return s;
77
+ });
78
+ }
79
+ function boolean(o) {
80
+ return field('boolean', o, {}, () => z.boolean());
81
+ }
82
+ function date(o) {
83
+ return field('date', o, {}, () => z.union([z.iso.date(), z.iso.datetime({ offset: true })]));
84
+ }
85
+ function select(o) {
86
+ const values = o.options.map((x) => (typeof x === 'string' ? x : x.value));
87
+ if (!values.length)
88
+ throw new Error('f.select() needs at least one option');
89
+ return field('select', o, { options: o.options }, () => z.enum(values));
90
+ }
91
+ function slug(o) {
92
+ return field('slug', o, { from: o?.from }, () => z.string().max(96).regex(SLUG_PATTERN, 'Usa lettere minuscole, numeri e trattini singoli'));
93
+ }
94
+ function media(o) {
95
+ return field('media', o, { accept: o?.accept ?? 'image/*', focalPoint: o?.focalPoint ?? true }, () => mediaSchema);
96
+ }
97
+ function link(o) {
98
+ return field('link', o, {}, () => linkSchema);
99
+ }
100
+ function relation(o) {
101
+ return field('relation', o, { to: o.to, many: o.many ?? false }, () => o.many ? z.array(z.string().min(1)) : z.string().min(1));
102
+ }
103
+ function blocks(defs, o) {
104
+ if (!defs.length)
105
+ throw new Error('f.blocks() needs at least one block');
106
+ return field('blocks', o, { min: o?.min, max: o?.max }, () => {
107
+ const variants = defs.map((b) => z.object({
108
+ _type: z.literal(b.name),
109
+ _key: z.string().min(1),
110
+ // A name chosen in the editor's layers, for people only: the site ignores it.
111
+ _label: z.string().max(80).optional(),
112
+ _style: blockStyleSchema.optional(),
113
+ ...objectShape(b.fields),
114
+ }));
115
+ const item = variants.length === 1
116
+ ? variants[0]
117
+ : z.discriminatedUnion('_type', variants);
118
+ let s = z.array(item);
119
+ if (o?.min !== undefined)
120
+ s = s.min(o.min);
121
+ if (o?.max !== undefined)
122
+ s = s.max(o.max);
123
+ return s;
124
+ }, { blocks: defs });
125
+ }
126
+ function group(fields, o) {
127
+ return field('group', o, {}, () => z.object(objectShape(fields)), { children: fields });
128
+ }
129
+ function array(item, o) {
130
+ return field('array', o, { min: o?.min, max: o?.max }, () => {
131
+ let s = z.array(item.schema());
132
+ if (o?.min !== undefined)
133
+ s = s.min(o.min);
134
+ if (o?.max !== undefined)
135
+ s = s.max(o.max);
136
+ return s;
137
+ }, { item });
138
+ }
139
+ function seo(o) {
140
+ return field('seo', o, {}, () => z.object({
141
+ title: z.string().max(70).optional(),
142
+ description: z.string().max(170).optional(),
143
+ image: mediaSchema.optional(),
144
+ noindex: z.boolean().optional(),
145
+ nofollow: z.boolean().optional(),
146
+ socialTitle: z.string().max(90).optional(),
147
+ socialDescription: z.string().max(200).optional(),
148
+ canonical: z.string().max(500).optional(),
149
+ schema: z.enum(['article', 'project', 'service', 'event', 'faq']).optional(),
150
+ faq: z
151
+ .array(z.object({ q: z.string().max(300), a: z.string().max(3000) }))
152
+ .max(50)
153
+ .optional(),
154
+ event: z
155
+ .object({
156
+ start: z.string().optional(),
157
+ end: z.string().optional(),
158
+ place: z.string().optional(),
159
+ address: z.string().optional(),
160
+ online: z.boolean().optional(),
161
+ price: z.string().optional(),
162
+ })
163
+ .optional(),
164
+ }));
165
+ }
166
+ function color(o) {
167
+ return field('color', o, {}, () => z.string().regex(/^#[0-9a-fA-F]{6}$/, 'Usa un colore nel formato #rrggbb'));
168
+ }
169
+ const pageNodeSchema = z.lazy(() => z
170
+ .object({
171
+ id: z.string().min(1),
172
+ type: z.enum(['frame', 'text', 'image', 'button', 'video', 'embed', 'scene', 'icon', 'input']),
173
+ name: z.string().max(120).optional(),
174
+ children: z.array(pageNodeSchema).optional(),
175
+ })
176
+ .loose());
177
+ /**
178
+ * A page composed on the canvas: sections of frames, text, images and buttons, with their design.
179
+ * Render it with `renderPage` from `@materializecms/core/page`, or the Astro `<Page>` component.
180
+ */
181
+ function page(o) {
182
+ return field('page', { localized: true, ...o }, {}, () => z.array(pageNodeSchema).max(200));
183
+ }
184
+ function json(o) {
185
+ return field('json', o, {}, () => z.json());
186
+ }
187
+ /** Field builders: `f.text({ required: true })`, `f.blocks([hero, gallery])`, … */
188
+ export const f = {
189
+ text,
190
+ richText,
191
+ number,
192
+ boolean,
193
+ date,
194
+ select,
195
+ slug,
196
+ media,
197
+ link,
198
+ relation,
199
+ blocks,
200
+ page,
201
+ group,
202
+ array,
203
+ seo,
204
+ color,
205
+ json,
206
+ };
207
+ export function objectShape(fields) {
208
+ const shape = {};
209
+ for (const [name, def] of Object.entries(fields)) {
210
+ shape[name] = def.required ? def.schema() : def.schema().optional();
211
+ }
212
+ return shape;
213
+ }
package/src/i18n.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { Fields } from './fields.ts';
2
+ import { type PageNode } from './page.ts';
3
+ import type { Locales } from './schema.ts';
4
+ /** The site's languages: the ones saved in the panel when valid, else the config's. */
5
+ export declare function withLocales<T extends {
6
+ locales: Locales;
7
+ }>(config: T, saved: unknown): T;
8
+ export declare const isLocaleCode: (code: string) => boolean;
9
+ /** Node properties a translation changes: the words, the links, the pictures. */
10
+ export declare const TRANSLATED_KEYS: readonly string[];
11
+ /**
12
+ * The original's structure and design with a translation's words: what a translated header or
13
+ * footer shows, and how a translated page catches up with a changed original. Elements the
14
+ * translation does not have keep the original's words.
15
+ */
16
+ export declare function mergeTranslation(original: PageNode[], translation: PageNode[]): PageNode[];
17
+ /** Ids of the elements in `original` the translation does not have yet. */
18
+ export declare function untranslatedIds(original: PageNode[], translation: PageNode[]): string[];
19
+ /** One text of a document, as sent to the translator, and where its translation goes. */
20
+ export interface TextSlot {
21
+ text: string;
22
+ set: (value: string) => void;
23
+ }
24
+ /** The texts of a canvas page: words, formatted sentences, alternative texts, field options. */
25
+ export declare function nodeSlots(nodes: PageNode[]): TextSlot[];
26
+ /**
27
+ * Every text of a document that a translation changes, read from its fields: texts, rich text,
28
+ * link labels, alternative texts, SEO, canvas pages, blocks, groups and lists. `data` is changed
29
+ * in place by the slots, so pass a copy.
30
+ */
31
+ export declare function documentSlots(fields: Fields, data: Record<string, unknown>): TextSlot[];
32
+ /**
33
+ * Internal links pointing at pages of the main language, sent to the same pages in this
34
+ * language where they are translated: "/contatti#form" becomes "/en/contact#form".
35
+ */
36
+ export declare function localizeLinks(nodes: PageNode[], urls: Record<string, string>): PageNode[];