@glw907/cairn-cms 0.2.0 → 0.3.0
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/dist/components/AdminLayout.svelte +120 -8
- package/dist/components/AdminLayout.svelte.d.ts +6 -0
- package/dist/components/AdminLayout.svelte.d.ts.map +1 -1
- package/dist/components/AdminList.svelte +5 -18
- package/dist/components/AdminList.svelte.d.ts +0 -3
- package/dist/components/AdminList.svelte.d.ts.map +1 -1
- package/dist/components/EditPage.svelte +4 -4
- package/dist/components/LoginPage.svelte +1 -1
- package/dist/components/ManageAdmins.svelte +6 -9
- package/dist/components/ManageAdmins.svelte.d.ts.map +1 -1
- package/dist/sveltekit/index.d.ts +5 -0
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/index.js +4 -1
- package/package.json +1 -1
- package/src/lib/components/AdminLayout.svelte +120 -8
- package/src/lib/components/AdminList.svelte +5 -18
- package/src/lib/components/EditPage.svelte +4 -4
- package/src/lib/components/LoginPage.svelte +1 -1
- package/src/lib/components/ManageAdmins.svelte +6 -9
- package/src/lib/sveltekit/index.ts +6 -2
|
@@ -1,18 +1,130 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
// Neutral admin chrome
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Neutral admin chrome, shared across sites so the tool looks identical everywhere (only the
|
|
3
|
+
// adapter's siteName varies). When signed in it's a responsive DaisyUI drawer+navbar shell
|
|
4
|
+
// (`drawer lg:drawer-open` — sidebar pinned on desktop, slide-over + hamburger on mobile),
|
|
5
|
+
// patterned on scosman/CMSaasStarter's `(admin)/(menu)` layout. The nav is data-driven and
|
|
6
|
+
// role-gated, so a new surface is one entry in `nav` (plus its route + component). Signed out
|
|
7
|
+
// (the login page lives under this layout) it falls back to a minimal centered shell.
|
|
8
|
+
// Each site's `admin/+layout.svelte` is a one-line shim that forwards `data` + `children`.
|
|
5
9
|
import type { Snippet } from 'svelte';
|
|
10
|
+
import type { Editor } from '../auth';
|
|
6
11
|
|
|
7
|
-
let {
|
|
12
|
+
let {
|
|
13
|
+
data,
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
data: { siteName: string; editor: Editor | null; pathname: string };
|
|
17
|
+
children: Snippet;
|
|
18
|
+
} = $props();
|
|
19
|
+
|
|
20
|
+
interface NavItem {
|
|
21
|
+
href: string;
|
|
22
|
+
label: string;
|
|
23
|
+
icon: Snippet;
|
|
24
|
+
active: boolean;
|
|
25
|
+
/** Owner-only surface — hidden from regular editors. */
|
|
26
|
+
owner?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const nav = $derived<NavItem[]>([
|
|
30
|
+
{
|
|
31
|
+
href: '/admin',
|
|
32
|
+
label: 'Content',
|
|
33
|
+
icon: contentIcon,
|
|
34
|
+
active: data.pathname === '/admin' || data.pathname.startsWith('/admin/edit'),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
href: '/admin/admins',
|
|
38
|
+
label: 'Editors',
|
|
39
|
+
icon: editorsIcon,
|
|
40
|
+
owner: true,
|
|
41
|
+
active: data.pathname.startsWith('/admin/admins'),
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
const visibleNav = $derived(nav.filter((item) => !item.owner || data.editor?.role === 'owner'));
|
|
45
|
+
|
|
46
|
+
// Close the slide-over after a nav tap on mobile (no-op on desktop where it's pinned open).
|
|
47
|
+
function closeDrawer(): void {
|
|
48
|
+
const toggle = document.getElementById('admin-drawer');
|
|
49
|
+
if (toggle instanceof HTMLInputElement) toggle.checked = false;
|
|
50
|
+
}
|
|
8
51
|
</script>
|
|
9
52
|
|
|
53
|
+
{#snippet contentIcon()}
|
|
54
|
+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
55
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
56
|
+
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
57
|
+
</svg>
|
|
58
|
+
{/snippet}
|
|
59
|
+
|
|
60
|
+
{#snippet editorsIcon()}
|
|
61
|
+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
62
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
63
|
+
d="M17 20h5v-2a4 4 0 00-3-3.87M9 20H4v-2a4 4 0 013-3.87m6-1.13a4 4 0 10-4-4 4 4 0 004 4zm6 0a4 4 0 10-3.5-2.1" />
|
|
64
|
+
</svg>
|
|
65
|
+
{/snippet}
|
|
66
|
+
|
|
10
67
|
<svelte:head>
|
|
11
68
|
<meta name="robots" content="noindex, nofollow" />
|
|
12
69
|
</svelte:head>
|
|
13
70
|
|
|
14
|
-
|
|
15
|
-
<div class="
|
|
16
|
-
|
|
71
|
+
{#if data.editor}
|
|
72
|
+
<div class="drawer min-h-screen bg-base-200 lg:drawer-open" data-pagefind-ignore>
|
|
73
|
+
<input id="admin-drawer" type="checkbox" class="drawer-toggle" />
|
|
74
|
+
|
|
75
|
+
<div class="drawer-content">
|
|
76
|
+
<!-- Mobile top bar — the desktop sidebar replaces this at lg. -->
|
|
77
|
+
<div class="navbar bg-base-100 lg:hidden">
|
|
78
|
+
<div class="flex-1">
|
|
79
|
+
<span class="px-2 text-xl font-bold">{data.siteName} CMS</span>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="flex-none">
|
|
82
|
+
<label for="admin-drawer" class="btn btn-square btn-ghost" aria-label="Open menu">
|
|
83
|
+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
84
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
|
|
85
|
+
</svg>
|
|
86
|
+
</label>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<main class="container px-4 py-6 lg:px-8">
|
|
91
|
+
{@render children()}
|
|
92
|
+
</main>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="drawer-side z-10">
|
|
96
|
+
<label for="admin-drawer" class="drawer-overlay" aria-label="Close menu"></label>
|
|
97
|
+
<div class="flex min-h-full w-80 flex-col bg-base-100 lg:border-r lg:border-base-300">
|
|
98
|
+
<ul class="menu menu-lg grow p-4">
|
|
99
|
+
<li class="menu-title flex flex-row items-center text-xl font-bold text-base-content">
|
|
100
|
+
<span class="grow">{data.siteName} CMS</span>
|
|
101
|
+
<label for="admin-drawer" class="ml-3 cursor-pointer lg:hidden" aria-label="Close menu">✕</label>
|
|
102
|
+
</li>
|
|
103
|
+
{#each visibleNav as item (item.href)}
|
|
104
|
+
<li>
|
|
105
|
+
<a href={item.href} class={item.active ? 'active' : ''} onclick={closeDrawer}>
|
|
106
|
+
{@render item.icon()}
|
|
107
|
+
{item.label}
|
|
108
|
+
</a>
|
|
109
|
+
</li>
|
|
110
|
+
{/each}
|
|
111
|
+
</ul>
|
|
112
|
+
|
|
113
|
+
<div class="border-t border-base-300 p-4">
|
|
114
|
+
<p class="text-sm font-medium">{data.editor.name}</p>
|
|
115
|
+
<p class="text-xs opacity-60">{data.editor.email}</p>
|
|
116
|
+
<form method="POST" action="/admin/auth/logout" class="mt-3">
|
|
117
|
+
<button type="submit" class="btn btn-ghost btn-sm btn-block justify-start">Sign out</button>
|
|
118
|
+
</form>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
{:else}
|
|
124
|
+
<!-- Signed out (login page): no nav, just a centered surface. -->
|
|
125
|
+
<div class="min-h-screen bg-base-200" data-pagefind-ignore>
|
|
126
|
+
<div class="mx-auto max-w-3xl px-4 py-8">
|
|
127
|
+
{@render children()}
|
|
128
|
+
</div>
|
|
17
129
|
</div>
|
|
18
|
-
|
|
130
|
+
{/if}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { Editor } from '../auth';
|
|
2
3
|
type $$ComponentProps = {
|
|
4
|
+
data: {
|
|
5
|
+
siteName: string;
|
|
6
|
+
editor: Editor | null;
|
|
7
|
+
pathname: string;
|
|
8
|
+
};
|
|
3
9
|
children: Snippet;
|
|
4
10
|
};
|
|
5
11
|
declare const AdminLayout: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminLayout.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AdminLayout.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AdminLayout.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AdminLayout.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAWrC,KAAK,gBAAgB,GAAI;IACtB,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACpE,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAyHJ,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -1,30 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// The /admin content list: every collection's files, linking into the editor. Data comes
|
|
3
|
-
// from `adminListLoad` (collections) merged with `adminLayoutLoad` (
|
|
4
|
-
|
|
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.
|
|
5
6
|
import type { AdminCollectionList } from '../sveltekit';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
|
-
data: {
|
|
9
|
+
data: { collections: AdminCollectionList[] };
|
|
9
10
|
}
|
|
10
11
|
let { data }: Props = $props();
|
|
11
12
|
</script>
|
|
12
13
|
|
|
13
|
-
<
|
|
14
|
-
<h1 class="text-2xl font-bold">{data.siteName} CMS</h1>
|
|
15
|
-
<div class="flex items-center gap-2">
|
|
16
|
-
{#if data.editor?.role === 'owner'}
|
|
17
|
-
<a href="/admin/admins" class="btn btn-ghost btn-sm">Editors</a>
|
|
18
|
-
{/if}
|
|
19
|
-
<form method="POST" action="/admin/auth/logout">
|
|
20
|
-
<button type="submit" class="btn btn-ghost btn-sm">Sign out</button>
|
|
21
|
-
</form>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
|
-
<p class="mt-2 text-sm opacity-70">
|
|
26
|
-
Signed in as {data.editor?.name} ({data.editor?.email})
|
|
27
|
-
</p>
|
|
14
|
+
<h1 class="text-2xl font-bold">Content</h1>
|
|
28
15
|
|
|
29
16
|
{#each data.collections as collection (collection.type)}
|
|
30
17
|
<section class="mt-8">
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AdminList.svelte.ts"],"names":[],"mappings":"
|
|
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"}
|
|
@@ -73,14 +73,14 @@
|
|
|
73
73
|
name={field.name}
|
|
74
74
|
required={field.required}
|
|
75
75
|
value={fmString(field.name)}
|
|
76
|
-
class="input
|
|
76
|
+
class="input w-full"
|
|
77
77
|
/>
|
|
78
78
|
</label>
|
|
79
79
|
{:else if field.type === 'textarea'}
|
|
80
80
|
<label class="flex flex-col gap-1">
|
|
81
81
|
<span class="text-sm font-medium">{field.label}</span>
|
|
82
82
|
<textarea name={field.name} required={field.required} rows={field.rows ?? 4}
|
|
83
|
-
class="textarea
|
|
83
|
+
class="textarea w-full">{fmString(field.name)}</textarea>
|
|
84
84
|
</label>
|
|
85
85
|
{:else if field.type === 'tags'}
|
|
86
86
|
<div class="flex flex-col gap-1">
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
<label class="flex flex-col gap-1">
|
|
100
100
|
<span class="text-sm font-medium">{field.label}</span>
|
|
101
101
|
<input type="text" name={field.name} value={fmFreeTags(field.name)}
|
|
102
|
-
placeholder={field.placeholder ?? 'comma, separated'} class="input
|
|
102
|
+
placeholder={field.placeholder ?? 'comma, separated'} class="input w-full" />
|
|
103
103
|
</label>
|
|
104
104
|
{:else if field.type === 'boolean'}
|
|
105
105
|
<label class="flex items-center gap-2 text-sm font-medium">
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
{#if mounted}
|
|
116
116
|
<MarkdownEditor {carta} bind:value={body} mode="tabs" />
|
|
117
117
|
{:else}
|
|
118
|
-
<textarea bind:value={body} rows="20" class="textarea
|
|
118
|
+
<textarea bind:value={body} rows="20" class="textarea w-full font-mono"></textarea>
|
|
119
119
|
{/if}
|
|
120
120
|
</div>
|
|
121
121
|
|
|
@@ -15,12 +15,9 @@
|
|
|
15
15
|
<title>Editors · {data.siteName} CMS</title>
|
|
16
16
|
</svelte:head>
|
|
17
17
|
|
|
18
|
-
<div
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
<h1 class="mt-1 text-2xl font-bold">Editors</h1>
|
|
22
|
-
<p class="text-sm opacity-60">Who can sign in to {data.siteName} CMS.</p>
|
|
23
|
-
</div>
|
|
18
|
+
<div>
|
|
19
|
+
<h1 class="text-2xl font-bold">Editors</h1>
|
|
20
|
+
<p class="text-sm opacity-60">Who can sign in to {data.siteName} CMS.</p>
|
|
24
21
|
</div>
|
|
25
22
|
|
|
26
23
|
{#if data.saved}
|
|
@@ -70,15 +67,15 @@
|
|
|
70
67
|
<label class="flex flex-col gap-1">
|
|
71
68
|
<span class="text-sm font-medium">Email</span>
|
|
72
69
|
<input type="email" name="email" required autocomplete="off" placeholder="you@example.com"
|
|
73
|
-
class="input
|
|
70
|
+
class="input w-full" />
|
|
74
71
|
</label>
|
|
75
72
|
<label class="flex flex-col gap-1">
|
|
76
73
|
<span class="text-sm font-medium">Name</span>
|
|
77
|
-
<input type="text" name="name" required placeholder="Display name" class="input
|
|
74
|
+
<input type="text" name="name" required placeholder="Display name" class="input w-full" />
|
|
78
75
|
</label>
|
|
79
76
|
<label class="flex flex-col gap-1">
|
|
80
77
|
<span class="text-sm font-medium">Role</span>
|
|
81
|
-
<select name="role" class="select
|
|
78
|
+
<select name="role" class="select">
|
|
82
79
|
<option value="editor">editor</option>
|
|
83
80
|
<option value="owner">owner</option>
|
|
84
81
|
</select>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ManageAdmins.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ManageAdmins.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG7C,UAAU,KAAK;IACb,IAAI,EAAE,UAAU,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CACzC;
|
|
1
|
+
{"version":3,"file":"ManageAdmins.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ManageAdmins.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG7C,UAAU,KAAK;IACb,IAAI,EAAE,UAAU,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CACzC;AAmFH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -24,15 +24,20 @@ interface PlatformEvent {
|
|
|
24
24
|
export interface AdminLayoutData {
|
|
25
25
|
editor: Editor | null;
|
|
26
26
|
siteName: string;
|
|
27
|
+
pathname: string;
|
|
27
28
|
}
|
|
28
29
|
/**
|
|
29
30
|
* Branding + session for every admin page. `siteName` flows from the adapter without pulling
|
|
30
31
|
* its plugin graph into client bundles — the import stays server-side in the layout load.
|
|
32
|
+
* `pathname` lets the shared shell highlight the active nav item without a `$app/*` import
|
|
33
|
+
* (those kit virtual modules have no types outside a kit app, so they can't live in the
|
|
34
|
+
* package); reading `event.url` here also opts the layout load into rerunning on navigation.
|
|
31
35
|
*/
|
|
32
36
|
export declare function adminLayoutLoad(event: {
|
|
33
37
|
locals: {
|
|
34
38
|
editor: Editor | null;
|
|
35
39
|
};
|
|
40
|
+
url: URL;
|
|
36
41
|
}, adapter: CairnAdapter): AdminLayoutData;
|
|
37
42
|
export interface AdminCollectionList {
|
|
38
43
|
type: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAmB,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,OAAO,EAUL,KAAK,MAAM,EAEZ,MAAM,SAAS,CAAC;AACjB,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAwD,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEhG,OAAO,EAAuC,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAErG,4FAA4F;AAC5F,MAAM,WAAW,QAAQ;IACvB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,kFAAkF;IAClF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,UAAU,aAAa;IACrB,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,QAAQ,CAAA;KAAE,CAAC;CAC/B;AAMD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/index.ts"],"names":[],"mappings":"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"}
|
package/dist/sveltekit/index.js
CHANGED
|
@@ -18,9 +18,12 @@ const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
|
18
18
|
/**
|
|
19
19
|
* Branding + session for every admin page. `siteName` flows from the adapter without pulling
|
|
20
20
|
* its plugin graph into client bundles — the import stays server-side in the layout load.
|
|
21
|
+
* `pathname` lets the shared shell highlight the active nav item without a `$app/*` import
|
|
22
|
+
* (those kit virtual modules have no types outside a kit app, so they can't live in the
|
|
23
|
+
* package); reading `event.url` here also opts the layout load into rerunning on navigation.
|
|
21
24
|
*/
|
|
22
25
|
export function adminLayoutLoad(event, adapter) {
|
|
23
|
-
return { editor: event.locals.editor, siteName: adapter.siteName };
|
|
26
|
+
return { editor: event.locals.editor, siteName: adapter.siteName, pathname: event.url.pathname };
|
|
24
27
|
}
|
|
25
28
|
/** List every collection's markdown files. A failed listing degrades to an inline error. */
|
|
26
29
|
export async function adminListLoad(adapter) {
|
package/package.json
CHANGED
|
@@ -1,18 +1,130 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
// Neutral admin chrome
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Neutral admin chrome, shared across sites so the tool looks identical everywhere (only the
|
|
3
|
+
// adapter's siteName varies). When signed in it's a responsive DaisyUI drawer+navbar shell
|
|
4
|
+
// (`drawer lg:drawer-open` — sidebar pinned on desktop, slide-over + hamburger on mobile),
|
|
5
|
+
// patterned on scosman/CMSaasStarter's `(admin)/(menu)` layout. The nav is data-driven and
|
|
6
|
+
// role-gated, so a new surface is one entry in `nav` (plus its route + component). Signed out
|
|
7
|
+
// (the login page lives under this layout) it falls back to a minimal centered shell.
|
|
8
|
+
// Each site's `admin/+layout.svelte` is a one-line shim that forwards `data` + `children`.
|
|
5
9
|
import type { Snippet } from 'svelte';
|
|
10
|
+
import type { Editor } from '../auth';
|
|
6
11
|
|
|
7
|
-
let {
|
|
12
|
+
let {
|
|
13
|
+
data,
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
data: { siteName: string; editor: Editor | null; pathname: string };
|
|
17
|
+
children: Snippet;
|
|
18
|
+
} = $props();
|
|
19
|
+
|
|
20
|
+
interface NavItem {
|
|
21
|
+
href: string;
|
|
22
|
+
label: string;
|
|
23
|
+
icon: Snippet;
|
|
24
|
+
active: boolean;
|
|
25
|
+
/** Owner-only surface — hidden from regular editors. */
|
|
26
|
+
owner?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const nav = $derived<NavItem[]>([
|
|
30
|
+
{
|
|
31
|
+
href: '/admin',
|
|
32
|
+
label: 'Content',
|
|
33
|
+
icon: contentIcon,
|
|
34
|
+
active: data.pathname === '/admin' || data.pathname.startsWith('/admin/edit'),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
href: '/admin/admins',
|
|
38
|
+
label: 'Editors',
|
|
39
|
+
icon: editorsIcon,
|
|
40
|
+
owner: true,
|
|
41
|
+
active: data.pathname.startsWith('/admin/admins'),
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
const visibleNav = $derived(nav.filter((item) => !item.owner || data.editor?.role === 'owner'));
|
|
45
|
+
|
|
46
|
+
// Close the slide-over after a nav tap on mobile (no-op on desktop where it's pinned open).
|
|
47
|
+
function closeDrawer(): void {
|
|
48
|
+
const toggle = document.getElementById('admin-drawer');
|
|
49
|
+
if (toggle instanceof HTMLInputElement) toggle.checked = false;
|
|
50
|
+
}
|
|
8
51
|
</script>
|
|
9
52
|
|
|
53
|
+
{#snippet contentIcon()}
|
|
54
|
+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
55
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
56
|
+
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
57
|
+
</svg>
|
|
58
|
+
{/snippet}
|
|
59
|
+
|
|
60
|
+
{#snippet editorsIcon()}
|
|
61
|
+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
62
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
63
|
+
d="M17 20h5v-2a4 4 0 00-3-3.87M9 20H4v-2a4 4 0 013-3.87m6-1.13a4 4 0 10-4-4 4 4 0 004 4zm6 0a4 4 0 10-3.5-2.1" />
|
|
64
|
+
</svg>
|
|
65
|
+
{/snippet}
|
|
66
|
+
|
|
10
67
|
<svelte:head>
|
|
11
68
|
<meta name="robots" content="noindex, nofollow" />
|
|
12
69
|
</svelte:head>
|
|
13
70
|
|
|
14
|
-
|
|
15
|
-
<div class="
|
|
16
|
-
|
|
71
|
+
{#if data.editor}
|
|
72
|
+
<div class="drawer min-h-screen bg-base-200 lg:drawer-open" data-pagefind-ignore>
|
|
73
|
+
<input id="admin-drawer" type="checkbox" class="drawer-toggle" />
|
|
74
|
+
|
|
75
|
+
<div class="drawer-content">
|
|
76
|
+
<!-- Mobile top bar — the desktop sidebar replaces this at lg. -->
|
|
77
|
+
<div class="navbar bg-base-100 lg:hidden">
|
|
78
|
+
<div class="flex-1">
|
|
79
|
+
<span class="px-2 text-xl font-bold">{data.siteName} CMS</span>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="flex-none">
|
|
82
|
+
<label for="admin-drawer" class="btn btn-square btn-ghost" aria-label="Open menu">
|
|
83
|
+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
84
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
|
|
85
|
+
</svg>
|
|
86
|
+
</label>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<main class="container px-4 py-6 lg:px-8">
|
|
91
|
+
{@render children()}
|
|
92
|
+
</main>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="drawer-side z-10">
|
|
96
|
+
<label for="admin-drawer" class="drawer-overlay" aria-label="Close menu"></label>
|
|
97
|
+
<div class="flex min-h-full w-80 flex-col bg-base-100 lg:border-r lg:border-base-300">
|
|
98
|
+
<ul class="menu menu-lg grow p-4">
|
|
99
|
+
<li class="menu-title flex flex-row items-center text-xl font-bold text-base-content">
|
|
100
|
+
<span class="grow">{data.siteName} CMS</span>
|
|
101
|
+
<label for="admin-drawer" class="ml-3 cursor-pointer lg:hidden" aria-label="Close menu">✕</label>
|
|
102
|
+
</li>
|
|
103
|
+
{#each visibleNav as item (item.href)}
|
|
104
|
+
<li>
|
|
105
|
+
<a href={item.href} class={item.active ? 'active' : ''} onclick={closeDrawer}>
|
|
106
|
+
{@render item.icon()}
|
|
107
|
+
{item.label}
|
|
108
|
+
</a>
|
|
109
|
+
</li>
|
|
110
|
+
{/each}
|
|
111
|
+
</ul>
|
|
112
|
+
|
|
113
|
+
<div class="border-t border-base-300 p-4">
|
|
114
|
+
<p class="text-sm font-medium">{data.editor.name}</p>
|
|
115
|
+
<p class="text-xs opacity-60">{data.editor.email}</p>
|
|
116
|
+
<form method="POST" action="/admin/auth/logout" class="mt-3">
|
|
117
|
+
<button type="submit" class="btn btn-ghost btn-sm btn-block justify-start">Sign out</button>
|
|
118
|
+
</form>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
{:else}
|
|
124
|
+
<!-- Signed out (login page): no nav, just a centered surface. -->
|
|
125
|
+
<div class="min-h-screen bg-base-200" data-pagefind-ignore>
|
|
126
|
+
<div class="mx-auto max-w-3xl px-4 py-8">
|
|
127
|
+
{@render children()}
|
|
128
|
+
</div>
|
|
17
129
|
</div>
|
|
18
|
-
|
|
130
|
+
{/if}
|
|
@@ -1,30 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// The /admin content list: every collection's files, linking into the editor. Data comes
|
|
3
|
-
// from `adminListLoad` (collections) merged with `adminLayoutLoad` (
|
|
4
|
-
|
|
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.
|
|
5
6
|
import type { AdminCollectionList } from '../sveltekit';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
|
-
data: {
|
|
9
|
+
data: { collections: AdminCollectionList[] };
|
|
9
10
|
}
|
|
10
11
|
let { data }: Props = $props();
|
|
11
12
|
</script>
|
|
12
13
|
|
|
13
|
-
<
|
|
14
|
-
<h1 class="text-2xl font-bold">{data.siteName} CMS</h1>
|
|
15
|
-
<div class="flex items-center gap-2">
|
|
16
|
-
{#if data.editor?.role === 'owner'}
|
|
17
|
-
<a href="/admin/admins" class="btn btn-ghost btn-sm">Editors</a>
|
|
18
|
-
{/if}
|
|
19
|
-
<form method="POST" action="/admin/auth/logout">
|
|
20
|
-
<button type="submit" class="btn btn-ghost btn-sm">Sign out</button>
|
|
21
|
-
</form>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
|
-
<p class="mt-2 text-sm opacity-70">
|
|
26
|
-
Signed in as {data.editor?.name} ({data.editor?.email})
|
|
27
|
-
</p>
|
|
14
|
+
<h1 class="text-2xl font-bold">Content</h1>
|
|
28
15
|
|
|
29
16
|
{#each data.collections as collection (collection.type)}
|
|
30
17
|
<section class="mt-8">
|
|
@@ -73,14 +73,14 @@
|
|
|
73
73
|
name={field.name}
|
|
74
74
|
required={field.required}
|
|
75
75
|
value={fmString(field.name)}
|
|
76
|
-
class="input
|
|
76
|
+
class="input w-full"
|
|
77
77
|
/>
|
|
78
78
|
</label>
|
|
79
79
|
{:else if field.type === 'textarea'}
|
|
80
80
|
<label class="flex flex-col gap-1">
|
|
81
81
|
<span class="text-sm font-medium">{field.label}</span>
|
|
82
82
|
<textarea name={field.name} required={field.required} rows={field.rows ?? 4}
|
|
83
|
-
class="textarea
|
|
83
|
+
class="textarea w-full">{fmString(field.name)}</textarea>
|
|
84
84
|
</label>
|
|
85
85
|
{:else if field.type === 'tags'}
|
|
86
86
|
<div class="flex flex-col gap-1">
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
<label class="flex flex-col gap-1">
|
|
100
100
|
<span class="text-sm font-medium">{field.label}</span>
|
|
101
101
|
<input type="text" name={field.name} value={fmFreeTags(field.name)}
|
|
102
|
-
placeholder={field.placeholder ?? 'comma, separated'} class="input
|
|
102
|
+
placeholder={field.placeholder ?? 'comma, separated'} class="input w-full" />
|
|
103
103
|
</label>
|
|
104
104
|
{:else if field.type === 'boolean'}
|
|
105
105
|
<label class="flex items-center gap-2 text-sm font-medium">
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
{#if mounted}
|
|
116
116
|
<MarkdownEditor {carta} bind:value={body} mode="tabs" />
|
|
117
117
|
{:else}
|
|
118
|
-
<textarea bind:value={body} rows="20" class="textarea
|
|
118
|
+
<textarea bind:value={body} rows="20" class="textarea w-full font-mono"></textarea>
|
|
119
119
|
{/if}
|
|
120
120
|
</div>
|
|
121
121
|
|
|
@@ -15,12 +15,9 @@
|
|
|
15
15
|
<title>Editors · {data.siteName} CMS</title>
|
|
16
16
|
</svelte:head>
|
|
17
17
|
|
|
18
|
-
<div
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
<h1 class="mt-1 text-2xl font-bold">Editors</h1>
|
|
22
|
-
<p class="text-sm opacity-60">Who can sign in to {data.siteName} CMS.</p>
|
|
23
|
-
</div>
|
|
18
|
+
<div>
|
|
19
|
+
<h1 class="text-2xl font-bold">Editors</h1>
|
|
20
|
+
<p class="text-sm opacity-60">Who can sign in to {data.siteName} CMS.</p>
|
|
24
21
|
</div>
|
|
25
22
|
|
|
26
23
|
{#if data.saved}
|
|
@@ -70,15 +67,15 @@
|
|
|
70
67
|
<label class="flex flex-col gap-1">
|
|
71
68
|
<span class="text-sm font-medium">Email</span>
|
|
72
69
|
<input type="email" name="email" required autocomplete="off" placeholder="you@example.com"
|
|
73
|
-
class="input
|
|
70
|
+
class="input w-full" />
|
|
74
71
|
</label>
|
|
75
72
|
<label class="flex flex-col gap-1">
|
|
76
73
|
<span class="text-sm font-medium">Name</span>
|
|
77
|
-
<input type="text" name="name" required placeholder="Display name" class="input
|
|
74
|
+
<input type="text" name="name" required placeholder="Display name" class="input w-full" />
|
|
78
75
|
</label>
|
|
79
76
|
<label class="flex flex-col gap-1">
|
|
80
77
|
<span class="text-sm font-medium">Role</span>
|
|
81
|
-
<select name="role" class="select
|
|
78
|
+
<select name="role" class="select">
|
|
82
79
|
<option value="editor">editor</option>
|
|
83
80
|
<option value="owner">owner</option>
|
|
84
81
|
</select>
|
|
@@ -52,17 +52,21 @@ const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
|
52
52
|
export interface AdminLayoutData {
|
|
53
53
|
editor: Editor | null;
|
|
54
54
|
siteName: string;
|
|
55
|
+
pathname: string;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
59
|
* Branding + session for every admin page. `siteName` flows from the adapter without pulling
|
|
59
60
|
* its plugin graph into client bundles — the import stays server-side in the layout load.
|
|
61
|
+
* `pathname` lets the shared shell highlight the active nav item without a `$app/*` import
|
|
62
|
+
* (those kit virtual modules have no types outside a kit app, so they can't live in the
|
|
63
|
+
* package); reading `event.url` here also opts the layout load into rerunning on navigation.
|
|
60
64
|
*/
|
|
61
65
|
export function adminLayoutLoad(
|
|
62
|
-
event: { locals: { editor: Editor | null } },
|
|
66
|
+
event: { locals: { editor: Editor | null }; url: URL },
|
|
63
67
|
adapter: CairnAdapter,
|
|
64
68
|
): AdminLayoutData {
|
|
65
|
-
return { editor: event.locals.editor, siteName: adapter.siteName };
|
|
69
|
+
return { editor: event.locals.editor, siteName: adapter.siteName, pathname: event.url.pathname };
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
// ── /admin (content list) ────────────────────────────────────────────────────
|