smoodly 0.0.8 → 0.0.9
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 +145 -11
- package/dist/admin/forms/FieldWidget.d.ts +7 -3
- package/dist/admin/forms/FieldWidget.js +85 -7
- package/dist/admin/forms/list.d.ts +18 -0
- package/dist/admin/forms/list.js +81 -0
- package/dist/admin/richtext.d.ts +13 -11
- package/dist/admin/richtext.js +94 -13
- package/dist/admin/serialize.d.ts +4 -0
- package/dist/admin/serialize.js +3 -0
- package/dist/admin/shell/EntryForm.js +1 -1
- package/dist/admin/shell/SharedForm.js +1 -1
- package/dist/admin/ui/theme.d.ts +1 -1
- package/dist/admin/ui/theme.js +13 -0
- package/dist/admin/validate.d.ts +7 -2
- package/dist/admin/validate.js +43 -13
- package/dist/entry-store.js +7 -1
- package/dist/fields.d.ts +16 -1
- package/dist/fields.js +49 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/refs.js +5 -0
- package/dist/resolve.js +3 -0
- package/dist/richtext-render.d.ts +3 -0
- package/dist/richtext-render.js +62 -0
- package/dist/shared-id.d.ts +3 -0
- package/dist/shared-id.js +36 -0
- package/dist/sql-space.d.ts +4 -0
- package/dist/sql-space.js +5 -1
- package/dist/sql.js +1 -1
- package/dist/supabase-entry-store.d.ts +8 -0
- package/dist/supabase-entry-store.js +37 -2
- package/package.json +1 -1
package/dist/admin/ui/theme.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const ADMIN_CSS = "\n@import url(\"https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;500;600;700&display=swap\");\n\n.sm-admin{\n --font-sans:'Public Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;\n --font-mono:ui-monospace,Menlo,Consolas,monospace;\n --text-2xs:9px; --text-xs:10px; --text-sm:11px; --text-md:12px;\n --text-lg:13px; --text-xl:14px; --text-2xl:15px; --text-3xl:20px; --text-4xl:30px;\n --tracking-tight:-0.02em; --tracking-label:0.1em; --tracking-tag:0.08em;\n\n --radius-xs:1px; --radius-sm:3px; --radius-md:4px; --radius-round:999px;\n --control-h:28px; --control-h-lg:36px; --row-h:38px; --topbar-h:48px;\n --nav-w:200px; --inspector-w:300px;\n --border:1px solid var(--line);\n --outline-selection:1.5px solid var(--selection);\n\n --brand-600:#0038B1; --brand-500:#2554D6; --brand-300:#6E96FF;\n --brand-200:#A9C0FF; --brand-100:#DCE4F9; --brand-50:#E3EAFA;\n --gray-0:#FFFFFF; --gray-50:#F1F2F6; --gray-100:#E1E4EC; --gray-300:#C9D0E2;\n --gray-500:#8B93A7; --gray-600:#6B7386; --gray-700:#4A5165; --gray-900:#0F1420;\n --green-700:#1F7A46; --green-50:#E3F3EA; --green-300:#6FD39A; --green-500:#2E9E5B;\n --amber-700:#8A5A00; --amber-50:#FBF1DC; --amber-300:#E8B85A;\n --red-700:#B42318; --red-50:#FDECEC; --red-300:#F5A5A0;\n\n --bg:var(--gray-50); --surface:var(--gray-0); --surface-inset:var(--gray-50);\n --input-bg:var(--gray-0); --line:var(--gray-100); --line-dashed:var(--gray-300);\n --text:var(--gray-900); --text-muted:var(--gray-600); --text-subtle:var(--gray-500);\n --accent:var(--brand-600); --on-accent:#FFFFFF; --accent-soft:var(--brand-50);\n --selection:var(--brand-600); --nav-active-bg:var(--brand-100); --nav-active-fg:var(--brand-600);\n --hover:rgba(15,20,32,.045); --press:rgba(15,20,32,.08);\n --control-hover:var(--gray-100);\n\n --status-published-fg:var(--green-700); --status-published-bg:var(--green-50);\n --status-draft-fg:var(--amber-700); --status-draft-bg:var(--amber-50);\n --status-scheduled-fg:var(--accent); --status-scheduled-bg:var(--accent-soft);\n --status-saved-dot:var(--green-500);\n --warn:var(--amber-700); --warn-soft:var(--amber-50);\n --danger:var(--red-700); --danger-soft:var(--red-50);\n\n --page-bg:#FFFFFF; --page-text:var(--gray-900);\n --shadow-page:0 1px 2px rgba(0,0,0,.08), 0 8px 24px rgba(0,0,0,.06);\n --shadow-float:0 2px 8px rgba(0,0,0,.25);\n --shadow-seg:0 1px 2px rgba(0,0,0,.08);\n --focus-ring:0 0 0 3px var(--accent-soft);\n --shadow-pop:0 8px 32px rgba(0,0,0,.18);\n --logo-filter:none;\n\n /* Which half of the theme toggle is visible. A token, so the icon can never\n disagree with the colors \u2014 both follow the one cascade below. */\n --when-light:inline-flex; --when-dark:none;\n}\nhtml[data-theme=\"dark\"] .sm-admin,.sm-admin[data-theme=\"dark\"]{\n --bg:#0D0F14; --surface:#15181F; --surface-inset:#0D0F14; --input-bg:#1B1F28;\n --line:#2A2F3B; --line-dashed:#2A2F3B;\n --text:#EEF0F5; --text-muted:#8C94A8; --text-subtle:#6B7386;\n --accent:var(--brand-300); --on-accent:#06122F; --accent-soft:rgba(110,150,255,.16);\n --nav-active-bg:rgba(110,150,255,.14); --nav-active-fg:var(--brand-200);\n --hover:rgba(238,240,245,.05); --press:rgba(238,240,245,.09);\n --control-hover:var(--input-bg);\n\n --status-published-fg:var(--green-300); --status-published-bg:rgba(111,211,154,.14);\n --status-draft-fg:var(--amber-300); --status-draft-bg:rgba(232,184,90,.14);\n --warn:var(--amber-300); --warn-soft:rgba(232,184,90,.14);\n --danger:var(--red-300); --danger-soft:rgba(244,102,92,.14);\n\n --shadow-page:0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.35);\n --shadow-pop:0 8px 32px rgba(0,0,0,.5);\n --logo-filter:brightness(0) invert(1);\n --when-light:none; --when-dark:inline-flex;\n}\n@media (prefers-color-scheme:dark){\n html:not([data-theme=\"light\"]) .sm-admin:not([data-theme=\"light\"]){\n --bg:#0D0F14; --surface:#15181F; --surface-inset:#0D0F14; --input-bg:#1B1F28;\n --line:#2A2F3B; --line-dashed:#2A2F3B;\n --text:#EEF0F5; --text-muted:#8C94A8; --text-subtle:#6B7386;\n --accent:var(--brand-300); --on-accent:#06122F; --accent-soft:rgba(110,150,255,.16);\n --nav-active-bg:rgba(110,150,255,.14); --nav-active-fg:var(--brand-200);\n --hover:rgba(238,240,245,.05); --press:rgba(238,240,245,.09);\n --control-hover:var(--input-bg);\n\n --status-published-fg:var(--green-300); --status-published-bg:rgba(111,211,154,.14);\n --status-draft-fg:var(--amber-300); --status-draft-bg:rgba(232,184,90,.14);\n --warn:var(--amber-300); --warn-soft:rgba(232,184,90,.14);\n --danger:var(--red-300); --danger-soft:rgba(244,102,92,.14);\n\n --shadow-page:0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.35);\n --shadow-pop:0 8px 32px rgba(0,0,0,.5);\n --logo-filter:brightness(0) invert(1);\n --when-light:none; --when-dark:inline-flex;\n}\n}\n/* Last, and the same specificity as the host-attribute rule: an explicit\n choice from the theme toggle wins over a dark <html> and over the OS. */\n.sm-admin[data-theme=\"light\"]{\n --brand-600:#0038B1; --brand-500:#2554D6; --brand-300:#6E96FF;\n --brand-200:#A9C0FF; --brand-100:#DCE4F9; --brand-50:#E3EAFA;\n --gray-0:#FFFFFF; --gray-50:#F1F2F6; --gray-100:#E1E4EC; --gray-300:#C9D0E2;\n --gray-500:#8B93A7; --gray-600:#6B7386; --gray-700:#4A5165; --gray-900:#0F1420;\n --green-700:#1F7A46; --green-50:#E3F3EA; --green-300:#6FD39A; --green-500:#2E9E5B;\n --amber-700:#8A5A00; --amber-50:#FBF1DC; --amber-300:#E8B85A;\n --red-700:#B42318; --red-50:#FDECEC; --red-300:#F5A5A0;\n\n --bg:var(--gray-50); --surface:var(--gray-0); --surface-inset:var(--gray-50);\n --input-bg:var(--gray-0); --line:var(--gray-100); --line-dashed:var(--gray-300);\n --text:var(--gray-900); --text-muted:var(--gray-600); --text-subtle:var(--gray-500);\n --accent:var(--brand-600); --on-accent:#FFFFFF; --accent-soft:var(--brand-50);\n --selection:var(--brand-600); --nav-active-bg:var(--brand-100); --nav-active-fg:var(--brand-600);\n --hover:rgba(15,20,32,.045); --press:rgba(15,20,32,.08);\n --control-hover:var(--gray-100);\n\n --status-published-fg:var(--green-700); --status-published-bg:var(--green-50);\n --status-draft-fg:var(--amber-700); --status-draft-bg:var(--amber-50);\n --status-scheduled-fg:var(--accent); --status-scheduled-bg:var(--accent-soft);\n --status-saved-dot:var(--green-500);\n --warn:var(--amber-700); --warn-soft:var(--amber-50);\n --danger:var(--red-700); --danger-soft:var(--red-50);\n\n --page-bg:#FFFFFF; --page-text:var(--gray-900);\n --shadow-page:0 1px 2px rgba(0,0,0,.08), 0 8px 24px rgba(0,0,0,.06);\n --shadow-float:0 2px 8px rgba(0,0,0,.25);\n --shadow-seg:0 1px 2px rgba(0,0,0,.08);\n --focus-ring:0 0 0 3px var(--accent-soft);\n --shadow-pop:0 8px 32px rgba(0,0,0,.18);\n --logo-filter:none;\n\n /* Which half of the theme toggle is visible. A token, so the icon can never\n disagree with the colors \u2014 both follow the one cascade below. */\n --when-light:inline-flex; --when-dark:none;\n}\n\n/* The admin owns the viewport; a host <body> margin would push it into a\n page scrollbar. Scoped by :has, so only a page holding the admin is touched. */\nbody:has(.sm-admin){margin:0}\n.sm-admin,.sm-admin *,.sm-admin *::before,.sm-admin *::after{box-sizing:border-box}\n.sm-admin{\n font-family:var(--font-sans);font-size:var(--text-md);line-height:1.4;\n color:var(--text);background:var(--surface);\n -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;\n}\n.sm-admin h1,.sm-admin h2,.sm-admin h3,.sm-admin p,.sm-admin ul{margin:0}\n/* :where() keeps this at zero specificity \u2014 a bare <a> is accent, but any\n component class on it (.sm-nav-item, .sm-btn, .sm-back) decides its own\n colour. Without this the element selector silently beat every one of them. */\n:where(.sm-admin a){color:var(--accent);text-decoration:none}\n:where(.sm-admin a:hover){color:var(--brand-500)}\n.sm-admin button,.sm-admin input,.sm-admin select,.sm-admin textarea{font-family:inherit}\n.sm-admin ::placeholder{color:var(--text-subtle)}\n.sm-admin ::selection{background:var(--accent-soft);color:var(--text)}\n.sm-mono{font-family:var(--font-mono)}\n\n/* \u2500\u2500 shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-shell{display:grid;grid-template-columns:var(--nav-w) 1fr;height:100vh;height:100dvh;overflow:hidden}\n.sm-shell>.sm-main{min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden}\n.sm-nav{\n display:flex;flex-direction:column;gap:24px;padding:16px 12px;\n border-right:var(--border);background:var(--surface);min-width:0;overflow:auto;\n}\n.sm-nav__logo{height:18px;width:auto;align-self:flex-start;filter:var(--logo-filter);flex:none}\n.sm-nav>.sm-nav__logo{margin-left:10px}\n.sm-nav__list{display:flex;flex-direction:column;gap:1px}\n.sm-nav-item{\n display:block;padding:6px 10px;border-radius:var(--radius-md);line-height:1.3;\n color:var(--text);font-weight:500;text-decoration:none;cursor:pointer;\n background:transparent;border:0;width:100%;text-align:left;font-size:var(--text-md);\n overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\n}\n.sm-nav-item:hover{background:var(--surface-inset);color:var(--text)}\n.sm-nav-item[aria-current=\"page\"]{background:var(--nav-active-bg);color:var(--nav-active-fg);font-weight:600}\n.sm-nav-item[aria-current=\"page\"]:hover{background:var(--nav-active-bg);color:var(--nav-active-fg)}\n/* Not greyed: the reference renders every inactive item at --text and marks\n the unavailable ones only by being inert (no pointer, no hover). */\n.sm-nav-item[aria-disabled=\"true\"]{cursor:default}\n.sm-nav-item[aria-disabled=\"true\"]:hover{background:transparent}\n.sm-nav__foot{margin-top:auto;display:flex;flex-direction:column;gap:2px}\n.sm-nav__user{display:flex;align-items:center;gap:10px;padding:6px 10px;min-width:0}\n.sm-nav-item--icon{display:flex;align-items:center;gap:8px}\n.sm-when-light{display:var(--when-light);align-items:center;gap:8px}\n.sm-when-dark{display:var(--when-dark);align-items:center;gap:8px}\n.sm-avatar{\n width:24px;height:24px;flex:none;border-radius:var(--radius-round);\n background:var(--accent-soft);color:var(--accent);\n display:grid;place-items:center;font-size:var(--text-sm);font-weight:600;\n}\n\n/* \u2500\u2500 top bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-topbar{\n height:var(--topbar-h);flex:none;display:flex;align-items:center;justify-content:space-between;\n gap:16px;padding:0 16px;border-bottom:var(--border);background:var(--surface);\n}\n.sm-topbar__left{display:flex;align-items:center;gap:10px;min-width:0}\n.sm-topbar__right{display:flex;align-items:center;gap:8px;flex:none}\n.sm-crumbs{display:flex;align-items:center;gap:8px;color:var(--text-muted);min-width:0}\n.sm-crumbs__sep{opacity:.5}\n.sm-crumbs__here{color:var(--text);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.sm-back{border:0;background:transparent;padding:0;color:var(--text);font:inherit;cursor:pointer;line-height:1}\n.sm-back:hover{color:var(--accent)}\n.sm-title{font-size:var(--text-xl);font-weight:600}\n.sm-count{color:var(--text-muted)}\n\n/* \u2500\u2500 buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-btn{\n display:inline-flex;align-items:center;justify-content:center;gap:6px;\n height:var(--control-h);padding:0 12px;border-radius:var(--radius-md);border:0;\n font-size:var(--text-md);font-weight:500;line-height:1;white-space:nowrap;cursor:pointer;\n text-decoration:none;\n}\n.sm-btn--lg{height:var(--control-h-lg);padding:0 16px}\n.sm-btn--primary{background:var(--accent);color:var(--on-accent);font-weight:600;padding:0 14px}\n.sm-btn--primary:hover{background:var(--brand-500);color:var(--on-accent)}\n.sm-btn--secondary{background:var(--surface-inset);color:var(--text)}\n.sm-btn--secondary:hover{background:var(--control-hover);color:var(--text)}\n.sm-btn--ghost{background:transparent;color:var(--accent);font-weight:600;padding:0 8px}\n.sm-btn--ghost:hover{background:var(--accent-soft);color:var(--accent)}\n.sm-btn--danger{background:var(--surface-inset);color:var(--danger)}\n.sm-btn--danger:hover{background:var(--danger-soft);color:var(--danger)}\n.sm-btn--ok{background:var(--status-published-bg);color:var(--status-published-fg);font-weight:600;padding:0 14px}\n.sm-btn:disabled,.sm-btn[aria-disabled=\"true\"]{opacity:.5;cursor:default;pointer-events:none}\n.sm-btn:focus-visible,.sm-iconbtn:focus-visible,.sm-nav-item:focus-visible,\n.sm-tab:focus-visible,.sm-seg__opt:focus-visible,.sm-row:focus-visible{\n outline:2px solid var(--accent);outline-offset:1px\n}\n\n.sm-iconbtn{\n width:24px;height:24px;flex:none;display:grid;place-items:center;padding:0;\n border:0;border-radius:var(--radius-sm);background:transparent;color:var(--text-muted);\n font-size:12px;line-height:1;cursor:pointer;\n}\n.sm-iconbtn:hover{background:var(--hover);color:var(--text)}\n.sm-iconbtn--inverse{color:#EEF0F5}\n.sm-iconbtn--inverse:hover{background:rgba(255,255,255,.14);color:#fff}\n.sm-iconbtn:disabled{opacity:.3;cursor:default}\n.sm-iconbtn:disabled:hover{background:transparent}\n\n/* \u2500\u2500 fields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-field{display:flex;flex-direction:column;gap:6px;min-width:0}\n.sm-field__head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}\n.sm-label{\n font-size:var(--text-2xs);font-weight:600;letter-spacing:var(--tracking-label);\n text-transform:uppercase;color:var(--text-muted);\n}\n.sm-counter,.sm-hint{font-size:var(--text-sm);color:var(--text-muted)}\n.sm-input,.sm-textarea,.sm-select{\n width:100%;min-width:0;border:1.5px solid transparent;border-radius:var(--radius-md);\n background:var(--surface-inset);color:var(--text);font-size:var(--text-md);\n outline:0;box-shadow:none;\n}\n.sm-input,.sm-select{height:var(--control-h);padding:0 10px}\n.sm-textarea{padding:8px 10px;line-height:1.5;resize:vertical;min-height:60px}\n.sm-input--lg{height:var(--control-h-lg);font-size:var(--text-2xl);font-weight:600}\n.sm-input--mono{font-family:var(--font-mono);font-size:var(--text-sm)}\n.sm-input:hover,.sm-textarea:hover,.sm-select:hover{background:var(--control-hover)}\n.sm-input:focus,.sm-textarea:focus,.sm-select:focus,\n.sm-inputwrap:focus-within{\n background:var(--input-bg);border-color:var(--selection);box-shadow:var(--focus-ring);\n}\n.sm-input:disabled,.sm-textarea:disabled,.sm-select:disabled{opacity:.6;cursor:default}\n.sm-select{\n appearance:none;-webkit-appearance:none;cursor:pointer;padding-right:26px;font-weight:500;\n background-image:linear-gradient(45deg,transparent 50%,var(--text-muted) 50%),\n linear-gradient(135deg,var(--text-muted) 50%,transparent 50%);\n background-position:calc(100% - 14px) 12px,calc(100% - 10px) 12px;\n background-size:4px 4px,4px 4px;background-repeat:no-repeat;\n}\n.sm-inputwrap{\n display:flex;align-items:center;gap:0;height:var(--control-h);padding:0 10px;\n border:1.5px solid transparent;border-radius:var(--radius-md);background:var(--surface-inset);\n}\n.sm-inputwrap>.sm-input{height:auto;padding:0;background:transparent;border:0;box-shadow:none}\n.sm-inputwrap>.sm-input:hover,.sm-inputwrap>.sm-input:focus{background:transparent;box-shadow:none}\n.sm-prefix{color:var(--text-muted);font-family:var(--font-mono);font-size:var(--text-sm);flex:none}\n.sm-error{color:var(--danger);font-size:var(--text-sm)}\n\n/* \u2500\u2500 data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-table{display:flex;flex-direction:column;padding:0 16px 16px}\n.sm-table__head,.sm-row{display:grid;gap:12px;align-items:center;padding:0 8px}\n.sm-table__head{\n height:34px;border-bottom:var(--border);font-family:var(--font-mono);\n font-size:var(--text-2xs);letter-spacing:var(--tracking-tag);color:var(--text-muted);\n text-transform:uppercase;position:sticky;top:0;background:var(--surface);z-index:1;\n}\n.sm-table__body{display:flex;flex-direction:column;padding-top:4px}\n.sm-row{\n height:var(--row-h);border-radius:var(--radius-md);background:transparent;\n border:0;font:inherit;color:inherit;text-align:left;width:100%;\n}\n.sm-row--clickable{cursor:pointer}\n.sm-row--clickable:hover{background:var(--surface-inset)}\n.sm-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}\n.sm-cell--muted{color:var(--text-muted)}\n.sm-cell--mono{font-family:var(--font-mono);font-size:var(--text-sm);color:var(--text-muted)}\n.sm-cell--title{font-weight:500}\n.sm-empty{padding:28px 8px;color:var(--text-muted)}\n.sm-chevron{\n width:12px;flex:none;border:0;background:none;padding:0;font:inherit;font-size:10px;\n color:var(--text-muted);cursor:pointer;line-height:1;\n}\n.sm-chevron:hover{color:var(--text)}\n.sm-cell--tree{display:flex;align-items:center;gap:6px;font-weight:500}\n.sm-cell--tree.sm-cell--parent{font-weight:600}\n.sm-rowmenu{\n border:0;background:none;padding:0 6px;font:inherit;color:var(--text-muted);cursor:pointer;\n border-radius:var(--radius-md);line-height:1;height:24px;\n}\n.sm-rowmenu:hover{background:var(--surface-inset);color:var(--text)}\n\n.sm-badge{\n display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:var(--radius-md);\n font-size:var(--text-xs);font-weight:600;letter-spacing:.04em;white-space:nowrap;\n}\n.sm-savestate{\n display:inline-flex;align-items:center;gap:6px;padding:0 6px;\n font-size:var(--text-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;\n}\n.sm-dot{width:5px;height:5px;border-radius:var(--radius-round);flex:none}\n\n/* \u2500\u2500 navigation bits \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-tabs{display:flex;gap:18px;padding:0 14px;height:36px;align-items:stretch;border-bottom:var(--border);flex:none}\n.sm-tab{\n display:flex;align-items:center;border:0;background:transparent;padding:0;font:inherit;\n cursor:pointer;color:var(--text-muted);border-bottom:2px solid transparent;margin-bottom:-1px;\n}\n.sm-tab:hover{color:var(--text)}\n.sm-tab[aria-selected=\"true\"]{color:var(--accent);font-weight:600;border-bottom-color:var(--accent)}\n.sm-seg{display:flex;gap:2px;padding:2px;background:var(--surface-inset);border-radius:var(--radius-md)}\n.sm-seg--stretch{width:100%}\n.sm-seg--disabled{opacity:.6}\n.sm-seg__opt:disabled{cursor:default}\n.sm-seg__opt{\n flex:1;padding:3px 10px;border:0;border-radius:var(--radius-sm);background:transparent;\n color:var(--text-muted);font:inherit;cursor:pointer;white-space:nowrap;\n}\n.sm-seg__opt:hover{color:var(--text)}\n.sm-seg__opt[aria-pressed=\"true\"]{background:var(--surface);color:var(--text);font-weight:600;box-shadow:var(--shadow-seg)}\n.sm-divider{height:1px;background:var(--line);flex:none}\n.sm-sectionlabel{display:flex;align-items:center;justify-content:space-between;gap:8px}\n.sm-sectionlabel--mono>.sm-label{\n font-family:var(--font-mono);font-weight:400;letter-spacing:var(--tracking-tag);\n}\n.sm-banner{\n display:flex;gap:14px;align-items:flex-start;padding:10px 14px;border-radius:var(--radius-md);\n font-size:var(--text-md);\n}\n.sm-banner--error{background:var(--danger-soft);color:var(--danger)}\n.sm-banner--ok{background:var(--status-published-bg);color:var(--status-published-fg)}\n.sm-banner--warn{background:var(--warn-soft);color:var(--warn)}\n\n/* \u2500\u2500 editor \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-editor{flex:1;min-height:0;display:grid;grid-template-columns:1fr var(--inspector-w)}\n.sm-canvas{display:flex;flex-direction:column;background:var(--bg);min-width:0;min-height:0;overflow:hidden}\n.sm-canvas__meta{\n display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 20px;flex:none;\n font-family:var(--font-mono);font-size:var(--text-xs);letter-spacing:.04em;color:var(--text-muted);\n}\n.sm-canvas__scroll{flex:1;min-height:0;overflow:auto;padding:0 20px 20px}\n.sm-page{\n position:relative;background:var(--page-bg);border-radius:var(--radius-md);\n box-shadow:var(--shadow-page);min-height:100%;\n}\n.sm-inspector{\n display:flex;flex-direction:column;min-height:0;overflow:hidden;\n border-left:var(--border);background:var(--surface);\n}\n.sm-inspector__head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:12px 14px 10px;flex:none}\n.sm-inspector__body{display:flex;flex-direction:column;gap:14px;padding:14px;overflow:auto}\n.sm-blockglyph{width:14px;height:10px;border:1.5px solid var(--text-muted);border-radius:var(--radius-xs);flex:none}\n.sm-blocktag{\n position:absolute;top:0;left:0;background:var(--selection);color:#fff;\n font-family:var(--font-mono);font-size:var(--text-2xs);font-weight:700;\n letter-spacing:var(--tracking-tag);text-transform:uppercase;padding:3px 7px;\n border-radius:var(--radius-sm) 0 var(--radius-sm) 0;white-space:nowrap;\n}\n.sm-blocktoolbar{\n display:flex;gap:2px;padding:2px;background:#15181F;border-radius:var(--radius-md);\n box-shadow:var(--shadow-float);\n}\n.sm-plus{\n width:24px;height:24px;border-radius:var(--radius-round);border:0;background:var(--selection);\n color:#fff;display:grid;place-items:center;font-size:15px;line-height:1;cursor:pointer;\n box-shadow:0 2px 6px rgba(0,56,177,.35);padding:0;\n}\n.sm-plus:hover{background:var(--brand-500)}\n.sm-guide{height:1px;background:color-mix(in srgb,var(--selection) 35%,transparent)}\n.sm-tooltip{\n background:#15181F;color:#EEF0F5;font-size:var(--text-sm);padding:4px 8px;\n border-radius:var(--radius-sm);white-space:nowrap;\n}\n.sm-outline-row{\n display:flex;align-items:center;justify-content:space-between;gap:4px;\n height:var(--control-h);border-radius:var(--radius-md);padding:0 4px 0 10px;\n color:var(--text);font-weight:500;background:transparent;\n border:1px solid var(--line);\n}\n.sm-outline-row--clickable{cursor:pointer}\n.sm-outline-row--clickable:hover{background:var(--surface-inset)}\n/* Selected and locked rows are read by their fill, so they drop the hairline \u2014\n border-color, not border, so every row keeps the same 28px box. */\n.sm-outline-row--selected,.sm-outline-row--selected:hover{\n background:var(--nav-active-bg);color:var(--nav-active-fg);font-weight:600;border-color:transparent;\n}\n.sm-outline-row--empty{border:1px dashed var(--line-dashed);color:var(--text-muted);padding:0 10px}\n.sm-outline-row--locked{background:var(--surface-inset);color:var(--text-muted);padding:0 10px;border-color:transparent}\n.sm-outline-row__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}\n\n.sm-picker{\n width:300px;background:var(--surface);border:var(--border);border-radius:var(--radius-md);\n padding:6px;box-shadow:var(--shadow-pop);\n}\n.sm-picker__item{\n display:flex;gap:10px;align-items:center;width:100%;text-align:left;background:transparent;\n border:0;border-radius:var(--radius-md);padding:8px 8px;cursor:pointer;color:var(--text);font:inherit;\n}\n.sm-picker__item:hover{background:var(--nav-active-bg)}\n.sm-picker__icon{\n width:26px;height:26px;flex:none;border-radius:var(--radius-sm);background:var(--accent-soft);\n color:var(--accent);display:grid;place-items:center;font-size:13px;\n}\n.sm-dashed{\n border:1px dashed var(--line-dashed);border-radius:var(--radius-md);\n display:flex;align-items:center;justify-content:center;gap:8px;color:var(--text-muted);\n}\n";
|
|
1
|
+
export declare const ADMIN_CSS = "\n@import url(\"https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;500;600;700&display=swap\");\n\n.sm-admin{\n --font-sans:'Public Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;\n --font-mono:ui-monospace,Menlo,Consolas,monospace;\n --text-2xs:9px; --text-xs:10px; --text-sm:11px; --text-md:12px;\n --text-lg:13px; --text-xl:14px; --text-2xl:15px; --text-3xl:20px; --text-4xl:30px;\n --tracking-tight:-0.02em; --tracking-label:0.1em; --tracking-tag:0.08em;\n\n --radius-xs:1px; --radius-sm:3px; --radius-md:4px; --radius-round:999px;\n --control-h:28px; --control-h-lg:36px; --row-h:38px; --topbar-h:48px;\n --nav-w:200px; --inspector-w:300px;\n --border:1px solid var(--line);\n --outline-selection:1.5px solid var(--selection);\n\n --brand-600:#0038B1; --brand-500:#2554D6; --brand-300:#6E96FF;\n --brand-200:#A9C0FF; --brand-100:#DCE4F9; --brand-50:#E3EAFA;\n --gray-0:#FFFFFF; --gray-50:#F1F2F6; --gray-100:#E1E4EC; --gray-300:#C9D0E2;\n --gray-500:#8B93A7; --gray-600:#6B7386; --gray-700:#4A5165; --gray-900:#0F1420;\n --green-700:#1F7A46; --green-50:#E3F3EA; --green-300:#6FD39A; --green-500:#2E9E5B;\n --amber-700:#8A5A00; --amber-50:#FBF1DC; --amber-300:#E8B85A;\n --red-700:#B42318; --red-50:#FDECEC; --red-300:#F5A5A0;\n\n --bg:var(--gray-50); --surface:var(--gray-0); --surface-inset:var(--gray-50);\n --input-bg:var(--gray-0); --line:var(--gray-100); --line-dashed:var(--gray-300);\n --text:var(--gray-900); --text-muted:var(--gray-600); --text-subtle:var(--gray-500);\n --accent:var(--brand-600); --on-accent:#FFFFFF; --accent-soft:var(--brand-50);\n --selection:var(--brand-600); --nav-active-bg:var(--brand-100); --nav-active-fg:var(--brand-600);\n --hover:rgba(15,20,32,.045); --press:rgba(15,20,32,.08);\n --control-hover:var(--gray-100);\n\n --status-published-fg:var(--green-700); --status-published-bg:var(--green-50);\n --status-draft-fg:var(--amber-700); --status-draft-bg:var(--amber-50);\n --status-scheduled-fg:var(--accent); --status-scheduled-bg:var(--accent-soft);\n --status-saved-dot:var(--green-500);\n --warn:var(--amber-700); --warn-soft:var(--amber-50);\n --danger:var(--red-700); --danger-soft:var(--red-50);\n\n --page-bg:#FFFFFF; --page-text:var(--gray-900);\n --shadow-page:0 1px 2px rgba(0,0,0,.08), 0 8px 24px rgba(0,0,0,.06);\n --shadow-float:0 2px 8px rgba(0,0,0,.25);\n --shadow-seg:0 1px 2px rgba(0,0,0,.08);\n --focus-ring:0 0 0 3px var(--accent-soft);\n --shadow-pop:0 8px 32px rgba(0,0,0,.18);\n --logo-filter:none;\n\n /* Which half of the theme toggle is visible. A token, so the icon can never\n disagree with the colors \u2014 both follow the one cascade below. */\n --when-light:inline-flex; --when-dark:none;\n}\nhtml[data-theme=\"dark\"] .sm-admin,.sm-admin[data-theme=\"dark\"]{\n --bg:#0D0F14; --surface:#15181F; --surface-inset:#0D0F14; --input-bg:#1B1F28;\n --line:#2A2F3B; --line-dashed:#2A2F3B;\n --text:#EEF0F5; --text-muted:#8C94A8; --text-subtle:#6B7386;\n --accent:var(--brand-300); --on-accent:#06122F; --accent-soft:rgba(110,150,255,.16);\n --nav-active-bg:rgba(110,150,255,.14); --nav-active-fg:var(--brand-200);\n --hover:rgba(238,240,245,.05); --press:rgba(238,240,245,.09);\n --control-hover:var(--input-bg);\n\n --status-published-fg:var(--green-300); --status-published-bg:rgba(111,211,154,.14);\n --status-draft-fg:var(--amber-300); --status-draft-bg:rgba(232,184,90,.14);\n --warn:var(--amber-300); --warn-soft:rgba(232,184,90,.14);\n --danger:var(--red-300); --danger-soft:rgba(244,102,92,.14);\n\n --shadow-page:0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.35);\n --shadow-pop:0 8px 32px rgba(0,0,0,.5);\n --logo-filter:brightness(0) invert(1);\n --when-light:none; --when-dark:inline-flex;\n}\n@media (prefers-color-scheme:dark){\n html:not([data-theme=\"light\"]) .sm-admin:not([data-theme=\"light\"]){\n --bg:#0D0F14; --surface:#15181F; --surface-inset:#0D0F14; --input-bg:#1B1F28;\n --line:#2A2F3B; --line-dashed:#2A2F3B;\n --text:#EEF0F5; --text-muted:#8C94A8; --text-subtle:#6B7386;\n --accent:var(--brand-300); --on-accent:#06122F; --accent-soft:rgba(110,150,255,.16);\n --nav-active-bg:rgba(110,150,255,.14); --nav-active-fg:var(--brand-200);\n --hover:rgba(238,240,245,.05); --press:rgba(238,240,245,.09);\n --control-hover:var(--input-bg);\n\n --status-published-fg:var(--green-300); --status-published-bg:rgba(111,211,154,.14);\n --status-draft-fg:var(--amber-300); --status-draft-bg:rgba(232,184,90,.14);\n --warn:var(--amber-300); --warn-soft:rgba(232,184,90,.14);\n --danger:var(--red-300); --danger-soft:rgba(244,102,92,.14);\n\n --shadow-page:0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.35);\n --shadow-pop:0 8px 32px rgba(0,0,0,.5);\n --logo-filter:brightness(0) invert(1);\n --when-light:none; --when-dark:inline-flex;\n}\n}\n/* Last, and the same specificity as the host-attribute rule: an explicit\n choice from the theme toggle wins over a dark <html> and over the OS. */\n.sm-admin[data-theme=\"light\"]{\n --brand-600:#0038B1; --brand-500:#2554D6; --brand-300:#6E96FF;\n --brand-200:#A9C0FF; --brand-100:#DCE4F9; --brand-50:#E3EAFA;\n --gray-0:#FFFFFF; --gray-50:#F1F2F6; --gray-100:#E1E4EC; --gray-300:#C9D0E2;\n --gray-500:#8B93A7; --gray-600:#6B7386; --gray-700:#4A5165; --gray-900:#0F1420;\n --green-700:#1F7A46; --green-50:#E3F3EA; --green-300:#6FD39A; --green-500:#2E9E5B;\n --amber-700:#8A5A00; --amber-50:#FBF1DC; --amber-300:#E8B85A;\n --red-700:#B42318; --red-50:#FDECEC; --red-300:#F5A5A0;\n\n --bg:var(--gray-50); --surface:var(--gray-0); --surface-inset:var(--gray-50);\n --input-bg:var(--gray-0); --line:var(--gray-100); --line-dashed:var(--gray-300);\n --text:var(--gray-900); --text-muted:var(--gray-600); --text-subtle:var(--gray-500);\n --accent:var(--brand-600); --on-accent:#FFFFFF; --accent-soft:var(--brand-50);\n --selection:var(--brand-600); --nav-active-bg:var(--brand-100); --nav-active-fg:var(--brand-600);\n --hover:rgba(15,20,32,.045); --press:rgba(15,20,32,.08);\n --control-hover:var(--gray-100);\n\n --status-published-fg:var(--green-700); --status-published-bg:var(--green-50);\n --status-draft-fg:var(--amber-700); --status-draft-bg:var(--amber-50);\n --status-scheduled-fg:var(--accent); --status-scheduled-bg:var(--accent-soft);\n --status-saved-dot:var(--green-500);\n --warn:var(--amber-700); --warn-soft:var(--amber-50);\n --danger:var(--red-700); --danger-soft:var(--red-50);\n\n --page-bg:#FFFFFF; --page-text:var(--gray-900);\n --shadow-page:0 1px 2px rgba(0,0,0,.08), 0 8px 24px rgba(0,0,0,.06);\n --shadow-float:0 2px 8px rgba(0,0,0,.25);\n --shadow-seg:0 1px 2px rgba(0,0,0,.08);\n --focus-ring:0 0 0 3px var(--accent-soft);\n --shadow-pop:0 8px 32px rgba(0,0,0,.18);\n --logo-filter:none;\n\n /* Which half of the theme toggle is visible. A token, so the icon can never\n disagree with the colors \u2014 both follow the one cascade below. */\n --when-light:inline-flex; --when-dark:none;\n}\n\n/* The admin owns the viewport; a host <body> margin would push it into a\n page scrollbar. Scoped by :has, so only a page holding the admin is touched. */\nbody:has(.sm-admin){margin:0}\n.sm-admin,.sm-admin *,.sm-admin *::before,.sm-admin *::after{box-sizing:border-box}\n.sm-admin{\n font-family:var(--font-sans);font-size:var(--text-md);line-height:1.4;\n color:var(--text);background:var(--surface);\n -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;\n}\n.sm-admin h1,.sm-admin h2,.sm-admin h3,.sm-admin p,.sm-admin ul{margin:0}\n/* :where() keeps this at zero specificity \u2014 a bare <a> is accent, but any\n component class on it (.sm-nav-item, .sm-btn, .sm-back) decides its own\n colour. Without this the element selector silently beat every one of them. */\n:where(.sm-admin a){color:var(--accent);text-decoration:none}\n:where(.sm-admin a:hover){color:var(--brand-500)}\n.sm-admin button,.sm-admin input,.sm-admin select,.sm-admin textarea{font-family:inherit}\n.sm-admin ::placeholder{color:var(--text-subtle)}\n.sm-admin ::selection{background:var(--accent-soft);color:var(--text)}\n.sm-mono{font-family:var(--font-mono)}\n\n/* \u2500\u2500 shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-shell{display:grid;grid-template-columns:var(--nav-w) 1fr;height:100vh;height:100dvh;overflow:hidden}\n.sm-shell>.sm-main{min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden}\n.sm-nav{\n display:flex;flex-direction:column;gap:24px;padding:16px 12px;\n border-right:var(--border);background:var(--surface);min-width:0;overflow:auto;\n}\n.sm-nav__logo{height:18px;width:auto;align-self:flex-start;filter:var(--logo-filter);flex:none}\n.sm-nav>.sm-nav__logo{margin-left:10px}\n.sm-nav__list{display:flex;flex-direction:column;gap:1px}\n.sm-nav-item{\n display:block;padding:6px 10px;border-radius:var(--radius-md);line-height:1.3;\n color:var(--text);font-weight:500;text-decoration:none;cursor:pointer;\n background:transparent;border:0;width:100%;text-align:left;font-size:var(--text-md);\n overflow:hidden;text-overflow:ellipsis;white-space:nowrap;\n}\n.sm-nav-item:hover{background:var(--surface-inset);color:var(--text)}\n.sm-nav-item[aria-current=\"page\"]{background:var(--nav-active-bg);color:var(--nav-active-fg);font-weight:600}\n.sm-nav-item[aria-current=\"page\"]:hover{background:var(--nav-active-bg);color:var(--nav-active-fg)}\n/* Not greyed: the reference renders every inactive item at --text and marks\n the unavailable ones only by being inert (no pointer, no hover). */\n.sm-nav-item[aria-disabled=\"true\"]{cursor:default}\n.sm-nav-item[aria-disabled=\"true\"]:hover{background:transparent}\n.sm-nav__foot{margin-top:auto;display:flex;flex-direction:column;gap:2px}\n.sm-nav__user{display:flex;align-items:center;gap:10px;padding:6px 10px;min-width:0}\n.sm-nav-item--icon{display:flex;align-items:center;gap:8px}\n.sm-when-light{display:var(--when-light);align-items:center;gap:8px}\n.sm-when-dark{display:var(--when-dark);align-items:center;gap:8px}\n.sm-avatar{\n width:24px;height:24px;flex:none;border-radius:var(--radius-round);\n background:var(--accent-soft);color:var(--accent);\n display:grid;place-items:center;font-size:var(--text-sm);font-weight:600;\n}\n\n/* \u2500\u2500 top bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-topbar{\n height:var(--topbar-h);flex:none;display:flex;align-items:center;justify-content:space-between;\n gap:16px;padding:0 16px;border-bottom:var(--border);background:var(--surface);\n}\n.sm-topbar__left{display:flex;align-items:center;gap:10px;min-width:0}\n.sm-topbar__right{display:flex;align-items:center;gap:8px;flex:none}\n.sm-crumbs{display:flex;align-items:center;gap:8px;color:var(--text-muted);min-width:0}\n.sm-crumbs__sep{opacity:.5}\n.sm-crumbs__here{color:var(--text);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.sm-back{border:0;background:transparent;padding:0;color:var(--text);font:inherit;cursor:pointer;line-height:1}\n.sm-back:hover{color:var(--accent)}\n.sm-title{font-size:var(--text-xl);font-weight:600}\n.sm-count{color:var(--text-muted)}\n\n/* \u2500\u2500 buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-btn{\n display:inline-flex;align-items:center;justify-content:center;gap:6px;\n height:var(--control-h);padding:0 12px;border-radius:var(--radius-md);border:0;\n font-size:var(--text-md);font-weight:500;line-height:1;white-space:nowrap;cursor:pointer;\n text-decoration:none;\n}\n.sm-btn--lg{height:var(--control-h-lg);padding:0 16px}\n.sm-btn--primary{background:var(--accent);color:var(--on-accent);font-weight:600;padding:0 14px}\n.sm-btn--primary:hover{background:var(--brand-500);color:var(--on-accent)}\n.sm-btn--secondary{background:var(--surface-inset);color:var(--text)}\n.sm-btn--secondary:hover{background:var(--control-hover);color:var(--text)}\n.sm-btn--ghost{background:transparent;color:var(--accent);font-weight:600;padding:0 8px}\n.sm-btn--ghost:hover{background:var(--accent-soft);color:var(--accent)}\n.sm-btn--danger{background:var(--surface-inset);color:var(--danger)}\n.sm-btn--danger:hover{background:var(--danger-soft);color:var(--danger)}\n.sm-btn--ok{background:var(--status-published-bg);color:var(--status-published-fg);font-weight:600;padding:0 14px}\n.sm-btn:disabled,.sm-btn[aria-disabled=\"true\"]{opacity:.5;cursor:default;pointer-events:none}\n.sm-btn:focus-visible,.sm-iconbtn:focus-visible,.sm-nav-item:focus-visible,\n.sm-tab:focus-visible,.sm-seg__opt:focus-visible,.sm-row:focus-visible{\n outline:2px solid var(--accent);outline-offset:1px\n}\n\n.sm-iconbtn{\n width:24px;height:24px;flex:none;display:grid;place-items:center;padding:0;\n border:0;border-radius:var(--radius-sm);background:transparent;color:var(--text-muted);\n font-size:12px;line-height:1;cursor:pointer;\n}\n.sm-iconbtn:hover{background:var(--hover);color:var(--text)}\n.sm-iconbtn--inverse{color:#EEF0F5}\n.sm-iconbtn--inverse:hover{background:rgba(255,255,255,.14);color:#fff}\n.sm-iconbtn:disabled{opacity:.3;cursor:default}\n.sm-iconbtn:disabled:hover{background:transparent}\n\n/* \u2500\u2500 fields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-field{display:flex;flex-direction:column;gap:6px;min-width:0}\n.sm-field__head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}\n.sm-label{\n font-size:var(--text-2xs);font-weight:600;letter-spacing:var(--tracking-label);\n text-transform:uppercase;color:var(--text-muted);\n}\n.sm-counter,.sm-hint{font-size:var(--text-sm);color:var(--text-muted)}\n.sm-input,.sm-textarea,.sm-select{\n width:100%;min-width:0;border:1.5px solid transparent;border-radius:var(--radius-md);\n background:var(--surface-inset);color:var(--text);font-size:var(--text-md);\n outline:0;box-shadow:none;\n}\n.sm-input,.sm-select{height:var(--control-h);padding:0 10px}\n.sm-textarea{padding:8px 10px;line-height:1.5;resize:vertical;min-height:60px}\n.sm-input--lg{height:var(--control-h-lg);font-size:var(--text-2xl);font-weight:600}\n.sm-input--mono{font-family:var(--font-mono);font-size:var(--text-sm)}\n.sm-input:hover,.sm-textarea:hover,.sm-select:hover{background:var(--control-hover)}\n.sm-input:focus,.sm-textarea:focus,.sm-select:focus,\n.sm-inputwrap:focus-within{\n background:var(--input-bg);border-color:var(--selection);box-shadow:var(--focus-ring);\n}\n.sm-input:disabled,.sm-textarea:disabled,.sm-select:disabled{opacity:.6;cursor:default}\n.sm-select{\n appearance:none;-webkit-appearance:none;cursor:pointer;padding-right:26px;font-weight:500;\n background-image:linear-gradient(45deg,transparent 50%,var(--text-muted) 50%),\n linear-gradient(135deg,var(--text-muted) 50%,transparent 50%);\n background-position:calc(100% - 14px) 12px,calc(100% - 10px) 12px;\n background-size:4px 4px,4px 4px;background-repeat:no-repeat;\n}\n.sm-inputwrap{\n display:flex;align-items:center;gap:0;height:var(--control-h);padding:0 10px;\n border:1.5px solid transparent;border-radius:var(--radius-md);background:var(--surface-inset);\n}\n.sm-inputwrap>.sm-input{height:auto;padding:0;background:transparent;border:0;box-shadow:none}\n.sm-inputwrap>.sm-input:hover,.sm-inputwrap>.sm-input:focus{background:transparent;box-shadow:none}\n.sm-prefix{color:var(--text-muted);font-family:var(--font-mono);font-size:var(--text-sm);flex:none}\n.sm-error{color:var(--danger);font-size:var(--text-sm)}\n\n.sm-group{display:flex;flex-direction:column;gap:10px;padding:2px 0 0 10px;border-left:2px solid var(--line)}\n.sm-list{display:flex;flex-direction:column;gap:6px;align-items:stretch}\n.sm-listrow{border:1px solid var(--line);border-radius:var(--radius-md);min-width:0}\n.sm-listrow__head{display:flex;align-items:center;gap:2px;height:var(--control-h);padding:0 4px 0 4px}\n.sm-listrow__toggle{\n flex:1;min-width:0;display:flex;align-items:center;gap:6px;height:100%;padding:0 4px;\n border:0;background:transparent;color:var(--text);font:inherit;font-size:var(--text-md);font-weight:500;\n text-align:left;cursor:pointer;\n}\n.sm-listrow__toggle:hover{color:var(--accent)}\n.sm-listrow__summary{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}\n.sm-listrow__body{display:flex;flex-direction:column;gap:10px;padding:8px 10px 10px;border-top:1px solid var(--line)}\n\n/* \u2500\u2500 data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-table{display:flex;flex-direction:column;padding:0 16px 16px}\n.sm-table__head,.sm-row{display:grid;gap:12px;align-items:center;padding:0 8px}\n.sm-table__head{\n height:34px;border-bottom:var(--border);font-family:var(--font-mono);\n font-size:var(--text-2xs);letter-spacing:var(--tracking-tag);color:var(--text-muted);\n text-transform:uppercase;position:sticky;top:0;background:var(--surface);z-index:1;\n}\n.sm-table__body{display:flex;flex-direction:column;padding-top:4px}\n.sm-row{\n height:var(--row-h);border-radius:var(--radius-md);background:transparent;\n border:0;font:inherit;color:inherit;text-align:left;width:100%;\n}\n.sm-row--clickable{cursor:pointer}\n.sm-row--clickable:hover{background:var(--surface-inset)}\n.sm-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}\n.sm-cell--muted{color:var(--text-muted)}\n.sm-cell--mono{font-family:var(--font-mono);font-size:var(--text-sm);color:var(--text-muted)}\n.sm-cell--title{font-weight:500}\n.sm-empty{padding:28px 8px;color:var(--text-muted)}\n.sm-chevron{\n width:12px;flex:none;border:0;background:none;padding:0;font:inherit;font-size:10px;\n color:var(--text-muted);cursor:pointer;line-height:1;\n}\n.sm-chevron:hover{color:var(--text)}\n.sm-cell--tree{display:flex;align-items:center;gap:6px;font-weight:500}\n.sm-cell--tree.sm-cell--parent{font-weight:600}\n.sm-rowmenu{\n border:0;background:none;padding:0 6px;font:inherit;color:var(--text-muted);cursor:pointer;\n border-radius:var(--radius-md);line-height:1;height:24px;\n}\n.sm-rowmenu:hover{background:var(--surface-inset);color:var(--text)}\n\n.sm-badge{\n display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:var(--radius-md);\n font-size:var(--text-xs);font-weight:600;letter-spacing:.04em;white-space:nowrap;\n}\n.sm-savestate{\n display:inline-flex;align-items:center;gap:6px;padding:0 6px;\n font-size:var(--text-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;\n}\n.sm-dot{width:5px;height:5px;border-radius:var(--radius-round);flex:none}\n\n/* \u2500\u2500 navigation bits \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-tabs{display:flex;gap:18px;padding:0 14px;height:36px;align-items:stretch;border-bottom:var(--border);flex:none}\n.sm-tab{\n display:flex;align-items:center;border:0;background:transparent;padding:0;font:inherit;\n cursor:pointer;color:var(--text-muted);border-bottom:2px solid transparent;margin-bottom:-1px;\n}\n.sm-tab:hover{color:var(--text)}\n.sm-tab[aria-selected=\"true\"]{color:var(--accent);font-weight:600;border-bottom-color:var(--accent)}\n.sm-seg{display:flex;gap:2px;padding:2px;background:var(--surface-inset);border-radius:var(--radius-md)}\n.sm-seg--stretch{width:100%}\n.sm-seg--disabled{opacity:.6}\n.sm-seg__opt:disabled{cursor:default}\n.sm-seg__opt{\n flex:1;padding:3px 10px;border:0;border-radius:var(--radius-sm);background:transparent;\n color:var(--text-muted);font:inherit;cursor:pointer;white-space:nowrap;\n}\n.sm-seg__opt:hover{color:var(--text)}\n.sm-seg__opt[aria-pressed=\"true\"]{background:var(--surface);color:var(--text);font-weight:600;box-shadow:var(--shadow-seg)}\n.sm-divider{height:1px;background:var(--line);flex:none}\n.sm-sectionlabel{display:flex;align-items:center;justify-content:space-between;gap:8px}\n.sm-sectionlabel--mono>.sm-label{\n font-family:var(--font-mono);font-weight:400;letter-spacing:var(--tracking-tag);\n}\n.sm-banner{\n display:flex;gap:14px;align-items:flex-start;padding:10px 14px;border-radius:var(--radius-md);\n font-size:var(--text-md);\n}\n.sm-banner--error{background:var(--danger-soft);color:var(--danger)}\n.sm-banner--ok{background:var(--status-published-bg);color:var(--status-published-fg)}\n.sm-banner--warn{background:var(--warn-soft);color:var(--warn)}\n\n/* \u2500\u2500 editor \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.sm-editor{flex:1;min-height:0;display:grid;grid-template-columns:1fr var(--inspector-w)}\n.sm-canvas{display:flex;flex-direction:column;background:var(--bg);min-width:0;min-height:0;overflow:hidden}\n.sm-canvas__meta{\n display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 20px;flex:none;\n font-family:var(--font-mono);font-size:var(--text-xs);letter-spacing:.04em;color:var(--text-muted);\n}\n.sm-canvas__scroll{flex:1;min-height:0;overflow:auto;padding:0 20px 20px}\n.sm-page{\n position:relative;background:var(--page-bg);border-radius:var(--radius-md);\n box-shadow:var(--shadow-page);min-height:100%;\n}\n.sm-inspector{\n display:flex;flex-direction:column;min-height:0;overflow:hidden;\n border-left:var(--border);background:var(--surface);\n}\n.sm-inspector__head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:12px 14px 10px;flex:none}\n.sm-inspector__body{display:flex;flex-direction:column;gap:14px;padding:14px;overflow:auto}\n.sm-blockglyph{width:14px;height:10px;border:1.5px solid var(--text-muted);border-radius:var(--radius-xs);flex:none}\n.sm-blocktag{\n position:absolute;top:0;left:0;background:var(--selection);color:#fff;\n font-family:var(--font-mono);font-size:var(--text-2xs);font-weight:700;\n letter-spacing:var(--tracking-tag);text-transform:uppercase;padding:3px 7px;\n border-radius:var(--radius-sm) 0 var(--radius-sm) 0;white-space:nowrap;\n}\n.sm-blocktoolbar{\n display:flex;gap:2px;padding:2px;background:#15181F;border-radius:var(--radius-md);\n box-shadow:var(--shadow-float);\n}\n.sm-plus{\n width:24px;height:24px;border-radius:var(--radius-round);border:0;background:var(--selection);\n color:#fff;display:grid;place-items:center;font-size:15px;line-height:1;cursor:pointer;\n box-shadow:0 2px 6px rgba(0,56,177,.35);padding:0;\n}\n.sm-plus:hover{background:var(--brand-500)}\n.sm-guide{height:1px;background:color-mix(in srgb,var(--selection) 35%,transparent)}\n.sm-tooltip{\n background:#15181F;color:#EEF0F5;font-size:var(--text-sm);padding:4px 8px;\n border-radius:var(--radius-sm);white-space:nowrap;\n}\n.sm-outline-row{\n display:flex;align-items:center;justify-content:space-between;gap:4px;\n height:var(--control-h);border-radius:var(--radius-md);padding:0 4px 0 10px;\n color:var(--text);font-weight:500;background:transparent;\n border:1px solid var(--line);\n}\n.sm-outline-row--clickable{cursor:pointer}\n.sm-outline-row--clickable:hover{background:var(--surface-inset)}\n/* Selected and locked rows are read by their fill, so they drop the hairline \u2014\n border-color, not border, so every row keeps the same 28px box. */\n.sm-outline-row--selected,.sm-outline-row--selected:hover{\n background:var(--nav-active-bg);color:var(--nav-active-fg);font-weight:600;border-color:transparent;\n}\n.sm-outline-row--empty{border:1px dashed var(--line-dashed);color:var(--text-muted);padding:0 10px}\n.sm-outline-row--locked{background:var(--surface-inset);color:var(--text-muted);padding:0 10px;border-color:transparent}\n.sm-outline-row__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}\n\n.sm-picker{\n width:300px;background:var(--surface);border:var(--border);border-radius:var(--radius-md);\n padding:6px;box-shadow:var(--shadow-pop);\n}\n.sm-picker__item{\n display:flex;gap:10px;align-items:center;width:100%;text-align:left;background:transparent;\n border:0;border-radius:var(--radius-md);padding:8px 8px;cursor:pointer;color:var(--text);font:inherit;\n}\n.sm-picker__item:hover{background:var(--nav-active-bg)}\n.sm-picker__icon{\n width:26px;height:26px;flex:none;border-radius:var(--radius-sm);background:var(--accent-soft);\n color:var(--accent);display:grid;place-items:center;font-size:13px;\n}\n.sm-dashed{\n border:1px dashed var(--line-dashed);border-radius:var(--radius-md);\n display:flex;align-items:center;justify-content:center;gap:8px;color:var(--text-muted);\n}\n";
|
|
2
2
|
/** Where the theme toggle's choice is remembered, browser-side only. */
|
|
3
3
|
export declare const THEME_KEY = "smoodly-theme";
|
|
4
4
|
/** One <style> element and the theme boot, rendered once by AdminLayout. */
|
package/dist/admin/ui/theme.js
CHANGED
|
@@ -232,6 +232,19 @@ body:has(.sm-admin){margin:0}
|
|
|
232
232
|
.sm-prefix{color:var(--text-muted);font-family:var(--font-mono);font-size:var(--text-sm);flex:none}
|
|
233
233
|
.sm-error{color:var(--danger);font-size:var(--text-sm)}
|
|
234
234
|
|
|
235
|
+
.sm-group{display:flex;flex-direction:column;gap:10px;padding:2px 0 0 10px;border-left:2px solid var(--line)}
|
|
236
|
+
.sm-list{display:flex;flex-direction:column;gap:6px;align-items:stretch}
|
|
237
|
+
.sm-listrow{border:1px solid var(--line);border-radius:var(--radius-md);min-width:0}
|
|
238
|
+
.sm-listrow__head{display:flex;align-items:center;gap:2px;height:var(--control-h);padding:0 4px 0 4px}
|
|
239
|
+
.sm-listrow__toggle{
|
|
240
|
+
flex:1;min-width:0;display:flex;align-items:center;gap:6px;height:100%;padding:0 4px;
|
|
241
|
+
border:0;background:transparent;color:var(--text);font:inherit;font-size:var(--text-md);font-weight:500;
|
|
242
|
+
text-align:left;cursor:pointer;
|
|
243
|
+
}
|
|
244
|
+
.sm-listrow__toggle:hover{color:var(--accent)}
|
|
245
|
+
.sm-listrow__summary{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
246
|
+
.sm-listrow__body{display:flex;flex-direction:column;gap:10px;padding:8px 10px 10px;border-top:1px solid var(--line)}
|
|
247
|
+
|
|
235
248
|
/* ── data ──────────────────────────────────────────────────────────── */
|
|
236
249
|
.sm-table{display:flex;flex-direction:column;padding:0 16px 16px}
|
|
237
250
|
.sm-table__head,.sm-row{display:grid;gap:12px;align-items:center;padding:0 8px}
|
package/dist/admin/validate.d.ts
CHANGED
|
@@ -2,9 +2,14 @@ export type FieldError = {
|
|
|
2
2
|
field: string;
|
|
3
3
|
message: string;
|
|
4
4
|
};
|
|
5
|
-
|
|
5
|
+
type Desc = {
|
|
6
6
|
type: string;
|
|
7
7
|
optional?: unknown;
|
|
8
8
|
options?: unknown;
|
|
9
9
|
max?: unknown;
|
|
10
|
-
|
|
10
|
+
min?: unknown;
|
|
11
|
+
fields?: unknown;
|
|
12
|
+
item?: unknown;
|
|
13
|
+
};
|
|
14
|
+
export declare function validateFields(values: Record<string, unknown>, descriptors: Record<string, Desc>): FieldError[];
|
|
15
|
+
export {};
|
package/dist/admin/validate.js
CHANGED
|
@@ -1,19 +1,49 @@
|
|
|
1
|
+
const subfields = (d) => d.fields && typeof d.fields === "object" ? d.fields : {};
|
|
2
|
+
const itemOf = (d) => d.item && typeof d.item === "object" ? d.item : undefined;
|
|
1
3
|
const isEmpty = (v) => v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
/** An object whose every subfield is optional — or is itself an object that
|
|
5
|
+
* may be absent — may be absent altogether. */
|
|
6
|
+
const mayBeAbsent = (d) => d.optional === true || (d.type === "object" && Object.values(subfields(d)).every(mayBeAbsent));
|
|
7
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
8
|
+
function validateValue(path, value, d, errors) {
|
|
9
|
+
if (isEmpty(value)) {
|
|
10
|
+
if (!mayBeAbsent(d))
|
|
11
|
+
errors.push({ field: path, message: "Required." });
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (d.type === "list") {
|
|
15
|
+
if (!Array.isArray(value)) {
|
|
16
|
+
errors.push({ field: path, message: "Must be a list." });
|
|
17
|
+
return;
|
|
13
18
|
}
|
|
14
|
-
if (typeof d.
|
|
15
|
-
errors.push({ field, message: `
|
|
19
|
+
if (typeof d.min === "number" && value.length < d.min)
|
|
20
|
+
errors.push({ field: path, message: `At least ${plural(d.min, "item")}.` });
|
|
21
|
+
if (typeof d.max === "number" && value.length > d.max)
|
|
22
|
+
errors.push({ field: path, message: `At most ${plural(d.max, "item")}.` });
|
|
23
|
+
const item = itemOf(d);
|
|
24
|
+
if (item)
|
|
25
|
+
value.forEach((v, i) => validateValue(`${path}.${i}`, v, item, errors));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (d.type === "object") {
|
|
29
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
30
|
+
errors.push({ field: path, message: "Must be an object." });
|
|
31
|
+
return;
|
|
16
32
|
}
|
|
33
|
+
for (const [k, sub] of Object.entries(subfields(d)))
|
|
34
|
+
validateValue(`${path}.${k}`, value[k], sub, errors);
|
|
35
|
+
return;
|
|
17
36
|
}
|
|
37
|
+
if (d.type === "select" && Array.isArray(d.options) && !d.options.includes(value)) {
|
|
38
|
+
errors.push({ field: path, message: `Must be one of: ${d.options.join(", ")}.` });
|
|
39
|
+
}
|
|
40
|
+
if (typeof d.max === "number" && typeof value === "string" && value.length > d.max) {
|
|
41
|
+
errors.push({ field: path, message: `Must be at most ${d.max} characters.` });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function validateFields(values, descriptors) {
|
|
45
|
+
const errors = [];
|
|
46
|
+
for (const [field, d] of Object.entries(descriptors))
|
|
47
|
+
validateValue(field, values[field], d, errors);
|
|
18
48
|
return errors;
|
|
19
49
|
}
|
package/dist/entry-store.js
CHANGED
|
@@ -13,6 +13,8 @@ import { collectionDepth, collectionOrder } from "./collections.js";
|
|
|
13
13
|
import { collectEntryRefs } from "./refs.js";
|
|
14
14
|
import { MemoryRefIndex } from "./ref-index.js";
|
|
15
15
|
import { MemoryPathIndex } from "./path-index.js";
|
|
16
|
+
import { sharedEntryId } from "./shared-id.js";
|
|
17
|
+
import { DEFAULT_SPACE_ID } from "./sql-space.js";
|
|
16
18
|
import { assertDepth, assertSupportedLocale, chainOf, entryPathRows, slugOf, unsupportedLocale, } from "./paths.js";
|
|
17
19
|
export function schemaOf(collections, name) {
|
|
18
20
|
const schema = collections.find((c) => c.name === name);
|
|
@@ -272,7 +274,11 @@ export class MemoryEntryStore {
|
|
|
272
274
|
this.assertSiblingSlug(collection, parentId, options.locale, slug);
|
|
273
275
|
const now = Date.now();
|
|
274
276
|
const entry = {
|
|
275
|
-
id
|
|
277
|
+
// A shared item's id is derived, not minted (spec 2026-09-07 §10):
|
|
278
|
+
// the same uuid v5 the Supabase adapter computes, so self-host and
|
|
279
|
+
// cloud agree on the row a materialization would create.
|
|
280
|
+
id: isSharedSchema(schema) ? await sharedEntryId(DEFAULT_SPACE_ID, collection) : uid("entry"),
|
|
281
|
+
collection, parentId, fields: shared,
|
|
276
282
|
locales: {
|
|
277
283
|
[options.locale]: { slug, status: "draft", fields: localized, draftVersionId: null, publishedVersionId: null, updatedAt: now },
|
|
278
284
|
},
|
package/dist/fields.d.ts
CHANGED
|
@@ -31,6 +31,15 @@ export type BuilderFlags = {
|
|
|
31
31
|
optional?: true;
|
|
32
32
|
localized?: true;
|
|
33
33
|
};
|
|
34
|
+
/** A builder whose flags carry `localized: true` becomes `never` here, so
|
|
35
|
+
* `f.list(f.text().localized())` and `f.object({ a: f.text().localized() })`
|
|
36
|
+
* are type errors: localization is whole-value on lists and objects
|
|
37
|
+
* (spec 2026-09-07, list field §2). */
|
|
38
|
+
type NotLocalized<B> = B extends {
|
|
39
|
+
__flags?: infer Fl;
|
|
40
|
+
} ? Fl extends {
|
|
41
|
+
localized: true;
|
|
42
|
+
} ? never : B : B;
|
|
34
43
|
export declare const f: {
|
|
35
44
|
text: () => FieldBuilder<string, {}>;
|
|
36
45
|
link: () => FieldBuilder<string, {}>;
|
|
@@ -54,7 +63,12 @@ export declare const f: {
|
|
|
54
63
|
}) => FieldBuilder<unknown, {}>;
|
|
55
64
|
dataAttributes: () => FieldBuilder<Record<string, string>, {}>;
|
|
56
65
|
select: <const O extends readonly string[]>(options: O) => FieldBuilder<O[number], {}>;
|
|
57
|
-
object: <S extends Record<string, FieldBuilder<any, any>>>(shape: S) => FieldBuilder<{ [K in keyof S]: ValueOf<S[K]>; }, {}>;
|
|
66
|
+
object: <S extends Record<string, FieldBuilder<any, any>>>(shape: S & { [K in keyof S]: NotLocalized<S[K]>; }) => FieldBuilder<{ [K in keyof S]: ValueOf<S[K]>; }, {}>;
|
|
67
|
+
/** A repeating item: any builder, but no list at any depth inside the item
|
|
68
|
+
* — a list in an object in a list is still a nested list on screen, and a
|
|
69
|
+
* 300px sidebar cannot show one legibly (spec 2026-09-07, list field §1).
|
|
70
|
+
* `.min`/`.max` count items. */
|
|
71
|
+
list: <B extends FieldBuilder<any, any>>(item: B & NotLocalized<B>) => FieldBuilder<ValueOf<B>[], {}>;
|
|
58
72
|
ref: <T>(target: () => T) => FieldBuilder<Resolved<T>, {}>;
|
|
59
73
|
refList: <T>(target: () => T) => FieldBuilder<Resolved<T>[], {}>;
|
|
60
74
|
};
|
|
@@ -65,3 +79,4 @@ export type ValueOf<B> = B extends FieldBuilder<infer V, infer Flags> ? Flags ex
|
|
|
65
79
|
export type Resolved<T> = T extends {
|
|
66
80
|
__entry?: infer E;
|
|
67
81
|
} ? E : Record<string, unknown>;
|
|
82
|
+
export {};
|
package/dist/fields.js
CHANGED
|
@@ -20,6 +20,35 @@ function builder(descriptor) {
|
|
|
20
20
|
tab: (tab) => chain({ tab }),
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
+
const describePath = (path) => (path === "" ? "the item" : `"${path}"`);
|
|
24
|
+
/** First path at which `matches` is true of the descriptor, walking into
|
|
25
|
+
* object subfields and a list's item, or null. */
|
|
26
|
+
function findMatch(d, matches, path = "") {
|
|
27
|
+
if (matches(d))
|
|
28
|
+
return path;
|
|
29
|
+
if (d.type === "object") {
|
|
30
|
+
for (const [k, sub] of Object.entries(d.fields)) {
|
|
31
|
+
const hit = findMatch(sub, matches, path ? `${path}.${k}` : k);
|
|
32
|
+
if (hit !== null)
|
|
33
|
+
return hit;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (d.type === "list")
|
|
37
|
+
return findMatch(d.item, matches, path);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** Refs are addressed by top-level key everywhere (resolver, refs index,
|
|
41
|
+
* admin); `.localized()` decides storage per top-level field. Neither may
|
|
42
|
+
* hide inside a container. */
|
|
43
|
+
function assertContainerItem(kind, d, path = "") {
|
|
44
|
+
const ref = findMatch(d, (sub) => sub.type === "ref" || sub.type === "refList", path);
|
|
45
|
+
if (ref !== null)
|
|
46
|
+
throw new Error(`smoodly: ${kind} cannot hold a ref (at ${describePath(ref)}) — refs live at the top level of fields.`);
|
|
47
|
+
const loc = findMatch(d, (sub) => sub.localized === true, path);
|
|
48
|
+
if (loc !== null) {
|
|
49
|
+
throw new Error(`smoodly: .localized() inside ${kind} (at ${describePath(loc)}) — localize the ${kind === "f.list" ? "list" : "object"} itself.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
23
52
|
export const f = {
|
|
24
53
|
text: () => builder({ type: "text" }),
|
|
25
54
|
link: () => builder({ type: "link" }),
|
|
@@ -32,10 +61,26 @@ export const f = {
|
|
|
32
61
|
richtext: (opts) => builder({ type: "richtext", ...(opts?.toolset !== undefined ? { toolset: opts.toolset } : {}) }),
|
|
33
62
|
dataAttributes: () => builder({ type: "dataAttributes" }),
|
|
34
63
|
select: (options) => builder({ type: "select", options: [...options] }),
|
|
35
|
-
object: (shape) =>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
64
|
+
object: (shape) => {
|
|
65
|
+
for (const [k, v] of Object.entries(shape))
|
|
66
|
+
assertContainerItem("f.object", v.descriptor, k);
|
|
67
|
+
return builder({
|
|
68
|
+
type: "object",
|
|
69
|
+
fields: Object.fromEntries(Object.entries(shape).map(([k, v]) => [k, v.descriptor])),
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
/** A repeating item: any builder, but no list at any depth inside the item
|
|
73
|
+
* — a list in an object in a list is still a nested list on screen, and a
|
|
74
|
+
* 300px sidebar cannot show one legibly (spec 2026-09-07, list field §1).
|
|
75
|
+
* `.min`/`.max` count items. */
|
|
76
|
+
list: (item) => {
|
|
77
|
+
const d = item.descriptor;
|
|
78
|
+
const nested = findMatch(d, (sub) => sub.type === "list");
|
|
79
|
+
if (nested !== null)
|
|
80
|
+
throw new Error(`smoodly: f.list cannot hold another list (at ${describePath(nested)}) — nested lists are not supported.`);
|
|
81
|
+
assertContainerItem("f.list", d);
|
|
82
|
+
return builder({ type: "list", item: d });
|
|
83
|
+
},
|
|
39
84
|
ref: (target) => builder({ type: "ref", target }),
|
|
40
85
|
refList: (target) => builder({ type: "refList", target }),
|
|
41
86
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { entryPage } from "./entry-page.tsx";
|
|
|
6
6
|
import { shared } from "./shared.ts";
|
|
7
7
|
export { Zone, renderPage, EMPTY_PAGE_CONTEXT } from "./render.tsx";
|
|
8
8
|
export type { PageTree, TreeNode, ZoneContent, PairedPage, Registry, PageContext, PageContextLink, PageLink, PageViewProps } from "./render.tsx";
|
|
9
|
+
export { RichText } from "./richtext-render.tsx";
|
|
9
10
|
export type { PageSchema } from "./page.ts";
|
|
10
11
|
export { fixedSlug } from "./page.ts";
|
|
11
12
|
export type { EntryPageSchema, PairedEntryPage, EntryPageProps } from "./entry-page.tsx";
|
|
@@ -42,6 +43,8 @@ export type { CollectionSchema, Toolset, EntryOrder } from "./collections.ts";
|
|
|
42
43
|
export { collectionOrder, collectionDepth, DEFAULT_ORDER } from "./collections.ts";
|
|
43
44
|
export type { ZonePolicy } from "./zones.ts";
|
|
44
45
|
export { SHARED_NODE_TYPE, STYLES_KEY, sharedLink } from "./shared.ts";
|
|
46
|
+
export { sharedEntryId } from "./shared-id.ts";
|
|
47
|
+
export { DEFAULT_SPACE_ID } from "./sql-space.ts";
|
|
45
48
|
export type { SharedSchema, SharedLink } from "./shared.ts";
|
|
46
49
|
export declare const smoodly: {
|
|
47
50
|
schema: {
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { page } from "./render.js";
|
|
|
6
6
|
import { entryPage } from "./entry-page.js";
|
|
7
7
|
import { shared } from "./shared.js";
|
|
8
8
|
export { Zone, renderPage, EMPTY_PAGE_CONTEXT } from "./render.js";
|
|
9
|
+
export { RichText } from "./richtext-render.js";
|
|
9
10
|
export { fixedSlug } from "./page.js";
|
|
10
11
|
export { f } from "./fields.js";
|
|
11
12
|
export { z } from "./zones.js";
|
|
@@ -27,6 +28,8 @@ export { fixedPathFor } from "./localize.js";
|
|
|
27
28
|
export { pathOptions, pageErrors, localeRow, pageRefEdges, DEFAULT_LOCALES } from "./store.js";
|
|
28
29
|
export { collectionOrder, collectionDepth, DEFAULT_ORDER } from "./collections.js";
|
|
29
30
|
export { SHARED_NODE_TYPE, STYLES_KEY, sharedLink } from "./shared.js";
|
|
31
|
+
export { sharedEntryId } from "./shared-id.js";
|
|
32
|
+
export { DEFAULT_SPACE_ID } from "./sql-space.js";
|
|
30
33
|
export const smoodly = {
|
|
31
34
|
schema,
|
|
32
35
|
section,
|
package/dist/refs.js
CHANGED
|
@@ -25,6 +25,11 @@ function collectFrom(values, descriptors, out) {
|
|
|
25
25
|
out.push({ field, targetCollection: targetOf(d), targetId: id });
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
else if (d.type === "object" || d.type === "list") {
|
|
29
|
+
// Deliberate: refs are top-level only — f.list/f.object refuse them at
|
|
30
|
+
// build time (spec 2026-09-07, list field §1), so there is nothing to walk.
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
export function collectEntryRefs(fields, schema) {
|
package/dist/resolve.js
CHANGED
|
@@ -174,5 +174,8 @@ function collectTasks(values, descriptors, out) {
|
|
|
174
174
|
else if (d.type === "refList" && Array.isArray(value)) {
|
|
175
175
|
out.push({ holder: values, key, collection: targetOf(d), id: value, isList: true, mode: modeOf(d), ancestors: [] });
|
|
176
176
|
}
|
|
177
|
+
else if (d.type === "object" || d.type === "list") {
|
|
178
|
+
continue; // refs are top-level only; containers hold none (spec 2026-09-07)
|
|
179
|
+
}
|
|
177
180
|
}
|
|
178
181
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// The site-side richtext renderer (spec 2026-09-07, list field §5):
|
|
3
|
+
// TipTap JSON → React, a fragment with no wrapper so the developer owns
|
|
4
|
+
// the surrounding element and its class. Pure React, no Node imports, so
|
|
5
|
+
// it works in shared, server and client components alike. Unknown nodes
|
|
6
|
+
// render their children — a document from a later, richer editor degrades
|
|
7
|
+
// instead of crashing. Elements as node views are Phase 3.
|
|
8
|
+
import { Fragment, createElement } from "react";
|
|
9
|
+
/** Only schemes an editor's link may carry reach the page: http(s), mailto, tel,
|
|
10
|
+
* and site-relative targets. Anything else (javascript:, data:, an empty href,
|
|
11
|
+
* a protocol-relative `//host` or a backslash form `/\host` that a browser
|
|
12
|
+
* treats the same way) renders the text without an anchor. */
|
|
13
|
+
const SAFE_HREF = /^(https?:|mailto:|tel:|\/(?![/\\])|#|\?|\.)/i;
|
|
14
|
+
function safeHref(raw) {
|
|
15
|
+
if (typeof raw !== "string")
|
|
16
|
+
return null;
|
|
17
|
+
// URL parsers drop tab, LF and CR, so normalise before testing to see what the parser sees.
|
|
18
|
+
const href = raw.replace(/[\t\n\r]/g, "").trim();
|
|
19
|
+
return href !== "" && SAFE_HREF.test(href) ? href : null;
|
|
20
|
+
}
|
|
21
|
+
export function RichText({ doc }) {
|
|
22
|
+
const d = doc;
|
|
23
|
+
if (!d || typeof d !== "object" || d.type !== "doc" || !Array.isArray(d.content))
|
|
24
|
+
return null;
|
|
25
|
+
return _jsx(_Fragment, { children: renderNodes(d.content) });
|
|
26
|
+
}
|
|
27
|
+
function renderNodes(nodes) {
|
|
28
|
+
return nodes.map((n, i) => _jsx(Fragment, { children: renderNode(n) }, i));
|
|
29
|
+
}
|
|
30
|
+
function renderNode(node) {
|
|
31
|
+
if (!node || typeof node !== "object")
|
|
32
|
+
return null;
|
|
33
|
+
const children = Array.isArray(node.content) ? renderNodes(node.content) : null;
|
|
34
|
+
switch (node.type) {
|
|
35
|
+
case "text": return withMarks(node.text ?? "", node.marks ?? []);
|
|
36
|
+
case "paragraph": return _jsx("p", { children: children });
|
|
37
|
+
case "heading": {
|
|
38
|
+
const level = Math.min(6, Math.max(1, Math.round(Number(node.attrs?.level ?? 1) || 1)));
|
|
39
|
+
return createElement(`h${level}`, null, children);
|
|
40
|
+
}
|
|
41
|
+
case "bulletList": return _jsx("ul", { children: children });
|
|
42
|
+
case "orderedList": return _jsx("ol", { children: children });
|
|
43
|
+
case "listItem": return _jsx("li", { children: children });
|
|
44
|
+
case "blockquote": return _jsx("blockquote", { children: children });
|
|
45
|
+
case "hardBreak": return _jsx("br", {});
|
|
46
|
+
default: return children;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Marks wrap from the first outward: [bold, italic] → <em><strong>…</strong></em>. */
|
|
50
|
+
function withMarks(text, marks) {
|
|
51
|
+
return marks.reduce((inner, m) => {
|
|
52
|
+
switch (m.type) {
|
|
53
|
+
case "bold": return _jsx("strong", { children: inner });
|
|
54
|
+
case "italic": return _jsx("em", { children: inner });
|
|
55
|
+
case "link": {
|
|
56
|
+
const href = safeHref(m.attrs?.href);
|
|
57
|
+
return href === null ? inner : _jsx("a", { href: href, children: inner });
|
|
58
|
+
}
|
|
59
|
+
default: return inner;
|
|
60
|
+
}
|
|
61
|
+
}, text);
|
|
62
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The row id of a shared item (spec 2026-09-07 §10 "§3 row id"): uuid v5
|
|
2
|
+
// of (space id as the namespace, item name as the name). Deterministic in
|
|
3
|
+
// both adapters, so a second concurrent materialization of the same item
|
|
4
|
+
// hits the `entries` primary key instead of adding an unremovable
|
|
5
|
+
// duplicate — `delete` refuses every shared row. The space is part of it
|
|
6
|
+
// because the primary key is `id` alone and, in the cloud, many spaces
|
|
7
|
+
// share one table: a name-only id would make space B's footer collide
|
|
8
|
+
// with space A's.
|
|
9
|
+
//
|
|
10
|
+
// No imports — Web Crypto (`globalThis.crypto.subtle`) exists in Node
|
|
11
|
+
// >= 20 and in every browser, so this module carries no node builtin
|
|
12
|
+
// into the package root's graph. It matters because every scaffolded
|
|
13
|
+
// section does `import { f, smoodly } from "smoodly"`, and a "use client"
|
|
14
|
+
// section's browser build would fail on `node:crypto`.
|
|
15
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16
|
+
/** The namespace's 16 raw bytes — RFC 4122 hashes the bytes, not the text. */
|
|
17
|
+
function uuidBytes(uuid) {
|
|
18
|
+
if (!UUID_RE.test(uuid))
|
|
19
|
+
throw new Error(`smoodly: "${uuid}" is not a uuid — a shared item's namespace is the space id.`);
|
|
20
|
+
const hex = uuid.replace(/-/g, "");
|
|
21
|
+
const bytes = new Uint8Array(16);
|
|
22
|
+
for (let i = 0; i < 16; i++)
|
|
23
|
+
bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
|
24
|
+
return bytes;
|
|
25
|
+
}
|
|
26
|
+
/** RFC 4122 §4.3 uuid v5: sha1(namespace bytes ‖ name bytes), version 5,
|
|
27
|
+
* variant 10xx, first 16 bytes of the digest. */
|
|
28
|
+
export async function sharedEntryId(space, name) {
|
|
29
|
+
const input = new Uint8Array([...uuidBytes(space), ...new TextEncoder().encode(name)]);
|
|
30
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-1", input));
|
|
31
|
+
const bytes = digest.subarray(0, 16);
|
|
32
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x50; // version 5 in the high nibble of octet 6
|
|
33
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10xx in the top bits of octet 8
|
|
34
|
+
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
35
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
36
|
+
}
|
package/dist/sql-space.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** The one space a self-host install has; the space a request with no
|
|
2
|
+
* claim resolves to. The bare uuid — code needs the value (a shared
|
|
3
|
+
* item's id namespace), SQL needs it quoted. */
|
|
4
|
+
export declare const DEFAULT_SPACE_ID = "00000000-0000-0000-0000-000000000000";
|
|
1
5
|
export declare const DEFAULT_SPACE = "'00000000-0000-0000-0000-000000000000'";
|
|
2
6
|
export declare const SPACE_COL = "\"space_id\" uuid not null default '00000000-0000-0000-0000-000000000000'";
|
|
3
7
|
export declare const CONTENT_TABLES: readonly ["pages", "page_locales", "page_versions", "entries", "entry_locales", "entry_versions", "refs", "paths"];
|
package/dist/sql-space.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
// is one cloud environment; a self-host install is one space with the
|
|
3
3
|
// default id. Everything that needs "which space is this request in"
|
|
4
4
|
// reads smoodly_current_space() — the adapters never mention spaces.
|
|
5
|
-
|
|
5
|
+
/** The one space a self-host install has; the space a request with no
|
|
6
|
+
* claim resolves to. The bare uuid — code needs the value (a shared
|
|
7
|
+
* item's id namespace), SQL needs it quoted. */
|
|
8
|
+
export const DEFAULT_SPACE_ID = "00000000-0000-0000-0000-000000000000";
|
|
9
|
+
export const DEFAULT_SPACE = `'${DEFAULT_SPACE_ID}'`;
|
|
6
10
|
export const SPACE_COL = `"space_id" uuid not null default ${DEFAULT_SPACE}`;
|
|
7
11
|
export const CONTENT_TABLES = [
|
|
8
12
|
"pages",
|
package/dist/sql.js
CHANGED
|
@@ -31,7 +31,7 @@ function fieldExpr(key, d, source) {
|
|
|
31
31
|
case "date":
|
|
32
32
|
return `(${source}->>'${key}')::timestamptz`;
|
|
33
33
|
default:
|
|
34
|
-
return `${source}->'${key}'`; // image, video, file, object, richtext, dataAttributes, refList
|
|
34
|
+
return `${source}->'${key}'`; // image, video, file, object, list, richtext, dataAttributes, refList
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
export function collectionSQL(collection) {
|
|
@@ -6,9 +6,17 @@ export declare class SupabaseEntryStore implements EntryStore {
|
|
|
6
6
|
private paths;
|
|
7
7
|
private locales;
|
|
8
8
|
private schemas;
|
|
9
|
+
private spaceId?;
|
|
9
10
|
constructor(db: SupabaseClient, collections: CollectionSchema<any>[], options?: EntryStoreOptions);
|
|
10
11
|
private fail;
|
|
11
12
|
private schema;
|
|
13
|
+
/** The space this client acts in — the ONE function the policies and
|
|
14
|
+
* the insert trigger read (src/sql-space.ts): a key's claim in the
|
|
15
|
+
* cloud, the default space under the service role. Constant for the
|
|
16
|
+
* life of the client, so the PROMISE is cached and concurrent creates
|
|
17
|
+
* share one round trip; a failed lookup is not cached. */
|
|
18
|
+
private space;
|
|
19
|
+
private fetchSpace;
|
|
12
20
|
private fetch;
|
|
13
21
|
private must;
|
|
14
22
|
private siblings;
|