@materializecms/server 0.7.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ Ogni versione, dalla più recente. Le novità in arrivo si scrivono sotto «Pros
4
4
 
5
5
  ## Prossima versione
6
6
 
7
+ ## 0.8.0 — 2026-11-07
8
+
9
+ - **Un documento alla volta**: se un'altra persona sta modificando la pagina che apri, la vedi in sola lettura con chi ci lavora e da quanto, e puoi prendere il controllo. Nessuno sovrascrive più il lavoro di un altro senza saperlo, nemmeno da due schede.
10
+ - **Sezioni con effetti**: nuova categoria **Effetti** nella libreria, con 7 sezioni pronte che usano le scene WebGL e il 3D: apertura su aurora, apertura con seta, logo 3D (carica il tuo SVG), oggetto 3D (scegli un modello `.glb`), numeri sul globo, immagine animata e invito con particelle. Si trovano anche cercando «webgl».
11
+ - Nuovo schema di colori **Scuro** per le sezioni: resta scuro con il testo chiaro in qualsiasi tema.
12
+ - **Oscura** ora funziona anche sugli sfondi animati delle sezioni, non solo sulle foto.
7
13
  ## 0.7.0 — 2026-11-06
8
14
 
9
15
  - **Tour del pannello**: la prima volta che entri, un tour mostra il pannello una zona alla volta, come quello dell'editor. Si rifà dal menu del profilo (**Tour del pannello**) o cercando «tour» con ⌘K.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@materializecms/server",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Il server di Materialize: API, accessi, media, backup, aggiornamenti dal pannello e server MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,9 +20,9 @@
20
20
  "dependencies": {
21
21
  "@anthropic-ai/sdk": "1.3.0",
22
22
  "@hono/node-server": "2.1.5",
23
- "@materializecms/admin": "0.7.0",
24
- "@materializecms/core": "0.7.0",
25
- "@materializecms/db": "0.7.0",
23
+ "@materializecms/admin": "0.8.0",
24
+ "@materializecms/core": "0.8.0",
25
+ "@materializecms/db": "0.8.0",
26
26
  "@modelcontextprotocol/sdk": "1.32.2",
27
27
  "hono": "4.13.14",
28
28
  "htmlparser2": "12.0.0",
package/src/app.js CHANGED
@@ -1676,6 +1676,68 @@ export function createApp(base) {
1676
1676
  await comments.remove(comment.id);
1677
1677
  return c.body(null, 204);
1678
1678
  });
1679
+ const turns = new Map();
1680
+ const TURN_TTL = 45_000;
1681
+ const turnOf = (docId) => {
1682
+ const turn = turns.get(docId);
1683
+ if (turn && Date.now() - turn.at > TURN_TTL) {
1684
+ turns.delete(docId);
1685
+ return null;
1686
+ }
1687
+ return turn ?? null;
1688
+ };
1689
+ const turnJson = (turn, user, tab) => ({
1690
+ mine: !!turn && turn.userId === user.id && turn.tab === tab,
1691
+ holder: turn ? { name: turn.name, since: new Date(turn.since).toISOString(), self: turn.userId === user.id } : null,
1692
+ });
1693
+ /** Holds the turn (or asks for it); take: true takes it from whoever has it. */
1694
+ app.post('/api/documents/:id/turn', requireUser, async (c) => {
1695
+ const input = await body(c, z.object({ tab: z.string().min(8).max(64), take: z.boolean().optional() }));
1696
+ const user = c.get('user');
1697
+ const docId = c.req.param('id');
1698
+ const now = Date.now();
1699
+ for (const [k, t] of turns)
1700
+ if (now - t.at > TURN_TTL * 4)
1701
+ turns.delete(k);
1702
+ const turn = turnOf(docId);
1703
+ if (!turn || (turn.userId === user.id && turn.tab === input.tab) || input.take) {
1704
+ if (input.take && turn && turn.tab !== input.tab) {
1705
+ // The one who had it learns who took it, at their next beat.
1706
+ turns.set(`lost:${docId}:${turn.tab}`, { ...turn, takenBy: user.name, at: now });
1707
+ await record(c, 'document.turn_taken', '', { id: docId, from: turn.name });
1708
+ }
1709
+ const keep = turn && turn.userId === user.id && turn.tab === input.tab;
1710
+ turns.set(docId, { userId: user.id, name: user.name, tab: input.tab, since: keep ? turn.since : now, at: now });
1711
+ return c.json(turnJson(turnOf(docId), user, input.tab));
1712
+ }
1713
+ const taken = turns.get(`lost:${docId}:${input.tab}`);
1714
+ if (taken)
1715
+ turns.delete(`lost:${docId}:${input.tab}`);
1716
+ return c.json({ ...turnJson(turn, user, input.tab), takenBy: taken?.takenBy ?? null });
1717
+ });
1718
+ /** Leaves the turn: the document closed. */
1719
+ app.delete('/api/documents/:id/turn', requireUser, async (c) => {
1720
+ const docId = c.req.param('id');
1721
+ const turn = turnOf(docId);
1722
+ if (turn && turn.userId === c.get('user').id && turn.tab === c.req.query('tab'))
1723
+ turns.delete(docId);
1724
+ return c.body(null, 204);
1725
+ });
1726
+ /**
1727
+ * Changes to a document from someone who is not on turn are refused: from another person, or
1728
+ * from another tab of the same person. The API and AI tools of the person on turn pass.
1729
+ */
1730
+ const guardTurn = (c, docId) => {
1731
+ const turn = turnOf(docId);
1732
+ if (!turn)
1733
+ return;
1734
+ const user = c.get('user');
1735
+ const tab = c.req.header('x-mz-tab');
1736
+ if (turn.userId !== user.id)
1737
+ throw new ApiError(409, 'not_your_turn', `${turn.name} sta modificando questo documento: le tue modifiche non sono state salvate`);
1738
+ if (tab && tab !== turn.tab)
1739
+ throw new ApiError(409, 'not_your_turn', 'Questo documento è aperto in un’altra scheda: le modifiche di qui non sono state salvate');
1740
+ };
1679
1741
  app.post('/api/documents/:id/presence', requireUser, async (c) => {
1680
1742
  const input = await body(c, z.object({ selected: z.string().max(200).nullable() }));
1681
1743
  const user = c.get('user');
@@ -2888,6 +2950,7 @@ When done, say in one or two short sentences what you changed. Reply in the pers
2888
2950
  app.get('/api/documents/:id', requireUser, async (c) => c.json(await toJson(await loadDocument(c.req.param('id')))));
2889
2951
  app.put('/api/documents/:id', requireUser, async (c) => {
2890
2952
  const existing = await loadDocument(c.req.param('id'));
2953
+ guardTurn(c, existing.id);
2891
2954
  const input = await body(c, documentBody);
2892
2955
  const def = collection(existing.collection);
2893
2956
  const draft = draftData(def, input.data);
@@ -3811,6 +3874,7 @@ When done, say in one or two short sentences what you changed. Reply in the pers
3811
3874
  });
3812
3875
  app.post('/api/documents/:id/revisions/:revision/restore', requireUser, async (c) => {
3813
3876
  const existing = await loadDocument(c.req.param('id'));
3877
+ guardTurn(c, existing.id);
3814
3878
  const doc = await content.restore(existing.id, c.req.param('revision'), c.get('user').id);
3815
3879
  if (!doc)
3816
3880
  throw new ApiError(404, 'not_found', 'This version does not exist');
package/src/tools.d.ts CHANGED
@@ -50,7 +50,7 @@ type Schema = {
50
50
  collections: CollectionInfo[];
51
51
  };
52
52
  /** How the page model works, for a model that is about to edit one. */
53
- export declare const PAGE_GUIDE = "Pages are trees of nodes. The top level holds sections (type \"frame\"); sections hold frames, texts, images, buttons, icons, videos, scenes and form inputs.\nEvery node has an \"id\" (keep existing ids; new nodes may omit it, one is assigned) and a \"type\".\n\nCommon properties:\n- Layout of a frame: layout \"col\" | \"row\" | \"grid\", cols (grid columns), gap (px), align \"start\"|\"center\"|\"end\"|\"stretch\", justify \"start\"|\"center\"|\"end\"|\"between\", wrap.\n- Size: width \"fill\"|\"hug\"|\"fixed\" with fixedW, maxW, h (px or \"60vh\"), minH (percent of screen height, sections), padX/padY (px), size \"xs\"|\"s\"|\"m\"|\"l\" (a section's vertical padding).\n- Look: bg (theme token or CSS color), scheme \"default\"|\"surface\"|\"inverse\"|\"accent\" (a section's color scheme), radius (px | \"btn\" | \"card\"), border, shadow \"none\"|\"sm\"|\"lg\", gradient, bgImage {src, alt}, bgDim 0-1.\n- Text (\"text\"): text (plain) or runs [{t, b?, i?, href?}], style \"display\"|\"h1\"|\"h2\"|\"h3\"|\"body\"|\"small\"|\"eyebrow\"|\"logo\"|\"button\", tag \"h1\"..\"h4\"|\"p\" (HTML element, defaults from style), color, talign \"left\"|\"center\"|\"right\", fontSize, weight, upper, italic.\n- Button (\"button\"): text, href, variant \"primary\"|\"secondary\"|\"ghost\", newTab.\n- Image (\"image\"): src (a URL from the media library, e.g. /media/photo.jpg), alt, ratio (\"16/9\", \"1/1\"\u2026), fit \"cover\"|\"contain\".\n- Icon (\"icon\"): icon \"lucide:arrow-right\" style names.\n- Motion: anim \"fade\"|\"up\"|\"down\"|\"left\"|\"right\"|\"zoom\"|\"blur\"|\"reveal\"|\"pop\", animTrig \"load\"|\"scroll\", hover \"lift\"|\"grow\"|\"dim\"|\"zoomimg\".\n- Tablet and mobile changes go in over: {tablet: {...}, mobile: {...}} (only the properties that differ).\nTheme color tokens: bg, surface, text, muted, accent, onAccent, inverse, onInverse, border, line.\nPrefer adding ready-made sections from the library (add_section) and then changing their texts, images and colors: they are designed and responsive. Write content in the site's language.";
53
+ export declare const PAGE_GUIDE = "Pages are trees of nodes. The top level holds sections (type \"frame\"); sections hold frames, texts, images, buttons, icons, videos, scenes and form inputs.\nEvery node has an \"id\" (keep existing ids; new nodes may omit it, one is assigned) and a \"type\".\n\nCommon properties:\n- Layout of a frame: layout \"col\" | \"row\" | \"grid\", cols (grid columns), gap (px), align \"start\"|\"center\"|\"end\"|\"stretch\", justify \"start\"|\"center\"|\"end\"|\"between\", wrap.\n- Size: width \"fill\"|\"hug\"|\"fixed\" with fixedW, maxW, h (px or \"60vh\"), minH (percent of screen height, sections), padX/padY (px), size \"xs\"|\"s\"|\"m\"|\"l\" (a section's vertical padding).\n- Look: bg (theme token or CSS color), scheme \"default\"|\"surface\"|\"inverse\"|\"accent\"|\"dark\" (a section's color scheme; \"dark\" stays dark with light text in any theme), radius (px | \"btn\" | \"card\"), border, shadow \"none\"|\"sm\"|\"lg\", gradient, bgImage {src, alt}, bgDim 0-1.\n- Text (\"text\"): text (plain) or runs [{t, b?, i?, href?}], style \"display\"|\"h1\"|\"h2\"|\"h3\"|\"body\"|\"small\"|\"eyebrow\"|\"logo\"|\"button\", tag \"h1\"..\"h4\"|\"p\" (HTML element, defaults from style), color, talign \"left\"|\"center\"|\"right\", fontSize, weight, upper, italic.\n- Button (\"button\"): text, href, variant \"primary\"|\"secondary\"|\"ghost\", newTab.\n- Image (\"image\"): src (a URL from the media library, e.g. /media/photo.jpg), alt, ratio (\"16/9\", \"1/1\"\u2026), fit \"cover\"|\"contain\".\n- Icon (\"icon\"): icon \"lucide:arrow-right\" style names.\n- Motion: anim \"fade\"|\"up\"|\"down\"|\"left\"|\"right\"|\"zoom\"|\"blur\"|\"reveal\"|\"pop\", animTrig \"load\"|\"scroll\", hover \"lift\"|\"grow\"|\"dim\"|\"zoomimg\".\n- Tablet and mobile changes go in over: {tablet: {...}, mobile: {...}} (only the properties that differ).\nTheme color tokens: bg, surface, text, muted, accent, onAccent, inverse, onInverse, border, line.\nPrefer adding ready-made sections from the library (add_section) and then changing their texts, images and colors: they are designed and responsive. Write content in the site's language.";
54
54
  /** One line per node, indented: what the model reads before editing. */
55
55
  export declare function outline(nodes: PageNode[]): string;
56
56
  declare const operation: z.ZodDiscriminatedUnion<[z.ZodObject<{
package/src/tools.js CHANGED
@@ -13,7 +13,7 @@ Every node has an "id" (keep existing ids; new nodes may omit it, one is assigne
13
13
  Common properties:
14
14
  - Layout of a frame: layout "col" | "row" | "grid", cols (grid columns), gap (px), align "start"|"center"|"end"|"stretch", justify "start"|"center"|"end"|"between", wrap.
15
15
  - Size: width "fill"|"hug"|"fixed" with fixedW, maxW, h (px or "60vh"), minH (percent of screen height, sections), padX/padY (px), size "xs"|"s"|"m"|"l" (a section's vertical padding).
16
- - Look: bg (theme token or CSS color), scheme "default"|"surface"|"inverse"|"accent" (a section's color scheme), radius (px | "btn" | "card"), border, shadow "none"|"sm"|"lg", gradient, bgImage {src, alt}, bgDim 0-1.
16
+ - Look: bg (theme token or CSS color), scheme "default"|"surface"|"inverse"|"accent"|"dark" (a section's color scheme; "dark" stays dark with light text in any theme), radius (px | "btn" | "card"), border, shadow "none"|"sm"|"lg", gradient, bgImage {src, alt}, bgDim 0-1.
17
17
  - Text ("text"): text (plain) or runs [{t, b?, i?, href?}], style "display"|"h1"|"h2"|"h3"|"body"|"small"|"eyebrow"|"logo"|"button", tag "h1".."h4"|"p" (HTML element, defaults from style), color, talign "left"|"center"|"right", fontSize, weight, upper, italic.
18
18
  - Button ("button"): text, href, variant "primary"|"secondary"|"ghost", newTab.
19
19
  - Image ("image"): src (a URL from the media library, e.g. /media/photo.jpg), alt, ratio ("16/9", "1/1"…), fit "cover"|"contain".