smoodly 0.0.62 → 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.
Files changed (61) hide show
  1. package/dist/admin/client-ops.js +1 -1
  2. package/dist/admin/drift.js +3 -1
  3. package/dist/admin/editor/EditorView.js +65 -9
  4. package/dist/admin/editor/InsertPicker.d.ts +8 -27
  5. package/dist/admin/editor/InsertPicker.js +56 -47
  6. package/dist/admin/editor/SamplePreview.d.ts +7 -0
  7. package/dist/admin/editor/SamplePreview.js +86 -0
  8. package/dist/admin/editor/SectionIcon.d.ts +4 -0
  9. package/dist/admin/editor/SectionIcon.js +8 -0
  10. package/dist/admin/editor/StructureView.d.ts +2 -2
  11. package/dist/admin/editor/StructureView.js +2 -1
  12. package/dist/admin/editor/canvas-address.d.ts +2 -0
  13. package/dist/admin/editor/canvas-address.js +3 -1
  14. package/dist/admin/editor/picker-rows.d.ts +33 -0
  15. package/dist/admin/editor/picker-rows.js +38 -0
  16. package/dist/admin/editor/sample-frame.d.ts +36 -0
  17. package/dist/admin/editor/sample-frame.js +58 -0
  18. package/dist/admin/editor/sample-picks.d.ts +34 -0
  19. package/dist/admin/editor/sample-picks.js +49 -0
  20. package/dist/admin/editor/view-memory.d.ts +5 -0
  21. package/dist/admin/editor/view-memory.js +19 -0
  22. package/dist/admin/files.generated.js +1 -1
  23. package/dist/admin/forms/MediaWidget.d.ts +1 -1
  24. package/dist/admin/forms/MediaWidget.js +10 -2
  25. package/dist/admin/ops.d.ts +5 -0
  26. package/dist/admin/shell/GlobalForm.js +1 -1
  27. package/dist/admin/shell/GlobalList.js +1 -1
  28. package/dist/admin/shell/NavBar.js +1 -1
  29. package/dist/admin/shell/PagesList.js +4 -4
  30. package/dist/admin/shell/console-links.js +1 -1
  31. package/dist/admin/shell/health-list.js +1 -1
  32. package/dist/admin/tree-ops.d.ts +11 -4
  33. package/dist/admin/tree-ops.js +28 -17
  34. package/dist/admin/ui/theme.js +32 -13
  35. package/dist/admin/validate.js +3 -2
  36. package/dist/api-client.js +12 -2
  37. package/dist/astro/middleware.js +2 -0
  38. package/dist/astro/shell.d.ts +3 -0
  39. package/dist/astro/shell.js +8 -1
  40. package/dist/canvas-marker.d.ts +5 -0
  41. package/dist/canvas-marker.js +18 -6
  42. package/dist/conditions.js +5 -2
  43. package/dist/media.d.ts +15 -2
  44. package/dist/media.js +68 -10
  45. package/dist/next/page-renderer.js +12 -5
  46. package/dist/resolve.d.ts +6 -1
  47. package/dist/resolve.js +7 -7
  48. package/dist/sample-fill.d.ts +40 -0
  49. package/dist/sample-fill.js +145 -0
  50. package/dist/sample-tree.d.ts +30 -0
  51. package/dist/sample-tree.js +76 -0
  52. package/dist/site.d.ts +13 -0
  53. package/dist/site.js +10 -1
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/docs/content-modeling/sections.md +19 -3
  57. package/docs/content-modeling/zones.md +2 -2
  58. package/docs/getting-started/working-together.md +1 -1
  59. package/docs/globals/defining.md +1 -1
  60. package/docs/globals/header-and-footer.md +1 -1
  61. package/package.json +1 -1
@@ -0,0 +1,36 @@
1
+ /** The frame's fixed viewport: a desktop width, and a height a section sized in viewport units cannot outgrow. */
2
+ export declare const SAMPLE_VIEWPORT: {
3
+ readonly width: 1280;
4
+ readonly height: 800;
5
+ };
6
+ export type FrameElement = {
7
+ tagName: string;
8
+ parentElement: FrameElement | null;
9
+ children: ArrayLike<FrameElement>;
10
+ getAttribute(name: string): string | null;
11
+ getBoundingClientRect(): {
12
+ top: number;
13
+ height: number;
14
+ };
15
+ };
16
+ export type FrameDocument = {
17
+ body: FrameElement | null;
18
+ querySelectorAll(selector: string): ArrayLike<FrameElement>;
19
+ };
20
+ /** Smoodly rendered this document as a draft: the marker its renderers write is there. */
21
+ export declare function isSmoodlyDraft(doc: FrameDocument): boolean;
22
+ /** The rules that hide the site's layout around the samples: at every level from the body down to the samples'
23
+ * common parent, everything beside the path (never a style, link or script). What shares the common parent with the
24
+ * samples stays, since that parent is the zone's own container; the body is never one, so there everything else
25
+ * goes. By position, as a stylesheet, and no element is touched: an attribute or an inline style written before
26
+ * the site's own script takes the page over is a hydration mismatch (Next's dev overlay reports it), and a rule
27
+ * also hides what the site adds to the body later. Empty when the document holds no sample. */
28
+ export declare function isolationCss(doc: FrameDocument): string;
29
+ /** A row's node in the frame; null when the site drew none, or drew the crash placeholder in its place. */
30
+ export declare function sampleNode(doc: FrameDocument, id: string): FrameElement | null;
31
+ /** Where the crop sits: the node's top in the document (its viewport top plus the frame's scroll) and its height,
32
+ * capped at the viewport's (a taller section shows its first screen). Whole pixels. */
33
+ export declare function sampleBox(node: FrameElement, scrollY: number): {
34
+ top: number;
35
+ height: number;
36
+ };
@@ -0,0 +1,58 @@
1
+ // The picker's samples frame, read from outside (spec 2026-11-14 add section previews): the frame is the site's own
2
+ // draft page with a zone's samples in place of its content, same-origin, so the admin reads its document directly.
3
+ // It hides the site's layout around the samples (a sticky header would sit over every picture) and crops the frame
4
+ // to one row's node. Pure over the few members of a document it uses, so the unit suite runs it without a DOM.
5
+ import { CRASHED_ATTR, NODE_ATTR } from "../../marks.js";
6
+ import { DRAFT_META_NAME } from "./canvas-diagnosis.js";
7
+ /** The frame's fixed viewport: a desktop width, and a height a section sized in viewport units cannot outgrow. */
8
+ export const SAMPLE_VIEWPORT = { width: 1280, height: 800 };
9
+ const SAMPLE_PREFIX = "sample-";
10
+ const nodesOf = (doc) => Array.from(doc.querySelectorAll(`[${NODE_ATTR}]`));
11
+ const chainOf = (el) => { const out = []; for (let cur = el; cur; cur = cur.parentElement)
12
+ out.push(cur); return out; };
13
+ /** Smoodly rendered this document as a draft: the marker its renderers write is there. */
14
+ export function isSmoodlyDraft(doc) {
15
+ return doc.querySelectorAll(`meta[name="${DRAFT_META_NAME}"]`).length > 0;
16
+ }
17
+ /** The rules that hide the site's layout around the samples: at every level from the body down to the samples'
18
+ * common parent, everything beside the path (never a style, link or script). What shares the common parent with the
19
+ * samples stays, since that parent is the zone's own container; the body is never one, so there everything else
20
+ * goes. By position, as a stylesheet, and no element is touched: an attribute or an inline style written before
21
+ * the site's own script takes the page over is a hydration mismatch (Next's dev overlay reports it), and a rule
22
+ * also hides what the site adds to the body later. Empty when the document holds no sample. */
23
+ export function isolationCss(doc) {
24
+ const body = doc.body;
25
+ const samples = nodesOf(doc).filter((n) => n.getAttribute(NODE_ATTR).startsWith(SAMPLE_PREFIX));
26
+ if (!body || samples.length === 0)
27
+ return "";
28
+ // The sample nodes and everything above them: never hidden.
29
+ const kept = new Set(samples.flatMap(chainOf));
30
+ // The common parent: the deepest element above the first sample that is above them all.
31
+ const above = samples.map((n) => new Set(chainOf(n).slice(1)));
32
+ const common = chainOf(samples[0]).slice(1).find((el) => above.every((set) => set.has(el)));
33
+ if (!common || !chainOf(common).includes(body))
34
+ return "";
35
+ const indexIn = (parent, child) => Array.from(parent.children).indexOf(child) + 1;
36
+ const selectorOf = (el) => (el === body || !el.parentElement ? "html > body" : `${selectorOf(el.parentElement)} > :nth-child(${indexIn(el.parentElement, el)})`);
37
+ const rule = (parent) => {
38
+ const keep = Array.from(parent.children).map((child, i) => (kept.has(child) ? `:not(:nth-child(${i + 1}))` : "")).join("");
39
+ return `${selectorOf(parent)} > :not(style):not(link):not(script)${keep}{display:none!important}`;
40
+ };
41
+ const rules = [];
42
+ if (common === body)
43
+ rules.push(rule(body));
44
+ for (let cur = common; cur !== body && cur.parentElement; cur = cur.parentElement)
45
+ rules.push(rule(cur.parentElement));
46
+ return rules.join("");
47
+ }
48
+ /** A row's node in the frame; null when the site drew none, or drew the crash placeholder in its place. */
49
+ export function sampleNode(doc, id) {
50
+ const node = nodesOf(doc).find((n) => n.getAttribute(NODE_ATTR) === id);
51
+ return node && node.getAttribute(CRASHED_ATTR) === null ? node : null;
52
+ }
53
+ /** Where the crop sits: the node's top in the document (its viewport top plus the frame's scroll) and its height,
54
+ * capped at the viewport's (a taller section shows its first screen). Whole pixels. */
55
+ export function sampleBox(node, scrollY) {
56
+ const rect = node.getBoundingClientRect();
57
+ return { top: Math.max(0, Math.round(rect.top + scrollY)), height: Math.max(0, Math.min(Math.round(rect.height), SAMPLE_VIEWPORT.height)) };
58
+ }
@@ -0,0 +1,34 @@
1
+ import type { AdminOps } from "../ops.ts";
2
+ import type { AdminSection } from "../serialize.ts";
3
+ import { type SamplePicks } from "../../sample-fill.ts";
4
+ /** `images: false`: store no image placeholder. `retry`: the call failed for a reason that may pass (the network, a
5
+ * session), so the next insert asks again; absent, the answer stands for the editor's life. */
6
+ export type PicksAnswer = {
7
+ picks: SamplePicks;
8
+ images: boolean;
9
+ retry?: true;
10
+ };
11
+ /** One operation call for every collection a top-level reference field of these sections targets. It is made even
12
+ * when there are none: the answer is how the admin learns that the service knows the placeholder. */
13
+ export declare function loadPicks(ops: Pick<AdminOps, "entries">, sections: Pick<AdminSection, "fields">[], locale: string): Promise<PicksAnswer>;
14
+ /** One answer per locale for a page editor's life. An answer that may be retried is handed out once and forgotten. */
15
+ export declare function picksCache(): {
16
+ get(locale: string, load: () => Promise<PicksAnswer>): Promise<PicksAnswer>;
17
+ /** The answer already in hand, for a caller that cannot wait. */
18
+ known: (locale: string) => PicksAnswer | undefined;
19
+ };
20
+ /** Where the editor stood when an insert asked for the picks: the page, the locale, which load of the draft, and the
21
+ * picker that was clicked (null for a gesture with no modal). */
22
+ export type InsertAsk = {
23
+ page: string;
24
+ locale: string;
25
+ load: number;
26
+ picker: {
27
+ zone: string;
28
+ index: number;
29
+ } | null;
30
+ };
31
+ /** Whether an insert that waited for the picks may still land (final review 2026-11-14): the same page, locale and
32
+ * load, and the picker it was clicked in still open at the same place. An editor who closed the modal, opened
33
+ * another, switched page or locale, or left, gets nothing inserted behind their back. */
34
+ export declare const insertStands: (asked: InsertAsk, now: InsertAsk) => boolean;
@@ -0,0 +1,49 @@
1
+ import { sampleCollections } from "../../sample-fill.js";
2
+ /** One operation call for every collection a top-level reference field of these sections targets. It is made even
3
+ * when there are none: the answer is how the admin learns that the service knows the placeholder. */
4
+ export async function loadPicks(ops, sections, locale) {
5
+ try {
6
+ const r = await ops.entries.samplePicks(sampleCollections(sections), locale);
7
+ if (!r.ok)
8
+ return r.code === "not_found" ? { picks: {}, images: false } : { picks: {}, images: false, retry: true };
9
+ const picks = {};
10
+ if (r.data && typeof r.data === "object") {
11
+ for (const [collection, ids] of Object.entries(r.data)) {
12
+ if (Array.isArray(ids))
13
+ picks[collection] = ids.filter((id) => typeof id === "string");
14
+ }
15
+ }
16
+ return { picks, images: true };
17
+ }
18
+ catch {
19
+ return { picks: {}, images: false, retry: true };
20
+ }
21
+ }
22
+ /** One answer per locale for a page editor's life. An answer that may be retried is handed out once and forgotten. */
23
+ export function picksCache() {
24
+ const asked = new Map();
25
+ const answers = new Map();
26
+ return {
27
+ get(locale, load) {
28
+ let pending = asked.get(locale);
29
+ if (!pending) {
30
+ pending = load().then((answer) => {
31
+ if (answer.retry)
32
+ asked.delete(locale);
33
+ else
34
+ answers.set(locale, answer);
35
+ return answer;
36
+ });
37
+ asked.set(locale, pending);
38
+ }
39
+ return pending;
40
+ },
41
+ /** The answer already in hand, for a caller that cannot wait. */
42
+ known: (locale) => answers.get(locale),
43
+ };
44
+ }
45
+ /** Whether an insert that waited for the picks may still land (final review 2026-11-14): the same page, locale and
46
+ * load, and the picker it was clicked in still open at the same place. An editor who closed the modal, opened
47
+ * another, switched page or locale, or left, gets nothing inserted behind their back. */
48
+ export const insertStands = (asked, now) => asked.page === now.page && asked.locale === now.locale && asked.load === now.load &&
49
+ (asked.picker === null || (now.picker !== null && now.picker.zone === asked.picker.zone && now.picker.index === asked.picker.index));
@@ -6,4 +6,9 @@ type Storage = {
6
6
  };
7
7
  export declare function rememberedView(storage?: Storage): EditorViewMode;
8
8
  export declare function rememberView(view: EditorViewMode, storage?: Storage): void;
9
+ /** The insert picker's "Insert filled" switch (spec 2026-11-14 filled samples): one setting for the whole browser,
10
+ * not per page or zone. On unless the stored value is exactly "0"; a storage that throws leaves it on. */
11
+ export declare const FILLED_KEY = "smoodly-insert-filled";
12
+ export declare function rememberedFilled(storage?: Storage): boolean;
13
+ export declare function rememberFilled(filled: boolean, storage?: Storage): void;
9
14
  export {};
@@ -15,3 +15,22 @@ export function rememberView(view, storage) {
15
15
  // not remembered — the view still switches
16
16
  }
17
17
  }
18
+ /** The insert picker's "Insert filled" switch (spec 2026-11-14 filled samples): one setting for the whole browser,
19
+ * not per page or zone. On unless the stored value is exactly "0"; a storage that throws leaves it on. */
20
+ export const FILLED_KEY = "smoodly-insert-filled";
21
+ export function rememberedFilled(storage) {
22
+ try {
23
+ return (storage ?? window.localStorage).getItem(FILLED_KEY) !== "0";
24
+ }
25
+ catch {
26
+ return true;
27
+ }
28
+ }
29
+ export function rememberFilled(filled, storage) {
30
+ try {
31
+ (storage ?? window.localStorage).setItem(FILLED_KEY, filled ? "1" : "0");
32
+ }
33
+ catch {
34
+ // not remembered — the switch still turns
35
+ }
36
+ }