smoodly 0.0.60 → 0.0.62

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 (76) hide show
  1. package/README.md +3 -1
  2. package/dist/admin/editor/CanvasOverlay.d.ts +4 -1
  3. package/dist/admin/editor/CanvasOverlay.js +3 -3
  4. package/dist/admin/editor/GlobalCanvas.d.ts +27 -0
  5. package/dist/admin/editor/GlobalCanvas.js +159 -0
  6. package/dist/admin/editor/InsertPicker.js +1 -1
  7. package/dist/admin/files.generated.js +1 -1
  8. package/dist/admin/serialize.d.ts +2 -0
  9. package/dist/admin/serialize.js +1 -0
  10. package/dist/admin/shell/AdminApp.js +5 -5
  11. package/dist/admin/shell/GlobalForm.js +80 -7
  12. package/dist/admin/shell/GlobalList.js +1 -1
  13. package/dist/admin/shell/{IconRail.d.ts → NavBar.d.ts} +3 -3
  14. package/dist/admin/shell/NavBar.js +120 -0
  15. package/dist/admin/shell/global-canvas.d.ts +40 -0
  16. package/dist/admin/shell/global-canvas.js +60 -0
  17. package/dist/admin/shell/health-list.js +1 -1
  18. package/dist/admin/ui/primitives.js +1 -1
  19. package/dist/admin/ui/theme.js +99 -91
  20. package/dist/api-client.js +5 -3
  21. package/dist/astro/index.js +3 -3
  22. package/dist/cli/push.js +2 -3
  23. package/dist/env.js +3 -2
  24. package/dist/push.d.ts +7 -0
  25. package/dist/push.js +8 -0
  26. package/dist/space-kind.d.ts +2 -0
  27. package/dist/space-kind.js +1 -0
  28. package/dist/version.d.ts +1 -1
  29. package/dist/version.js +1 -1
  30. package/docs/collections/defining.md +57 -0
  31. package/docs/collections/entry-pages.md +168 -0
  32. package/docs/collections/ordering.md +47 -0
  33. package/docs/collections/queries.md +173 -0
  34. package/docs/content-modeling/changing-schemas.md +79 -0
  35. package/docs/content-modeling/custom-section.md +94 -0
  36. package/docs/content-modeling/marks.md +97 -0
  37. package/docs/content-modeling/overview.md +55 -0
  38. package/docs/content-modeling/page-types.md +237 -0
  39. package/docs/content-modeling/sections.md +264 -0
  40. package/docs/content-modeling/styles-and-settings.md +184 -0
  41. package/docs/content-modeling/zones.md +80 -0
  42. package/docs/fields/choices.md +42 -0
  43. package/docs/fields/conditional.md +72 -0
  44. package/docs/fields/link.md +31 -0
  45. package/docs/fields/list-and-object.md +58 -0
  46. package/docs/fields/media.md +103 -0
  47. package/docs/fields/overview.md +56 -0
  48. package/docs/fields/reference-and-tags.md +62 -0
  49. package/docs/fields/richtext.md +168 -0
  50. package/docs/fields/text.md +152 -0
  51. package/docs/frameworks/astro.md +110 -0
  52. package/docs/frameworks/nextjs.md +147 -0
  53. package/docs/getting-started/content-from-code.md +263 -0
  54. package/docs/getting-started/deploying.md +131 -0
  55. package/docs/getting-started/export-and-import.md +73 -0
  56. package/docs/getting-started/first-page.md +98 -0
  57. package/docs/getting-started/install.md +108 -0
  58. package/docs/getting-started/keys.md +40 -0
  59. package/docs/getting-started/project-structure.md +142 -0
  60. package/docs/getting-started/working-together.md +42 -0
  61. package/docs/globals/defining.md +99 -0
  62. package/docs/globals/header-and-footer.md +117 -0
  63. package/docs/index.md +83 -0
  64. package/docs/languages/language-switcher.md +103 -0
  65. package/docs/languages/locales.md +119 -0
  66. package/docs/languages/localized-fields.md +61 -0
  67. package/docs/media/images-and-crops.md +131 -0
  68. package/docs/media/library.md +40 -0
  69. package/docs/pages/drafts-and-publishing.md +44 -0
  70. package/docs/pages/page-tree.md +51 -0
  71. package/docs/pages/paths-and-slugs.md +74 -0
  72. package/docs/pages/seo.md +126 -0
  73. package/docs/pages/version-history.md +23 -0
  74. package/docs/reference/upgrading.md +312 -0
  75. package/package.json +4 -3
  76. package/dist/admin/shell/IconRail.js +0 -109
@@ -34,8 +34,8 @@ const TOKENS_LIGHT = `
34
34
  --selection-global:#C4620C;
35
35
  --hover:rgba(21,23,39,.05); --press:rgba(21,23,39,.09); --control-hover:#ECEDF1; --canvas-ground:#EDEDED;
36
36
  --btn-secondary-bg:var(--surface); --btn-secondary-hover:#ECEDF1;
37
- /* The rail sits on the frame: its own greys, the active tile a white tint. */
38
- --rail-fg:#B9BDCE; --rail-active-fg:#FFFFFF; --rail-active-bg:rgba(255,255,255,.14); --rail-hover-bg:rgba(255,255,255,.08);
37
+ /* The navigation bar sits on the frame: its own greys, the active item a white tint, its dark menus a step lighter than the frame. */
38
+ --navbar-fg:#B9BDCE; --navbar-active-fg:#FFFFFF; --navbar-active-bg:rgba(255,255,255,.14); --navbar-hover-bg:rgba(255,255,255,.08); --navbar-menu-bg:#242840;
39
39
  --bar-bg:transparent; --bar-border:0; --menu-border:0; --card-divider:0;
40
40
  --card-title-size:var(--text-md); --card-title-color:var(--text);
41
41
  /* In-card segmented (tabs, the span bar): white track, the chosen option in the tile grey. */
@@ -52,7 +52,7 @@ const TOKENS_LIGHT = `
52
52
  dark mode that washed out over the white page, 2026-09-10). */
53
53
  --plus-bg:#DDEBE4; --plus-hover:#D0E3D9; --plus-fg:#1C6E5B;
54
54
  --plus-icon-bg:#1C6E5B; --plus-icon-fg:#FFFFFF; --plus-shadow:none;
55
- --logo-color:var(--accent); --logo-outer:#0B5A49; --logo-inner:#5FC9A0; /* the rail's app icon: deep green square, mint s (2026-10-23) */
55
+ --logo-color:var(--accent); --logo-outer:#0B5A49; --logo-inner:#5FC9A0; /* the bar's app icon: deep green square, mint s (2026-10-23) */
56
56
 
57
57
  --status-published-fg:var(--green-700); --status-published-bg:var(--green-50);
58
58
  --status-changes-fg:var(--amber-700); --status-changes-bg:var(--amber-50);
@@ -78,7 +78,7 @@ const TOKENS_LIGHT = `
78
78
  `;
79
79
  /* The handoff's dark proposal (VisualEditor.md, 8c right board): the frame
80
80
  and sheet a step darker, tiles a step lighter, fields in the sheet tone;
81
- the rail and the logo unchanged. */
81
+ the bar and the logo unchanged. */
82
82
  const TOKENS_DARK = `
83
83
  --accent:#6CC4A6; --accent-hover:#82D2B8; --accent-soft:rgba(108,196,166,.18); --accent-line:#6CC4A6; --on-accent:#151727;
84
84
  --selection-global:#F09A5C;
@@ -133,10 +133,10 @@ const SCALE = `
133
133
  --control-h:40px; --control-h-sm:34px; --control-h-lg:48px; --button-h:36px; --button-h-sm:28px; --row-h:48px; --topbar-h:64px;
134
134
  --outline-row-h:36px; --seg-pad-y:7px; --tabs-pad:3px;
135
135
  --plus-h:32px; --plus-pad:0 14px 0 6px; --plus-label:inline;
136
- --nav-w:72px; --inspector-w:360px; --gutter:20px;
137
- /* The frame is 8px above, right of and below the sheet (6px in the handoff, +2px 2026-10-23); the rail fills the left edge. */
138
- --frame-pad:8px; --frame-pad-top:8px; --shell-gap:0px; --sheet-radius:20px; --sheet-pad:0 24px 24px;
139
- --rail-pad:18px 6px 16px; --rail-logo:block; --rail-logo-opacity:1; --rail-tile-w:44px; --rail-tile-h:40px; --rail-tile-radius:12px;
136
+ --inspector-w:360px; --gutter:20px;
137
+ /* The frame is 8px left of, right of and below the sheet (6px in the handoff, +2px 2026-10-23); the bar fills the top edge. */
138
+ --frame-pad:8px; --shell-gap:0px; --sheet-radius:20px; --sheet-pad:0 24px 24px;
139
+ --navbar-h:64px; --navbar-pad-x:12px; --navbar-item-h:40px; --navbar-item-radius:10px;
140
140
  --topbar-pad:0; --back-size:32px; --back-bg:var(--surface);
141
141
  --table-pad:8px 12px; --table-top:80px; --canvas-pad:28px 40px 0; --card-pad:16px 18px; --card-gap:12px; --form-pad:32px 40px;
142
142
  --border:1px solid var(--line);
@@ -182,106 +182,106 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
182
182
  .sm-code{background:var(--surface);border-radius:8px;padding:10px 12px;margin:0;font:13px/1.5 var(--font-mono);overflow:auto}
183
183
 
184
184
  /* ── shell ─────────────────────────────────────────────────────────── */
185
- /* Rail | main. The shell is the frame (design_handoff_smoodly_neutral/
186
- components/AppShell.html): navy, 8px above, right of and below the main
185
+ /* Bar over main (2026-11-14; a 72px rail on the left before it). The shell is
186
+ the frame (design_handoff_smoodly_neutral/components/AppShell.html): navy,
187
+ the navigation bar across its top, 8px right of, left of and below the main
187
188
  column, which is the white sheet with a 20px radius and its own inner
188
- padding; the rail fills the frame's left edge.
189
+ padding. The shell owns the viewport and the sheet scrolls inside itself,
190
+ so the bar stays put.
189
191
  The main column stacks the top bar over one tile-shaped region: list,
190
192
  form or editor. */
191
193
  .sm-shell{
192
- display:grid;grid-template-columns:var(--nav-w) 1fr;column-gap:var(--shell-gap);
193
- padding:var(--frame-pad-top) var(--frame-pad) var(--frame-pad) 0;height:100vh;height:100dvh;overflow:hidden;background:var(--frame);
194
+ display:grid;grid-template-rows:var(--navbar-h) minmax(0,1fr);grid-template-columns:minmax(0,1fr);row-gap:var(--shell-gap);
195
+ padding:0 var(--frame-pad) var(--frame-pad);height:100vh;height:100dvh;overflow:hidden;background:var(--frame);
194
196
  }
195
197
  .sm-shell>.sm-main{
196
198
  min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden;
197
199
  background:var(--sheet);border-radius:var(--sheet-radius);padding:var(--sheet-pad);
198
200
  }
199
201
  .sm-wordmark{height:18px;width:auto;color:var(--logo-color);filter:var(--logo-filter);flex:none}
200
- /* The IconRail (design_handoff_smoodly_neutral/components/IconRail.html):
201
- a 72px rail on the frame with the app icon on top, 18px Lucide icons
202
- over a 10px label and a 44×40 active tile in a white tint — the rail's
203
- own colour tokens, since the frame is navy in both themes. */
204
- .sm-rail{
205
- width:var(--nav-w);height:100%;display:flex;flex-direction:column;align-items:center;gap:18px;
206
- padding:var(--rail-pad);background:var(--bar-bg);border-right:var(--bar-border);color:var(--rail-fg);
207
- font:500 10px/1 var(--font-sans);position:relative;
208
- }
202
+ /* The navigation bar: the app icon on the left, the items centred, Deploy and
203
+ the avatar on the right. The rail's colours, icons and order: 18px Lucide
204
+ icons beside a 14px label, the active item a white-tint pill. */
205
+ .sm-navbar{
206
+ display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:16px;min-width:0;
207
+ padding:0 var(--navbar-pad-x);background:var(--bar-bg);color:var(--navbar-fg);
208
+ font:500 var(--text-md)/1 var(--font-sans);position:relative;z-index:30;
209
+ }
210
+ .sm-navbar__start{display:flex;align-items:center;justify-self:start}
211
+ .sm-navbar__end{display:flex;align-items:center;gap:10px;justify-self:end}
209
212
  /* The app icon (the Smoodly menu's trigger): the deep green square with the
210
- mint s, 26px wide (2026-10-23; the lozenge before it was 20px, asked for
211
- 2026-09-26, at .8 opacity). */
212
- .sm-rail__brand{appearance:none;background:none;border:0;padding:0;margin-bottom:6px;cursor:pointer;display:block;border-radius:6px;color:inherit}
213
+ mint s (2026-10-23). */
214
+ .sm-navbar__brand{appearance:none;background:none;border:0;padding:0;cursor:pointer;display:block;border-radius:8px;color:inherit}
213
215
  /* Hover (2026-10-23): the app icon and the avatar brighten a touch. */
214
- .sm-rail__logo{transition:filter .15s}
215
- .sm-rail__brand:hover .sm-rail__logo{filter:brightness(1.3)}
216
- .sm-rail__user{transition:background .15s}
217
- .sm-rail__user:hover{background:color-mix(in srgb,var(--accent-soft),#fff 60%)}
218
- .sm-rail__brand:focus-visible{outline:2px solid var(--rail-active-fg);outline-offset:2px;box-shadow:none}
219
- .sm-rail__logo{width:26px;height:auto;display:var(--rail-logo);flex:none;opacity:var(--rail-logo-opacity)}
220
- .sm-rail__ext{width:13px;height:13px;flex:none;margin-left:12px;opacity:.45}
221
- .sm-rail__menu-item:hover .sm-rail__ext,.sm-rail__menu-item:focus-visible .sm-rail__ext{opacity:.8}
222
- .sm-rail__menu-version{font-size:var(--text-xs);color:var(--text-subtle);padding:7px 10px 4px;margin-top:3px;border-top:1px solid var(--line)}
223
- .sm-rail__list{display:flex;flex-direction:column;gap:8px;width:100%;margin:0;padding:0;list-style:none}
224
- .sm-rail__item{position:relative}
225
- .sm-rail__btn{
226
- appearance:none;background:none;border:0;margin:0;padding:2px 0;width:100%;display:flex;flex-direction:column;
227
- align-items:center;gap:6px;color:inherit;font:inherit;cursor:pointer;border-radius:var(--radius-md);text-decoration:none;
228
- }
229
- .sm-rail__btn:hover{color:inherit}
230
- .sm-rail__btn:focus-visible{outline:2px solid var(--rail-active-fg);outline-offset:-2px;box-shadow:none}
231
- .sm-rail__tile{position:relative;width:var(--rail-tile-w);height:var(--rail-tile-h);border-radius:var(--rail-tile-radius);display:grid;place-items:center;transition:background .12s}
232
- .sm-rail__btn:hover .sm-rail__tile{background:var(--rail-hover-bg)}
233
- .sm-rail__btn[aria-current="page"],.sm-rail__btn[aria-expanded="true"]{color:var(--rail-active-fg);font-weight:600}
234
- .sm-rail__btn[aria-current="page"]:hover,.sm-rail__btn[aria-expanded="true"]:hover{color:var(--rail-active-fg)}
235
- .sm-rail__btn[aria-current="page"] .sm-rail__tile,.sm-rail__btn[aria-expanded="true"] .sm-rail__tile{background:var(--rail-active-bg)}
236
- .sm-rail__tile svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
237
- /* A collection's own title can be long: it ends in an ellipsis inside the rail (2026-11-13). */
238
- .sm-rail__label{letter-spacing:.01em;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}
239
- /* Not greyed: the reference renders every inactive item at --text and marks
240
- the unavailable ones only by being inert (no pointer, no hover). */
241
- .sm-rail__btn[aria-disabled="true"]{cursor:default}
242
- .sm-rail__btn[aria-disabled="true"]:hover .sm-rail__tile{background:transparent}
243
- /* The Deploy tile (spec 2026-10-09 static mode §6): above the avatar; the badge is the count waiting, the
244
- lit tile a build in flight, the warning tile a build that has not landed or a site that did not answer. */
245
- .sm-rail__deploy{margin-top:auto}
246
- .sm-rail__deploy+.sm-rail__foot{margin-top:0}
247
- /* A publish on its way to the site (plan 2026-10-14 publish through the queue): a quiet line above the Deploy tile
248
- and the avatar, the warning colour when a delivery landed late or with an error. */
249
- .sm-rail__badge{position:absolute;top:-4px;right:-6px;min-width:16px;height:16px;padding:0 4px;border-radius:var(--radius-round);background:var(--accent);color:#fff;font-size:10px;font-weight:600;line-height:16px;text-align:center}
250
- .sm-rail__tile--lit{background:var(--accent-soft);color:var(--accent);animation:sm-rail-pulse 1.6s ease-in-out infinite}
251
- .sm-rail__tile--warn{background:var(--warn-soft);color:var(--warn)}
252
- @keyframes sm-rail-pulse{0%,100%{opacity:1}50%{opacity:.55}}
253
- .sm-rail__menu-detail{font-size:var(--text-sm);color:var(--text);padding:6px 8px 4px;white-space:normal}
254
- .sm-rail__menu-note{font-size:var(--text-xs);color:var(--text-muted);padding:4px 8px 6px;white-space:normal}
255
- .sm-rail__menu-item:disabled{cursor:default;color:var(--text-muted)}
256
- .sm-rail__menu-item:disabled:hover{background:none}
257
- .sm-rail__foot{margin-top:auto;display:flex;flex-direction:column;align-items:center}
258
- .sm-rail__user{
259
- width:28px;height:28px;border-radius:var(--radius-round);background:var(--accent-soft);color:var(--accent);
260
- display:grid;place-items:center;font-size:var(--text-xs);font-weight:600;border:0;cursor:pointer;padding:0;
261
- }
262
- .sm-rail__user:focus-visible{outline:2px solid var(--rail-active-fg);outline-offset:1px;box-shadow:none}
263
- /* Flyout: 6px right of the rail, aligned with its item (the user menu aligns to the bottom).
216
+ .sm-navbar__logo{width:30px;height:auto;display:block;flex:none;transition:filter .15s}
217
+ .sm-navbar__brand:hover .sm-navbar__logo{filter:brightness(1.3)}
218
+ .sm-navbar__brand:focus-visible{outline:2px solid var(--navbar-active-fg);outline-offset:2px;box-shadow:none}
219
+ .sm-navbar__user{
220
+ width:32px;height:32px;border-radius:var(--radius-round);background:var(--accent-soft);color:var(--accent);
221
+ display:grid;place-items:center;font-size:var(--text-xs);font-weight:600;border:0;cursor:pointer;padding:0;transition:background .15s;
222
+ }
223
+ .sm-navbar__user:hover{background:color-mix(in srgb,var(--accent-soft),#fff 60%)}
224
+ .sm-navbar__user:focus-visible{outline:2px solid var(--navbar-active-fg);outline-offset:2px;box-shadow:none}
225
+ .sm-navbar__ext{width:13px;height:13px;flex:none;margin-left:12px;opacity:.45}
226
+ .sm-navbar__menu-item:hover .sm-navbar__ext,.sm-navbar__menu-item:focus-visible .sm-navbar__ext{opacity:.8}
227
+ .sm-navbar__menu-version{font-size:var(--text-xs);color:var(--text-subtle);padding:7px 10px 4px;margin-top:3px;border-top:1px solid var(--line)}
228
+ .sm-navbar__list{display:flex;align-items:center;gap:4px;margin:0;padding:0;list-style:none;min-width:0}
229
+ .sm-navbar__item{position:relative;list-style:none}
230
+ .sm-navbar__btn{
231
+ appearance:none;background:none;border:0;margin:0;height:var(--navbar-item-h);padding:0 12px;display:flex;align-items:center;gap:8px;
232
+ color:inherit;font:inherit;cursor:pointer;border-radius:var(--navbar-item-radius);text-decoration:none;white-space:nowrap;transition:background .12s,color .12s;
233
+ }
234
+ .sm-navbar__btn--icon{padding:0;width:var(--navbar-item-h);justify-content:center}
235
+ .sm-navbar__btn:hover{color:var(--navbar-active-fg);background:var(--navbar-hover-bg)}
236
+ .sm-navbar__btn:focus-visible{outline:2px solid var(--navbar-active-fg);outline-offset:-2px;box-shadow:none}
237
+ .sm-navbar__btn[aria-current="page"],.sm-navbar__btn[aria-expanded="true"]{color:var(--navbar-active-fg);background:var(--navbar-active-bg)}
238
+ .sm-navbar__btn svg{flex:none;stroke:currentColor;fill:none;stroke-linecap:round;stroke-linejoin:round}
239
+ .sm-navbar__chevron{margin-left:-2px;opacity:.8}
240
+ /* A collection's own title can be long: it ends in an ellipsis (2026-11-13). */
241
+ .sm-navbar__label{letter-spacing:.01em;white-space:nowrap;max-width:160px;overflow:hidden;text-overflow:ellipsis}
242
+ /* A narrow window keeps the icons and drops the labels; each item keeps its name for a screen reader and a tooltip. */
243
+ @media (max-width:820px){
244
+ .sm-navbar__list .sm-navbar__label,.sm-navbar__deploy .sm-navbar__label{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
245
+ .sm-navbar__btn{padding:0 10px}
246
+ }
247
+ /* The Deploy item (spec 2026-10-09 static mode §6): next to the avatar; the badge is the count waiting, the
248
+ lit item a build in flight, the warning item a build that has not landed or a site that did not answer. */
249
+ .sm-navbar__icon{position:relative;display:grid;place-items:center}
250
+ .sm-navbar__badge{position:absolute;top:-7px;right:-9px;min-width:16px;height:16px;padding:0 4px;border-radius:var(--radius-round);background:var(--accent);color:#fff;font-size:10px;font-weight:600;line-height:16px;text-align:center}
251
+ .sm-navbar__btn--lit,.sm-navbar__btn--lit:hover{background:var(--accent-soft);color:var(--accent);animation:sm-navbar-pulse 1.6s ease-in-out infinite}
252
+ .sm-navbar__btn--warn,.sm-navbar__btn--warn:hover{background:var(--warn-soft);color:var(--warn)}
253
+ @keyframes sm-navbar-pulse{0%,100%{opacity:1}50%{opacity:.55}}
254
+ .sm-navbar__menu-detail{font-size:var(--text-sm);color:var(--text);padding:6px 8px 4px;white-space:normal}
255
+ .sm-navbar__menu-note{font-size:var(--text-xs);color:var(--text-muted);padding:4px 8px 6px;white-space:normal}
256
+ .sm-navbar__menu-item:disabled{cursor:default;color:var(--text-muted)}
257
+ .sm-navbar__menu-item:disabled:hover{background:none}
258
+ /* A menu drops 6px below its item, aligned with its left edge (the end group's with its right edge).
264
259
  Menus float on --menu-bg (the sheet white in the neutral look, so they read over the frame). */
265
- .sm-rail__menu{
266
- position:absolute;left:calc(100% + 6px);top:0;z-index:20;width:168px;padding:6px;margin:0;list-style:none;
260
+ .sm-navbar__menu{
261
+ position:absolute;left:0;top:calc(100% + 6px);z-index:20;width:200px;padding:6px;margin:0;list-style:none;
267
262
  background:var(--menu-bg);border:var(--menu-border);border-radius:var(--radius-md);box-shadow:var(--shadow-pop);
268
- display:none;flex-direction:column;gap:1px;font-size:var(--text-md);font-weight:500;color:var(--text);
269
- }
270
- .sm-rail__menu[data-open="true"]{display:flex}
271
- .sm-rail__menu--up{top:auto;bottom:0}
272
- /* The Collections flyout opens on hover, so it is roomier (the names at --text-lg) and an invisible
273
- bridge spans the 6px gap so the pointer can cross from the rail without closing it. */
274
- .sm-rail__menu--brand{width:220px}
275
- .sm-rail__menu--collections{width:208px;padding:8px;gap:2px;font-size:var(--text-lg)}
276
- .sm-rail__menu--collections::before{content:"";position:absolute;left:-8px;top:0;bottom:0;width:8px}
277
- .sm-rail__menu--collections .sm-rail__menu-item{padding:9px 12px}
278
- .sm-rail__menu-title{font-size:9px;font-weight:600;letter-spacing:var(--tracking-label);text-transform:uppercase;color:var(--text-muted);padding:6px 8px 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
279
- .sm-rail__menu-item{
263
+ display:none;flex-direction:column;gap:1px;font-size:var(--text-md);font-weight:500;line-height:1.4;color:var(--text);
264
+ }
265
+ .sm-navbar__menu[data-open="true"]{display:flex}
266
+ .sm-navbar__menu--end{left:auto;right:0;width:240px}
267
+ .sm-navbar__menu--brand{width:240px}
268
+ /* An invisible bridge spans the 6px gap so the pointer can cross from Collections into its hover menu. */
269
+ .sm-navbar__menu--collections::before{content:"";position:absolute;left:0;right:0;top:-8px;height:8px}
270
+ .sm-navbar__menu-title{font-size:9px;font-weight:600;letter-spacing:var(--tracking-label);text-transform:uppercase;color:var(--text-muted);padding:6px 8px 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
271
+ .sm-navbar__menu-item{
280
272
  appearance:none;background:none;border:0;width:100%;display:flex;align-items:center;justify-content:space-between;
281
273
  padding:7px 10px;border-radius:var(--radius-sm);color:inherit;font:inherit;cursor:pointer;text-align:left;text-decoration:none;
282
274
  }
283
- .sm-rail__menu-item:hover,.sm-rail__menu-item:focus-visible{background:var(--hover);color:inherit;outline:0;box-shadow:none}
284
- .sm-rail__menu-item[aria-current="page"],.sm-rail__menu-item[aria-current="page"]:hover{background:var(--nav-active-bg);color:var(--nav-active-fg);font-weight:600}
275
+ .sm-navbar__menu-item:hover,.sm-navbar__menu-item:focus-visible{background:var(--hover);color:inherit;outline:0;box-shadow:none}
276
+ .sm-navbar__menu-item[aria-current="page"],.sm-navbar__menu-item[aria-current="page"]:hover{background:var(--nav-active-bg);color:var(--nav-active-fg);font-weight:600}
277
+ .sm-navbar__menu-label{display:flex;align-items:center;gap:10px;min-width:0}
278
+ .sm-navbar__menu-label>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
279
+ .sm-navbar__menu-label svg{flex:none;stroke:currentColor;fill:none}
280
+ /* Collections and ⋮ are dark (2026-11-14): they carry the bar's own items, so they take its colours. */
281
+ .sm-navbar__menu--dark{background:var(--navbar-menu-bg);color:var(--navbar-fg);border:0;padding:6px;gap:2px}
282
+ .sm-navbar__menu--dark .sm-navbar__menu-item{padding:9px 12px}
283
+ .sm-navbar__menu--dark .sm-navbar__menu-item:hover,.sm-navbar__menu--dark .sm-navbar__menu-item:focus-visible{background:var(--navbar-hover-bg);color:var(--navbar-active-fg)}
284
+ .sm-navbar__menu--dark .sm-navbar__menu-item[aria-current="page"],.sm-navbar__menu--dark .sm-navbar__menu-item[aria-current="page"]:hover{background:var(--navbar-active-bg);color:var(--navbar-active-fg)}
285
285
  .sm-when-light{display:var(--when-light);align-items:center;gap:8px}
286
286
  .sm-when-dark{display:var(--when-dark);align-items:center;gap:8px}
287
287
  /* .sm-nav-item survives as the inspector's link style (EditorView). */
@@ -488,6 +488,7 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
488
488
  /* Inline editing in the canvas (spec 2026-09-10). The outlines are drawn in
489
489
  the overlay from the bridge's field rects; the toolbar floats above the
490
490
  selection. */
491
+ .sm-dim{background:color-mix(in srgb,var(--canvas) 62%,transparent);pointer-events:none}
491
492
  .sm-fieldhover{outline:1px solid color-mix(in srgb,var(--selection) 55%,transparent);outline-offset:2px;border-radius:2px;pointer-events:none}
492
493
  .sm-fieldedit{outline:1.5px solid var(--selection);outline-offset:3px;border-radius:2px;pointer-events:none}
493
494
  .sm-floatbar{
@@ -800,6 +801,13 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
800
801
  min-width:0;min-height:0;overflow:hidden;
801
802
  }
802
803
  .sm-canvas__scroll{flex:1;min-height:0;overflow:auto;padding:var(--canvas-pad)}
804
+ /* A global item's canvas (spec 2026-11-14): a row above the frame — where the item is used, and which page shows it. */
805
+ .sm-globalcanvas{display:flex;flex-direction:column;min-width:0;min-height:0}
806
+ .sm-globalcanvas>.sm-canvas{flex:1}
807
+ .sm-globalcanvas__bar{flex:none;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:0 4px 8px;min-height:28px}
808
+ .sm-globalcanvas__bar .sm-hint{margin:0}
809
+ .sm-globalcanvas__pick{display:flex;align-items:center;gap:8px}
810
+ .sm-globalcanvas__pick .sm-select{width:auto;height:30px;padding:0 10px}
803
811
  .sm-page{
804
812
  position:relative;background:var(--page-bg);border-radius:var(--page-radius);
805
813
  box-shadow:var(--shadow-page);min-height:100%;
@@ -3,7 +3,7 @@
3
3
  // config exactly as the service's site computes them (site.ts siteStatics), so a cache key never disagrees across
4
4
  // the wire. A document's tags ride on the hit (SiteHit.tags), where the read layer takes them.
5
5
  import { countRead } from "./read-counter.js";
6
- import { pushBody, pushSchema } from "./push.js";
6
+ import { pushBody, pushLine, pushSchema } from "./push.js";
7
7
  import { siteOptions, siteStatics, } from "./site.js";
8
8
  /** The content API refused a read: its status, its code (`unauthorized`, `not_found`, `validation`, `internal`) and its message. */
9
9
  export class SmoodlyApiError extends Error {
@@ -24,9 +24,11 @@ export function createSmoodlyApi(options) {
24
24
  const localeOf = (o) => o?.locale ?? statics.locales.default;
25
25
  const env = typeof process === "undefined" ? {} : process.env;
26
26
  const push = options.push ?? (env.NODE_ENV === "development" ? pushBody(options.config, env, "live") : false);
27
- /** One push per client, awaited by every call until it settles; its failure is logged, never thrown. */
27
+ /** One push per client, awaited by every call until it settles; its failure is logged, never thrown. What landed is
28
+ * said once, with the kind of space it reached (spec 2026-11-13 production first and the preview copy §7): a dev
29
+ * server pushes its schema to the space it reads, so the developer must see when that space is production. */
28
30
  let pushed = null;
29
- const pushOnce = (body) => (pushed ?? (pushed = pushSchema({ apiUrl, spaceId: options.spaceId, key: options.key, fetch: doFetch }, body).then(() => undefined, (e) => console.error(`smoodly: the schema push failed — ${e instanceof Error ? e.message : String(e)}; the site runs against the schema the space has.`))));
31
+ const pushOnce = (body) => (pushed ?? (pushed = pushSchema({ apiUrl, spaceId: options.spaceId, key: options.key, fetch: doFetch }, body).then((result) => { const line = pushLine(options.spaceId, result); console.log(`smoodly: ${line[0].toLowerCase()}${line.slice(1)}`); }, (e) => console.error(`smoodly: the schema push failed — ${e instanceof Error ? e.message : String(e)}; the site runs against the schema the space has.`))));
30
32
  async function call(path, params) {
31
33
  if (push)
32
34
  await pushOnce(push);
@@ -11,7 +11,7 @@ import { apiTransport, createSmoodlyContent } from "../content/content-api.js";
11
11
  import { markerJson, MARKER_PATH } from "../deploy-state.js";
12
12
  import { DEFAULT_API_URL, apiEnv, siteUrl } from "../env.js";
13
13
  import { createSmoodlyApi } from "../api-client.js";
14
- import { pushBody, pushSchema } from "../push.js";
14
+ import { pushBody, pushLine, pushSchema } from "../push.js";
15
15
  import { loadSmoodlyEnv } from "./env-files.js";
16
16
  import { astroRouting, localeHref } from "./locale-paths.js";
17
17
  import { PREVIEW_BASE } from "./preview.js";
@@ -159,8 +159,8 @@ export default function smoodly(options = {}) {
159
159
  astroRouting(i18n, loaded.locales, loaded.routing);
160
160
  try {
161
161
  const env = apiEnv(process.env);
162
- const { version, changed } = await pushSchema(env, pushBody(loaded, process.env, output === "static" ? "static" : "live"));
163
- logger.info(changed ? `smoodly: pushed schema version ${version} to ${env.spaceId}.` : `smoodly: schema unchanged (version ${version}).`);
162
+ const line = pushLine(env.spaceId, await pushSchema(env, pushBody(loaded, process.env, output === "static" ? "static" : "live")));
163
+ logger.info(`smoodly: ${line[0].toLowerCase()}${line.slice(1)}`);
164
164
  }
165
165
  catch (e) {
166
166
  logger.warn(`smoodly: the schema push failed — ${e instanceof Error ? e.message : String(e)}; the site runs against the schema the space has.`);
package/dist/cli/push.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // except when npm runs the command as a build's `prebuild`: a push never fails a build (the spec's rule), so there it
5
5
  // says what failed and exits 0, and the site runs against the schema the space has.
6
6
  import { apiEnv } from "../env.js";
7
- import { pushBody, pushSchema } from "../push.js";
7
+ import { pushBody, pushLine, pushSchema } from "../push.js";
8
8
  import { loadAppEnv } from "./app-env.js";
9
9
  import { loadConfig } from "./load-config.js";
10
10
  export async function runPush(_args, out, deps = {}) {
@@ -15,8 +15,7 @@ export async function runPush(_args, out, deps = {}) {
15
15
  const config = await loadConfig(cwd);
16
16
  const { apiUrl, spaceId, key } = apiEnv(env);
17
17
  const body = pushBody(config, env, env.SMOODLY_STATIC === "1" ? "static" : "live");
18
- const { version, changed } = await pushSchema({ apiUrl, spaceId, key, fetch: deps.fetch }, body);
19
- out(changed ? `Pushed schema version ${version} to ${spaceId}.` : `Schema unchanged (version ${version}).`);
18
+ out(pushLine(spaceId, await pushSchema({ apiUrl, spaceId, key, fetch: deps.fetch }, body)));
20
19
  return 0;
21
20
  }
22
21
  catch (e) {
package/dist/env.js CHANGED
@@ -11,8 +11,9 @@ const ADMIN_NEEDS = SITE_NEEDS;
11
11
  const NEEDS = { site: SITE_NEEDS, admin: ADMIN_NEEDS };
12
12
  const isHttp = (v) => /^https?:\/\/[^/\s]+/.test(v);
13
13
  const DEPLOYING = "https://smoodly.io/docs/getting-started/deploying";
14
- /** A Smoodly key: its kind, the space it belongs to, the random part and the stamp (spec 2026-10-16 the control plane §1). */
15
- const SMOODLY_KEY = (kind) => new RegExp(`^${kind}_sp_(eu_)?[a-z2-7]{20}_[a-z2-7]{32}_[a-z2-7]{16}$`);
14
+ /** A Smoodly key: its kind, the space it belongs to (a preview space's id carries `preview_`, spec 2026-11-13 production
15
+ * first and the preview copy §8), the random part and the stamp (spec 2026-10-16 the control plane §1). */
16
+ const SMOODLY_KEY = (kind) => new RegExp(`^${kind}_sp_(eu_)?(preview_)?[a-z2-7]{20}_[a-z2-7]{32}_[a-z2-7]{16}$`);
16
17
  /** The live site and its admin read over the API with its address, the space and the secret key (spec 2026-10-16 the
17
18
  * control plane §5). A key in the wrong variable or of no known form is named here, not by a failed read later. */
18
19
  export function resolveSmoodlyEnv(env, need = "site") {
package/dist/push.d.ts CHANGED
@@ -23,6 +23,13 @@ export type PushApi = {
23
23
  key: string;
24
24
  fetch?: typeof fetch;
25
25
  };
26
+ /** What a push says when it landed: the version, and which kind of space it reached, read from the id (spec 2026-11-13
27
+ * production first and the preview copy §7), so a developer who meant the preview space sees at once that the machine
28
+ * reads production. */
29
+ export declare function pushLine(spaceId: string, result: {
30
+ version: number;
31
+ changed: boolean;
32
+ }): string;
26
33
  /** One push: the space's version of the registry, and whether this push changed it. A refusal throws `SmoodlyApiError`. */
27
34
  export declare function pushSchema(api: PushApi, body: PushBody): Promise<{
28
35
  version: number;
package/dist/push.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // delivery mode, and the package version, at `POST /v1/spaces/:space/schema` under the secret key. `smoodly push`, Astro's
3
3
  // build hook, the Next templates' prebuild and the API client in development all send this one body. The space keeps
4
4
  // the registry, versions it and purges its pages when it changed; a failed push never fails a build.
5
+ import { spaceKind } from "./space-kind.js";
5
6
  import { SmoodlyApiError } from "./api-client.js";
6
7
  import { serializeRegistry } from "./registry-json.js";
7
8
  import { SMOODLY_VERSION } from "./version.js";
@@ -29,6 +30,13 @@ export function siteInfo(config, env, mode) {
29
30
  export function pushBody(config, env, mode) {
30
31
  return { registry: serializeRegistry(config), site: siteInfo(config, env, mode), package: SMOODLY_VERSION };
31
32
  }
33
+ /** What a push says when it landed: the version, and which kind of space it reached, read from the id (spec 2026-11-13
34
+ * production first and the preview copy §7), so a developer who meant the preview space sees at once that the machine
35
+ * reads production. */
36
+ export function pushLine(spaceId, result) {
37
+ const where = `the ${spaceKind(spaceId)} space ${spaceId}`;
38
+ return result.changed ? `Pushed schema version ${result.version} to ${where}.` : `Schema unchanged (version ${result.version}) in ${where}.`;
39
+ }
32
40
  /** One push: the space's version of the registry, and whether this push changed it. A refusal throws `SmoodlyApiError`. */
33
41
  export async function pushSchema(api, body) {
34
42
  const apiUrl = api.apiUrl.replace(/\/+$/, "");
@@ -0,0 +1,2 @@
1
+ export type SpaceKind = "production" | "preview";
2
+ export declare const spaceKind: (spaceId: string) => SpaceKind;
@@ -0,0 +1 @@
1
+ export const spaceKind = (spaceId) => (/^sp_(eu_)?preview_/.test(spaceId) ? "preview" : "production");
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SMOODLY_VERSION = "0.0.60";
1
+ export declare const SMOODLY_VERSION = "0.0.62";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/write-version.mjs from package.json — do not edit.
2
- export const SMOODLY_VERSION = "0.0.60";
2
+ export const SMOODLY_VERSION = "0.0.62";
@@ -0,0 +1,57 @@
1
+ # Defining a collection
2
+
3
+ A collection is a content type with a fixed shape and many entries: posts, people, products. You declare its fields once; editors get a list, a form and publishing for it.
4
+
5
+ ## The schema
6
+
7
+ `smoodly.collection()` takes a name, a title and the fields of one entry:
8
+
9
+ File: `smoodly/collections.ts`
10
+
11
+ ```ts
12
+ import { f, smoodly } from "smoodly";
13
+ // …
14
+ export const people = smoodly.collection({
15
+ name: "people",
16
+ title: "People",
17
+ titleField: "name",
18
+ order: "manual",
19
+ fields: {
20
+ name: f.text(),
21
+ role: f.text().localized(),
22
+ },
23
+ });
24
+ ```
25
+
26
+ | Name | Description |
27
+ | --- | --- |
28
+ | `name` | What references and queries use. Keep it stable once entries exist. global, asset and media are reserved. |
29
+ | `title` | The label in the admin's navigation and list. |
30
+ | `titleField` | The field whose value names an entry in lists and pickers. Without one, or while it is empty, an entry reads Untitled. |
31
+ | `fields` | Any builders. A field named slug gives entries a URL part; .localized() gives a field a value per language. |
32
+ | `path` | Serves each entry at its own URL under this segment. See Entry pages. |
33
+ | `order` | How the list and queries sort. See Ordering. |
34
+ | `admin.showInNav` | true gives the collection its own item in the admin's main navigation, after Pages, instead of a line in the Collections menu. Default false. |
35
+ | `seo` | Adds the SEO & Social panel to the entry form; `{ description, image }` names the fields that fill an empty description and social image. See SEO. |
36
+
37
+ Register the export in the `collections` list of `smoodly/collections.ts`, which the config registers. Every collection stores its entries in the same tables, so the admin lists a new one at once. Smoodly's service indexes the new collection's lists itself: there is nothing to migrate.
38
+
39
+ ## Entries in the admin
40
+
41
+ The *Collections* item in the admin's top bar opens a menu of every collection. A collection editors open all day can sit on the bar itself: add `admin: { showInNav: true }` to its schema and it appears after *Pages* under its title, and leaves the menu. A collection's list shows its entries with their title, status and when they changed, 20 to a page; *Previous* and *Next* under the rows move through a longer list, and *Per page* shows 100 or 500 instead, a choice the browser remembers. The box above the rows filters by the collection's `titleField`: it finds the entries whose title holds the text, in any language, and searches the whole collection, not the page on screen. A collection without a `titleField` has no filter. While a filter is on, a hand-ordered list does not drag. *+ New entry* creates an empty draft in the selected language and opens the form at once. The form autosaves, and carries the same *Publish* control as a page, so an entry is published per language like a page. *History* and *Discard changes* work the same way.
42
+
43
+ An entry cannot be deleted while a page, a section or another entry references it. The message says how many, and the media library shows the same for assets.
44
+
45
+ ## Shared and localized
46
+
47
+ A field's value is shared across languages unless it chains `.localized()`. In the starter's people, a person's name is the same everywhere and the role is translated. The form marks shared fields with a globe-off icon on a multilingual site, and the slug is always per language. [Localized fields](../languages/localized-fields.md) has the rules.
48
+
49
+ ## Where entries appear
50
+
51
+ Three ways, which the next pages cover:
52
+
53
+ - **Referenced** from a section or another entry with `f.ref()` or `f.refList()`: the starter's *People* section picks team members by hand. See [Reference and tags](../fields/reference-and-tags.md).
54
+ - **At their own URL**, when the collection has a `path` and an [entry page](../collections/entry-pages.md).
55
+ - **Queried** from your code with `q()`, for an index grid or a related list. See [Queries](../collections/queries.md).
56
+
57
+ Next, [Entry pages](../collections/entry-pages.md).