smoodly 0.0.4 → 0.0.6
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 +408 -34
- package/dist/admin/client-ops.js +2 -2
- package/dist/admin/editor/EditorView.d.ts +3 -2
- package/dist/admin/editor/EditorView.js +133 -80
- package/dist/admin/editor/PageSettings.d.ts +13 -0
- package/dist/admin/editor/PageSettings.js +40 -0
- package/dist/admin/fixed-nodes.d.ts +23 -0
- package/dist/admin/fixed-nodes.js +83 -0
- package/dist/admin/forms/FieldWidget.d.ts +2 -1
- package/dist/admin/forms/FieldWidget.js +16 -16
- package/dist/admin/index.d.ts +2 -1
- package/dist/admin/index.js +1 -0
- package/dist/admin/next/create-admin.js +7 -2
- package/dist/admin/next/op-handler.d.ts +1 -0
- package/dist/admin/next/op-handler.js +7 -2
- package/dist/admin/ops-impl.d.ts +4 -1
- package/dist/admin/ops-impl.js +190 -48
- package/dist/admin/ops.d.ts +39 -11
- package/dist/admin/serialize.d.ts +10 -1
- package/dist/admin/serialize.js +13 -2
- package/dist/admin/shell/AdminApp.js +36 -74
- package/dist/admin/shell/EntryForm.js +34 -10
- package/dist/admin/shell/ErrorPane.d.ts +4 -0
- package/dist/admin/shell/ErrorPane.js +9 -0
- package/dist/admin/shell/ListView.d.ts +8 -5
- package/dist/admin/shell/ListView.js +12 -11
- package/dist/admin/shell/LoginView.js +6 -2
- package/dist/admin/shell/PagesList.d.ts +6 -0
- package/dist/admin/shell/PagesList.js +148 -0
- package/dist/admin/shell/base.d.ts +1 -0
- package/dist/admin/shell/base.js +4 -0
- package/dist/admin/shell/pages-tree.d.ts +39 -0
- package/dist/admin/shell/pages-tree.js +65 -0
- package/dist/admin/ui/format.d.ts +1 -0
- package/dist/admin/ui/format.js +25 -0
- package/dist/admin/ui/primitives.d.ts +89 -0
- package/dist/admin/ui/primitives.js +115 -0
- package/dist/admin/ui/theme.d.ts +5 -0
- package/dist/admin/ui/theme.js +396 -0
- package/dist/collections.d.ts +23 -2
- package/dist/collections.js +19 -0
- package/dist/config.d.ts +11 -1
- package/dist/config.js +71 -13
- package/dist/entry-page.d.ts +16 -0
- package/dist/entry-page.js +6 -0
- package/dist/entry-store.d.ts +94 -7
- package/dist/entry-store.js +175 -26
- package/dist/index.d.ts +24 -8
- package/dist/index.js +14 -3
- package/dist/localize.d.ts +19 -0
- package/dist/localize.js +42 -0
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +1 -1
- package/dist/next/meta.d.ts +0 -6
- package/dist/next/meta.js +0 -11
- package/dist/next/page-renderer.d.ts +26 -18
- package/dist/next/page-renderer.js +79 -56
- package/dist/page.d.ts +10 -3
- package/dist/page.js +12 -2
- package/dist/pairing.d.ts +7 -6
- package/dist/path-index.d.ts +16 -0
- package/dist/path-index.js +33 -0
- package/dist/paths.d.ts +80 -0
- package/dist/paths.js +110 -0
- package/dist/render.d.ts +34 -5
- package/dist/render.js +9 -2
- package/dist/resolve.d.ts +8 -0
- package/dist/resolve.js +19 -7
- package/dist/revalidate.d.ts +10 -2
- package/dist/revalidate.js +13 -8
- package/dist/site.d.ts +85 -0
- package/dist/site.js +164 -0
- package/dist/sql-space.d.ts +1 -1
- package/dist/sql-space.js +2 -0
- package/dist/sql.d.ts +1 -1
- package/dist/sql.js +92 -15
- package/dist/store.d.ts +106 -30
- package/dist/store.js +242 -66
- package/dist/supabase-entry-store.d.ts +17 -3
- package/dist/supabase-entry-store.js +198 -22
- package/dist/supabase-path-index.d.ts +11 -0
- package/dist/supabase-path-index.js +52 -0
- package/dist/supabase-store.d.ts +46 -16
- package/dist/supabase-store.js +322 -110
- package/package.json +5 -4
- package/dist/admin/page-path.d.ts +0 -1
- package/dist/admin/page-path.js +0 -6
- package/dist/admin/shell/pages-list.d.ts +0 -22
- package/dist/admin/shell/pages-list.js +0 -31
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// The design system, as one injected stylesheet.
|
|
3
|
+
//
|
|
4
|
+
// Smoodly ships as a library into someone else's Next app, so the admin
|
|
5
|
+
// can't rely on the host importing CSS: `AdminLayout` renders <AdminStyles/>
|
|
6
|
+
// once and every admin surface styles itself from these tokens. Everything
|
|
7
|
+
// is scoped under `.sm-admin` — never `:root` — so the host's own custom
|
|
8
|
+
// properties and class names can't collide with ours.
|
|
9
|
+
//
|
|
10
|
+
// Values come from design_handoff_smoodly_cms/tokens/*.css and are final:
|
|
11
|
+
// don't round them to a grid. Hover/focus/disabled live here rather than in
|
|
12
|
+
// inline styles, which is the whole reason for the stylesheet.
|
|
13
|
+
const TOKENS_LIGHT = `
|
|
14
|
+
--brand-600:#0038B1; --brand-500:#2554D6; --brand-300:#6E96FF;
|
|
15
|
+
--brand-200:#A9C0FF; --brand-100:#DCE4F9; --brand-50:#E3EAFA;
|
|
16
|
+
--gray-0:#FFFFFF; --gray-50:#F1F2F6; --gray-100:#E1E4EC; --gray-300:#C9D0E2;
|
|
17
|
+
--gray-500:#8B93A7; --gray-600:#6B7386; --gray-700:#4A5165; --gray-900:#0F1420;
|
|
18
|
+
--green-700:#1F7A46; --green-50:#E3F3EA; --green-300:#6FD39A; --green-500:#2E9E5B;
|
|
19
|
+
--amber-700:#8A5A00; --amber-50:#FBF1DC; --amber-300:#E8B85A;
|
|
20
|
+
--red-700:#B42318; --red-50:#FDECEC; --red-300:#F5A5A0;
|
|
21
|
+
|
|
22
|
+
--bg:var(--gray-50); --surface:var(--gray-0); --surface-inset:var(--gray-50);
|
|
23
|
+
--input-bg:var(--gray-0); --line:var(--gray-100); --line-dashed:var(--gray-300);
|
|
24
|
+
--text:var(--gray-900); --text-muted:var(--gray-600); --text-subtle:var(--gray-500);
|
|
25
|
+
--accent:var(--brand-600); --on-accent:#FFFFFF; --accent-soft:var(--brand-50);
|
|
26
|
+
--selection:var(--brand-600); --nav-active-bg:var(--brand-100); --nav-active-fg:var(--brand-600);
|
|
27
|
+
--hover:rgba(15,20,32,.045); --press:rgba(15,20,32,.08);
|
|
28
|
+
--control-hover:var(--gray-100);
|
|
29
|
+
|
|
30
|
+
--status-published-fg:var(--green-700); --status-published-bg:var(--green-50);
|
|
31
|
+
--status-draft-fg:var(--amber-700); --status-draft-bg:var(--amber-50);
|
|
32
|
+
--status-scheduled-fg:var(--accent); --status-scheduled-bg:var(--accent-soft);
|
|
33
|
+
--status-saved-dot:var(--green-500);
|
|
34
|
+
--warn:var(--amber-700); --warn-soft:var(--amber-50);
|
|
35
|
+
--danger:var(--red-700); --danger-soft:var(--red-50);
|
|
36
|
+
|
|
37
|
+
--page-bg:#FFFFFF; --page-text:var(--gray-900);
|
|
38
|
+
--shadow-page:0 1px 2px rgba(0,0,0,.08), 0 8px 24px rgba(0,0,0,.06);
|
|
39
|
+
--shadow-float:0 2px 8px rgba(0,0,0,.25);
|
|
40
|
+
--shadow-seg:0 1px 2px rgba(0,0,0,.08);
|
|
41
|
+
--focus-ring:0 0 0 3px var(--accent-soft);
|
|
42
|
+
--shadow-pop:0 8px 32px rgba(0,0,0,.18);
|
|
43
|
+
--logo-filter:none;
|
|
44
|
+
|
|
45
|
+
/* Which half of the theme toggle is visible. A token, so the icon can never
|
|
46
|
+
disagree with the colors — both follow the one cascade below. */
|
|
47
|
+
--when-light:inline-flex; --when-dark:none;
|
|
48
|
+
`;
|
|
49
|
+
const TOKENS_DARK = `
|
|
50
|
+
--bg:#0D0F14; --surface:#15181F; --surface-inset:#0D0F14; --input-bg:#1B1F28;
|
|
51
|
+
--line:#2A2F3B; --line-dashed:#2A2F3B;
|
|
52
|
+
--text:#EEF0F5; --text-muted:#8C94A8; --text-subtle:#6B7386;
|
|
53
|
+
--accent:var(--brand-300); --on-accent:#06122F; --accent-soft:rgba(110,150,255,.16);
|
|
54
|
+
--nav-active-bg:rgba(110,150,255,.14); --nav-active-fg:var(--brand-200);
|
|
55
|
+
--hover:rgba(238,240,245,.05); --press:rgba(238,240,245,.09);
|
|
56
|
+
--control-hover:var(--input-bg);
|
|
57
|
+
|
|
58
|
+
--status-published-fg:var(--green-300); --status-published-bg:rgba(111,211,154,.14);
|
|
59
|
+
--status-draft-fg:var(--amber-300); --status-draft-bg:rgba(232,184,90,.14);
|
|
60
|
+
--warn:var(--amber-300); --warn-soft:rgba(232,184,90,.14);
|
|
61
|
+
--danger:var(--red-300); --danger-soft:rgba(244,102,92,.14);
|
|
62
|
+
|
|
63
|
+
--shadow-page:0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.35);
|
|
64
|
+
--shadow-pop:0 8px 32px rgba(0,0,0,.5);
|
|
65
|
+
--logo-filter:brightness(0) invert(1);
|
|
66
|
+
--when-light:none; --when-dark:inline-flex;
|
|
67
|
+
`;
|
|
68
|
+
const SCALE = `
|
|
69
|
+
--font-sans:'Public Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;
|
|
70
|
+
--font-mono:ui-monospace,Menlo,Consolas,monospace;
|
|
71
|
+
--text-2xs:9px; --text-xs:10px; --text-sm:11px; --text-md:12px;
|
|
72
|
+
--text-lg:13px; --text-xl:14px; --text-2xl:15px; --text-3xl:20px; --text-4xl:30px;
|
|
73
|
+
--tracking-tight:-0.02em; --tracking-label:0.1em; --tracking-tag:0.08em;
|
|
74
|
+
|
|
75
|
+
--radius-xs:1px; --radius-sm:3px; --radius-md:4px; --radius-round:999px;
|
|
76
|
+
--control-h:28px; --control-h-lg:36px; --row-h:38px; --topbar-h:48px;
|
|
77
|
+
--nav-w:200px; --inspector-w:300px;
|
|
78
|
+
--border:1px solid var(--line);
|
|
79
|
+
--outline-selection:1.5px solid var(--selection);
|
|
80
|
+
`;
|
|
81
|
+
export const ADMIN_CSS = `
|
|
82
|
+
@import url("https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;500;600;700&display=swap");
|
|
83
|
+
|
|
84
|
+
.sm-admin{${SCALE}${TOKENS_LIGHT}}
|
|
85
|
+
html[data-theme="dark"] .sm-admin,.sm-admin[data-theme="dark"]{${TOKENS_DARK}}
|
|
86
|
+
@media (prefers-color-scheme:dark){
|
|
87
|
+
html:not([data-theme="light"]) .sm-admin:not([data-theme="light"]){${TOKENS_DARK}}
|
|
88
|
+
}
|
|
89
|
+
/* Last, and the same specificity as the host-attribute rule: an explicit
|
|
90
|
+
choice from the theme toggle wins over a dark <html> and over the OS. */
|
|
91
|
+
.sm-admin[data-theme="light"]{${TOKENS_LIGHT}}
|
|
92
|
+
|
|
93
|
+
/* The admin owns the viewport; a host <body> margin would push it into a
|
|
94
|
+
page scrollbar. Scoped by :has, so only a page holding the admin is touched. */
|
|
95
|
+
body:has(.sm-admin){margin:0}
|
|
96
|
+
.sm-admin,.sm-admin *,.sm-admin *::before,.sm-admin *::after{box-sizing:border-box}
|
|
97
|
+
.sm-admin{
|
|
98
|
+
font-family:var(--font-sans);font-size:var(--text-md);line-height:1.4;
|
|
99
|
+
color:var(--text);background:var(--surface);
|
|
100
|
+
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
|
101
|
+
}
|
|
102
|
+
.sm-admin h1,.sm-admin h2,.sm-admin h3,.sm-admin p,.sm-admin ul{margin:0}
|
|
103
|
+
/* :where() keeps this at zero specificity — a bare <a> is accent, but any
|
|
104
|
+
component class on it (.sm-nav-item, .sm-btn, .sm-back) decides its own
|
|
105
|
+
colour. Without this the element selector silently beat every one of them. */
|
|
106
|
+
:where(.sm-admin a){color:var(--accent);text-decoration:none}
|
|
107
|
+
:where(.sm-admin a:hover){color:var(--brand-500)}
|
|
108
|
+
.sm-admin button,.sm-admin input,.sm-admin select,.sm-admin textarea{font-family:inherit}
|
|
109
|
+
.sm-admin ::placeholder{color:var(--text-subtle)}
|
|
110
|
+
.sm-admin ::selection{background:var(--accent-soft);color:var(--text)}
|
|
111
|
+
.sm-mono{font-family:var(--font-mono)}
|
|
112
|
+
|
|
113
|
+
/* ── shell ─────────────────────────────────────────────────────────── */
|
|
114
|
+
.sm-shell{display:grid;grid-template-columns:var(--nav-w) 1fr;height:100vh;height:100dvh;overflow:hidden}
|
|
115
|
+
.sm-shell>.sm-main{min-width:0;min-height:0;display:flex;flex-direction:column;overflow:hidden}
|
|
116
|
+
.sm-nav{
|
|
117
|
+
display:flex;flex-direction:column;gap:24px;padding:16px 12px;
|
|
118
|
+
border-right:var(--border);background:var(--surface);min-width:0;overflow:auto;
|
|
119
|
+
}
|
|
120
|
+
.sm-nav__logo{height:18px;width:auto;align-self:flex-start;filter:var(--logo-filter);flex:none}
|
|
121
|
+
.sm-nav>.sm-nav__logo{margin-left:10px}
|
|
122
|
+
.sm-nav__list{display:flex;flex-direction:column;gap:1px}
|
|
123
|
+
.sm-nav-item{
|
|
124
|
+
display:block;padding:6px 10px;border-radius:var(--radius-md);line-height:1.3;
|
|
125
|
+
color:var(--text);font-weight:500;text-decoration:none;cursor:pointer;
|
|
126
|
+
background:transparent;border:0;width:100%;text-align:left;font-size:var(--text-md);
|
|
127
|
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
128
|
+
}
|
|
129
|
+
.sm-nav-item:hover{background:var(--surface-inset);color:var(--text)}
|
|
130
|
+
.sm-nav-item[aria-current="page"]{background:var(--nav-active-bg);color:var(--nav-active-fg);font-weight:600}
|
|
131
|
+
.sm-nav-item[aria-current="page"]:hover{background:var(--nav-active-bg);color:var(--nav-active-fg)}
|
|
132
|
+
/* Not greyed: the reference renders every inactive item at --text and marks
|
|
133
|
+
the unavailable ones only by being inert (no pointer, no hover). */
|
|
134
|
+
.sm-nav-item[aria-disabled="true"]{cursor:default}
|
|
135
|
+
.sm-nav-item[aria-disabled="true"]:hover{background:transparent}
|
|
136
|
+
.sm-nav__foot{margin-top:auto;display:flex;flex-direction:column;gap:2px}
|
|
137
|
+
.sm-nav__user{display:flex;align-items:center;gap:10px;padding:6px 10px;min-width:0}
|
|
138
|
+
.sm-nav-item--icon{display:flex;align-items:center;gap:8px}
|
|
139
|
+
.sm-when-light{display:var(--when-light);align-items:center;gap:8px}
|
|
140
|
+
.sm-when-dark{display:var(--when-dark);align-items:center;gap:8px}
|
|
141
|
+
.sm-avatar{
|
|
142
|
+
width:24px;height:24px;flex:none;border-radius:var(--radius-round);
|
|
143
|
+
background:var(--accent-soft);color:var(--accent);
|
|
144
|
+
display:grid;place-items:center;font-size:var(--text-sm);font-weight:600;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ── top bar ───────────────────────────────────────────────────────── */
|
|
148
|
+
.sm-topbar{
|
|
149
|
+
height:var(--topbar-h);flex:none;display:flex;align-items:center;justify-content:space-between;
|
|
150
|
+
gap:16px;padding:0 16px;border-bottom:var(--border);background:var(--surface);
|
|
151
|
+
}
|
|
152
|
+
.sm-topbar__left{display:flex;align-items:center;gap:10px;min-width:0}
|
|
153
|
+
.sm-topbar__right{display:flex;align-items:center;gap:8px;flex:none}
|
|
154
|
+
.sm-crumbs{display:flex;align-items:center;gap:8px;color:var(--text-muted);min-width:0}
|
|
155
|
+
.sm-crumbs__sep{opacity:.5}
|
|
156
|
+
.sm-crumbs__here{color:var(--text);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
157
|
+
.sm-back{border:0;background:transparent;padding:0;color:var(--text);font:inherit;cursor:pointer;line-height:1}
|
|
158
|
+
.sm-back:hover{color:var(--accent)}
|
|
159
|
+
.sm-title{font-size:var(--text-xl);font-weight:600}
|
|
160
|
+
.sm-count{color:var(--text-muted)}
|
|
161
|
+
|
|
162
|
+
/* ── buttons ───────────────────────────────────────────────────────── */
|
|
163
|
+
.sm-btn{
|
|
164
|
+
display:inline-flex;align-items:center;justify-content:center;gap:6px;
|
|
165
|
+
height:var(--control-h);padding:0 12px;border-radius:var(--radius-md);border:0;
|
|
166
|
+
font-size:var(--text-md);font-weight:500;line-height:1;white-space:nowrap;cursor:pointer;
|
|
167
|
+
text-decoration:none;
|
|
168
|
+
}
|
|
169
|
+
.sm-btn--lg{height:var(--control-h-lg);padding:0 16px}
|
|
170
|
+
.sm-btn--primary{background:var(--accent);color:var(--on-accent);font-weight:600;padding:0 14px}
|
|
171
|
+
.sm-btn--primary:hover{background:var(--brand-500);color:var(--on-accent)}
|
|
172
|
+
.sm-btn--secondary{background:var(--surface-inset);color:var(--text)}
|
|
173
|
+
.sm-btn--secondary:hover{background:var(--control-hover);color:var(--text)}
|
|
174
|
+
.sm-btn--ghost{background:transparent;color:var(--accent);font-weight:600;padding:0 8px}
|
|
175
|
+
.sm-btn--ghost:hover{background:var(--accent-soft);color:var(--accent)}
|
|
176
|
+
.sm-btn--danger{background:var(--surface-inset);color:var(--danger)}
|
|
177
|
+
.sm-btn--danger:hover{background:var(--danger-soft);color:var(--danger)}
|
|
178
|
+
.sm-btn--ok{background:var(--status-published-bg);color:var(--status-published-fg);font-weight:600;padding:0 14px}
|
|
179
|
+
.sm-btn:disabled,.sm-btn[aria-disabled="true"]{opacity:.5;cursor:default;pointer-events:none}
|
|
180
|
+
.sm-btn:focus-visible,.sm-iconbtn:focus-visible,.sm-nav-item:focus-visible,
|
|
181
|
+
.sm-tab:focus-visible,.sm-seg__opt:focus-visible,.sm-row:focus-visible{
|
|
182
|
+
outline:2px solid var(--accent);outline-offset:1px
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.sm-iconbtn{
|
|
186
|
+
width:24px;height:24px;flex:none;display:grid;place-items:center;padding:0;
|
|
187
|
+
border:0;border-radius:var(--radius-sm);background:transparent;color:var(--text-muted);
|
|
188
|
+
font-size:12px;line-height:1;cursor:pointer;
|
|
189
|
+
}
|
|
190
|
+
.sm-iconbtn:hover{background:var(--hover);color:var(--text)}
|
|
191
|
+
.sm-iconbtn--inverse{color:#EEF0F5}
|
|
192
|
+
.sm-iconbtn--inverse:hover{background:rgba(255,255,255,.14);color:#fff}
|
|
193
|
+
.sm-iconbtn:disabled{opacity:.3;cursor:default}
|
|
194
|
+
.sm-iconbtn:disabled:hover{background:transparent}
|
|
195
|
+
|
|
196
|
+
/* ── fields ────────────────────────────────────────────────────────── */
|
|
197
|
+
.sm-field{display:flex;flex-direction:column;gap:6px;min-width:0}
|
|
198
|
+
.sm-field__head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}
|
|
199
|
+
.sm-label{
|
|
200
|
+
font-size:var(--text-2xs);font-weight:600;letter-spacing:var(--tracking-label);
|
|
201
|
+
text-transform:uppercase;color:var(--text-muted);
|
|
202
|
+
}
|
|
203
|
+
.sm-counter,.sm-hint{font-size:var(--text-sm);color:var(--text-muted)}
|
|
204
|
+
.sm-input,.sm-textarea,.sm-select{
|
|
205
|
+
width:100%;min-width:0;border:1.5px solid transparent;border-radius:var(--radius-md);
|
|
206
|
+
background:var(--surface-inset);color:var(--text);font-size:var(--text-md);
|
|
207
|
+
outline:0;box-shadow:none;
|
|
208
|
+
}
|
|
209
|
+
.sm-input,.sm-select{height:var(--control-h);padding:0 10px}
|
|
210
|
+
.sm-textarea{padding:8px 10px;line-height:1.5;resize:vertical;min-height:60px}
|
|
211
|
+
.sm-input--lg{height:var(--control-h-lg);font-size:var(--text-2xl);font-weight:600}
|
|
212
|
+
.sm-input--mono{font-family:var(--font-mono);font-size:var(--text-sm)}
|
|
213
|
+
.sm-input:hover,.sm-textarea:hover,.sm-select:hover{background:var(--control-hover)}
|
|
214
|
+
.sm-input:focus,.sm-textarea:focus,.sm-select:focus,
|
|
215
|
+
.sm-inputwrap:focus-within{
|
|
216
|
+
background:var(--input-bg);border-color:var(--selection);box-shadow:var(--focus-ring);
|
|
217
|
+
}
|
|
218
|
+
.sm-input:disabled,.sm-textarea:disabled,.sm-select:disabled{opacity:.6;cursor:default}
|
|
219
|
+
.sm-select{
|
|
220
|
+
appearance:none;-webkit-appearance:none;cursor:pointer;padding-right:26px;font-weight:500;
|
|
221
|
+
background-image:linear-gradient(45deg,transparent 50%,var(--text-muted) 50%),
|
|
222
|
+
linear-gradient(135deg,var(--text-muted) 50%,transparent 50%);
|
|
223
|
+
background-position:calc(100% - 14px) 12px,calc(100% - 10px) 12px;
|
|
224
|
+
background-size:4px 4px,4px 4px;background-repeat:no-repeat;
|
|
225
|
+
}
|
|
226
|
+
.sm-inputwrap{
|
|
227
|
+
display:flex;align-items:center;gap:0;height:var(--control-h);padding:0 10px;
|
|
228
|
+
border:1.5px solid transparent;border-radius:var(--radius-md);background:var(--surface-inset);
|
|
229
|
+
}
|
|
230
|
+
.sm-inputwrap>.sm-input{height:auto;padding:0;background:transparent;border:0;box-shadow:none}
|
|
231
|
+
.sm-inputwrap>.sm-input:hover,.sm-inputwrap>.sm-input:focus{background:transparent;box-shadow:none}
|
|
232
|
+
.sm-prefix{color:var(--text-muted);font-family:var(--font-mono);font-size:var(--text-sm);flex:none}
|
|
233
|
+
.sm-error{color:var(--danger);font-size:var(--text-sm)}
|
|
234
|
+
|
|
235
|
+
/* ── data ──────────────────────────────────────────────────────────── */
|
|
236
|
+
.sm-table{display:flex;flex-direction:column;padding:0 16px 16px}
|
|
237
|
+
.sm-table__head,.sm-row{display:grid;gap:12px;align-items:center;padding:0 8px}
|
|
238
|
+
.sm-table__head{
|
|
239
|
+
height:34px;border-bottom:var(--border);font-family:var(--font-mono);
|
|
240
|
+
font-size:var(--text-2xs);letter-spacing:var(--tracking-tag);color:var(--text-muted);
|
|
241
|
+
text-transform:uppercase;position:sticky;top:0;background:var(--surface);z-index:1;
|
|
242
|
+
}
|
|
243
|
+
.sm-table__body{display:flex;flex-direction:column;padding-top:4px}
|
|
244
|
+
.sm-row{
|
|
245
|
+
height:var(--row-h);border-radius:var(--radius-md);background:transparent;
|
|
246
|
+
border:0;font:inherit;color:inherit;text-align:left;width:100%;
|
|
247
|
+
}
|
|
248
|
+
.sm-row--clickable{cursor:pointer}
|
|
249
|
+
.sm-row--clickable:hover{background:var(--surface-inset)}
|
|
250
|
+
.sm-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
251
|
+
.sm-cell--muted{color:var(--text-muted)}
|
|
252
|
+
.sm-cell--mono{font-family:var(--font-mono);font-size:var(--text-sm);color:var(--text-muted)}
|
|
253
|
+
.sm-cell--title{font-weight:500}
|
|
254
|
+
.sm-empty{padding:28px 8px;color:var(--text-muted)}
|
|
255
|
+
.sm-chevron{
|
|
256
|
+
width:12px;flex:none;border:0;background:none;padding:0;font:inherit;font-size:10px;
|
|
257
|
+
color:var(--text-muted);cursor:pointer;line-height:1;
|
|
258
|
+
}
|
|
259
|
+
.sm-chevron:hover{color:var(--text)}
|
|
260
|
+
.sm-cell--tree{display:flex;align-items:center;gap:6px;font-weight:500}
|
|
261
|
+
.sm-cell--tree.sm-cell--parent{font-weight:600}
|
|
262
|
+
.sm-rowmenu{
|
|
263
|
+
border:0;background:none;padding:0 6px;font:inherit;color:var(--text-muted);cursor:pointer;
|
|
264
|
+
border-radius:var(--radius-md);line-height:1;height:24px;
|
|
265
|
+
}
|
|
266
|
+
.sm-rowmenu:hover{background:var(--surface-inset);color:var(--text)}
|
|
267
|
+
|
|
268
|
+
.sm-badge{
|
|
269
|
+
display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:var(--radius-md);
|
|
270
|
+
font-size:var(--text-xs);font-weight:600;letter-spacing:.04em;white-space:nowrap;
|
|
271
|
+
}
|
|
272
|
+
.sm-savestate{
|
|
273
|
+
display:inline-flex;align-items:center;gap:6px;padding:0 6px;
|
|
274
|
+
font-size:var(--text-xs);font-weight:600;letter-spacing:.06em;text-transform:uppercase;
|
|
275
|
+
}
|
|
276
|
+
.sm-dot{width:5px;height:5px;border-radius:var(--radius-round);flex:none}
|
|
277
|
+
|
|
278
|
+
/* ── navigation bits ───────────────────────────────────────────────── */
|
|
279
|
+
.sm-tabs{display:flex;gap:18px;padding:0 14px;height:36px;align-items:stretch;border-bottom:var(--border);flex:none}
|
|
280
|
+
.sm-tab{
|
|
281
|
+
display:flex;align-items:center;border:0;background:transparent;padding:0;font:inherit;
|
|
282
|
+
cursor:pointer;color:var(--text-muted);border-bottom:2px solid transparent;margin-bottom:-1px;
|
|
283
|
+
}
|
|
284
|
+
.sm-tab:hover{color:var(--text)}
|
|
285
|
+
.sm-tab[aria-selected="true"]{color:var(--accent);font-weight:600;border-bottom-color:var(--accent)}
|
|
286
|
+
.sm-seg{display:flex;gap:2px;padding:2px;background:var(--surface-inset);border-radius:var(--radius-md)}
|
|
287
|
+
.sm-seg--stretch{width:100%}
|
|
288
|
+
.sm-seg--disabled{opacity:.6}
|
|
289
|
+
.sm-seg__opt:disabled{cursor:default}
|
|
290
|
+
.sm-seg__opt{
|
|
291
|
+
flex:1;padding:3px 10px;border:0;border-radius:var(--radius-sm);background:transparent;
|
|
292
|
+
color:var(--text-muted);font:inherit;cursor:pointer;white-space:nowrap;
|
|
293
|
+
}
|
|
294
|
+
.sm-seg__opt:hover{color:var(--text)}
|
|
295
|
+
.sm-seg__opt[aria-pressed="true"]{background:var(--surface);color:var(--text);font-weight:600;box-shadow:var(--shadow-seg)}
|
|
296
|
+
.sm-divider{height:1px;background:var(--line);flex:none}
|
|
297
|
+
.sm-sectionlabel{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
298
|
+
.sm-sectionlabel--mono>.sm-label{
|
|
299
|
+
font-family:var(--font-mono);font-weight:400;letter-spacing:var(--tracking-tag);
|
|
300
|
+
}
|
|
301
|
+
.sm-banner{
|
|
302
|
+
display:flex;gap:14px;align-items:flex-start;padding:10px 14px;border-radius:var(--radius-md);
|
|
303
|
+
font-size:var(--text-md);
|
|
304
|
+
}
|
|
305
|
+
.sm-banner--error{background:var(--danger-soft);color:var(--danger)}
|
|
306
|
+
.sm-banner--ok{background:var(--status-published-bg);color:var(--status-published-fg)}
|
|
307
|
+
.sm-banner--warn{background:var(--warn-soft);color:var(--warn)}
|
|
308
|
+
|
|
309
|
+
/* ── editor ────────────────────────────────────────────────────────── */
|
|
310
|
+
.sm-editor{flex:1;min-height:0;display:grid;grid-template-columns:1fr var(--inspector-w)}
|
|
311
|
+
.sm-canvas{display:flex;flex-direction:column;background:var(--bg);min-width:0;min-height:0;overflow:hidden}
|
|
312
|
+
.sm-canvas__meta{
|
|
313
|
+
display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 20px;flex:none;
|
|
314
|
+
font-family:var(--font-mono);font-size:var(--text-xs);letter-spacing:.04em;color:var(--text-muted);
|
|
315
|
+
}
|
|
316
|
+
.sm-canvas__scroll{flex:1;min-height:0;overflow:auto;padding:0 20px 20px}
|
|
317
|
+
.sm-page{
|
|
318
|
+
position:relative;background:var(--page-bg);border-radius:var(--radius-md);
|
|
319
|
+
box-shadow:var(--shadow-page);min-height:100%;
|
|
320
|
+
}
|
|
321
|
+
.sm-inspector{
|
|
322
|
+
display:flex;flex-direction:column;min-height:0;overflow:hidden;
|
|
323
|
+
border-left:var(--border);background:var(--surface);
|
|
324
|
+
}
|
|
325
|
+
.sm-inspector__head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:12px 14px 10px;flex:none}
|
|
326
|
+
.sm-inspector__body{display:flex;flex-direction:column;gap:14px;padding:14px;overflow:auto}
|
|
327
|
+
.sm-blockglyph{width:14px;height:10px;border:1.5px solid var(--text-muted);border-radius:var(--radius-xs);flex:none}
|
|
328
|
+
.sm-blocktag{
|
|
329
|
+
position:absolute;top:0;left:0;background:var(--selection);color:#fff;
|
|
330
|
+
font-family:var(--font-mono);font-size:var(--text-2xs);font-weight:700;
|
|
331
|
+
letter-spacing:var(--tracking-tag);text-transform:uppercase;padding:3px 7px;
|
|
332
|
+
border-radius:var(--radius-sm) 0 var(--radius-sm) 0;white-space:nowrap;
|
|
333
|
+
}
|
|
334
|
+
.sm-blocktoolbar{
|
|
335
|
+
display:flex;gap:2px;padding:2px;background:#15181F;border-radius:var(--radius-md);
|
|
336
|
+
box-shadow:var(--shadow-float);
|
|
337
|
+
}
|
|
338
|
+
.sm-plus{
|
|
339
|
+
width:24px;height:24px;border-radius:var(--radius-round);border:0;background:var(--selection);
|
|
340
|
+
color:#fff;display:grid;place-items:center;font-size:15px;line-height:1;cursor:pointer;
|
|
341
|
+
box-shadow:0 2px 6px rgba(0,56,177,.35);padding:0;
|
|
342
|
+
}
|
|
343
|
+
.sm-plus:hover{background:var(--brand-500)}
|
|
344
|
+
.sm-guide{height:1px;background:color-mix(in srgb,var(--selection) 35%,transparent)}
|
|
345
|
+
.sm-tooltip{
|
|
346
|
+
background:#15181F;color:#EEF0F5;font-size:var(--text-sm);padding:4px 8px;
|
|
347
|
+
border-radius:var(--radius-sm);white-space:nowrap;
|
|
348
|
+
}
|
|
349
|
+
.sm-outline-row{
|
|
350
|
+
display:flex;align-items:center;justify-content:space-between;gap:4px;
|
|
351
|
+
height:var(--control-h);border-radius:var(--radius-md);padding:0 4px 0 10px;
|
|
352
|
+
color:var(--text);font-weight:500;background:transparent;
|
|
353
|
+
border:1px solid var(--line);
|
|
354
|
+
}
|
|
355
|
+
.sm-outline-row--clickable{cursor:pointer}
|
|
356
|
+
.sm-outline-row--clickable:hover{background:var(--surface-inset)}
|
|
357
|
+
/* Selected and locked rows are read by their fill, so they drop the hairline —
|
|
358
|
+
border-color, not border, so every row keeps the same 28px box. */
|
|
359
|
+
.sm-outline-row--selected,.sm-outline-row--selected:hover{
|
|
360
|
+
background:var(--nav-active-bg);color:var(--nav-active-fg);font-weight:600;border-color:transparent;
|
|
361
|
+
}
|
|
362
|
+
.sm-outline-row--empty{border:1px dashed var(--line-dashed);color:var(--text-muted);padding:0 10px}
|
|
363
|
+
.sm-outline-row--locked{background:var(--surface-inset);color:var(--text-muted);padding:0 10px;border-color:transparent}
|
|
364
|
+
.sm-outline-row__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
365
|
+
|
|
366
|
+
.sm-picker{
|
|
367
|
+
width:300px;background:var(--surface);border:var(--border);border-radius:var(--radius-md);
|
|
368
|
+
padding:6px;box-shadow:var(--shadow-pop);
|
|
369
|
+
}
|
|
370
|
+
.sm-picker__item{
|
|
371
|
+
display:flex;gap:10px;align-items:center;width:100%;text-align:left;background:transparent;
|
|
372
|
+
border:0;border-radius:var(--radius-md);padding:8px 8px;cursor:pointer;color:var(--text);font:inherit;
|
|
373
|
+
}
|
|
374
|
+
.sm-picker__item:hover{background:var(--nav-active-bg)}
|
|
375
|
+
.sm-picker__icon{
|
|
376
|
+
width:26px;height:26px;flex:none;border-radius:var(--radius-sm);background:var(--accent-soft);
|
|
377
|
+
color:var(--accent);display:grid;place-items:center;font-size:13px;
|
|
378
|
+
}
|
|
379
|
+
.sm-dashed{
|
|
380
|
+
border:1px dashed var(--line-dashed);border-radius:var(--radius-md);
|
|
381
|
+
display:flex;align-items:center;justify-content:center;gap:8px;color:var(--text-muted);
|
|
382
|
+
}
|
|
383
|
+
`;
|
|
384
|
+
/** Where the theme toggle's choice is remembered, browser-side only. */
|
|
385
|
+
export const THEME_KEY = "smoodly-theme";
|
|
386
|
+
/** Applies a remembered choice before first paint, so a dark-mode editor never
|
|
387
|
+
* gets a white flash. It runs INSIDE the admin wrapper, so
|
|
388
|
+
* `currentScript.parentElement` is that element — no id lookup, and the host
|
|
389
|
+
* document's <html> is left alone. */
|
|
390
|
+
const THEME_BOOT = `(function(){try{var e=document.currentScript.parentElement,` +
|
|
391
|
+
`t=localStorage.getItem(${JSON.stringify(THEME_KEY)});` +
|
|
392
|
+
`if(t==="dark"||t==="light")e.setAttribute("data-theme",t)}catch(e){}})()`;
|
|
393
|
+
/** One <style> element and the theme boot, rendered once by AdminLayout. */
|
|
394
|
+
export function AdminStyles() {
|
|
395
|
+
return (_jsxs(_Fragment, { children: [_jsx("style", { "data-smoodly-admin-styles": true, dangerouslySetInnerHTML: { __html: ADMIN_CSS } }), _jsx("script", { dangerouslySetInnerHTML: { __html: THEME_BOOT } })] }));
|
|
396
|
+
}
|
package/dist/collections.d.ts
CHANGED
|
@@ -1,26 +1,47 @@
|
|
|
1
1
|
import type { Descriptor } from "./fields.ts";
|
|
2
2
|
import type { BuilderMap, NamespaceValues } from "./schema.ts";
|
|
3
|
+
/** How a collection is ordered — in the admin list and in public reads
|
|
4
|
+
* alike (spec 2026-09-05 §3). "manual" is the drag order (`sort`). */
|
|
5
|
+
export type EntryOrder = "manual" | {
|
|
6
|
+
by: "createdAt" | "updatedAt" | (string & {});
|
|
7
|
+
direction: "asc" | "desc";
|
|
8
|
+
};
|
|
9
|
+
export declare const DEFAULT_ORDER: EntryOrder;
|
|
10
|
+
export declare const BUILT_IN_ORDER_FIELDS: readonly ["createdAt", "updatedAt"];
|
|
3
11
|
export type CollectionSchema<F extends BuilderMap = BuilderMap> = {
|
|
4
12
|
kind: "collection";
|
|
5
13
|
name: string;
|
|
6
14
|
title: string;
|
|
7
15
|
titleField?: string;
|
|
8
16
|
fields: Record<string, Descriptor>;
|
|
17
|
+
/** Base URL segment of the collection's entries, code-owned per locale
|
|
18
|
+
* (`"posts"` or `{ en: "posts", fi: "artikkelit" }`). Absent = no URLs. */
|
|
19
|
+
path?: string | Record<string, string>;
|
|
20
|
+
/** Entries nest under entries up to this depth. Absent = flat. */
|
|
21
|
+
tree?: {
|
|
22
|
+
depth: number;
|
|
23
|
+
};
|
|
24
|
+
order?: EntryOrder;
|
|
9
25
|
admin?: {
|
|
10
26
|
listColumns?: string[];
|
|
11
|
-
orderBy?: Record<string, "asc" | "desc">;
|
|
12
27
|
};
|
|
13
28
|
/** phantom: the entry value shape, used by f.ref target inference */
|
|
14
29
|
readonly __entry?: NamespaceValues<F>;
|
|
15
30
|
};
|
|
31
|
+
export declare const collectionOrder: (c: CollectionSchema<any>) => EntryOrder;
|
|
32
|
+
export declare const collectionDepth: (c: CollectionSchema<any>) => number;
|
|
16
33
|
export declare function collection<F extends BuilderMap>(input: {
|
|
17
34
|
name: string;
|
|
18
35
|
title: string;
|
|
19
36
|
titleField?: string;
|
|
20
37
|
fields: F;
|
|
38
|
+
path?: string | Record<string, string>;
|
|
39
|
+
tree?: {
|
|
40
|
+
depth: number;
|
|
41
|
+
};
|
|
42
|
+
order?: EntryOrder;
|
|
21
43
|
admin?: {
|
|
22
44
|
listColumns?: string[];
|
|
23
|
-
orderBy?: Record<string, "asc" | "desc">;
|
|
24
45
|
};
|
|
25
46
|
}): CollectionSchema<F>;
|
|
26
47
|
export type Toolset = {
|
package/dist/collections.js
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
|
+
export const DEFAULT_ORDER = { by: "createdAt", direction: "desc" };
|
|
2
|
+
export const BUILT_IN_ORDER_FIELDS = ["createdAt", "updatedAt"];
|
|
3
|
+
export const collectionOrder = (c) => c.order ?? DEFAULT_ORDER;
|
|
4
|
+
export const collectionDepth = (c) => c.tree?.depth ?? 1;
|
|
1
5
|
export function collection(input) {
|
|
2
6
|
if (!input?.name)
|
|
3
7
|
throw new Error("smoodly: a collection requires a name.");
|
|
8
|
+
if (input.tree !== undefined && (!Number.isInteger(input.tree.depth) || input.tree.depth < 1)) {
|
|
9
|
+
throw new Error(`smoodly: collection "${input.name}" declares an invalid tree depth.`);
|
|
10
|
+
}
|
|
11
|
+
if (input.order !== undefined &&
|
|
12
|
+
input.order !== "manual" &&
|
|
13
|
+
!BUILT_IN_ORDER_FIELDS.includes(input.order.by) &&
|
|
14
|
+
// own property only: `in` would accept "toString", "constructor" and
|
|
15
|
+
// the rest of the prototype chain as declared order fields. (The
|
|
16
|
+
// package targets ES2020, so not `Object.hasOwn`.)
|
|
17
|
+
!Object.prototype.hasOwnProperty.call(input.fields, input.order.by)) {
|
|
18
|
+
throw new Error(`smoodly: collection "${input.name}" orders by an unknown field "${input.order.by}".`);
|
|
19
|
+
}
|
|
4
20
|
return {
|
|
5
21
|
kind: "collection",
|
|
6
22
|
name: input.name,
|
|
7
23
|
title: input.title,
|
|
8
24
|
titleField: input.titleField,
|
|
9
25
|
fields: Object.fromEntries(Object.entries(input.fields).map(([k, b]) => [k, b.descriptor])),
|
|
26
|
+
...(input.path !== undefined ? { path: input.path } : {}),
|
|
27
|
+
...(input.tree !== undefined ? { tree: input.tree } : {}),
|
|
28
|
+
...(input.order !== undefined ? { order: input.order } : {}),
|
|
10
29
|
admin: input.admin,
|
|
11
30
|
};
|
|
12
31
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ type Registered = {
|
|
|
4
4
|
schema: {
|
|
5
5
|
kind: string;
|
|
6
6
|
name: string;
|
|
7
|
-
slug?: string
|
|
7
|
+
slug?: string | Record<string, string>;
|
|
8
|
+
collection?: string;
|
|
8
9
|
styles?: Record<string, Descriptor>;
|
|
9
10
|
styleDefaults?: false | {
|
|
10
11
|
omit: string[];
|
|
@@ -16,12 +17,19 @@ export type SmoodlyConfig = {
|
|
|
16
17
|
sections?: Registered[];
|
|
17
18
|
elements?: Registered[];
|
|
18
19
|
collections?: CollectionSchema<any>[];
|
|
20
|
+
/** Page shapes and entry pages alike — everything that renders a URL. */
|
|
19
21
|
pages?: Registered[];
|
|
20
22
|
};
|
|
21
23
|
locales: {
|
|
22
24
|
default: string;
|
|
23
25
|
supported: string[];
|
|
24
26
|
};
|
|
27
|
+
/** `tree.depth` — how deep editors may nest pages. Absent = flat. */
|
|
28
|
+
pages?: {
|
|
29
|
+
tree?: {
|
|
30
|
+
depth: number;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
25
33
|
styles?: {
|
|
26
34
|
sections?: Record<string, FieldBuilder<any, any>>;
|
|
27
35
|
elements?: Record<string, FieldBuilder<any, any>>;
|
|
@@ -42,6 +50,8 @@ export type SmoodlyConfig = {
|
|
|
42
50
|
};
|
|
43
51
|
export type ResolvedConfig = SmoodlyConfig & {
|
|
44
52
|
homePageSlug: string;
|
|
53
|
+
/** How deep editors may nest pages in the tree. Default 1 (flat). */
|
|
54
|
+
pageDepth: number;
|
|
45
55
|
/** The section's styles with the config standard set merged in (section keys win). */
|
|
46
56
|
resolvedStyles(sectionName: string): Record<string, Descriptor>;
|
|
47
57
|
};
|
package/dist/config.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { assertSegment, perLocaleSegments } from "./paths.js";
|
|
1
2
|
function assertUniqueNames(config) {
|
|
2
3
|
const seen = new Set();
|
|
3
4
|
const all = [
|
|
4
5
|
...(config.registry.sections ?? []).map((s) => s.schema.name),
|
|
5
6
|
...(config.registry.elements ?? []).map((e) => e.schema.name),
|
|
6
7
|
...(config.registry.collections ?? []).map((c) => c.name),
|
|
7
|
-
...(config.registry.pages ?? []).map((p) => p.schema.name),
|
|
8
|
+
...(config.registry.pages ?? []).filter((p) => p.schema.kind !== "entryPage").map((p) => p.schema.name),
|
|
8
9
|
];
|
|
9
10
|
for (const name of all) {
|
|
10
11
|
if (seen.has(name))
|
|
@@ -12,29 +13,86 @@ function assertUniqueNames(config) {
|
|
|
12
13
|
seen.add(name);
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
/** Every root-level segment a registration claims, per locale: fixed page
|
|
17
|
+
* slugs and collection paths. Two claims on one (locale, segment) are a
|
|
18
|
+
* conflict — except a fixed page sitting at a collection's segment in
|
|
19
|
+
* EVERY locale of that path, which is the collection's index page. */
|
|
20
|
+
function assertRootSegments(config) {
|
|
21
|
+
const { default: def, supported } = config.locales;
|
|
22
|
+
const perLocale = (v) => perLocaleSegments(v, supported);
|
|
23
|
+
const pages = (config.registry.pages ?? []).filter((p) => p.schema.kind === "page" && p.schema.slug !== undefined);
|
|
24
|
+
const owners = new Map(); // "<locale> <segment>" → page name
|
|
25
|
+
for (const p of pages) {
|
|
20
26
|
const slug = p.schema.slug;
|
|
21
|
-
if (slug === undefined)
|
|
27
|
+
if (typeof slug !== "string" && slug[def] === undefined) {
|
|
28
|
+
throw new Error(`smoodly: page "${p.schema.name}" declares a per-locale slug without the default locale "${def}".`);
|
|
29
|
+
}
|
|
30
|
+
for (const [locale, segment] of Object.entries(perLocale(slug))) {
|
|
31
|
+
assertSegment(segment, `page "${p.schema.name}" fixed slug`);
|
|
32
|
+
const owner = owners.get(`${locale} ${segment}`);
|
|
33
|
+
if (owner) {
|
|
34
|
+
throw new Error(`smoodly: page registrations "${owner}" and "${p.schema.name}" both claim the fixed slug "${segment}" (${locale}).`);
|
|
35
|
+
}
|
|
36
|
+
owners.set(`${locale} ${segment}`, p.schema.name);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const entryPages = new Set((config.registry.pages ?? []).filter((p) => p.schema.kind === "entryPage").map((p) => p.schema.collection));
|
|
40
|
+
const collectionOwners = new Map();
|
|
41
|
+
for (const c of config.registry.collections ?? []) {
|
|
42
|
+
if (c.path === undefined)
|
|
22
43
|
continue;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
if (!entryPages.has(c.name)) {
|
|
45
|
+
throw new Error(`smoodly: collection "${c.name}" declares a path but no entry page — register smoodly.entryPage(${c.name}, View).`);
|
|
46
|
+
}
|
|
47
|
+
if (typeof c.path !== "string" && c.path[def] === undefined) {
|
|
48
|
+
throw new Error(`smoodly: collection "${c.name}" declares a per-locale path without the default locale "${def}".`);
|
|
49
|
+
}
|
|
50
|
+
const segments = perLocale(c.path);
|
|
51
|
+
for (const [locale, segment] of Object.entries(segments)) {
|
|
52
|
+
assertSegment(segment, `collection "${c.name}" path`);
|
|
53
|
+
const other = collectionOwners.get(`${locale} ${segment}`);
|
|
54
|
+
if (other) {
|
|
55
|
+
throw new Error(`smoodly: collections "${other}" and "${c.name}" both claim the root segment "${segment}" (${locale}).`);
|
|
56
|
+
}
|
|
57
|
+
collectionOwners.set(`${locale} ${segment}`, c.name);
|
|
58
|
+
}
|
|
59
|
+
// A page may sit on the collection's segment only if it does so in every locale.
|
|
60
|
+
for (const [locale, segment] of Object.entries(segments)) {
|
|
61
|
+
const pageName = owners.get(`${locale} ${segment}`);
|
|
62
|
+
if (!pageName)
|
|
63
|
+
continue;
|
|
64
|
+
const page = pages.find((p) => p.schema.name === pageName);
|
|
65
|
+
const everywhere = Object.entries(segments).every(([l, s]) => perLocale(page.schema.slug)[l] === s);
|
|
66
|
+
if (!everywhere) {
|
|
67
|
+
throw new Error(`smoodly: collection "${c.name}" and page registration "${pageName}" both claim the root segment "${segment}" (${locale}).`);
|
|
68
|
+
}
|
|
26
69
|
}
|
|
27
|
-
owners.set(slug, p.schema.name);
|
|
28
70
|
}
|
|
29
71
|
}
|
|
30
72
|
export function defineConfig(config) {
|
|
31
73
|
assertUniqueNames(config);
|
|
32
|
-
|
|
74
|
+
assertRootSegments(config);
|
|
75
|
+
const depth = config.pages?.tree?.depth ?? 1;
|
|
76
|
+
if (!Number.isInteger(depth) || depth < 1)
|
|
77
|
+
throw new Error("smoodly: pages.tree.depth must be a positive integer.");
|
|
78
|
+
// The root record's slug is a segment like any other, and it cannot be a
|
|
79
|
+
// collection's mount: that node is a folder or an index page, never "/".
|
|
80
|
+
const homePageSlug = config.homePageSlug ?? "home";
|
|
81
|
+
assertSegment(homePageSlug, "homePageSlug");
|
|
82
|
+
for (const c of config.registry.collections ?? []) {
|
|
83
|
+
if (c.path === undefined)
|
|
84
|
+
continue;
|
|
85
|
+
const segment = typeof c.path === "string" ? c.path : c.path[config.locales.default];
|
|
86
|
+
if (segment === homePageSlug) {
|
|
87
|
+
throw new Error(`smoodly: homePageSlug "${homePageSlug}" is also the path of collection "${c.name}".`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
33
90
|
const standard = Object.fromEntries(Object.entries(config.styles?.sections ?? {}).map(([k, b]) => [k, b.descriptor]));
|
|
34
91
|
const byName = new Map((config.registry.sections ?? []).map((s) => [s.schema.name, s.schema]));
|
|
35
92
|
return {
|
|
36
93
|
...config,
|
|
37
|
-
homePageSlug
|
|
94
|
+
homePageSlug,
|
|
95
|
+
pageDepth: depth,
|
|
38
96
|
resolvedStyles(sectionName) {
|
|
39
97
|
const schema = byName.get(sectionName);
|
|
40
98
|
if (!schema)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ComponentType, ReactElement } from "react";
|
|
2
|
+
import type { CollectionSchema } from "./collections.ts";
|
|
3
|
+
import type { EntryRecord } from "./entry-store.ts";
|
|
4
|
+
export type EntryPageSchema = {
|
|
5
|
+
kind: "entryPage";
|
|
6
|
+
name: string;
|
|
7
|
+
collection: string;
|
|
8
|
+
};
|
|
9
|
+
export type EntryPageProps = {
|
|
10
|
+
entry: EntryRecord;
|
|
11
|
+
path: string;
|
|
12
|
+
};
|
|
13
|
+
export type PairedEntryPage = ((props: EntryPageProps) => ReactElement) & {
|
|
14
|
+
schema: EntryPageSchema;
|
|
15
|
+
};
|
|
16
|
+
export declare function entryPage(collection: CollectionSchema<any>, View: ComponentType<EntryPageProps>): PairedEntryPage;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export function entryPage(collection, View) {
|
|
3
|
+
const Component = ((props) => _jsx(View, { entry: props.entry, path: props.path }));
|
|
4
|
+
Component.schema = { kind: "entryPage", name: `entry:${collection.name}`, collection: collection.name };
|
|
5
|
+
return Component;
|
|
6
|
+
}
|