smoodly 0.0.15 → 0.0.17
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 +238 -36
- package/dist/admin/auth.d.ts +7 -4
- package/dist/admin/auth.js +1 -1
- package/dist/admin/client-ops.js +3 -3
- package/dist/admin/custom-section.d.ts +10 -0
- package/dist/admin/custom-section.js +10 -0
- package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
- package/dist/admin/editor/CanvasOverlay.js +6 -6
- package/dist/admin/editor/EditorView.d.ts +2 -0
- package/dist/admin/editor/EditorView.js +322 -62
- package/dist/admin/editor/GlobalInspector.d.ts +24 -0
- package/dist/admin/editor/GlobalInspector.js +13 -0
- package/dist/admin/editor/InsertPicker.d.ts +6 -5
- package/dist/admin/editor/InsertPicker.js +6 -4
- package/dist/admin/editor/InspectorFields.d.ts +21 -0
- package/dist/admin/editor/InspectorFields.js +22 -0
- package/dist/admin/editor/Outline.d.ts +0 -1
- package/dist/admin/editor/Outline.js +3 -3
- package/dist/admin/editor/PageSettings.js +3 -2
- package/dist/admin/editor/global-edit.d.ts +39 -0
- package/dist/admin/editor/global-edit.js +124 -0
- package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
- package/dist/admin/editor/useGlobalEdit.js +18 -0
- package/dist/admin/forms/FieldWidget.d.ts +14 -1
- package/dist/admin/forms/FieldWidget.js +38 -11
- package/dist/admin/forms/ListField.d.ts +9 -1
- package/dist/admin/forms/ListField.js +16 -8
- package/dist/admin/forms/RichtextElementCard.js +11 -5
- package/dist/admin/forms/RowsWidget.d.ts +13 -0
- package/dist/admin/forms/RowsWidget.js +26 -0
- package/dist/admin/forms/SpanBar.d.ts +5 -0
- package/dist/admin/forms/SpanBar.js +37 -0
- package/dist/admin/forms/richtext-insert.d.ts +2 -2
- package/dist/admin/forms/richtext-insert.js +3 -3
- package/dist/admin/forms/richtext-nodes.js +1 -1
- package/dist/admin/forms/tabs.d.ts +20 -8
- package/dist/admin/forms/tabs.js +19 -23
- package/dist/admin/forms/useRichtextEditor.js +8 -1
- package/dist/admin/global-items.d.ts +9 -0
- package/dist/admin/{shared-items.js → global-items.js} +29 -14
- package/dist/admin/index.d.ts +3 -3
- package/dist/admin/index.js +1 -1
- package/dist/admin/next/op-handler.js +4 -3
- package/dist/admin/ops-impl.d.ts +3 -0
- package/dist/admin/ops-impl.js +242 -56
- package/dist/admin/ops.d.ts +38 -6
- package/dist/admin/serialize.d.ts +18 -7
- package/dist/admin/serialize.js +27 -5
- package/dist/admin/shell/AdminApp.js +7 -7
- package/dist/admin/shell/EntriesList.js +19 -3
- package/dist/admin/shell/EntryForm.js +183 -77
- package/dist/admin/shell/GlobalFields.d.ts +24 -0
- package/dist/admin/shell/GlobalFields.js +19 -0
- package/dist/admin/shell/{SharedForm.d.ts → GlobalForm.d.ts} +1 -1
- package/dist/admin/shell/GlobalForm.js +258 -0
- package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
- package/dist/admin/shell/{SharedList.js → GlobalList.js} +12 -11
- package/dist/admin/shell/IconRail.js +14 -7
- package/dist/admin/shell/PagesList.js +4 -8
- package/dist/admin/shell/entries-list.d.ts +2 -2
- package/dist/admin/shell/entries-list.js +3 -3
- package/dist/admin/shell/{shared-list.d.ts → global-list.d.ts} +3 -3
- package/dist/admin/shell/{shared-list.js → global-list.js} +2 -2
- package/dist/admin/supabase-auth.js +1 -1
- package/dist/admin/tree-ops.d.ts +10 -8
- package/dist/admin/tree-ops.js +28 -16
- package/dist/admin/ui/ActionMenu.d.ts +11 -0
- package/dist/admin/ui/ActionMenu.js +25 -0
- package/dist/admin/ui/HistoryModal.d.ts +23 -0
- package/dist/admin/ui/HistoryModal.js +40 -0
- package/dist/admin/ui/Modal.d.ts +5 -1
- package/dist/admin/ui/Modal.js +3 -3
- package/dist/admin/ui/PublishControl.d.ts +31 -0
- package/dist/admin/ui/PublishControl.js +24 -0
- package/dist/admin/ui/format.d.ts +4 -0
- package/dist/admin/ui/format.js +9 -0
- package/dist/admin/ui/primitives.d.ts +16 -1
- package/dist/admin/ui/primitives.js +20 -4
- package/dist/admin/ui/status-label.d.ts +10 -0
- package/dist/admin/ui/status-label.js +14 -0
- package/dist/admin/ui/theme.js +93 -13
- package/dist/admin/validate.d.ts +27 -2
- package/dist/admin/validate.js +47 -10
- package/dist/collections.d.ts +3 -9
- package/dist/collections.js +1 -2
- package/dist/conditions.d.ts +33 -0
- package/dist/conditions.js +79 -0
- package/dist/config.d.ts +26 -19
- package/dist/config.js +26 -25
- package/dist/css.d.ts +7 -0
- package/dist/css.js +57 -0
- package/dist/custom/CustomSection.d.ts +26 -0
- package/dist/custom/CustomSection.js +20 -0
- package/dist/custom/css.d.ts +3 -0
- package/dist/custom/css.js +28 -0
- package/dist/custom/index.d.ts +26 -0
- package/dist/custom/index.js +16 -0
- package/dist/custom/rows.d.ts +28 -0
- package/dist/custom/rows.js +129 -0
- package/dist/custom/schema.d.ts +32 -0
- package/dist/custom/schema.js +39 -0
- package/dist/entry-store.d.ts +42 -17
- package/dist/entry-store.js +107 -40
- package/dist/fields.d.ts +5 -2
- package/dist/fields.js +4 -2
- package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
- package/dist/{shared-id.js → global-id.js} +4 -4
- package/dist/global.d.ts +57 -0
- package/dist/global.js +51 -0
- package/dist/index.d.ts +28 -18
- package/dist/index.js +10 -5
- package/dist/media.js +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/page-renderer.d.ts +4 -2
- package/dist/next/page-renderer.js +17 -13
- package/dist/next/queries.d.ts +6 -6
- package/dist/next/queries.js +7 -7
- package/dist/page.js +2 -2
- package/dist/pairing.d.ts +13 -7
- package/dist/pairing.js +4 -4
- package/dist/refs.d.ts +2 -2
- package/dist/refs.js +4 -4
- package/dist/render.d.ts +13 -1
- package/dist/render.js +10 -5
- package/dist/resolve.d.ts +11 -6
- package/dist/resolve.js +22 -12
- package/dist/revalidate.d.ts +4 -4
- package/dist/revalidate.js +4 -4
- package/dist/richtext-render.js +2 -2
- package/dist/richtext-walk.d.ts +5 -5
- package/dist/richtext-walk.js +4 -4
- package/dist/schema.d.ts +15 -10
- package/dist/schema.js +19 -7
- package/dist/section-wrapper.d.ts +18 -6
- package/dist/section-wrapper.js +24 -17
- package/dist/site.d.ts +17 -5
- package/dist/site.js +36 -12
- package/dist/sql-space.js +1 -1
- package/dist/sql.js +19 -16
- package/dist/store.d.ts +45 -13
- package/dist/store.js +88 -12
- package/dist/styles.d.ts +27 -0
- package/dist/styles.js +60 -0
- package/dist/supabase-entry-store.d.ts +11 -6
- package/dist/supabase-entry-store.js +115 -39
- package/dist/supabase-store.d.ts +13 -6
- package/dist/supabase-store.js +90 -20
- package/dist/zones.d.ts +9 -7
- package/dist/zones.js +5 -5
- package/package.json +1 -1
- package/dist/admin/shared-items.d.ts +0 -9
- package/dist/admin/shell/SharedForm.js +0 -134
- package/dist/shared.d.ts +0 -46
- package/dist/shared.js +0 -36
package/dist/admin/ui/theme.js
CHANGED
|
@@ -25,6 +25,7 @@ const TOKENS_LIGHT = `
|
|
|
25
25
|
--line:#E8E3DA; --line-dashed:#D8D1C4; --line-soft:#EFEBE3;
|
|
26
26
|
--text:#1F1D1A; --text-secondary:#4E4942; --text-muted:#7A746A; --text-subtle:#A39C90; --text-disabled:#C4BDB1;
|
|
27
27
|
--selection:var(--accent); --nav-active-bg:var(--accent-soft); --nav-active-fg:var(--accent);
|
|
28
|
+
--selection-global:#C4620C;
|
|
28
29
|
--hover:rgba(31,29,26,.05); --press:rgba(31,29,26,.09); --control-hover:#F8F6F1;
|
|
29
30
|
--btn-secondary-bg:var(--surface); --btn-secondary-hover:#F8F6F1; --tile-hover:var(--surface);
|
|
30
31
|
--bar-bg:transparent; --bar-border:0; --menu-border:0; --card-divider:0;
|
|
@@ -40,7 +41,8 @@ const TOKENS_LIGHT = `
|
|
|
40
41
|
--logo-color:var(--accent);
|
|
41
42
|
|
|
42
43
|
--status-published-fg:var(--green-700); --status-published-bg:var(--green-50);
|
|
43
|
-
--status-
|
|
44
|
+
--status-changes-fg:var(--amber-700); --status-changes-bg:var(--amber-50);
|
|
45
|
+
--status-draft-fg:var(--text-muted); --status-draft-bg:var(--surface-inset);
|
|
44
46
|
--status-scheduled-fg:var(--accent); --status-scheduled-bg:var(--accent-soft);
|
|
45
47
|
--status-saved-dot:var(--green-500);
|
|
46
48
|
--warn:var(--amber-700); --warn-soft:var(--amber-50);
|
|
@@ -61,6 +63,7 @@ const TOKENS_LIGHT = `
|
|
|
61
63
|
`;
|
|
62
64
|
const TOKENS_DARK = `
|
|
63
65
|
--accent:#6CC4A6; --accent-hover:#82D2B8; --accent-soft:rgba(108,196,166,.18); --accent-line:#6CC4A6; --on-accent:#0B1F18;
|
|
66
|
+
--selection-global:#F09A5C;
|
|
64
67
|
--bg:#1C1A17; --canvas:#141210; --surface:#27241F; --surface-form:#27241F; --surface-inset:#1C1A17;
|
|
65
68
|
--input-bg:#211E1A; --input-hover-bg:#211E1A; --input-focus-bg:#211E1A;
|
|
66
69
|
--line:#3A3630; --line-dashed:#4A453D; --line-soft:#3A3630;
|
|
@@ -70,7 +73,8 @@ const TOKENS_DARK = `
|
|
|
70
73
|
--toolbar-bg:#0F0E0C; --toolbar-fg:#F1EDE6;
|
|
71
74
|
|
|
72
75
|
--status-published-fg:var(--green-300); --status-published-bg:rgba(111,211,154,.14);
|
|
73
|
-
--status-
|
|
76
|
+
--status-changes-fg:var(--amber-300); --status-changes-bg:rgba(232,184,90,.14);
|
|
77
|
+
--status-draft-fg:var(--text-muted); --status-draft-bg:var(--press);
|
|
74
78
|
--warn:var(--amber-300); --warn-soft:rgba(232,184,90,.14);
|
|
75
79
|
--danger:var(--red-300); --danger-soft:rgba(244,102,92,.14);
|
|
76
80
|
|
|
@@ -106,6 +110,7 @@ const SCALE = `
|
|
|
106
110
|
--table-pad:8px 12px; --table-top:80px; --canvas-pad:24px 40px 0; --card-pad:16px 18px; --card-gap:12px; --form-pad:32px 40px;
|
|
107
111
|
--border:1px solid var(--line);
|
|
108
112
|
--outline-selection:2px solid var(--selection);
|
|
113
|
+
--outline-selection-global:2px solid var(--selection-global);
|
|
109
114
|
`;
|
|
110
115
|
/* ── TEMPORARY: the original look, kept for side-by-side comparison while
|
|
111
116
|
the warm restyle lands. Frozen copies of the three strings above as they
|
|
@@ -128,11 +133,13 @@ const TECHNICAL_LIGHT = `
|
|
|
128
133
|
--text:var(--gray-900); --text-muted:var(--gray-600); --text-subtle:var(--gray-500);
|
|
129
134
|
--accent:var(--brand-600); --on-accent:#FFFFFF; --accent-soft:var(--brand-50);
|
|
130
135
|
--selection:var(--brand-600); --nav-active-bg:var(--brand-100); --nav-active-fg:var(--brand-600);
|
|
136
|
+
--selection-global:#D2610A;
|
|
131
137
|
--hover:rgba(15,20,32,.045); --press:rgba(15,20,32,.08);
|
|
132
138
|
--control-hover:var(--gray-100);
|
|
133
139
|
|
|
134
140
|
--status-published-fg:var(--green-700); --status-published-bg:var(--green-50);
|
|
135
|
-
--status-
|
|
141
|
+
--status-changes-fg:var(--amber-700); --status-changes-bg:var(--amber-50);
|
|
142
|
+
--status-draft-fg:var(--text-muted); --status-draft-bg:var(--surface-inset);
|
|
136
143
|
--status-scheduled-fg:var(--accent); --status-scheduled-bg:var(--accent-soft);
|
|
137
144
|
--status-saved-dot:var(--green-500);
|
|
138
145
|
--warn:var(--amber-700); --warn-soft:var(--amber-50);
|
|
@@ -155,12 +162,14 @@ const TECHNICAL_DARK = `
|
|
|
155
162
|
--line:#2A2F3B; --line-dashed:#2A2F3B;
|
|
156
163
|
--text:#EEF0F5; --text-muted:#8C94A8; --text-subtle:#6B7386;
|
|
157
164
|
--accent:var(--brand-300); --on-accent:#06122F; --accent-soft:rgba(110,150,255,.16);
|
|
165
|
+
--selection-global:#F2A35F;
|
|
158
166
|
--nav-active-bg:rgba(110,150,255,.14); --nav-active-fg:var(--brand-200);
|
|
159
167
|
--hover:rgba(238,240,245,.05); --press:rgba(238,240,245,.09);
|
|
160
168
|
--control-hover:var(--input-bg);
|
|
161
169
|
|
|
162
170
|
--status-published-fg:var(--green-300); --status-published-bg:rgba(111,211,154,.14);
|
|
163
|
-
--status-
|
|
171
|
+
--status-changes-fg:var(--amber-300); --status-changes-bg:rgba(232,184,90,.14);
|
|
172
|
+
--status-draft-fg:var(--text-muted); --status-draft-bg:var(--press);
|
|
164
173
|
--warn:var(--amber-300); --warn-soft:rgba(232,184,90,.14);
|
|
165
174
|
--danger:var(--red-300); --danger-soft:rgba(244,102,92,.14);
|
|
166
175
|
|
|
@@ -181,6 +190,7 @@ const TECHNICAL_SCALE = `
|
|
|
181
190
|
--nav-w:72px; --inspector-w:380px;
|
|
182
191
|
--border:1px solid var(--line);
|
|
183
192
|
--outline-selection:1.5px solid var(--selection);
|
|
193
|
+
--outline-selection-global:1.5px solid var(--selection-global);
|
|
184
194
|
`;
|
|
185
195
|
/* The tokens the warm restyle added, with the values the technical look
|
|
186
196
|
implied, so the frozen block above renders the old shapes too. */
|
|
@@ -293,9 +303,7 @@ body:has(.sm-admin){margin:0}
|
|
|
293
303
|
.sm-rail__btn[aria-current="page"],.sm-rail__btn[aria-expanded="true"]{color:var(--nav-active-fg);font-weight:600}
|
|
294
304
|
.sm-rail__btn[aria-current="page"]:hover,.sm-rail__btn[aria-expanded="true"]:hover{color:var(--nav-active-fg)}
|
|
295
305
|
.sm-rail__btn[aria-current="page"] .sm-rail__tile,.sm-rail__btn[aria-expanded="true"] .sm-rail__tile{background:var(--nav-active-bg)}
|
|
296
|
-
.sm-rail__tile svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:
|
|
297
|
-
.sm-rail__chev{position:absolute;right:4px;bottom:3px;width:8px;height:8px}
|
|
298
|
-
.sm-rail__chev svg{width:8px;height:8px;stroke-width:3}
|
|
306
|
+
.sm-rail__tile svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
299
307
|
.sm-rail__label{letter-spacing:.01em;white-space:nowrap}
|
|
300
308
|
/* Not greyed: the reference renders every inactive item at --text and marks
|
|
301
309
|
the unavailable ones only by being inert (no pointer, no hover). */
|
|
@@ -315,6 +323,11 @@ body:has(.sm-admin){margin:0}
|
|
|
315
323
|
}
|
|
316
324
|
.sm-rail__menu[data-open="true"]{display:flex}
|
|
317
325
|
.sm-rail__menu--up{top:auto;bottom:0}
|
|
326
|
+
/* The Collections flyout opens on hover, so it is roomier (the names at --text-lg) and an invisible
|
|
327
|
+
bridge spans the 6px gap so the pointer can cross from the rail without closing it. */
|
|
328
|
+
.sm-rail__menu--collections{width:208px;padding:8px;gap:2px;font-size:var(--text-lg)}
|
|
329
|
+
.sm-rail__menu--collections::before{content:"";position:absolute;left:-8px;top:0;bottom:0;width:8px}
|
|
330
|
+
.sm-rail__menu--collections .sm-rail__menu-item{padding:9px 12px}
|
|
318
331
|
.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}
|
|
319
332
|
.sm-rail__menu-item{
|
|
320
333
|
appearance:none;background:none;border:0;width:100%;display:flex;align-items:center;justify-content:space-between;
|
|
@@ -375,7 +388,7 @@ body:has(.sm-admin){margin:0}
|
|
|
375
388
|
.sm-btn--ok{background:var(--status-published-bg);color:var(--status-published-fg);font-weight:600;padding:0 18px}
|
|
376
389
|
.sm-btn:disabled,.sm-btn[aria-disabled="true"]{opacity:.5;cursor:default;pointer-events:none}
|
|
377
390
|
.sm-btn:focus-visible,.sm-iconbtn:focus-visible,.sm-nav-item:focus-visible,
|
|
378
|
-
.sm-tab:focus-visible,.sm-seg__opt:focus-visible,.sm-row:focus-visible{
|
|
391
|
+
.sm-tab:focus-visible,.sm-seg__opt:focus-visible,.sm-pill:focus-visible,.sm-row:focus-visible{
|
|
379
392
|
outline:2px solid var(--accent);outline-offset:1px
|
|
380
393
|
}
|
|
381
394
|
|
|
@@ -403,6 +416,10 @@ body:has(.sm-admin){margin:0}
|
|
|
403
416
|
.sm-label__mark{display:inline-flex;color:var(--text-subtle);cursor:help}
|
|
404
417
|
.sm-label__mark svg{width:13px;height:13px}
|
|
405
418
|
.sm-counter{font-size:var(--text-sm);color:var(--text-subtle)}
|
|
419
|
+
.sm-field__aside{display:flex;align-items:baseline;gap:10px;flex:none}
|
|
420
|
+
/* A head's text action (a list's Collapse all / Expand all): a small accent link. */
|
|
421
|
+
.sm-field__action{background:none;border:0;padding:0;font:inherit;font-size:var(--text-sm);font-weight:600;color:var(--accent);cursor:pointer}
|
|
422
|
+
.sm-field__action:hover{color:var(--accent-hover)}
|
|
406
423
|
.sm-hint{font-size:var(--text-sm);color:var(--text-muted)}
|
|
407
424
|
.sm-input,.sm-textarea,.sm-select{
|
|
408
425
|
width:100%;min-width:0;border:1px solid var(--input-border);border-radius:var(--radius-md);
|
|
@@ -494,7 +511,8 @@ body:has(.sm-admin){margin:0}
|
|
|
494
511
|
.sm-rtcard__icon{width:18px;height:18px;flex:none;border-radius:var(--radius-xs);background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;font-size:11px}
|
|
495
512
|
.sm-rtcard__title{flex:1;min-width:0;font-weight:600;font-size:var(--text-sm);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
496
513
|
.sm-rtcard__body{display:flex;flex-direction:column;gap:10px;padding:10px 12px 12px;border-top:1px solid var(--line)}
|
|
497
|
-
.
|
|
514
|
+
/* The whole image, letterboxed on the inset tone, never cropped: the card is a preview, not a crop. */
|
|
515
|
+
.sm-rtcard__thumb{display:block;width:100%;height:160px;object-fit:contain;border-radius:var(--radius-sm);background:var(--surface-inset)}
|
|
498
516
|
.sm-rtcard__file{font-size:var(--text-sm);color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
499
517
|
.sm-richtext__content blockquote{padding-left:12px;border-left:2px solid var(--line);color:var(--text-muted)}
|
|
500
518
|
.sm-richtext__content a{color:var(--accent);text-decoration:underline;text-underline-offset:3px;text-decoration-color:var(--accent-line)}
|
|
@@ -523,6 +541,23 @@ body:has(.sm-admin){margin:0}
|
|
|
523
541
|
display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:var(--control-h);padding:0 6px 0 14px;
|
|
524
542
|
border-radius:var(--radius-md);background:var(--surface-inset);color:var(--text-muted);font-size:var(--text-sm);
|
|
525
543
|
}
|
|
544
|
+
/* The custom section's row cards and the width bar (spec 2026-09-14 §6). */
|
|
545
|
+
.sm-rowcards{display:flex;flex-direction:column;gap:12px;align-items:stretch}
|
|
546
|
+
.sm-rowcard{display:flex;flex-direction:column;gap:12px;padding:12px;border:1px solid var(--line);border-radius:var(--radius-md);background:var(--surface);min-width:0}
|
|
547
|
+
.sm-rowcard__head{display:flex;align-items:center;gap:4px}
|
|
548
|
+
.sm-rowcard__title{flex:1;font-weight:600}
|
|
549
|
+
.sm-rowcard__presets{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
|
550
|
+
/* Secondary buttons sit on the white card: an outline so they read as buttons. */
|
|
551
|
+
.sm-rowcard__presets .sm-btn{height:var(--control-h-sm);padding:0 10px;font-size:var(--text-sm);border:1px solid var(--line)}
|
|
552
|
+
.sm-rowcards > .sm-btn{align-self:flex-start;border:1px solid var(--line)}
|
|
553
|
+
.sm-stepper{display:flex;align-items:center;gap:8px}
|
|
554
|
+
.sm-stepper__value{min-width:20px;text-align:center;font-weight:600}
|
|
555
|
+
.sm-spanbar{display:flex;gap:2px;height:32px;background:var(--seg-bg);border-radius:var(--radius-md);padding:3px;user-select:none}
|
|
556
|
+
.sm-spanbar__col{position:relative;display:flex;align-items:center;justify-content:center;min-width:0;background:var(--surface);border-radius:6px;font-size:var(--text-sm);color:var(--text-muted)}
|
|
557
|
+
.sm-spanbar__handle{position:absolute;right:-6px;top:0;bottom:0;width:10px;padding:0;border:0;background:transparent;cursor:col-resize;touch-action:none;z-index:1}
|
|
558
|
+
.sm-spanbar__handle::after{content:"";position:absolute;left:4px;top:8px;bottom:8px;width:2px;border-radius:1px;background:var(--text-muted)}
|
|
559
|
+
.sm-spanbar__handle:focus-visible{outline:none}
|
|
560
|
+
.sm-spanbar__handle:focus-visible::after,.sm-spanbar__handle:hover::after{background:var(--accent)}
|
|
526
561
|
/* A field revealed from a canvas click: one pulse of the accent, then nothing. */
|
|
527
562
|
.sm-field--reveal{animation:sm-reveal .9s ease-out;border-radius:var(--radius-sm)}
|
|
528
563
|
@keyframes sm-reveal{0%{outline:2px solid var(--selection);outline-offset:4px}100%{outline:2px solid transparent;outline-offset:4px}}
|
|
@@ -592,6 +627,15 @@ body:has(.sm-admin){margin:0}
|
|
|
592
627
|
border-radius:var(--radius-sm);line-height:1;height:24px;
|
|
593
628
|
}
|
|
594
629
|
.sm-rowmenu:hover{background:var(--surface-inset);color:var(--text)}
|
|
630
|
+
.sm-dotmenu{
|
|
631
|
+
width:var(--button-h);height:var(--button-h);flex:none;border-radius:50%;border:0;padding:0;
|
|
632
|
+
background:var(--btn-secondary-bg);color:var(--text-muted);font:inherit;font-weight:700;letter-spacing:1px;
|
|
633
|
+
display:grid;place-items:center;cursor:pointer;
|
|
634
|
+
}
|
|
635
|
+
.sm-dotmenu:hover{background:var(--btn-secondary-hover);color:var(--text)}
|
|
636
|
+
.sm-dotmenu:disabled{opacity:.4;cursor:default}
|
|
637
|
+
.sm-dotmenu:disabled:hover{background:var(--btn-secondary-bg);color:var(--text-muted)}
|
|
638
|
+
.sm-picker--menu{position:absolute;top:calc(100% + 6px);right:0;width:220px;z-index:60}
|
|
595
639
|
|
|
596
640
|
/* Status pill: a dot before the word in the warm look (none in the technical). */
|
|
597
641
|
.sm-badge{
|
|
@@ -637,6 +681,18 @@ body:has(.sm-admin){margin:0}
|
|
|
637
681
|
.sm-seg__opt[aria-pressed="true"]{background:var(--surface);color:var(--text);font-weight:600;box-shadow:var(--shadow-seg)}
|
|
638
682
|
/* A locale the record does not have yet: subtle, still a button (it opens the add pane). */
|
|
639
683
|
.sm-seg__opt--absent{color:var(--text-subtle)}
|
|
684
|
+
/* f.radio as a pill row (spec 2026-09-14 §5): small chips a step below the tabs and the
|
|
685
|
+
segmented control — no track, the chosen pill filled in the soft accent, the rest bare.
|
|
686
|
+
Wraps when the labels need it. */
|
|
687
|
+
.sm-pills{display:flex;flex-wrap:wrap;gap:4px;padding:2px 0}
|
|
688
|
+
.sm-pill{
|
|
689
|
+
height:var(--control-h-sm);padding:0 10px;border:0;border-radius:var(--radius-btn);background:transparent;
|
|
690
|
+
color:var(--text-muted);font:inherit;font-size:var(--text-sm);font-weight:500;line-height:1;cursor:pointer;white-space:nowrap;
|
|
691
|
+
}
|
|
692
|
+
.sm-pill:hover{color:var(--text);background:var(--surface-inset)}
|
|
693
|
+
.sm-pill[aria-pressed="true"]{background:var(--accent-soft);color:var(--accent);font-weight:600}
|
|
694
|
+
/* The custom-CSS box: code, no wrapping, no spellcheck. */
|
|
695
|
+
.sm-css{font-family:var(--font-mono);font-size:12px;line-height:1.5;white-space:pre;overflow-x:auto}
|
|
640
696
|
/* Switch (f.boolean, the SEO noindex toggle): a pill track, knob slides right when on. */
|
|
641
697
|
.sm-switch{width:36px;height:20px;padding:2px;border:0;border-radius:var(--radius-round);background:var(--seg-bg);cursor:pointer;display:inline-flex;align-items:center;flex:none}
|
|
642
698
|
.sm-switch[aria-checked="true"]{background:var(--accent)}
|
|
@@ -664,9 +720,11 @@ body:has(.sm-admin){margin:0}
|
|
|
664
720
|
.sm-banner--error{background:var(--danger-soft);color:var(--danger)}
|
|
665
721
|
.sm-banner--ok{background:var(--status-published-bg);color:var(--status-published-fg)}
|
|
666
722
|
.sm-banner--warn{background:var(--warn-soft);color:var(--warn)}
|
|
723
|
+
.sm-fielderr{margin:6px 0 0;padding-left:18px;font:inherit;font-size:var(--text-sm);font-family:var(--font-mono)}
|
|
724
|
+
.sm-fielderr__field{background:none;border:0;padding:0;font:inherit;color:inherit;text-decoration:underline;cursor:pointer}
|
|
667
725
|
|
|
668
726
|
/* ── cards ─────────────────────────────────────────────────────────── */
|
|
669
|
-
/* The inspector column (editor, entry and
|
|
727
|
+
/* The inspector column (editor, entry and global forms) is a stack of
|
|
670
728
|
cards; the technical look draws them flush with a hairline between. */
|
|
671
729
|
.sm-inspector{
|
|
672
730
|
display:flex;flex-direction:column;gap:var(--card-gap);min-height:0;overflow:auto;
|
|
@@ -683,7 +741,10 @@ body:has(.sm-admin){margin:0}
|
|
|
683
741
|
}
|
|
684
742
|
.sm-inspector__head{display:flex;align-items:center;justify-content:space-between;gap:8px;flex:none}
|
|
685
743
|
.sm-inspector__body{display:flex;flex-direction:column;gap:14px}
|
|
686
|
-
/*
|
|
744
|
+
/* A tab's groups (spec 2026-09-14 §5): the section's own settings untitled, then Spacing & layout and Advanced under a hairline. */
|
|
745
|
+
.sm-inspector__group{display:flex;flex-direction:column;gap:12px}
|
|
746
|
+
.sm-inspector__group+.sm-inspector__group{margin-top:8px;padding-top:12px;border-top:1px solid var(--line)}
|
|
747
|
+
/* The form card: the main column of the entry and global forms. Every
|
|
687
748
|
field (and the banner) is a centred 680px column — a reading width —
|
|
688
749
|
while the card itself still fills the main area. */
|
|
689
750
|
.sm-form{
|
|
@@ -732,12 +793,13 @@ body:has(.sm-admin){margin:0}
|
|
|
732
793
|
letter-spacing:var(--blocktag-tracking);text-transform:var(--blocktag-transform);padding:3px 10px;
|
|
733
794
|
border-radius:var(--blocktag-radius);white-space:nowrap;
|
|
734
795
|
}
|
|
796
|
+
.sm-blocktag--global{background:var(--selection-global)}
|
|
735
797
|
.sm-blocktoolbar{
|
|
736
798
|
display:flex;gap:2px;padding:3px;background:var(--toolbar-bg);border-radius:var(--radius-btn);
|
|
737
799
|
box-shadow:var(--shadow-float);
|
|
738
800
|
}
|
|
739
801
|
.sm-blocktoolbar__note{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--toolbar-fg);opacity:.7;padding:4px 8px}
|
|
740
|
-
/* The insert affordance between
|
|
802
|
+
/* The insert affordance between sections: a labelled pill (the label is
|
|
741
803
|
hidden in the technical look, leaving the 24px circle). */
|
|
742
804
|
.sm-plus{
|
|
743
805
|
display:inline-flex;align-items:center;gap:8px;height:var(--plus-h);min-width:var(--plus-h);padding:var(--plus-pad);
|
|
@@ -785,6 +847,10 @@ body:has(.sm-admin){margin:0}
|
|
|
785
847
|
border:0;border-radius:var(--radius-sm);padding:8px 10px;cursor:pointer;color:var(--text);font:inherit;
|
|
786
848
|
}
|
|
787
849
|
.sm-picker__item:hover{background:var(--nav-active-bg)}
|
|
850
|
+
/* after the base item rule: same specificity, so source order decides (ActionMenu states) */
|
|
851
|
+
.sm-picker__item:disabled{opacity:.4;cursor:default}
|
|
852
|
+
.sm-picker__item:disabled:hover{background:transparent}
|
|
853
|
+
.sm-picker__item--danger{color:var(--danger)}
|
|
788
854
|
.sm-picker__icon{
|
|
789
855
|
width:26px;height:26px;flex:none;border-radius:var(--radius-sm);background:var(--accent-soft);
|
|
790
856
|
color:var(--accent);display:grid;place-items:center;font-size:13px;
|
|
@@ -796,7 +862,9 @@ body:has(.sm-admin){margin:0}
|
|
|
796
862
|
}
|
|
797
863
|
|
|
798
864
|
/* ── media ─────────────────────────────────────────────────────────── */
|
|
799
|
-
|
|
865
|
+
/* The single track is minmax(0,1fr), not auto: an auto track grows to the panel's own width, so the
|
|
866
|
+
panel's max-width:100% would cap at nothing and a 1200px History panel ran past a laptop viewport. */
|
|
867
|
+
.sm-modal{position:fixed;inset:0;z-index:70;background:rgba(0,0,0,.45);display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr);place-items:center;padding:24px}
|
|
800
868
|
.sm-modal__panel{
|
|
801
869
|
max-width:100%;max-height:100%;display:flex;flex-direction:column;background:var(--surface);
|
|
802
870
|
border:var(--menu-border);border-radius:var(--radius-card);box-shadow:var(--shadow-pop);overflow:hidden;
|
|
@@ -804,6 +872,18 @@ body:has(.sm-admin){margin:0}
|
|
|
804
872
|
.sm-modal__head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px 10px;border-bottom:var(--card-divider)}
|
|
805
873
|
.sm-modal__body{padding:12px 16px;overflow:auto;display:flex;flex-direction:column;gap:10px;min-height:0}
|
|
806
874
|
.sm-modal__foot{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px 14px;border-top:var(--card-divider)}
|
|
875
|
+
.sm-modal__body--flush{padding:0;gap:0;flex:1}
|
|
876
|
+
/* ── history (spec 2026-09-13) ─────────────────────────────────── */
|
|
877
|
+
.sm-history{display:flex;flex:1;min-height:0}
|
|
878
|
+
.sm-history__list{width:260px;flex:none;overflow:auto;display:flex;flex-direction:column;border-right:var(--card-divider)}
|
|
879
|
+
.sm-history__row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 12px;border:0;border-bottom:var(--card-divider);background:none;color:inherit;font:inherit;font-size:var(--text-sm);text-align:left;cursor:pointer}
|
|
880
|
+
.sm-history__row:hover{background:var(--input-bg)}
|
|
881
|
+
.sm-history__row--active,.sm-history__row--active:hover{background:var(--input-bg);font-weight:600}
|
|
882
|
+
.sm-history__tag{font-size:var(--text-sm);color:var(--text-muted);padding:1px 8px;border-radius:999px;border:1px solid var(--line-dashed)}
|
|
883
|
+
.sm-history__tag--live{color:var(--status-published-fg);background:var(--status-published-bg);border-color:transparent}
|
|
884
|
+
.sm-history__main{flex:1;min-width:0;display:flex;overflow:auto}
|
|
885
|
+
.sm-history__frame{flex:1;width:100%;height:100%;border:0;background:#fff}
|
|
886
|
+
.sm-history__form{flex:1;min-width:0;margin:0;padding:16px;border:0;pointer-events:none;display:flex;flex-direction:column;gap:14px}
|
|
807
887
|
.sm-picker-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
808
888
|
.sm-dropzone{
|
|
809
889
|
border:1px dashed var(--line-dashed);border-radius:var(--radius-lg);background:var(--input-bg);color:var(--text-muted);
|
package/dist/admin/validate.d.ts
CHANGED
|
@@ -10,11 +10,36 @@ type Desc = {
|
|
|
10
10
|
min?: unknown;
|
|
11
11
|
fields?: unknown;
|
|
12
12
|
item?: unknown;
|
|
13
|
+
hidden?: unknown;
|
|
14
|
+
};
|
|
15
|
+
/** What conditions read beyond the current level: the node's two namespaces. */
|
|
16
|
+
export type ValidationScope = {
|
|
17
|
+
fields?: Record<string, unknown>;
|
|
18
|
+
settings?: Record<string, unknown>;
|
|
13
19
|
};
|
|
14
20
|
/** The element schemas an element node is validated against (spec 2026-09-08 block nodes §2). */
|
|
15
21
|
export type ElementDescs = Record<string, {
|
|
16
22
|
fields: Record<string, Desc>;
|
|
17
|
-
|
|
23
|
+
settings?: Record<string, Desc>;
|
|
18
24
|
}>;
|
|
19
|
-
|
|
25
|
+
/** Validate one value map. Without `scope.fields` the values ARE the fields (an entry, a page, an object global). */
|
|
26
|
+
export declare function validateFields(values: Record<string, unknown>, descriptors: Record<string, Desc>, elements?: ElementDescs, scope?: ValidationScope): FieldError[];
|
|
27
|
+
/** A tree node (or a visual global, in node shape) against its schema: fields, settings (prefixed), css (spec 2026-09-14 §3, §4). */
|
|
28
|
+
export declare function validateNode(node: {
|
|
29
|
+
fields?: Record<string, unknown>;
|
|
30
|
+
settings?: Record<string, unknown>;
|
|
31
|
+
css?: unknown;
|
|
32
|
+
[other: string]: unknown;
|
|
33
|
+
}, schema: {
|
|
34
|
+
fields: Record<string, Desc>;
|
|
35
|
+
settings?: Record<string, Desc>;
|
|
36
|
+
}, elements?: ElementDescs): FieldError[];
|
|
37
|
+
/** A custom section node (spec 2026-09-14 §6): the columns' richtext (element nodes included), the row structure, the css.
|
|
38
|
+
* The generic walk's own words on `rows.<i>.spans` are dropped (`spans` is no descriptor type it knows) so rowsProblems
|
|
39
|
+
* is the one voice on structure; the columns' richtext errors keep their paths. */
|
|
40
|
+
export declare function validateCustomNode(node: {
|
|
41
|
+
fields?: Record<string, unknown>;
|
|
42
|
+
css?: unknown;
|
|
43
|
+
[other: string]: unknown;
|
|
44
|
+
}, fields: Record<string, Desc>, elements?: ElementDescs): FieldError[];
|
|
20
45
|
export {};
|
package/dist/admin/validate.js
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
// lives admin/server-side). Entry saves validate; page drafts do NOT
|
|
3
3
|
// (drafts must save freely) — publish validates every node. Objects and
|
|
4
4
|
// lists recurse; error paths are dotted (`cta.href`, `points.2.heading`).
|
|
5
|
+
// A field whose `.hidden()` condition holds is skipped entirely — a hidden
|
|
6
|
+
// required field is not required (spec 2026-09-14 §12); the same isHidden
|
|
7
|
+
// the form uses decides, over the same scope.
|
|
5
8
|
import { optionValues } from "../fields.js";
|
|
9
|
+
import { isHidden } from "../conditions.js";
|
|
10
|
+
import { cssProblem } from "../css.js";
|
|
6
11
|
import { hasContent } from "./richtext-doc.js";
|
|
7
12
|
import { ELEMENT_NODE, richtextBlocks } from "../richtext-walk.js";
|
|
13
|
+
import { rowsProblems } from "../custom/rows.js";
|
|
8
14
|
const subfields = (d) => d.fields && typeof d.fields === "object" ? d.fields : {};
|
|
9
15
|
const itemOf = (d) => d.item && typeof d.item === "object" ? d.item : undefined;
|
|
10
16
|
const isEmpty = (v) => v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
@@ -12,7 +18,7 @@ const isEmpty = (v) => v === undefined || v === null || v === "" || (Array.isArr
|
|
|
12
18
|
* may be absent — may be absent altogether. */
|
|
13
19
|
const mayBeAbsent = (d) => d.optional === true || d.type === "boolean" || (d.type === "object" && Object.values(subfields(d)).every(mayBeAbsent));
|
|
14
20
|
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
15
|
-
function validateValue(path, value, d, errors, elements) {
|
|
21
|
+
function validateValue(path, value, d, errors, elements, scope) {
|
|
16
22
|
if (isEmpty(value)) {
|
|
17
23
|
if (!mayBeAbsent(d))
|
|
18
24
|
errors.push({ field: path, message: "Required." });
|
|
@@ -34,8 +40,9 @@ function validateValue(path, value, d, errors, elements) {
|
|
|
34
40
|
const el = typeof a.name === "string" ? elements[a.name] : undefined;
|
|
35
41
|
if (el) {
|
|
36
42
|
const fields = (a.fields && typeof a.fields === "object" ? a.fields : {});
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
const settings = (a.settings && typeof a.settings === "object" ? a.settings : {});
|
|
44
|
+
validateMap(`${path}.elements.${i}`, fields, el.fields, errors, elements, { self: fields, fields, settings });
|
|
45
|
+
validateMap(`${path}.elements.${i}.settings`, settings, el.settings ?? {}, errors, elements, { self: settings, fields, settings });
|
|
39
46
|
}
|
|
40
47
|
i++;
|
|
41
48
|
}
|
|
@@ -57,8 +64,9 @@ function validateValue(path, value, d, errors, elements) {
|
|
|
57
64
|
if (typeof d.max === "number" && value.length > d.max)
|
|
58
65
|
errors.push({ field: path, message: `At most ${plural(d.max, "item")}.` });
|
|
59
66
|
const item = itemOf(d);
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
// A scalar item's condition reads the list's own level; an object item reads itself (the object branch).
|
|
68
|
+
if (item && !isHidden(item, scope))
|
|
69
|
+
value.forEach((v, i) => validateValue(`${path}.${i}`, v, item, errors, elements, scope));
|
|
62
70
|
return;
|
|
63
71
|
}
|
|
64
72
|
if (d.type === "object") {
|
|
@@ -66,8 +74,7 @@ function validateValue(path, value, d, errors, elements) {
|
|
|
66
74
|
errors.push({ field: path, message: "Must be an object." });
|
|
67
75
|
return;
|
|
68
76
|
}
|
|
69
|
-
|
|
70
|
-
validateValue(`${path}.${k}`, value[k], sub, errors, elements);
|
|
77
|
+
validateMap(path, value, subfields(d), errors, elements, { ...scope, self: value });
|
|
71
78
|
return;
|
|
72
79
|
}
|
|
73
80
|
if (d.type === "boolean") {
|
|
@@ -97,9 +104,39 @@ function validateValue(path, value, d, errors, elements) {
|
|
|
97
104
|
errors.push({ field: path, message: `Must be at most ${d.max} characters.` });
|
|
98
105
|
}
|
|
99
106
|
}
|
|
100
|
-
|
|
107
|
+
/** One descriptor map at one level: hidden entries are skipped. `prefix` is the dotted path above it ("" at the top). */
|
|
108
|
+
function validateMap(prefix, values, descriptors, errors, elements, scope) {
|
|
109
|
+
for (const [key, d] of Object.entries(descriptors)) {
|
|
110
|
+
if (isHidden(d, scope))
|
|
111
|
+
continue;
|
|
112
|
+
validateValue(prefix === "" ? key : `${prefix}.${key}`, values[key], d, errors, elements, scope);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Validate one value map. Without `scope.fields` the values ARE the fields (an entry, a page, an object global). */
|
|
116
|
+
export function validateFields(values, descriptors, elements = {}, scope = {}) {
|
|
101
117
|
const errors = [];
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
validateMap("", values, descriptors, errors, elements, { self: values, fields: scope.fields ?? values, settings: scope.settings });
|
|
119
|
+
return errors;
|
|
120
|
+
}
|
|
121
|
+
/** A tree node (or a visual global, in node shape) against its schema: fields, settings (prefixed), css (spec 2026-09-14 §3, §4). */
|
|
122
|
+
export function validateNode(node, schema, elements = {}) {
|
|
123
|
+
const fields = node.fields ?? {};
|
|
124
|
+
const settings = node.settings ?? {};
|
|
125
|
+
const scope = { fields, settings };
|
|
126
|
+
const errors = validateFields(fields, schema.fields, elements, scope);
|
|
127
|
+
for (const e of validateFields(settings, schema.settings ?? {}, elements, scope))
|
|
128
|
+
errors.push({ field: `settings.${e.field}`, message: e.message });
|
|
129
|
+
if (typeof node.css === "string" && node.css !== "") {
|
|
130
|
+
const problem = cssProblem(node.css);
|
|
131
|
+
if (problem)
|
|
132
|
+
errors.push({ field: "css", message: problem });
|
|
133
|
+
}
|
|
104
134
|
return errors;
|
|
105
135
|
}
|
|
136
|
+
/** A custom section node (spec 2026-09-14 §6): the columns' richtext (element nodes included), the row structure, the css.
|
|
137
|
+
* The generic walk's own words on `rows.<i>.spans` are dropped (`spans` is no descriptor type it knows) so rowsProblems
|
|
138
|
+
* is the one voice on structure; the columns' richtext errors keep their paths. */
|
|
139
|
+
export function validateCustomNode(node, fields, elements = {}) {
|
|
140
|
+
const errors = validateNode(node, { fields }, elements).filter((e) => !e.field.startsWith("rows.") || e.field.includes(".columns."));
|
|
141
|
+
return [...rowsProblems(node.fields?.rows), ...errors];
|
|
142
|
+
}
|
package/dist/collections.d.ts
CHANGED
|
@@ -9,10 +9,10 @@ export type EntryOrder = "manual" | {
|
|
|
9
9
|
export declare const DEFAULT_ORDER: EntryOrder;
|
|
10
10
|
export declare const BUILT_IN_ORDER_FIELDS: readonly ["createdAt", "updatedAt"];
|
|
11
11
|
export type CollectionSchema<F extends BuilderMap = BuilderMap, N extends string = string> = {
|
|
12
|
-
/** "
|
|
13
|
-
* 4): a
|
|
12
|
+
/** "global" marks a code-declared singleton (spec 2026-09-07 decision
|
|
13
|
+
* 4): a global item IS a collection with one entry, so both entry
|
|
14
14
|
* stores accept it in the same list and branch on this. */
|
|
15
|
-
kind: "collection" | "
|
|
15
|
+
kind: "collection" | "global";
|
|
16
16
|
/** Literal when declared inline (spec 2026-09-11 §6), so a queries object can check names. */
|
|
17
17
|
name: N;
|
|
18
18
|
title: string;
|
|
@@ -29,11 +29,6 @@ export type CollectionSchema<F extends BuilderMap = BuilderMap, N extends string
|
|
|
29
29
|
admin?: {
|
|
30
30
|
listColumns?: string[];
|
|
31
31
|
};
|
|
32
|
-
/** Per-locale version history for this collection's entries (spec
|
|
33
|
-
* 2026-09-06 §7): every save becomes an `entry_versions` snapshot and
|
|
34
|
-
* publish moves a pointer, as on pages. Off by default — a People list
|
|
35
|
-
* does not want history; articles do. */
|
|
36
|
-
versions?: boolean;
|
|
37
32
|
/** phantom: the entry value shape, used by f.ref target inference */
|
|
38
33
|
readonly __entry?: NamespaceValues<F>;
|
|
39
34
|
};
|
|
@@ -52,5 +47,4 @@ export declare function collection<F extends BuilderMap, const N extends string
|
|
|
52
47
|
admin?: {
|
|
53
48
|
listColumns?: string[];
|
|
54
49
|
};
|
|
55
|
-
versions?: boolean;
|
|
56
50
|
}): CollectionSchema<F, N>;
|
package/dist/collections.js
CHANGED
|
@@ -5,7 +5,7 @@ export const collectionDepth = (c) => c.tree?.depth ?? 1;
|
|
|
5
5
|
export function collection(input) {
|
|
6
6
|
if (!input?.name)
|
|
7
7
|
throw new Error("smoodly: a collection requires a name.");
|
|
8
|
-
if (["
|
|
8
|
+
if (["global", "asset", "media"].includes(input.name))
|
|
9
9
|
throw new Error(`smoodly: "${input.name}" is a reserved name.`);
|
|
10
10
|
if (input.tree !== undefined && (!Number.isInteger(input.tree.depth) || input.tree.depth < 1)) {
|
|
11
11
|
throw new Error(`smoodly: collection "${input.name}" declares an invalid tree depth.`);
|
|
@@ -43,7 +43,6 @@ export function collection(input) {
|
|
|
43
43
|
...(input.path !== undefined ? { path: input.path } : {}),
|
|
44
44
|
...(input.tree !== undefined ? { tree: input.tree } : {}),
|
|
45
45
|
...(input.order !== undefined ? { order: input.order } : {}),
|
|
46
|
-
...(input.versions ? { versions: true } : {}),
|
|
47
46
|
admin: input.admin,
|
|
48
47
|
};
|
|
49
48
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Descriptor } from "./fields.ts";
|
|
2
|
+
export type ConditionOp = "is" | "isNot" | "in" | "isEmpty" | "isNotEmpty";
|
|
3
|
+
export type Condition = {
|
|
4
|
+
path: string;
|
|
5
|
+
op: ConditionOp;
|
|
6
|
+
value?: unknown;
|
|
7
|
+
};
|
|
8
|
+
export declare function when(path: string): {
|
|
9
|
+
is: (value: unknown) => Condition;
|
|
10
|
+
isNot: (value: unknown) => Condition;
|
|
11
|
+
in: (values: readonly unknown[]) => Condition;
|
|
12
|
+
isEmpty: () => Condition;
|
|
13
|
+
isNotEmpty: () => Condition;
|
|
14
|
+
};
|
|
15
|
+
/** Where a condition reads: the current level's values, and the two namespaces. */
|
|
16
|
+
export type ConditionScope = {
|
|
17
|
+
self: Record<string, unknown> | undefined;
|
|
18
|
+
fields?: Record<string, unknown>;
|
|
19
|
+
settings?: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
/** `["settings", "mediaType"]` for a prefixed path, `[null, "kind"]` for a bare one. */
|
|
22
|
+
export declare function splitConditionPath(path: string): [("fields" | "settings") | null, string];
|
|
23
|
+
export declare function evaluate(c: Condition, scope: ConditionScope): boolean;
|
|
24
|
+
/** True when the descriptor's condition holds; false without one. */
|
|
25
|
+
export declare function isHidden(d: object, scope: ConditionScope): boolean;
|
|
26
|
+
/** Every condition in both namespaces names a field that exists: a typo
|
|
27
|
+
* fails at registration, not in the form. Walks objects (siblings are the
|
|
28
|
+
* object's keys) and lists (an object item's keys; a scalar item reads the
|
|
29
|
+
* list's own level). */
|
|
30
|
+
export declare function assertConditions(owner: string, maps: {
|
|
31
|
+
fields: Record<string, Descriptor>;
|
|
32
|
+
settings: Record<string, Descriptor>;
|
|
33
|
+
}): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export function when(path) {
|
|
2
|
+
if (typeof path !== "string" || path === "")
|
|
3
|
+
throw new Error("smoodly: f.when() needs a field path.");
|
|
4
|
+
return {
|
|
5
|
+
is: (value) => ({ path, op: "is", value }),
|
|
6
|
+
isNot: (value) => ({ path, op: "isNot", value }),
|
|
7
|
+
in: (values) => ({ path, op: "in", value: [...values] }),
|
|
8
|
+
isEmpty: () => ({ path, op: "isEmpty" }),
|
|
9
|
+
isNotEmpty: () => ({ path, op: "isNotEmpty" }),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const PREFIXES = ["fields", "settings"];
|
|
13
|
+
/** `["settings", "mediaType"]` for a prefixed path, `[null, "kind"]` for a bare one. */
|
|
14
|
+
export function splitConditionPath(path) {
|
|
15
|
+
for (const p of PREFIXES)
|
|
16
|
+
if (path.startsWith(`${p}.`))
|
|
17
|
+
return [p, path.slice(p.length + 1)];
|
|
18
|
+
return [null, path];
|
|
19
|
+
}
|
|
20
|
+
function lookup(path, scope) {
|
|
21
|
+
const [ns, key] = splitConditionPath(path);
|
|
22
|
+
const holder = ns === null ? scope.self : scope[ns];
|
|
23
|
+
return holder?.[key];
|
|
24
|
+
}
|
|
25
|
+
/** Empty as the validator sees it, plus a media value with no asset. */
|
|
26
|
+
const isEmptyValue = (v) => v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0) ||
|
|
27
|
+
(typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0) ||
|
|
28
|
+
(typeof v === "object" && !Array.isArray(v) && "asset" in v && typeof v.asset !== "string");
|
|
29
|
+
export function evaluate(c, scope) {
|
|
30
|
+
const v = lookup(c.path, scope);
|
|
31
|
+
switch (c.op) {
|
|
32
|
+
case "is": return v === c.value;
|
|
33
|
+
case "isNot": return v !== c.value;
|
|
34
|
+
case "in": return Array.isArray(c.value) && c.value.includes(v);
|
|
35
|
+
case "isEmpty": return isEmptyValue(v);
|
|
36
|
+
case "isNotEmpty": return !isEmptyValue(v);
|
|
37
|
+
default: return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const isCondition = (c) => typeof c === "object" && c !== null && typeof c.path === "string" && typeof c.op === "string";
|
|
41
|
+
/** True when the descriptor's condition holds; false without one. */
|
|
42
|
+
export function isHidden(d, scope) {
|
|
43
|
+
const { hidden } = d;
|
|
44
|
+
return isCondition(hidden) ? evaluate(hidden, scope) : false;
|
|
45
|
+
}
|
|
46
|
+
/** Every condition in both namespaces names a field that exists: a typo
|
|
47
|
+
* fails at registration, not in the form. Walks objects (siblings are the
|
|
48
|
+
* object's keys) and lists (an object item's keys; a scalar item reads the
|
|
49
|
+
* list's own level). */
|
|
50
|
+
export function assertConditions(owner, maps) {
|
|
51
|
+
const top = { fields: new Set(Object.keys(maps.fields)), settings: new Set(Object.keys(maps.settings)) };
|
|
52
|
+
const check = (key, d, siblings) => {
|
|
53
|
+
if (d.hidden === undefined)
|
|
54
|
+
return;
|
|
55
|
+
if (!isCondition(d.hidden))
|
|
56
|
+
throw new Error(`smoodly: ${owner}: "${key}" has a malformed hidden condition — use f.when(path).`);
|
|
57
|
+
const [ns, name] = splitConditionPath(d.hidden.path);
|
|
58
|
+
const known = ns === null ? siblings : top[ns];
|
|
59
|
+
if (!known.has(name))
|
|
60
|
+
throw new Error(`smoodly: ${owner}: "${key}" is hidden by "${d.hidden.path}", which is not a field.`);
|
|
61
|
+
};
|
|
62
|
+
const walk = (map) => {
|
|
63
|
+
const siblings = new Set(Object.keys(map));
|
|
64
|
+
for (const [key, d] of Object.entries(map)) {
|
|
65
|
+
check(key, d, siblings);
|
|
66
|
+
if (d.type === "object")
|
|
67
|
+
walk(d.fields);
|
|
68
|
+
if (d.type === "list") {
|
|
69
|
+
const item = d.item;
|
|
70
|
+
if (item.type === "object")
|
|
71
|
+
walk(item.fields);
|
|
72
|
+
else
|
|
73
|
+
check(key, item, siblings);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
walk(maps.fields);
|
|
78
|
+
walk(maps.settings);
|
|
79
|
+
}
|