smoodly 0.0.61 → 0.0.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/client-ops.js +1 -1
- package/dist/admin/drift.js +3 -1
- package/dist/admin/editor/CanvasOverlay.d.ts +4 -1
- package/dist/admin/editor/CanvasOverlay.js +3 -3
- package/dist/admin/editor/EditorView.js +65 -9
- package/dist/admin/editor/GlobalCanvas.d.ts +27 -0
- package/dist/admin/editor/GlobalCanvas.js +159 -0
- package/dist/admin/editor/InsertPicker.d.ts +8 -27
- package/dist/admin/editor/InsertPicker.js +56 -47
- package/dist/admin/editor/SamplePreview.d.ts +7 -0
- package/dist/admin/editor/SamplePreview.js +86 -0
- package/dist/admin/editor/SectionIcon.d.ts +4 -0
- package/dist/admin/editor/SectionIcon.js +8 -0
- package/dist/admin/editor/StructureView.d.ts +2 -2
- package/dist/admin/editor/StructureView.js +2 -1
- package/dist/admin/editor/canvas-address.d.ts +2 -0
- package/dist/admin/editor/canvas-address.js +3 -1
- package/dist/admin/editor/picker-rows.d.ts +33 -0
- package/dist/admin/editor/picker-rows.js +38 -0
- package/dist/admin/editor/sample-frame.d.ts +36 -0
- package/dist/admin/editor/sample-frame.js +58 -0
- package/dist/admin/editor/sample-picks.d.ts +34 -0
- package/dist/admin/editor/sample-picks.js +49 -0
- package/dist/admin/editor/view-memory.d.ts +5 -0
- package/dist/admin/editor/view-memory.js +19 -0
- package/dist/admin/files.generated.js +1 -1
- package/dist/admin/forms/MediaWidget.d.ts +1 -1
- package/dist/admin/forms/MediaWidget.js +10 -2
- package/dist/admin/ops.d.ts +5 -0
- 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/PagesList.js +4 -4
- package/dist/admin/shell/console-links.js +1 -1
- 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/tree-ops.d.ts +11 -4
- package/dist/admin/tree-ops.js +28 -17
- package/dist/admin/ui/primitives.js +1 -1
- package/dist/admin/ui/theme.js +128 -101
- package/dist/admin/validate.js +3 -2
- package/dist/api-client.js +12 -2
- package/dist/astro/middleware.js +2 -0
- package/dist/astro/shell.d.ts +3 -0
- package/dist/astro/shell.js +8 -1
- package/dist/canvas-marker.d.ts +5 -0
- package/dist/canvas-marker.js +18 -6
- package/dist/conditions.js +5 -2
- package/dist/media.d.ts +15 -2
- package/dist/media.js +68 -10
- package/dist/next/page-renderer.js +12 -5
- package/dist/resolve.d.ts +6 -1
- package/dist/resolve.js +7 -7
- package/dist/sample-fill.d.ts +40 -0
- package/dist/sample-fill.js +145 -0
- package/dist/sample-tree.d.ts +30 -0
- package/dist/sample-tree.js +76 -0
- package/dist/site.d.ts +13 -0
- package/dist/site.js +10 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/collections/defining.md +2 -2
- package/docs/content-modeling/sections.md +19 -3
- package/docs/content-modeling/zones.md +3 -3
- package/docs/getting-started/deploying.md +1 -1
- package/docs/getting-started/working-together.md +1 -1
- package/docs/globals/defining.md +1 -1
- package/docs/globals/header-and-footer.md +2 -2
- package/docs/media/library.md +1 -1
- package/package.json +1 -1
- package/dist/admin/shell/IconRail.js +0 -109
|
@@ -2,7 +2,7 @@ import { SEVERITY, groupKey } from "../drift.js";
|
|
|
2
2
|
import { driftHeadline } from "../drift-copy.js";
|
|
3
3
|
/** A group shows this many rows, then "Show all N". */
|
|
4
4
|
export const GROUP_PREVIEW = 10;
|
|
5
|
-
const kindLabel = (doc, registry) => doc.kind === "page" ? "Page" : doc.kind === "global" ? "
|
|
5
|
+
const kindLabel = (doc, registry) => doc.kind === "page" ? "Page" : doc.kind === "global" ? "Globals" : registry.collections.find((c) => c.name === doc.collection)?.title ?? doc.collection ?? "";
|
|
6
6
|
const href = (doc, locale) => {
|
|
7
7
|
const q = `?locale=${encodeURIComponent(locale)}`;
|
|
8
8
|
if (doc.kind === "page")
|
package/dist/admin/tree-ops.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PageTree, TreeNode } from "../page-context.ts";
|
|
2
2
|
import type { ZonePolicy } from "../zones.ts";
|
|
3
3
|
import type { AdminSection, AdminGlobal } from "./serialize.ts";
|
|
4
|
+
import { type FreshOptions } from "../sample-fill.ts";
|
|
4
5
|
export declare function zoneOf(tree: PageTree, id: string): string | null;
|
|
5
6
|
export declare function findNode(tree: PageTree, id: string): TreeNode | null;
|
|
6
7
|
export declare function insertNode(tree: PageTree, zone: string, index: number, node: TreeNode): PageTree;
|
|
@@ -24,15 +25,21 @@ export declare function allowedGlobals(policy: ZonePolicy | undefined, globals:
|
|
|
24
25
|
/** A page places a visual item as this node (spec 2026-09-07 §6): `item`
|
|
25
26
|
* names the registration, `ref` is the item's row id. */
|
|
26
27
|
export declare function placementNode(item: AdminGlobal, ref: string, id: string): TreeNode;
|
|
27
|
-
/**
|
|
28
|
-
|
|
28
|
+
/** The one builder of a fresh node (spec 2026-11-14 filled samples): the insert, a locked zone, a richtext element and
|
|
29
|
+
* the picker's sample tree all take it from here, so the picture is the insert. Per field: the `sample` (taken whole,
|
|
30
|
+
* nothing generated on it or inside it), then the `default`, a required choice's first option and a boolean's false,
|
|
31
|
+
* then the generated value (sample-fill.ts), then the empty value by type. `empty` skips the sample and the generated
|
|
32
|
+
* value. Settings are choices: sampled or defaulted, never generated.
|
|
33
|
+
* Also fills an element node (an `AdminElement` is a section shape plus contexts). Never seeds `styles` or `css`: Default stores nothing (spec 2026-09-14 §2). */
|
|
34
|
+
export declare function nodeFromSample(section: Pick<AdminSection, "name" | "fields" | "settings" | "sample">, id: string, options?: FreshOptions): TreeNode;
|
|
29
35
|
/** A locked zone holds exactly one node the editor can never insert or
|
|
30
36
|
* remove — so someone has to put it there. This does, from the component's
|
|
31
37
|
* sample (or, for a zone bound to a global item, a placement), for every
|
|
32
38
|
* locked zone that is empty or missing. Runs when the editor loads a draft,
|
|
33
39
|
* which also covers pages that predate a locked zone being added to their
|
|
34
|
-
* schema. Returns the same tree when nothing changed.
|
|
35
|
-
|
|
40
|
+
* schema. Returns the same tree when nothing changed. `fresh` carries the
|
|
41
|
+
* space's reference picks and whether the service takes an image placeholder. */
|
|
42
|
+
export declare function fillLockedZones(tree: PageTree, policies: Record<string, ZonePolicy>, sections: AdminSection[], globalIds?: Record<string, string>, fresh?: FreshOptions): PageTree;
|
|
36
43
|
export declare function newNodeId(type: string): string;
|
|
37
44
|
/** A page-level value — the template's page fields or the SEO meta (spec 2026-09-10 page fields §5). */
|
|
38
45
|
export declare function patchPage(tree: PageTree, ns: "fields" | "meta", key: string, value: unknown): PageTree;
|
package/dist/admin/tree-ops.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GLOBAL_NODE_TYPE } from "../global.js";
|
|
2
2
|
import { setAtPath, unsetAtPath } from "./field-paths.js";
|
|
3
|
-
import {
|
|
3
|
+
import { firstChoice, generated, starting } from "../sample-fill.js";
|
|
4
4
|
export function zoneOf(tree, id) {
|
|
5
5
|
for (const [zone, nodes] of Object.entries(tree.zones)) {
|
|
6
6
|
if (nodes.some((n) => n.id === id))
|
|
@@ -119,32 +119,41 @@ export function placementNode(item, ref, id) {
|
|
|
119
119
|
/** The empty value a fresh node's field starts at, by descriptor type.
|
|
120
120
|
* Shape matters: a list-valued field seeded with "" is a type lie that
|
|
121
121
|
* crashes the first section view that maps over it. */
|
|
122
|
-
const EMPTY_BY_TYPE = { refList: [], object: {} };
|
|
122
|
+
const EMPTY_BY_TYPE = { refList: () => [], list: () => [], tags: () => [], checkboxes: () => [], object: () => ({}) };
|
|
123
123
|
/** Media/number fields have no honest empty value — "" renders as a
|
|
124
|
-
* broken-image icon or a stray zero, worse than nothing at all. With
|
|
125
|
-
*
|
|
124
|
+
* broken-image icon or a stray zero, worse than nothing at all. With
|
|
125
|
+
* nothing to start from, leave the key out of `fields` entirely rather than seed a lie. */
|
|
126
126
|
const ABSENT_WHEN_UNSAMPLED = new Set(["image", "video", "file", "number"]);
|
|
127
|
-
/**
|
|
128
|
-
*
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
/** What the section's `sample` sets for a key: an empty array counts as left out (spec 2026-11-14 filled samples),
|
|
128
|
+
* since "start with nothing" for a list is what leaving it out already means. */
|
|
129
|
+
const sampledOf = (sample, key) => {
|
|
130
|
+
const v = sample?.[key];
|
|
131
|
+
return Array.isArray(v) && v.length === 0 ? undefined : v;
|
|
132
|
+
};
|
|
133
|
+
/** The one builder of a fresh node (spec 2026-11-14 filled samples): the insert, a locked zone, a richtext element and
|
|
134
|
+
* the picker's sample tree all take it from here, so the picture is the insert. Per field: the `sample` (taken whole,
|
|
135
|
+
* nothing generated on it or inside it), then the `default`, a required choice's first option and a boolean's false,
|
|
136
|
+
* then the generated value (sample-fill.ts), then the empty value by type. `empty` skips the sample and the generated
|
|
137
|
+
* value. Settings are choices: sampled or defaulted, never generated.
|
|
138
|
+
* Also fills an element node (an `AdminElement` is a section shape plus contexts). Never seeds `styles` or `css`: Default stores nothing (spec 2026-09-14 §2). */
|
|
139
|
+
export function nodeFromSample(section, id, options = {}) {
|
|
132
140
|
const fields = {};
|
|
133
141
|
const settings = {};
|
|
142
|
+
const sample = options.empty ? undefined : section.sample;
|
|
134
143
|
for (const [key, d] of Object.entries(section.fields)) {
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
fields[key] =
|
|
144
|
+
const value = sampledOf(sample, key) ?? starting(d) ?? (options.empty ? undefined : generated(key, d, options));
|
|
145
|
+
if (value !== undefined)
|
|
146
|
+
fields[key] = value;
|
|
138
147
|
else if (ABSENT_WHEN_UNSAMPLED.has(d.type))
|
|
139
148
|
continue;
|
|
140
149
|
// An optional object with no sample is absent: `{}` would read as present, its members required at publish.
|
|
141
150
|
else if (d.type === "object" && d.optional === true)
|
|
142
151
|
continue;
|
|
143
152
|
else
|
|
144
|
-
fields[key] = EMPTY_BY_TYPE[d.type] ?? "";
|
|
153
|
+
fields[key] = EMPTY_BY_TYPE[d.type]?.() ?? "";
|
|
145
154
|
}
|
|
146
155
|
for (const [key, d] of Object.entries(section.settings)) {
|
|
147
|
-
const v =
|
|
156
|
+
const v = sampledOf(sample, key) ?? d.default ?? firstChoice(d);
|
|
148
157
|
if (v !== undefined)
|
|
149
158
|
settings[key] = v;
|
|
150
159
|
}
|
|
@@ -155,8 +164,9 @@ export function nodeFromSample(section, id) {
|
|
|
155
164
|
* sample (or, for a zone bound to a global item, a placement), for every
|
|
156
165
|
* locked zone that is empty or missing. Runs when the editor loads a draft,
|
|
157
166
|
* which also covers pages that predate a locked zone being added to their
|
|
158
|
-
* schema. Returns the same tree when nothing changed.
|
|
159
|
-
|
|
167
|
+
* schema. Returns the same tree when nothing changed. `fresh` carries the
|
|
168
|
+
* space's reference picks and whether the service takes an image placeholder. */
|
|
169
|
+
export function fillLockedZones(tree, policies, sections, globalIds = {}, fresh) {
|
|
160
170
|
let next = null;
|
|
161
171
|
for (const [zone, policy] of Object.entries(policies)) {
|
|
162
172
|
if (policy.kind !== "locked")
|
|
@@ -177,7 +187,8 @@ export function fillLockedZones(tree, policies, sections, globalIds = {}) {
|
|
|
177
187
|
const section = sections.find((s) => s.name === policy.component);
|
|
178
188
|
if (!section)
|
|
179
189
|
continue;
|
|
180
|
-
node
|
|
190
|
+
// Always the filled node (spec 2026-11-14 filled samples): a locked zone has no picker and no switch.
|
|
191
|
+
node = nodeFromSample(section, newNodeId(section.name), { ...fresh, empty: false });
|
|
181
192
|
}
|
|
182
193
|
next ?? (next = { ...tree, zones: { ...tree.zones } });
|
|
183
194
|
next.zones[zone] = [node];
|
|
@@ -40,7 +40,7 @@ export function ThemeToggle() {
|
|
|
40
40
|
}
|
|
41
41
|
catch { /* not remembered */ }
|
|
42
42
|
};
|
|
43
|
-
return (_jsxs("button", { type: "button", role: "menuitem", className: "sm-
|
|
43
|
+
return (_jsxs("button", { type: "button", role: "menuitem", className: "sm-navbar__menu-item", onClick: toggle, children: [_jsxs("span", { className: "sm-when-light", children: [_jsx(Moon, { ...ICON }), "Dark mode"] }), _jsxs("span", { className: "sm-when-dark", children: [_jsx(Sun, { ...ICON }), "Light mode"] })] }));
|
|
44
44
|
}
|
|
45
45
|
export function Button({ variant = "primary", size = "md", className, ...rest }) {
|
|
46
46
|
return (_jsx("button", { type: "button", className: cx("sm-btn", `sm-btn--${variant}`, size === "lg" && "sm-btn--lg", size === "sm" && "sm-btn--sm", className), ...rest }));
|
package/dist/admin/ui/theme.js
CHANGED
|
@@ -26,7 +26,8 @@ const TOKENS_LIGHT = `
|
|
|
26
26
|
/* The shell: navy frame, white sheet, grey tiles on the sheet, white fields on the tiles. */
|
|
27
27
|
--frame:#151727; --sheet:${SHEET_LIGHT};
|
|
28
28
|
--bg:var(--sheet); --canvas:#ECEDF1; --surface:#F6F6F6; --surface-form:#F6F6F6; --surface-inset:#FFFFFF;
|
|
29
|
-
--menu-bg:#FFFFFF;
|
|
29
|
+
--menu-bg:#FFFFFF; --menu-hover:var(--hover);
|
|
30
|
+
--avatar-hover:color-mix(in srgb,var(--accent-soft),#fff 60%);
|
|
30
31
|
--input-bg:#FFFFFF; --input-hover-bg:#FFFFFF; --input-focus-bg:#FFFFFF; --input-border:var(--line);
|
|
31
32
|
--line:#E4E5EA; --line-dashed:#CFD1D9; --line-soft:#ECEDF1;
|
|
32
33
|
--text:#151727; --text-secondary:#454A5C; --text-muted:#6B6F80; --text-subtle:#9A9EAB; --text-disabled:#C5C8D2;
|
|
@@ -34,8 +35,8 @@ const TOKENS_LIGHT = `
|
|
|
34
35
|
--selection-global:#C4620C;
|
|
35
36
|
--hover:rgba(21,23,39,.05); --press:rgba(21,23,39,.09); --control-hover:#ECEDF1; --canvas-ground:#EDEDED;
|
|
36
37
|
--btn-secondary-bg:var(--surface); --btn-secondary-hover:#ECEDF1;
|
|
37
|
-
/* The
|
|
38
|
-
--
|
|
38
|
+
/* 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. */
|
|
39
|
+
--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
40
|
--bar-bg:transparent; --bar-border:0; --menu-border:0; --card-divider:0;
|
|
40
41
|
--card-title-size:var(--text-md); --card-title-color:var(--text);
|
|
41
42
|
/* In-card segmented (tabs, the span bar): white track, the chosen option in the tile grey. */
|
|
@@ -52,7 +53,7 @@ const TOKENS_LIGHT = `
|
|
|
52
53
|
dark mode that washed out over the white page, 2026-09-10). */
|
|
53
54
|
--plus-bg:#DDEBE4; --plus-hover:#D0E3D9; --plus-fg:#1C6E5B;
|
|
54
55
|
--plus-icon-bg:#1C6E5B; --plus-icon-fg:#FFFFFF; --plus-shadow:none;
|
|
55
|
-
--logo-color:var(--accent); --logo-outer:#0B5A49; --logo-inner:#5FC9A0; /* the
|
|
56
|
+
--logo-color:var(--accent); --logo-outer:#0B5A49; --logo-inner:#5FC9A0; /* the bar's app icon: deep green square, mint s (2026-10-23) */
|
|
56
57
|
|
|
57
58
|
--status-published-fg:var(--green-700); --status-published-bg:var(--green-50);
|
|
58
59
|
--status-changes-fg:var(--amber-700); --status-changes-bg:var(--amber-50);
|
|
@@ -61,6 +62,7 @@ const TOKENS_LIGHT = `
|
|
|
61
62
|
--status-saved-dot:var(--green-500);
|
|
62
63
|
--warn:var(--amber-700); --warn-soft:var(--amber-50);
|
|
63
64
|
--danger:var(--red-700); --danger-soft:var(--red-50);
|
|
65
|
+
--destructive-bg:var(--red-700); /* the filled destructive button: white text on it in both themes (2026-11-14) */
|
|
64
66
|
|
|
65
67
|
/* The edited page inside the canvas stays light in both themes. */
|
|
66
68
|
--page-bg:#FFFFFF; --page-text:#151727;
|
|
@@ -78,13 +80,13 @@ const TOKENS_LIGHT = `
|
|
|
78
80
|
`;
|
|
79
81
|
/* The handoff's dark proposal (VisualEditor.md, 8c right board): the frame
|
|
80
82
|
and sheet a step darker, tiles a step lighter, fields in the sheet tone;
|
|
81
|
-
the
|
|
83
|
+
the bar and the logo unchanged. */
|
|
82
84
|
const TOKENS_DARK = `
|
|
83
|
-
--accent:#6CC4A6; --accent-hover:#82D2B8; --accent-soft:rgba(108,196,166,.18); --accent-line:#6CC4A6; --on-accent:#151727;
|
|
85
|
+
--accent:#6CC4A6; --accent-hover:#82D2B8; --accent-soft:rgba(108,196,166,.18); --avatar-hover:rgba(108,196,166,.26); /* a step up from accent-soft: white over the tint read grey (2026-11-14) */ --accent-line:#6CC4A6; --on-accent:#151727;
|
|
84
86
|
--selection-global:#F09A5C;
|
|
85
87
|
--frame:#0F1120; --sheet:${SHEET_DARK};
|
|
86
88
|
--canvas:#131526; --surface:#242840; --surface-form:#242840; --surface-inset:#1B1E30;
|
|
87
|
-
--menu-bg:#242840;
|
|
89
|
+
--menu-bg:#242840; --menu-hover:#2A2E47; /* a step off the menu, not the brighter --hover (2026-11-14) */
|
|
88
90
|
/* A field's edge is drawn in dark: a dialog's fields take the card's own tone there (2026-11-13). */
|
|
89
91
|
--input-bg:#1B1E30; --input-hover-bg:#1B1E30; --input-focus-bg:#1B1E30; --input-border:var(--line-dashed);
|
|
90
92
|
--line:#33384F; --line-dashed:#444A63; --line-soft:#33384F;
|
|
@@ -126,19 +128,19 @@ const SCALE = `
|
|
|
126
128
|
|
|
127
129
|
--radius-xs:2px; --radius-sm:6px; --radius-md:8px; --radius-lg:10px;
|
|
128
130
|
--radius-card:16px; --radius-card-lg:16px; --radius-modal:20px; --radius-btn:999px; --radius-round:999px;
|
|
129
|
-
--page-radius:
|
|
131
|
+
--page-radius:0; --selection-radius:2px; /* the canvas hover/selection ring: 2px (asked for 2026-09-26; 8px before) */
|
|
130
132
|
--outline-row-radius:6px; --tabs-radius:10px; --tab-radius:6px; --row-radius:2px; --tag-radius:999px; /* list row hover fill: 2px (same day; 10px before) */
|
|
131
133
|
--plus-radius:8px; --toolbar-radius:8px;
|
|
132
134
|
--rtcard-bg:var(--surface); --rtcard-border:var(--line);
|
|
133
135
|
--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
136
|
--outline-row-h:36px; --seg-pad-y:7px; --tabs-pad:3px;
|
|
135
137
|
--plus-h:32px; --plus-pad:0 14px 0 6px; --plus-label:inline;
|
|
136
|
-
--
|
|
137
|
-
/* The frame is 8px
|
|
138
|
-
--frame-pad:8px; --
|
|
139
|
-
--
|
|
138
|
+
--inspector-w:360px; --gutter:20px;
|
|
139
|
+
/* 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. */
|
|
140
|
+
--frame-pad:8px; --shell-gap:0px; --sheet-radius:12px; --sheet-pad:0 24px 24px;
|
|
141
|
+
--navbar-h:64px; --navbar-pad-x:12px; --navbar-item-h:40px; --navbar-item-radius:10px;
|
|
140
142
|
--topbar-pad:0; --back-size:32px; --back-bg:var(--surface);
|
|
141
|
-
--table-pad:8px 12px; --table-top:80px; --canvas-pad:28px
|
|
143
|
+
--table-pad:8px 12px; --table-top:80px; --canvas-pad:28px 28px 0; --card-pad:16px 18px; --card-gap:12px; --form-pad:32px 40px;
|
|
142
144
|
--border:1px solid var(--line);
|
|
143
145
|
--outline-selection:2px solid var(--selection);
|
|
144
146
|
--outline-selection-global:2px solid var(--selection-global);
|
|
@@ -182,106 +184,106 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
|
|
|
182
184
|
.sm-code{background:var(--surface);border-radius:8px;padding:10px 12px;margin:0;font:13px/1.5 var(--font-mono);overflow:auto}
|
|
183
185
|
|
|
184
186
|
/* ── shell ─────────────────────────────────────────────────────────── */
|
|
185
|
-
/*
|
|
186
|
-
components/AppShell.html): navy,
|
|
187
|
+
/* Bar over main (2026-11-14; a 72px rail on the left before it). The shell is
|
|
188
|
+
the frame (design_handoff_smoodly_neutral/components/AppShell.html): navy,
|
|
189
|
+
the navigation bar across its top, 8px right of, left of and below the main
|
|
187
190
|
column, which is the white sheet with a 20px radius and its own inner
|
|
188
|
-
padding
|
|
191
|
+
padding. The shell owns the viewport and the sheet scrolls inside itself,
|
|
192
|
+
so the bar stays put.
|
|
189
193
|
The main column stacks the top bar over one tile-shaped region: list,
|
|
190
194
|
form or editor. */
|
|
191
195
|
.sm-shell{
|
|
192
|
-
display:grid;grid-template-
|
|
193
|
-
padding:
|
|
196
|
+
display:grid;grid-template-rows:var(--navbar-h) minmax(0,1fr);grid-template-columns:minmax(0,1fr);row-gap:var(--shell-gap);
|
|
197
|
+
padding:0 var(--frame-pad) var(--frame-pad);height:100vh;height:100dvh;overflow:hidden;background:var(--frame);
|
|
194
198
|
}
|
|
195
199
|
.sm-shell>.sm-main{
|
|
196
200
|
min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden;
|
|
197
201
|
background:var(--sheet);border-radius:var(--sheet-radius);padding:var(--sheet-pad);
|
|
198
202
|
}
|
|
199
203
|
.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
|
-
}
|
|
204
|
+
/* The navigation bar: the app icon on the left, the items centred, Deploy and
|
|
205
|
+
the avatar on the right. The rail's colours, icons and order: 18px Lucide
|
|
206
|
+
icons beside a 14px label, the active item a white-tint pill. */
|
|
207
|
+
.sm-navbar{
|
|
208
|
+
display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:16px;min-width:0;
|
|
209
|
+
padding:0 var(--navbar-pad-x);background:var(--bar-bg);color:var(--navbar-fg);
|
|
210
|
+
font:500 var(--text-md)/1 var(--font-sans);position:relative;z-index:30;
|
|
211
|
+
}
|
|
212
|
+
.sm-navbar__start{display:flex;align-items:center;justify-self:start}
|
|
213
|
+
.sm-navbar__end{display:flex;align-items:center;gap:10px;justify-self:end}
|
|
209
214
|
/* 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}
|
|
215
|
+
mint s (2026-10-23). */
|
|
216
|
+
.sm-navbar__brand{appearance:none;background:none;border:0;padding:0;cursor:pointer;display:block;border-radius:8px;color:inherit}
|
|
213
217
|
/* 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).
|
|
218
|
+
.sm-navbar__logo{width:30px;height:auto;display:block;flex:none;transition:filter .15s}
|
|
219
|
+
.sm-navbar__brand:hover .sm-navbar__logo{filter:brightness(1.3)}
|
|
220
|
+
.sm-navbar__brand:focus-visible{outline:2px solid var(--navbar-active-fg);outline-offset:2px;box-shadow:none}
|
|
221
|
+
.sm-navbar__user{
|
|
222
|
+
width:32px;height:32px;border-radius:var(--radius-round);background:var(--accent-soft);color:var(--accent);
|
|
223
|
+
display:grid;place-items:center;font-size:var(--text-xs);font-weight:600;border:0;cursor:pointer;padding:0;transition:background .15s;
|
|
224
|
+
}
|
|
225
|
+
.sm-navbar__user:hover{background:var(--avatar-hover)}
|
|
226
|
+
.sm-navbar__user:focus-visible{outline:2px solid var(--navbar-active-fg);outline-offset:2px;box-shadow:none}
|
|
227
|
+
.sm-navbar__ext{width:13px;height:13px;flex:none;margin-left:12px;opacity:.45}
|
|
228
|
+
.sm-navbar__menu-item:hover .sm-navbar__ext,.sm-navbar__menu-item:focus-visible .sm-navbar__ext{opacity:.8}
|
|
229
|
+
.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)}
|
|
230
|
+
.sm-navbar__list{display:flex;align-items:center;gap:4px;margin:0;padding:0;list-style:none;min-width:0}
|
|
231
|
+
.sm-navbar__item{position:relative;list-style:none}
|
|
232
|
+
.sm-navbar__btn{
|
|
233
|
+
appearance:none;background:none;border:0;margin:0;height:var(--navbar-item-h);padding:0 12px;display:flex;align-items:center;gap:8px;
|
|
234
|
+
color:inherit;font:inherit;cursor:pointer;border-radius:var(--navbar-item-radius);text-decoration:none;white-space:nowrap;transition:background .12s,color .12s;
|
|
235
|
+
}
|
|
236
|
+
.sm-navbar__btn--icon{padding:0;width:var(--navbar-item-h);justify-content:center}
|
|
237
|
+
.sm-navbar__btn:hover{color:var(--navbar-active-fg);background:var(--navbar-hover-bg)}
|
|
238
|
+
.sm-navbar__btn:focus-visible{outline:2px solid var(--navbar-active-fg);outline-offset:-2px;box-shadow:none}
|
|
239
|
+
.sm-navbar__btn[aria-current="page"],.sm-navbar__btn[aria-expanded="true"]{color:var(--navbar-active-fg);background:var(--navbar-active-bg)}
|
|
240
|
+
.sm-navbar__btn svg{flex:none;stroke:currentColor;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
241
|
+
.sm-navbar__chevron{margin-left:-2px;opacity:.8}
|
|
242
|
+
/* A collection's own title can be long: it ends in an ellipsis (2026-11-13). */
|
|
243
|
+
.sm-navbar__label{letter-spacing:.01em;white-space:nowrap;max-width:160px;overflow:hidden;text-overflow:ellipsis}
|
|
244
|
+
/* A narrow window keeps the icons and drops the labels; each item keeps its name for a screen reader and a tooltip. */
|
|
245
|
+
@media (max-width:820px){
|
|
246
|
+
.sm-navbar__list .sm-navbar__label,.sm-navbar__deploy .sm-navbar__label{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
|
|
247
|
+
.sm-navbar__btn{padding:0 10px}
|
|
248
|
+
}
|
|
249
|
+
/* The Deploy item (spec 2026-10-09 static mode §6): next to the avatar; the badge is the count waiting, the
|
|
250
|
+
lit item a build in flight, the warning item a build that has not landed or a site that did not answer. */
|
|
251
|
+
.sm-navbar__icon{position:relative;display:grid;place-items:center}
|
|
252
|
+
.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}
|
|
253
|
+
.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}
|
|
254
|
+
.sm-navbar__btn--warn,.sm-navbar__btn--warn:hover{background:var(--warn-soft);color:var(--warn)}
|
|
255
|
+
@keyframes sm-navbar-pulse{0%,100%{opacity:1}50%{opacity:.55}}
|
|
256
|
+
.sm-navbar__menu-detail{font-size:var(--text-sm);color:var(--text);padding:6px 8px 4px;white-space:normal}
|
|
257
|
+
.sm-navbar__menu-note{font-size:var(--text-xs);color:var(--text-muted);padding:4px 8px 6px;white-space:normal}
|
|
258
|
+
.sm-navbar__menu-item:disabled{cursor:default;color:var(--text-muted)}
|
|
259
|
+
.sm-navbar__menu-item:disabled:hover{background:none}
|
|
260
|
+
/* A menu drops 6px below its item, aligned with its left edge (the end group's with its right edge).
|
|
264
261
|
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);
|
|
262
|
+
.sm-navbar__menu{
|
|
263
|
+
position:absolute;left:0;top:calc(100% + 6px);z-index:20;width:200px;padding:6px;margin:0;list-style:none;
|
|
267
264
|
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{
|
|
265
|
+
display:none;flex-direction:column;gap:1px;font-size:var(--text-md);font-weight:500;line-height:1.4;color:var(--text);
|
|
266
|
+
}
|
|
267
|
+
.sm-navbar__menu[data-open="true"]{display:flex}
|
|
268
|
+
.sm-navbar__menu--end{left:auto;right:0;width:240px}
|
|
269
|
+
.sm-navbar__menu--brand{width:240px}
|
|
270
|
+
/* An invisible bridge spans the 6px gap so the pointer can cross from Collections into its hover menu. */
|
|
271
|
+
.sm-navbar__menu--collections::before{content:"";position:absolute;left:0;right:0;top:-8px;height:8px}
|
|
272
|
+
.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}
|
|
273
|
+
.sm-navbar__menu-item{
|
|
280
274
|
appearance:none;background:none;border:0;width:100%;display:flex;align-items:center;justify-content:space-between;
|
|
281
275
|
padding:7px 10px;border-radius:var(--radius-sm);color:inherit;font:inherit;cursor:pointer;text-align:left;text-decoration:none;
|
|
282
276
|
}
|
|
283
|
-
.sm-
|
|
284
|
-
.sm-
|
|
277
|
+
.sm-navbar__menu-item:hover,.sm-navbar__menu-item:focus-visible{background:var(--menu-hover);color:inherit;outline:0;box-shadow:none}
|
|
278
|
+
.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}
|
|
279
|
+
.sm-navbar__menu-label{display:flex;align-items:center;gap:10px;min-width:0}
|
|
280
|
+
.sm-navbar__menu-label>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
281
|
+
.sm-navbar__menu-label svg{flex:none;stroke:currentColor;fill:none}
|
|
282
|
+
/* Collections and ⋮ are dark (2026-11-14): they carry the bar's own items, so they take its colours. */
|
|
283
|
+
.sm-navbar__menu--dark{background:var(--navbar-menu-bg);color:var(--navbar-fg);border:0;padding:6px;gap:2px}
|
|
284
|
+
.sm-navbar__menu--dark .sm-navbar__menu-item{padding:9px 12px}
|
|
285
|
+
.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)}
|
|
286
|
+
.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
287
|
.sm-when-light{display:var(--when-light);align-items:center;gap:8px}
|
|
286
288
|
.sm-when-dark{display:var(--when-dark);align-items:center;gap:8px}
|
|
287
289
|
/* .sm-nav-item survives as the inspector's link style (EditorView). */
|
|
@@ -336,7 +338,7 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
|
|
|
336
338
|
.sm-btn--danger{background:var(--btn-secondary-bg);color:var(--danger)}
|
|
337
339
|
.sm-btn--danger:hover{background:var(--danger-soft);color:var(--danger)}
|
|
338
340
|
/* The confirm dialog's destructive primary: filled red (ConfirmModal.html). */
|
|
339
|
-
.sm-btn--destructive{background:var(--
|
|
341
|
+
.sm-btn--destructive{background:var(--destructive-bg);color:#fff;font-weight:600;padding:0 18px}
|
|
340
342
|
.sm-btn--destructive:hover{filter:brightness(.94);color:#fff}
|
|
341
343
|
.sm-btn--ok{background:var(--status-published-bg);color:var(--status-published-fg);font-weight:600;padding:0 18px}
|
|
342
344
|
.sm-btn:disabled,.sm-btn[aria-disabled="true"]{opacity:.5;cursor:default;pointer-events:none}
|
|
@@ -455,12 +457,12 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
|
|
|
455
457
|
.sm-richtext__content ul,.sm-richtext__content ol{padding-left:20px}
|
|
456
458
|
.sm-richtext__content li+li{margin-top:.25em}
|
|
457
459
|
.sm-richtext__body{position:relative}
|
|
458
|
-
/* The "+" menu
|
|
460
|
+
/* The "+" menu hangs right under the toolbar's right end, no gap (2026-11-14; the "+" is its last
|
|
459
461
|
button) instead of pushing the content down; the picker inside drops
|
|
460
462
|
its own chrome. The slash menu floats under the caret the same way. */
|
|
461
|
-
.sm-richtext__head{position:relative}
|
|
463
|
+
.sm-richtext__head{position:relative;width:fit-content;max-width:100%} /* as wide as the toolbar, so the menu hangs from the "+" (2026-11-14) */
|
|
462
464
|
.sm-richtext__menu{
|
|
463
|
-
position:absolute;top:
|
|
465
|
+
position:absolute;top:100%;right:0;z-index:65;width:300px;max-width:calc(100vw - 32px);
|
|
464
466
|
display:flex;flex-direction:column;gap:6px;padding:6px;
|
|
465
467
|
background:var(--menu-bg);border:var(--menu-border);border-radius:var(--radius-md);box-shadow:var(--shadow-pop);
|
|
466
468
|
}
|
|
@@ -488,6 +490,7 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
|
|
|
488
490
|
/* Inline editing in the canvas (spec 2026-09-10). The outlines are drawn in
|
|
489
491
|
the overlay from the bridge's field rects; the toolbar floats above the
|
|
490
492
|
selection. */
|
|
493
|
+
.sm-dim{background:color-mix(in srgb,var(--canvas) 62%,transparent);pointer-events:none}
|
|
491
494
|
.sm-fieldhover{outline:1px solid color-mix(in srgb,var(--selection) 55%,transparent);outline-offset:2px;border-radius:2px;pointer-events:none}
|
|
492
495
|
.sm-fieldedit{outline:1.5px solid var(--selection);outline-offset:3px;border-radius:2px;pointer-events:none}
|
|
493
496
|
.sm-floatbar{
|
|
@@ -563,7 +566,7 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
|
|
|
563
566
|
.sm-listnotice:empty{display:none}
|
|
564
567
|
/* The rows sit in a centred column of at most 1200px inside the card,
|
|
565
568
|
under --table-top of air reserved for the coming search and filters. */
|
|
566
|
-
.sm-table{display:flex;flex-direction:column;padding:var(--table-pad);padding-top:var(--table-top);max-width:1200px;margin:0 auto}
|
|
569
|
+
.sm-table{display:flex;flex-direction:column;padding:var(--table-pad);padding-top:var(--table-top);padding-bottom:100px;max-width:1200px;margin:0 auto}
|
|
567
570
|
/* The filter sits in that air, so the column heads stay where they are on a list without one (2026-11-13). */
|
|
568
571
|
.sm-table--tools{padding-top:20px}
|
|
569
572
|
.sm-table__tools{display:flex;align-items:center;gap:12px;height:40px;margin-bottom:20px;padding:0 8px}
|
|
@@ -800,6 +803,13 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
|
|
|
800
803
|
min-width:0;min-height:0;overflow:hidden;
|
|
801
804
|
}
|
|
802
805
|
.sm-canvas__scroll{flex:1;min-height:0;overflow:auto;padding:var(--canvas-pad)}
|
|
806
|
+
/* A global item's canvas (spec 2026-11-14): a row above the frame — where the item is used, and which page shows it. */
|
|
807
|
+
.sm-globalcanvas{display:flex;flex-direction:column;min-width:0;min-height:0}
|
|
808
|
+
.sm-globalcanvas>.sm-canvas{flex:1}
|
|
809
|
+
.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}
|
|
810
|
+
.sm-globalcanvas__bar .sm-hint{margin:0}
|
|
811
|
+
.sm-globalcanvas__pick{display:flex;align-items:center;gap:8px}
|
|
812
|
+
.sm-globalcanvas__pick .sm-select{width:auto;height:30px;padding:0 10px}
|
|
803
813
|
.sm-page{
|
|
804
814
|
position:relative;background:var(--page-bg);border-radius:var(--page-radius);
|
|
805
815
|
box-shadow:var(--shadow-page);min-height:100%;
|
|
@@ -955,6 +965,22 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
|
|
|
955
965
|
width:26px;height:26px;flex:none;border-radius:var(--radius-sm);background:var(--accent-soft);
|
|
956
966
|
color:var(--accent);display:grid;place-items:center;font-size:13px;
|
|
957
967
|
}
|
|
968
|
+
/* The insert picker as a modal (spec 2026-11-14 add section previews): the rows keep the picker's item styles in a
|
|
969
|
+
plain column; beside them the current row as the site draws it, in one scaled, cropped frame. */
|
|
970
|
+
.sm-insert{display:flex;flex:1;min-height:0}
|
|
971
|
+
.sm-insert__list{width:auto;flex:1;min-width:0;overflow-y:auto;background:transparent;border:0;border-radius:0;box-shadow:none;padding:8px}
|
|
972
|
+
.sm-insert__side{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column}
|
|
973
|
+
.sm-insert--pane .sm-insert__side{flex:none;width:300px;border-right:var(--card-divider)}
|
|
974
|
+
.sm-insert__foot{flex:none;display:flex;align-items:center;gap:8px;padding:10px 16px;border-top:var(--card-divider);font-size:var(--text-sm);color:var(--text-muted);cursor:pointer}
|
|
975
|
+
/* The preview pane is a small canvas (2026-11-14): the canvas tone, inset from the dialog's edges, its corners rounded. */
|
|
976
|
+
.sm-insert__pane{flex:1;min-width:0;display:flex;flex-direction:column;gap:12px;padding:20px;margin:0 16px 16px 0;border-radius:12px;background:var(--canvas)}
|
|
977
|
+
.sm-insert__preview{flex:1;min-height:0;display:flex;flex-direction:column;justify-content:center;overflow:hidden;padding:4px 4px 10px} /* room for the crop's shadow */
|
|
978
|
+
.sm-insert__crop{position:relative;width:100%;overflow:hidden;border-radius:var(--radius-md);max-height:100%}
|
|
979
|
+
/* A hairline and a small shadow keep a light section off a light pane. */
|
|
980
|
+
.sm-insert__crop--shown{background:#fff;box-shadow:0 0 0 1px rgba(21,23,39,.1),0 2px 8px rgba(21,23,39,.1)}
|
|
981
|
+
.sm-insert__fallback{flex:1;display:grid;place-items:center;border-radius:var(--radius-md);border:1px dashed var(--line)}
|
|
982
|
+
.sm-insert__icon{width:72px;height:72px;border-radius:var(--radius-md);background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;font-size:32px}
|
|
983
|
+
.sm-insert__caption{flex:none;min-height:40px}
|
|
958
984
|
/* Combobox (spec 2026-09-17 §1): the input owns the width, the list hangs under it —
|
|
959
985
|
portaled and fixed at the input's width (--combo-w), above modals and below
|
|
960
986
|
the confirm scrim (2026-09-27); inline under the input in static markup. */
|
|
@@ -1106,6 +1132,7 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
|
|
|
1106
1132
|
}
|
|
1107
1133
|
.sm-media__thumb{position:relative;overflow:hidden;max-width:100%;max-height:300px;background:var(--surface-inset)}
|
|
1108
1134
|
.sm-media__thumb>img,.sm-media__thumb>video{position:absolute;max-width:none;display:block}
|
|
1135
|
+
.sm-media__thumb--placeholder>img{inset:0;width:100%;height:100%}
|
|
1109
1136
|
.sm-media__alt{font-size:var(--text-sm);color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1110
1137
|
.sm-media>.sm-media__alt{text-align:center}
|
|
1111
1138
|
.sm-media__actions{display:flex;gap:6px}
|
package/dist/admin/validate.js
CHANGED
|
@@ -15,7 +15,7 @@ import { isCalendarDay, isInstant } from "./forms/date-picker.js";
|
|
|
15
15
|
import { fits, misfitMessage } from "./drift.js";
|
|
16
16
|
import { isSegment } from "../paths.js";
|
|
17
17
|
import { safeHref } from "../richtext-href.js";
|
|
18
|
-
import { isCrop } from "../media.js";
|
|
18
|
+
import { isCrop, isImagePlaceholder } from "../media.js";
|
|
19
19
|
/** A slug outside the path grammar, at publish (spec 2026-09-28 ASCII slugs §3). */
|
|
20
20
|
export const SLUG_GRAMMAR_MESSAGE = "Use lowercase letters, digits and single hyphens, like my-post.";
|
|
21
21
|
/** A link field takes only what the site renders (richtext-href.ts's rule): the widget says so as the editor types,
|
|
@@ -36,7 +36,8 @@ function validateValue(path, value, d, errors, elements, scope) {
|
|
|
36
36
|
// An optional object with nothing filled in is absent (drafts inserted before 2026-09-26 carry `{}`).
|
|
37
37
|
if (d.type === "object" && d.optional === true && isBlankObject(value))
|
|
38
38
|
return;
|
|
39
|
-
|
|
39
|
+
// An image placeholder is unset to publish (spec 2026-11-14 filled samples): Required on a required image.
|
|
40
|
+
if (isEmpty(value) || (d.type === "image" && isImagePlaceholder(value))) {
|
|
40
41
|
if (!mayBeAbsent(d))
|
|
41
42
|
errors.push({ field: path, message: "Required." });
|
|
42
43
|
return;
|
package/dist/api-client.js
CHANGED
|
@@ -29,7 +29,8 @@ export function createSmoodlyApi(options) {
|
|
|
29
29
|
* server pushes its schema to the space it reads, so the developer must see when that space is production. */
|
|
30
30
|
let pushed = null;
|
|
31
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.`))));
|
|
32
|
-
|
|
32
|
+
/** `send`: a read that carries a body (the picker's samples) goes as a POST; every other read is a GET. */
|
|
33
|
+
async function call(path, params, send) {
|
|
33
34
|
if (push)
|
|
34
35
|
await pushOnce(push);
|
|
35
36
|
countRead();
|
|
@@ -39,7 +40,9 @@ export function createSmoodlyApi(options) {
|
|
|
39
40
|
url.searchParams.set(k, v);
|
|
40
41
|
let res;
|
|
41
42
|
try {
|
|
42
|
-
res = await doFetch(url.toString(),
|
|
43
|
+
res = await doFetch(url.toString(), send === undefined
|
|
44
|
+
? { headers: { authorization: `Bearer ${options.key}`, accept: "application/json" } }
|
|
45
|
+
: { method: "POST", headers: { authorization: `Bearer ${options.key}`, accept: "application/json", "content-type": "application/json" }, body: JSON.stringify(send) });
|
|
43
46
|
}
|
|
44
47
|
catch (e) {
|
|
45
48
|
throw new TypeError(`smoodly: can't reach SMOODLY_API_URL (${apiUrl}): ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -60,6 +63,8 @@ export function createSmoodlyApi(options) {
|
|
|
60
63
|
styles: statics.styles,
|
|
61
64
|
custom: statics.custom,
|
|
62
65
|
codePage: statics.codePage,
|
|
66
|
+
sampleTree: statics.sampleTree,
|
|
67
|
+
sampleCollections: statics.sampleCollections,
|
|
63
68
|
async locate(locale, path) {
|
|
64
69
|
return (await call("/locate", { locale, path })).data;
|
|
65
70
|
},
|
|
@@ -71,6 +76,11 @@ export function createSmoodlyApi(options) {
|
|
|
71
76
|
const r = await call("/load", { kind: target.kind, id: target.id, locale, path, draft: o?.draft ? "1" : undefined, version: o?.version });
|
|
72
77
|
return withTags(r.data, r.tags, draft);
|
|
73
78
|
},
|
|
79
|
+
async loadSamples(target, locale, path, tree) {
|
|
80
|
+
if (target.kind !== "page")
|
|
81
|
+
return null;
|
|
82
|
+
return (await call("/load-samples", {}, { id: target.id, locale, path, tree })).data;
|
|
83
|
+
},
|
|
74
84
|
async lookup(locale, path) {
|
|
75
85
|
const r = await call("/lookup", { locale, path });
|
|
76
86
|
return withTags(r.data.hit, r.tags, false);
|
package/dist/astro/middleware.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// for the body is what makes a late read count. A preview response gains the draft marker and, in the
|
|
6
6
|
// canvas, the bridge, when the shell rendered it; a live page gains nothing. Nothing under the preview
|
|
7
7
|
// address is ever stored, and neither is a render a publish overtook or one a read failed in.
|
|
8
|
+
import { SAMPLES_PARAM } from "../canvas-marker.js";
|
|
8
9
|
import { defineMiddleware } from "astro:middleware";
|
|
9
10
|
import { loadAdminFiles } from "../admin/files.js";
|
|
10
11
|
import { ADMIN_INTERNAL } from "../admin/page-data.js";
|
|
@@ -32,6 +33,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
32
33
|
draft,
|
|
33
34
|
editor: draft && searchParams.get("smoodly-editor") === "1",
|
|
34
35
|
version: (draft && searchParams.get("smoodly-version")) || undefined,
|
|
36
|
+
samples: (draft && searchParams.get(SAMPLES_PARAM)) || undefined,
|
|
35
37
|
cacheable: false,
|
|
36
38
|
shell: false,
|
|
37
39
|
};
|
package/dist/astro/shell.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export type SmoodlyLocals = {
|
|
|
14
14
|
editor: boolean;
|
|
15
15
|
/** History's stored version, inside a preview only. */
|
|
16
16
|
version?: string;
|
|
17
|
+
/** The picker's zone (`smoodly-samples`), inside a preview only: its rows are drawn from their samples in place of
|
|
18
|
+
* the page's content (spec 2026-11-14 add section previews). */
|
|
19
|
+
samples?: string;
|
|
17
20
|
/** Set by the shell when it rendered a page read from the service: the response may be cached under its tags. */
|
|
18
21
|
cacheable: boolean;
|
|
19
22
|
/** Set by the shell when it rendered a page at all: in a preview, only that response is a canvas. */
|