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.
Files changed (154) hide show
  1. package/README.md +238 -36
  2. package/dist/admin/auth.d.ts +7 -4
  3. package/dist/admin/auth.js +1 -1
  4. package/dist/admin/client-ops.js +3 -3
  5. package/dist/admin/custom-section.d.ts +10 -0
  6. package/dist/admin/custom-section.js +10 -0
  7. package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
  8. package/dist/admin/editor/CanvasOverlay.js +6 -6
  9. package/dist/admin/editor/EditorView.d.ts +2 -0
  10. package/dist/admin/editor/EditorView.js +322 -62
  11. package/dist/admin/editor/GlobalInspector.d.ts +24 -0
  12. package/dist/admin/editor/GlobalInspector.js +13 -0
  13. package/dist/admin/editor/InsertPicker.d.ts +6 -5
  14. package/dist/admin/editor/InsertPicker.js +6 -4
  15. package/dist/admin/editor/InspectorFields.d.ts +21 -0
  16. package/dist/admin/editor/InspectorFields.js +22 -0
  17. package/dist/admin/editor/Outline.d.ts +0 -1
  18. package/dist/admin/editor/Outline.js +3 -3
  19. package/dist/admin/editor/PageSettings.js +3 -2
  20. package/dist/admin/editor/global-edit.d.ts +39 -0
  21. package/dist/admin/editor/global-edit.js +124 -0
  22. package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
  23. package/dist/admin/editor/useGlobalEdit.js +18 -0
  24. package/dist/admin/forms/FieldWidget.d.ts +14 -1
  25. package/dist/admin/forms/FieldWidget.js +38 -11
  26. package/dist/admin/forms/ListField.d.ts +9 -1
  27. package/dist/admin/forms/ListField.js +16 -8
  28. package/dist/admin/forms/RichtextElementCard.js +11 -5
  29. package/dist/admin/forms/RowsWidget.d.ts +13 -0
  30. package/dist/admin/forms/RowsWidget.js +26 -0
  31. package/dist/admin/forms/SpanBar.d.ts +5 -0
  32. package/dist/admin/forms/SpanBar.js +37 -0
  33. package/dist/admin/forms/richtext-insert.d.ts +2 -2
  34. package/dist/admin/forms/richtext-insert.js +3 -3
  35. package/dist/admin/forms/richtext-nodes.js +1 -1
  36. package/dist/admin/forms/tabs.d.ts +20 -8
  37. package/dist/admin/forms/tabs.js +19 -23
  38. package/dist/admin/forms/useRichtextEditor.js +8 -1
  39. package/dist/admin/global-items.d.ts +9 -0
  40. package/dist/admin/{shared-items.js → global-items.js} +29 -14
  41. package/dist/admin/index.d.ts +3 -3
  42. package/dist/admin/index.js +1 -1
  43. package/dist/admin/next/op-handler.js +4 -3
  44. package/dist/admin/ops-impl.d.ts +3 -0
  45. package/dist/admin/ops-impl.js +242 -56
  46. package/dist/admin/ops.d.ts +38 -6
  47. package/dist/admin/serialize.d.ts +18 -7
  48. package/dist/admin/serialize.js +27 -5
  49. package/dist/admin/shell/AdminApp.js +7 -7
  50. package/dist/admin/shell/EntriesList.js +19 -3
  51. package/dist/admin/shell/EntryForm.js +183 -77
  52. package/dist/admin/shell/GlobalFields.d.ts +24 -0
  53. package/dist/admin/shell/GlobalFields.js +19 -0
  54. package/dist/admin/shell/{SharedForm.d.ts → GlobalForm.d.ts} +1 -1
  55. package/dist/admin/shell/GlobalForm.js +258 -0
  56. package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
  57. package/dist/admin/shell/{SharedList.js → GlobalList.js} +12 -11
  58. package/dist/admin/shell/IconRail.js +14 -7
  59. package/dist/admin/shell/PagesList.js +4 -8
  60. package/dist/admin/shell/entries-list.d.ts +2 -2
  61. package/dist/admin/shell/entries-list.js +3 -3
  62. package/dist/admin/shell/{shared-list.d.ts → global-list.d.ts} +3 -3
  63. package/dist/admin/shell/{shared-list.js → global-list.js} +2 -2
  64. package/dist/admin/supabase-auth.js +1 -1
  65. package/dist/admin/tree-ops.d.ts +10 -8
  66. package/dist/admin/tree-ops.js +28 -16
  67. package/dist/admin/ui/ActionMenu.d.ts +11 -0
  68. package/dist/admin/ui/ActionMenu.js +25 -0
  69. package/dist/admin/ui/HistoryModal.d.ts +23 -0
  70. package/dist/admin/ui/HistoryModal.js +40 -0
  71. package/dist/admin/ui/Modal.d.ts +5 -1
  72. package/dist/admin/ui/Modal.js +3 -3
  73. package/dist/admin/ui/PublishControl.d.ts +31 -0
  74. package/dist/admin/ui/PublishControl.js +24 -0
  75. package/dist/admin/ui/format.d.ts +4 -0
  76. package/dist/admin/ui/format.js +9 -0
  77. package/dist/admin/ui/primitives.d.ts +16 -1
  78. package/dist/admin/ui/primitives.js +20 -4
  79. package/dist/admin/ui/status-label.d.ts +10 -0
  80. package/dist/admin/ui/status-label.js +14 -0
  81. package/dist/admin/ui/theme.js +93 -13
  82. package/dist/admin/validate.d.ts +27 -2
  83. package/dist/admin/validate.js +47 -10
  84. package/dist/collections.d.ts +3 -9
  85. package/dist/collections.js +1 -2
  86. package/dist/conditions.d.ts +33 -0
  87. package/dist/conditions.js +79 -0
  88. package/dist/config.d.ts +26 -19
  89. package/dist/config.js +26 -25
  90. package/dist/css.d.ts +7 -0
  91. package/dist/css.js +57 -0
  92. package/dist/custom/CustomSection.d.ts +26 -0
  93. package/dist/custom/CustomSection.js +20 -0
  94. package/dist/custom/css.d.ts +3 -0
  95. package/dist/custom/css.js +28 -0
  96. package/dist/custom/index.d.ts +26 -0
  97. package/dist/custom/index.js +16 -0
  98. package/dist/custom/rows.d.ts +28 -0
  99. package/dist/custom/rows.js +129 -0
  100. package/dist/custom/schema.d.ts +32 -0
  101. package/dist/custom/schema.js +39 -0
  102. package/dist/entry-store.d.ts +42 -17
  103. package/dist/entry-store.js +107 -40
  104. package/dist/fields.d.ts +5 -2
  105. package/dist/fields.js +4 -2
  106. package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
  107. package/dist/{shared-id.js → global-id.js} +4 -4
  108. package/dist/global.d.ts +57 -0
  109. package/dist/global.js +51 -0
  110. package/dist/index.d.ts +28 -18
  111. package/dist/index.js +10 -5
  112. package/dist/media.js +1 -1
  113. package/dist/next/index.d.ts +1 -1
  114. package/dist/next/page-renderer.d.ts +4 -2
  115. package/dist/next/page-renderer.js +17 -13
  116. package/dist/next/queries.d.ts +6 -6
  117. package/dist/next/queries.js +7 -7
  118. package/dist/page.js +2 -2
  119. package/dist/pairing.d.ts +13 -7
  120. package/dist/pairing.js +4 -4
  121. package/dist/refs.d.ts +2 -2
  122. package/dist/refs.js +4 -4
  123. package/dist/render.d.ts +13 -1
  124. package/dist/render.js +10 -5
  125. package/dist/resolve.d.ts +11 -6
  126. package/dist/resolve.js +22 -12
  127. package/dist/revalidate.d.ts +4 -4
  128. package/dist/revalidate.js +4 -4
  129. package/dist/richtext-render.js +2 -2
  130. package/dist/richtext-walk.d.ts +5 -5
  131. package/dist/richtext-walk.js +4 -4
  132. package/dist/schema.d.ts +15 -10
  133. package/dist/schema.js +19 -7
  134. package/dist/section-wrapper.d.ts +18 -6
  135. package/dist/section-wrapper.js +24 -17
  136. package/dist/site.d.ts +17 -5
  137. package/dist/site.js +36 -12
  138. package/dist/sql-space.js +1 -1
  139. package/dist/sql.js +19 -16
  140. package/dist/store.d.ts +45 -13
  141. package/dist/store.js +88 -12
  142. package/dist/styles.d.ts +27 -0
  143. package/dist/styles.js +60 -0
  144. package/dist/supabase-entry-store.d.ts +11 -6
  145. package/dist/supabase-entry-store.js +115 -39
  146. package/dist/supabase-store.d.ts +13 -6
  147. package/dist/supabase-store.js +90 -20
  148. package/dist/zones.d.ts +9 -7
  149. package/dist/zones.js +5 -5
  150. package/package.json +1 -1
  151. package/dist/admin/shared-items.d.ts +0 -9
  152. package/dist/admin/shell/SharedForm.js +0 -134
  153. package/dist/shared.d.ts +0 -46
  154. package/dist/shared.js +0 -36
@@ -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-draft-fg:var(--amber-700); --status-draft-bg:var(--amber-50);
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-draft-fg:var(--amber-300); --status-draft-bg:rgba(232,184,90,.14);
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-draft-fg:var(--amber-700); --status-draft-bg:var(--amber-50);
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-draft-fg:var(--amber-300); --status-draft-bg:rgba(232,184,90,.14);
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:1.75;stroke-linecap:round;stroke-linejoin:round}
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
- .sm-rtcard__thumb{display:block;width:100%;max-height:160px;object-fit:cover;border-radius:var(--radius-sm);background:var(--surface-inset)}
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 shared forms) is a stack of
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
- /* The form card: the main column of the entry and shared forms. Every
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 blocks: a labelled pill (the label is
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
- .sm-modal{position:fixed;inset:0;z-index:70;background:rgba(0,0,0,.45);display:grid;place-items:center;padding:24px}
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);
@@ -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
- styles?: Record<string, Desc>;
23
+ settings?: Record<string, Desc>;
18
24
  }>;
19
- export declare function validateFields(values: Record<string, unknown>, descriptors: Record<string, Desc>, elements?: ElementDescs): FieldError[];
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 {};
@@ -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
- for (const [k, sub] of Object.entries(el.fields))
38
- validateValue(`${path}.elements.${i}.${k}`, fields[k], sub, errors, elements);
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
- if (item)
61
- value.forEach((v, i) => validateValue(`${path}.${i}`, v, item, errors, elements));
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
- for (const [k, sub] of Object.entries(subfields(d)))
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
- export function validateFields(values, descriptors, elements = {}) {
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
- for (const [field, d] of Object.entries(descriptors))
103
- validateValue(field, values[field], d, errors, elements);
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
+ }
@@ -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
- /** "shared" marks a code-declared singleton (spec 2026-09-07 decision
13
- * 4): a shared item IS a collection with one entry, so both entry
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" | "shared";
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>;
@@ -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 (["shared", "asset", "media"].includes(input.name))
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
+ }