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.
- package/README.md +3 -1
- package/dist/admin/editor/CanvasOverlay.d.ts +4 -1
- package/dist/admin/editor/CanvasOverlay.js +3 -3
- package/dist/admin/editor/GlobalCanvas.d.ts +27 -0
- package/dist/admin/editor/GlobalCanvas.js +159 -0
- package/dist/admin/editor/InsertPicker.js +1 -1
- package/dist/admin/files.generated.js +1 -1
- package/dist/admin/serialize.d.ts +2 -0
- package/dist/admin/serialize.js +1 -0
- package/dist/admin/shell/AdminApp.js +5 -5
- package/dist/admin/shell/GlobalForm.js +80 -7
- package/dist/admin/shell/GlobalList.js +1 -1
- package/dist/admin/shell/{IconRail.d.ts → NavBar.d.ts} +3 -3
- package/dist/admin/shell/NavBar.js +120 -0
- package/dist/admin/shell/global-canvas.d.ts +40 -0
- package/dist/admin/shell/global-canvas.js +60 -0
- package/dist/admin/shell/health-list.js +1 -1
- package/dist/admin/ui/primitives.js +1 -1
- package/dist/admin/ui/theme.js +99 -91
- package/dist/api-client.js +5 -3
- package/dist/astro/index.js +3 -3
- package/dist/cli/push.js +2 -3
- package/dist/env.js +3 -2
- package/dist/push.d.ts +7 -0
- package/dist/push.js +8 -0
- package/dist/space-kind.d.ts +2 -0
- package/dist/space-kind.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/collections/defining.md +57 -0
- package/docs/collections/entry-pages.md +168 -0
- package/docs/collections/ordering.md +47 -0
- package/docs/collections/queries.md +173 -0
- package/docs/content-modeling/changing-schemas.md +79 -0
- package/docs/content-modeling/custom-section.md +94 -0
- package/docs/content-modeling/marks.md +97 -0
- package/docs/content-modeling/overview.md +55 -0
- package/docs/content-modeling/page-types.md +237 -0
- package/docs/content-modeling/sections.md +264 -0
- package/docs/content-modeling/styles-and-settings.md +184 -0
- package/docs/content-modeling/zones.md +80 -0
- package/docs/fields/choices.md +42 -0
- package/docs/fields/conditional.md +72 -0
- package/docs/fields/link.md +31 -0
- package/docs/fields/list-and-object.md +58 -0
- package/docs/fields/media.md +103 -0
- package/docs/fields/overview.md +56 -0
- package/docs/fields/reference-and-tags.md +62 -0
- package/docs/fields/richtext.md +168 -0
- package/docs/fields/text.md +152 -0
- package/docs/frameworks/astro.md +110 -0
- package/docs/frameworks/nextjs.md +147 -0
- package/docs/getting-started/content-from-code.md +263 -0
- package/docs/getting-started/deploying.md +131 -0
- package/docs/getting-started/export-and-import.md +73 -0
- package/docs/getting-started/first-page.md +98 -0
- package/docs/getting-started/install.md +108 -0
- package/docs/getting-started/keys.md +40 -0
- package/docs/getting-started/project-structure.md +142 -0
- package/docs/getting-started/working-together.md +42 -0
- package/docs/globals/defining.md +99 -0
- package/docs/globals/header-and-footer.md +117 -0
- package/docs/index.md +83 -0
- package/docs/languages/language-switcher.md +103 -0
- package/docs/languages/locales.md +119 -0
- package/docs/languages/localized-fields.md +61 -0
- package/docs/media/images-and-crops.md +131 -0
- package/docs/media/library.md +40 -0
- package/docs/pages/drafts-and-publishing.md +44 -0
- package/docs/pages/page-tree.md +51 -0
- package/docs/pages/paths-and-slugs.md +74 -0
- package/docs/pages/seo.md +126 -0
- package/docs/pages/version-history.md +23 -0
- package/docs/reference/upgrading.md +312 -0
- package/package.json +4 -3
- package/dist/admin/shell/IconRail.js +0 -109
package/dist/admin/ui/theme.js
CHANGED
|
@@ -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
|
|
38
|
-
--
|
|
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
|
|
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
|
|
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
|
-
--
|
|
137
|
-
/* The frame is 8px
|
|
138
|
-
--frame-pad:8px; --
|
|
139
|
-
--
|
|
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
|
-
/*
|
|
186
|
-
components/AppShell.html): navy,
|
|
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
|
|
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-
|
|
193
|
-
padding:
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
211
|
-
|
|
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-
|
|
215
|
-
.sm-
|
|
216
|
-
.sm-
|
|
217
|
-
.sm-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
.sm-
|
|
222
|
-
.sm-
|
|
223
|
-
.sm-
|
|
224
|
-
.sm-
|
|
225
|
-
.sm-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
.sm-
|
|
233
|
-
.sm-
|
|
234
|
-
.sm-
|
|
235
|
-
.sm-
|
|
236
|
-
.sm-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
.sm-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
.sm-
|
|
250
|
-
.sm-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
.sm-
|
|
254
|
-
.sm-
|
|
255
|
-
.sm-
|
|
256
|
-
.
|
|
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-
|
|
266
|
-
position:absolute;left:calc(100% + 6px);
|
|
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-
|
|
271
|
-
.sm-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
.sm-
|
|
275
|
-
.sm-
|
|
276
|
-
.sm-
|
|
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-
|
|
284
|
-
.sm-
|
|
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%;
|
package/dist/api-client.js
CHANGED
|
@@ -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(() =>
|
|
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);
|
package/dist/astro/index.js
CHANGED
|
@@ -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
|
|
163
|
-
logger.info(
|
|
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
|
-
|
|
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
|
|
15
|
-
|
|
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 @@
|
|
|
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.
|
|
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.
|
|
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).
|