@glw907/cairn-cms 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/adapter.d.ts +24 -0
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/auth/capabilities.d.ts +7 -0
  4. package/dist/auth/capabilities.d.ts.map +1 -0
  5. package/dist/auth/capabilities.js +26 -0
  6. package/dist/auth/index.d.ts +1 -0
  7. package/dist/auth/index.d.ts.map +1 -1
  8. package/dist/auth/index.js +1 -0
  9. package/dist/components/AdminLayout.svelte +72 -16
  10. package/dist/components/AdminLayout.svelte.d.ts +9 -0
  11. package/dist/components/AdminLayout.svelte.d.ts.map +1 -1
  12. package/dist/components/CollectionList.svelte +96 -0
  13. package/dist/components/CollectionList.svelte.d.ts +8 -0
  14. package/dist/components/CollectionList.svelte.d.ts.map +1 -0
  15. package/dist/components/ComponentPalette.svelte +34 -0
  16. package/dist/components/ComponentPalette.svelte.d.ts +9 -0
  17. package/dist/components/ComponentPalette.svelte.d.ts.map +1 -0
  18. package/dist/components/EditPage.svelte +66 -28
  19. package/dist/components/EditPage.svelte.d.ts +2 -0
  20. package/dist/components/EditPage.svelte.d.ts.map +1 -1
  21. package/dist/components/NavTree.svelte +128 -0
  22. package/dist/components/NavTree.svelte.d.ts +8 -0
  23. package/dist/components/NavTree.svelte.d.ts.map +1 -0
  24. package/dist/components/index.d.ts +3 -1
  25. package/dist/components/index.d.ts.map +1 -1
  26. package/dist/components/index.js +3 -1
  27. package/dist/editor.d.ts +25 -0
  28. package/dist/editor.d.ts.map +1 -0
  29. package/dist/editor.js +20 -0
  30. package/dist/frontmatter.d.ts +3 -0
  31. package/dist/frontmatter.d.ts.map +1 -0
  32. package/dist/frontmatter.js +16 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -0
  36. package/dist/nav.d.ts +58 -0
  37. package/dist/nav.d.ts.map +1 -0
  38. package/dist/nav.js +86 -0
  39. package/dist/slug.d.ts +7 -0
  40. package/dist/slug.d.ts.map +1 -0
  41. package/dist/slug.js +15 -0
  42. package/dist/sveltekit/index.d.ts +102 -12
  43. package/dist/sveltekit/index.d.ts.map +1 -1
  44. package/dist/sveltekit/index.js +219 -20
  45. package/package.json +7 -2
  46. package/src/lib/adapter.ts +25 -0
  47. package/src/lib/auth/capabilities.ts +35 -0
  48. package/src/lib/auth/index.ts +1 -0
  49. package/src/lib/components/AdminLayout.svelte +72 -16
  50. package/src/lib/components/CollectionList.svelte +96 -0
  51. package/src/lib/components/ComponentPalette.svelte +34 -0
  52. package/src/lib/components/EditPage.svelte +66 -28
  53. package/src/lib/components/NavTree.svelte +128 -0
  54. package/src/lib/components/index.ts +3 -1
  55. package/src/lib/editor.ts +38 -0
  56. package/src/lib/frontmatter.ts +17 -0
  57. package/src/lib/index.ts +2 -0
  58. package/src/lib/nav.ts +117 -0
  59. package/src/lib/slug.ts +16 -0
  60. package/src/lib/sveltekit/index.ts +303 -26
  61. package/dist/components/AdminList.svelte +0 -33
  62. package/dist/components/AdminList.svelte.d.ts +0 -10
  63. package/dist/components/AdminList.svelte.d.ts.map +0 -1
  64. package/src/lib/components/AdminList.svelte +0 -33
@@ -11,6 +11,7 @@
11
11
  import { redirect, error } from '@sveltejs/kit';
12
12
  import matter from 'gray-matter';
13
13
  import type { CairnUser } from '../auth/guard';
14
+ import { can, requireCapability } from '../auth/capabilities';
14
15
  import {
15
16
  listMarkdown,
16
17
  readRaw,
@@ -22,6 +23,7 @@ import {
22
23
  } from '../github';
23
24
  import { serializeMarkdown } from '../content';
24
25
  import { findCollection, frontmatterFromForm, type CairnAdapter, type CairnField } from '../adapter';
26
+ import { validateNavTree, extractMenu, parseSiteConfig, setMenu, type NavNode } from '../nav';
25
27
 
26
28
  /** The `platform.env` bindings the content routes read. All optional; the handlers guard. */
27
29
  export interface AdminEnv {
@@ -59,52 +61,195 @@ async function readToken(env: AdminEnv | undefined): Promise<string | undefined>
59
61
 
60
62
  // ── /admin layout ──────────────────────────────────────────────────────────
61
63
 
64
+ /** A collection reduced to what the sidebar nav needs (no plugin graph crosses to the client). */
65
+ export interface NavCollection {
66
+ type: string;
67
+ label: string;
68
+ }
69
+
62
70
  export interface AdminLayoutData {
63
71
  user: CairnUser | null;
64
72
  siteName: string;
65
73
  pathname: string;
74
+ collections: NavCollection[];
75
+ /** Managed menus (name+label only) so the shell can show a Navigation entry. */
76
+ navMenus: { name: string; label: string }[];
77
+ /** Whether the viewer may manage navigation (gates the Navigation nav entry). */
78
+ canManageNav: boolean;
66
79
  }
67
80
 
68
81
  /**
69
- * Branding + session for every admin page. `siteName` flows from the adapter without pulling
70
- * its plugin graph into client bundles; the import stays server-side in the layout load.
71
- * `pathname` lets the shared shell highlight the active nav item without a `$app/*` import
72
- * (those kit virtual modules have no types outside a kit app, so they can't live in the
73
- * package); reading `event.url` here also opts the layout load into rerunning on navigation.
82
+ * Branding, session, and collection nav for every admin page. `siteName` and the collection
83
+ * list flow from the adapter without pulling its plugin graph into client bundles (the import
84
+ * stays server-side in the layout load; only `{type,label}` crosses). `pathname` lets the
85
+ * shared shell highlight the active nav item without a `$app/*` import (those kit virtual
86
+ * modules have no types outside a kit app); reading `event.url` also opts the layout load into
87
+ * rerunning on navigation, keeping the active class correct.
74
88
  */
75
89
  export function adminLayoutLoad(
76
90
  event: { locals: { user: CairnUser | null }; url: URL },
77
91
  adapter: CairnAdapter,
78
92
  ): AdminLayoutData {
79
- return { user: event.locals.user, siteName: adapter.siteName, pathname: event.url.pathname };
93
+ return {
94
+ user: event.locals.user,
95
+ siteName: adapter.siteName,
96
+ pathname: event.url.pathname,
97
+ collections: adapter.collections.map(({ type, label }) => ({ type, label })),
98
+ navMenus: adapter.navMenu ? [{ name: adapter.navMenu.menuName, label: adapter.navMenu.label }] : [],
99
+ canManageNav: can(event.locals.user, 'nav:manage'),
100
+ };
101
+ }
102
+
103
+ /**
104
+ * The `/admin` index has no content of its own now that each collection is its own page; send
105
+ * the editor straight to the first collection's entries list (a Sveltia-style landing).
106
+ */
107
+ export function adminIndexRedirect(adapter: CairnAdapter): never {
108
+ const first = adapter.collections[0];
109
+ if (!first) throw error(404, 'No collections configured');
110
+ throw redirect(307, `/admin/${first.type}`);
80
111
  }
81
112
 
82
- // ── /admin (content list) ────────────────────────────────────────────────────
113
+ // ── /admin/[collection] (entries list) ─────────────────────────────────────
114
+
115
+ /** One entry row: id (filename stem), display title, optional date, draft flag. */
116
+ export interface CollectionEntry {
117
+ id: string;
118
+ path: string;
119
+ title: string;
120
+ date: string | null;
121
+ draft: boolean;
122
+ }
83
123
 
84
- export interface AdminCollectionList {
124
+ export interface CollectionListData {
85
125
  type: string;
86
126
  label: string;
87
- files: RepoFile[];
127
+ kind: 'page' | 'story';
128
+ entries: CollectionEntry[];
129
+ /** Set when the directory listing itself failed (rate limit, network). */
88
130
  error?: string;
131
+ /** A create-flow error bounced back via `?error=` (an invalid or taken slug). */
132
+ formError: string | null;
133
+ /** Whether the viewer may create an entry in this collection (page-create is owner-only). */
134
+ canCreate: boolean;
135
+ }
136
+
137
+ /** Coerce a frontmatter `date` (gray-matter may parse YAML dates to `Date`) to `YYYY-MM-DD`. */
138
+ function entryDate(value: unknown): string | null {
139
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
140
+ if (typeof value === 'string') return value;
141
+ return null;
89
142
  }
90
143
 
91
- /** List every collection's markdown files. A failed listing degrades to an inline error. */
92
- export async function adminListLoad(
93
- event: PlatformEvent,
144
+ /**
145
+ * List one collection's entries, reading each file's frontmatter for the display title, date,
146
+ * and draft badge. Reads run in parallel; a single failed read degrades that row to the slug
147
+ * (rather than failing the page), and a failed directory listing returns an inline `error`.
148
+ * Collections are small here; the 1,000-entry / Git-Trees sharding concern is risk #11, deferred.
149
+ */
150
+ export async function collectionListLoad(
151
+ event: PlatformEvent & { params: { collection: string }; url: URL; locals: { user: CairnUser | null } },
94
152
  adapter: CairnAdapter,
95
- ): Promise<{ collections: AdminCollectionList[] }> {
153
+ ): Promise<CollectionListData> {
154
+ const collection = findCollection(adapter, event.params.collection);
155
+ if (!collection) throw error(404, 'Unknown collection');
156
+
157
+ const kind = collection.kind ?? 'story';
158
+ const canCreate = can(event.locals.user, kind === 'page' ? 'page:create' : 'story:create');
159
+ const formError = event.url.searchParams.get('error');
96
160
  const token = await readToken(event.platform?.env);
97
- const collections = await Promise.all(
98
- adapter.collections.map(async ({ type, label, dir }): Promise<AdminCollectionList> => {
161
+
162
+ let files: RepoFile[];
163
+ try {
164
+ files = await listMarkdown(adapter.backend, collection.dir, token);
165
+ } catch (err) {
166
+ return {
167
+ type: collection.type,
168
+ label: collection.label,
169
+ kind,
170
+ entries: [],
171
+ error: err instanceof Error ? err.message : 'Failed to load',
172
+ formError,
173
+ canCreate,
174
+ };
175
+ }
176
+
177
+ const entries = await Promise.all(
178
+ files.map(async (file): Promise<CollectionEntry> => {
179
+ const fallback: CollectionEntry = {
180
+ id: file.id,
181
+ path: file.path,
182
+ title: file.id,
183
+ date: null,
184
+ draft: false,
185
+ };
99
186
  try {
100
- return { type, label, files: await listMarkdown(adapter.backend, dir, token) };
101
- } catch (err) {
102
- // A failed listing (rate limit, network) shouldn't 500 the whole admin.
103
- return { type, label, files: [], error: err instanceof Error ? err.message : 'Failed to load' };
187
+ const raw = await readRaw(adapter.backend, file.path, token);
188
+ if (raw === null) return fallback;
189
+ const { data } = matter(raw);
190
+ return {
191
+ id: file.id,
192
+ path: file.path,
193
+ title: typeof data.title === 'string' ? data.title : file.id,
194
+ date: entryDate(data.date),
195
+ draft: data.draft === true,
196
+ };
197
+ } catch {
198
+ return fallback;
104
199
  }
105
200
  }),
106
201
  );
107
- return { collections };
202
+
203
+ return {
204
+ type: collection.type,
205
+ label: collection.label,
206
+ kind,
207
+ entries,
208
+ formError,
209
+ canCreate,
210
+ };
211
+ }
212
+
213
+ // ── /admin/[collection]?/create (POST) ─────────────────────────────────────
214
+
215
+ /** A safe filename stem: starts and ends with a lowercase alphanumeric, hyphens allowed within. */
216
+ const SLUG_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
217
+
218
+ /**
219
+ * The "New entry" form action. Validates the requested slug, rejects one that already exists,
220
+ * then redirects into the editor in create mode (`?new=1`, where `editLoad` serves a blank
221
+ * document and `saveCommit`'s create path commits a new file). cairn is filename-based, so the
222
+ * slug is the filename stem the author types; a title-driven auto-slug is a later (Pass K) concern.
223
+ */
224
+ export async function createEntry(
225
+ event: PlatformEvent & {
226
+ params: { collection: string };
227
+ locals: { user: CairnUser | null };
228
+ request: Request;
229
+ },
230
+ adapter: CairnAdapter,
231
+ ): Promise<never> {
232
+ const collection = findCollection(adapter, event.params.collection);
233
+ if (!collection) throw error(404, 'Unknown collection');
234
+ const kind = collection.kind ?? 'story';
235
+ requireCapability(event.locals.user, kind === 'page' ? 'page:create' : 'story:create');
236
+
237
+ const form = await event.request.formData();
238
+ const id = String(form.get('id') ?? '').trim();
239
+ const back = (message: string) =>
240
+ redirect(303, `/admin/${collection.type}?error=${encodeURIComponent(message)}`);
241
+
242
+ if (!SLUG_RE.test(id)) {
243
+ throw back('Enter a slug using lowercase letters, numbers, and hyphens (for example 2026-05-my-entry).');
244
+ }
245
+
246
+ const token = await readToken(event.platform?.env);
247
+ const existing = await readRaw(adapter.backend, `${collection.dir}/${id}.md`, token);
248
+ if (existing !== null) throw back(`An entry named "${id}" already exists.`);
249
+ const date = String(form.get('date') ?? '').trim();
250
+ const dateSuffix = kind === 'story' && date ? `&date=${encodeURIComponent(date)}` : '';
251
+
252
+ throw redirect(303, `/admin/edit/${collection.type}/${id}?new=1${dateSuffix}`);
108
253
  }
109
254
 
110
255
  // ── /admin/edit/[type]/[id] ─────────────────────────────────────────────────
@@ -113,6 +258,7 @@ export interface EditData {
113
258
  type: string;
114
259
  id: string;
115
260
  label: string;
261
+ kind: 'page' | 'story';
116
262
  fields: CairnField[];
117
263
  path: string;
118
264
  body: string;
@@ -120,6 +266,8 @@ export interface EditData {
120
266
  title: string;
121
267
  saved: boolean;
122
268
  error: string | null;
269
+ /** True when editing a not-yet-committed new entry (reached via `?new=1`). */
270
+ isNew: boolean;
123
271
  }
124
272
 
125
273
  export async function editLoad(
@@ -132,16 +280,27 @@ export async function editLoad(
132
280
  const token = await readToken(event.platform?.env);
133
281
  const path = `${collection.dir}/${event.params.id}.md`;
134
282
  const raw = await readRaw(adapter.backend, path, token);
135
- if (raw === null) throw error(404, 'Content not found');
283
+ const isNew = event.url.searchParams.get('new') === '1';
136
284
 
137
- // Split frontmatter from body server-side; the editor form binds to the frontmatter and
138
- // the Carta editor binds to the body, and /admin/save reassembles them on commit.
139
- const { data: frontmatter, content: body } = matter(raw);
285
+ // A missing file is a 404 normally, but in create mode (`?new=1`) it's a blank new document.
286
+ if (raw === null && !isNew) throw error(404, 'Content not found');
287
+
288
+ // Split frontmatter from body server-side; the editor form binds to the frontmatter and the
289
+ // Carta editor to the body, and /admin/save reassembles them on commit. A new document starts
290
+ // empty so the author fills the fields from scratch.
291
+ const { data: frontmatter, content: body } =
292
+ raw === null ? { data: {} as Record<string, unknown>, content: '' } : matter(raw);
293
+
294
+ const seedDate = event.url.searchParams.get('date');
295
+ if (isNew && seedDate && frontmatter.date === undefined) {
296
+ frontmatter.date = seedDate;
297
+ }
140
298
 
141
299
  return {
142
300
  type: event.params.type,
143
301
  id: event.params.id,
144
302
  label: collection.label,
303
+ kind: collection.kind ?? 'story',
145
304
  fields: collection.fields,
146
305
  path,
147
306
  body,
@@ -149,6 +308,7 @@ export async function editLoad(
149
308
  title: typeof frontmatter.title === 'string' ? frontmatter.title : event.params.id,
150
309
  saved: event.url.searchParams.get('saved') === '1',
151
310
  error: event.url.searchParams.get('error'),
311
+ isNew,
152
312
  };
153
313
  }
154
314
 
@@ -170,6 +330,7 @@ export async function saveCommit(
170
330
  const type = String(form.get('type') ?? '');
171
331
  const id = String(form.get('id') ?? '');
172
332
  const body = String(form.get('body') ?? '');
333
+ const newSuffix = form.get('new') === '1' ? '&new=1' : '';
173
334
  const collection = findCollection(adapter, type);
174
335
  if (!collection || !id) throw error(400, 'Bad request');
175
336
 
@@ -180,7 +341,7 @@ export async function saveCommit(
180
341
  frontmatter = collection.validate(frontmatterFromForm(collection, form), `${id}.md`);
181
342
  } catch (err) {
182
343
  const message = err instanceof Error ? err.message : 'Invalid frontmatter';
183
- throw redirect(303, `/admin/edit/${type}/${id}?error=${encodeURIComponent(message)}`);
344
+ throw redirect(303, `/admin/edit/${type}/${id}?error=${encodeURIComponent(message)}${newSuffix}`);
184
345
  }
185
346
 
186
347
  const markdown = serializeMarkdown(frontmatter, body);
@@ -203,7 +364,7 @@ export async function saveCommit(
203
364
  // the current version and reapplies. Any other error is unexpected, so rethrow.
204
365
  if (err instanceof CommitConflictError) {
205
366
  const message = 'This file changed since you opened it. Reload and reapply your edits.';
206
- throw redirect(303, `/admin/edit/${type}/${id}?error=${encodeURIComponent(message)}`);
367
+ throw redirect(303, `/admin/edit/${type}/${id}?error=${encodeURIComponent(message)}${newSuffix}`);
207
368
  }
208
369
  throw err;
209
370
  }
@@ -211,6 +372,122 @@ export async function saveCommit(
211
372
  throw redirect(303, `/admin/edit/${type}/${id}?saved=1`);
212
373
  }
213
374
 
375
+ // ── /admin/nav (navigation tree) ───────────────────────────────────────────
376
+
377
+ /** A page the picker can insert: its display label and the URL the nav item points at. */
378
+ export interface NavPageOption {
379
+ label: string;
380
+ url: string;
381
+ }
382
+
383
+ export interface NavLoadData {
384
+ menu: { name: string; label: string; maxDepth: number };
385
+ tree: NavNode[];
386
+ pages: NavPageOption[];
387
+ saved: boolean;
388
+ error: string | null;
389
+ }
390
+
391
+ /** List page-collection entries for the picker (one directory listing per page collection). */
392
+ async function navPageOptions(adapter: CairnAdapter, env: AdminEnv | undefined): Promise<NavPageOption[]> {
393
+ const token = await readToken(env);
394
+ const pageCollections = adapter.collections.filter((c) => (c.kind ?? 'story') === 'page');
395
+ const lists = await Promise.all(
396
+ pageCollections.map(async (c) => {
397
+ try {
398
+ const files = await listMarkdown(adapter.backend, c.dir, token);
399
+ return files.map((f): NavPageOption => ({ label: f.id, url: `/${f.id}` }));
400
+ } catch {
401
+ return [];
402
+ }
403
+ }),
404
+ );
405
+ return lists.flat();
406
+ }
407
+
408
+ export async function navLoad(
409
+ event: PlatformEvent & { locals: { user: CairnUser | null }; url: URL },
410
+ adapter: CairnAdapter,
411
+ ): Promise<NavLoadData> {
412
+ requireCapability(event.locals.user, 'nav:manage');
413
+ const config = adapter.navMenu;
414
+ if (!config) throw error(404, 'No navigation menu configured');
415
+ const maxDepth = config.maxDepth ?? 2;
416
+ const menu = { name: config.menuName, label: config.label, maxDepth };
417
+
418
+ // Read the menu from the committed YAML. A missing/unparsable file degrades to an empty tree so
419
+ // the editor still loads (a first edit then creates the menu); only the read itself is best-effort.
420
+ const token = await readToken(event.platform?.env);
421
+ let tree: NavNode[] = [];
422
+ try {
423
+ const raw = await readRaw(adapter.backend, config.configPath, token);
424
+ if (raw !== null) tree = extractMenu(parseSiteConfig(raw), config.menuName, maxDepth);
425
+ } catch (err) {
426
+ console.error(`cairn nav: failed to read "${config.configPath}":`, err);
427
+ }
428
+
429
+ return {
430
+ menu,
431
+ tree,
432
+ pages: await navPageOptions(adapter, event.platform?.env),
433
+ saved: event.url.searchParams.get('saved') === '1',
434
+ error: event.url.searchParams.get('error'),
435
+ };
436
+ }
437
+
438
+ export async function navSave(
439
+ event: PlatformEvent & { locals: { user: CairnUser | null }; request: Request },
440
+ adapter: CairnAdapter,
441
+ ): Promise<never> {
442
+ const user = requireCapability(event.locals.user, 'nav:manage');
443
+ const config = adapter.navMenu;
444
+ if (!config) throw error(404, 'No navigation menu configured');
445
+ const maxDepth = config.maxDepth ?? 2;
446
+
447
+ const env = event.platform?.env;
448
+ if (!env?.GITHUB_APP_ID || !env.GITHUB_APP_INSTALLATION_ID || !env.GITHUB_APP_PRIVATE_KEY_B64) {
449
+ throw error(500, 'GitHub App is not configured');
450
+ }
451
+
452
+ const form = await event.request.formData();
453
+ let tree: NavNode[];
454
+ try {
455
+ tree = validateNavTree(JSON.parse(String(form.get('tree') ?? '[]')), maxDepth);
456
+ } catch (err) {
457
+ const message = err instanceof Error ? err.message : 'Invalid navigation';
458
+ throw redirect(303, `/admin/nav?error=${encodeURIComponent(message)}`);
459
+ }
460
+
461
+ const token = await installationToken({
462
+ appId: env.GITHUB_APP_ID,
463
+ installationId: env.GITHUB_APP_INSTALLATION_ID,
464
+ privateKeyB64: env.GITHUB_APP_PRIVATE_KEY_B64,
465
+ });
466
+
467
+ // Read-modify-commit: replace only this menu in the current file, preserving the rest.
468
+ const raw = await readRaw(adapter.backend, config.configPath, token);
469
+ if (raw === null) throw error(404, `Site config not found at ${config.configPath}`);
470
+
471
+ try {
472
+ await commitFile(
473
+ adapter.backend,
474
+ config.configPath,
475
+ setMenu(raw, config.menuName, tree),
476
+ { message: `Update ${config.label.toLowerCase()}`, author: { name: user.name, email: user.email } },
477
+ token,
478
+ );
479
+ } catch (err) {
480
+ // Concurrent-edit 409 (C3): fail safe, same as the content save path.
481
+ if (err instanceof CommitConflictError) {
482
+ const message = 'The site config changed since you opened it. Reload and reapply your edits.';
483
+ throw redirect(303, `/admin/nav?error=${encodeURIComponent(message)}`);
484
+ }
485
+ throw err;
486
+ }
487
+
488
+ throw redirect(303, '/admin/nav?saved=1');
489
+ }
490
+
214
491
  // ── /admin/healthz (GET) ──────────────────────────────────────────────────────
215
492
 
216
493
  export interface HealthData {
@@ -1,33 +0,0 @@
1
- <script lang="ts">
2
- // The /admin content list: every collection's files, linking into the editor. Data comes
3
- // from `adminListLoad` (collections) merged with `adminLayoutLoad` (siteName). The shell
4
- // (AdminLayout) owns the chrome (site title, signed-in identity, nav, sign out), so this
5
- // page renders only the content body.
6
- import type { AdminCollectionList } from '../sveltekit';
7
-
8
- interface Props {
9
- data: { collections: AdminCollectionList[] };
10
- }
11
- let { data }: Props = $props();
12
- </script>
13
-
14
- <h1 class="text-2xl font-bold">Content</h1>
15
-
16
- {#each data.collections as collection (collection.type)}
17
- <section class="mt-8">
18
- <h2 class="mb-3 text-lg font-semibold">{collection.label}</h2>
19
- {#if collection.error}
20
- <div class="alert alert-warning">Couldn't load {collection.label.toLowerCase()}: {collection.error}</div>
21
- {:else if collection.files.length === 0}
22
- <p class="opacity-60">No content yet.</p>
23
- {:else}
24
- <ul class="menu rounded-box border border-base-300 bg-base-100 p-2">
25
- {#each collection.files as file (file.path)}
26
- <li>
27
- <a href="/admin/edit/{collection.type}/{file.id}">{file.id}</a>
28
- </li>
29
- {/each}
30
- </ul>
31
- {/if}
32
- </section>
33
- {/each}
@@ -1,10 +0,0 @@
1
- import type { AdminCollectionList } from '../sveltekit';
2
- interface Props {
3
- data: {
4
- collections: AdminCollectionList[];
5
- };
6
- }
7
- declare const AdminList: import("svelte").Component<Props, {}, "">;
8
- type AdminList = ReturnType<typeof AdminList>;
9
- export default AdminList;
10
- //# sourceMappingURL=AdminList.svelte.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AdminList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AdminList.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAGtD,UAAU,KAAK;IACb,IAAI,EAAE;QAAE,WAAW,EAAE,mBAAmB,EAAE,CAAA;KAAE,CAAC;CAC9C;AAkCH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -1,33 +0,0 @@
1
- <script lang="ts">
2
- // The /admin content list: every collection's files, linking into the editor. Data comes
3
- // from `adminListLoad` (collections) merged with `adminLayoutLoad` (siteName). The shell
4
- // (AdminLayout) owns the chrome (site title, signed-in identity, nav, sign out), so this
5
- // page renders only the content body.
6
- import type { AdminCollectionList } from '../sveltekit';
7
-
8
- interface Props {
9
- data: { collections: AdminCollectionList[] };
10
- }
11
- let { data }: Props = $props();
12
- </script>
13
-
14
- <h1 class="text-2xl font-bold">Content</h1>
15
-
16
- {#each data.collections as collection (collection.type)}
17
- <section class="mt-8">
18
- <h2 class="mb-3 text-lg font-semibold">{collection.label}</h2>
19
- {#if collection.error}
20
- <div class="alert alert-warning">Couldn't load {collection.label.toLowerCase()}: {collection.error}</div>
21
- {:else if collection.files.length === 0}
22
- <p class="opacity-60">No content yet.</p>
23
- {:else}
24
- <ul class="menu rounded-box border border-base-300 bg-base-100 p-2">
25
- {#each collection.files as file (file.path)}
26
- <li>
27
- <a href="/admin/edit/{collection.type}/{file.id}">{file.id}</a>
28
- </li>
29
- {/each}
30
- </ul>
31
- {/if}
32
- </section>
33
- {/each}