@ampless/admin 0.2.0-alpha.8 → 1.0.0-alpha.27
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.ja.md +73 -0
- package/README.md +3 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/chunk-2ITWLRYF.js +38 -0
- package/dist/chunk-2U3POKAZ.js +198 -0
- package/dist/{chunk-VXEVLHGL.js → chunk-6LQGVDCW.js} +2 -2
- package/dist/chunk-6NPYUTV6.js +250 -0
- package/dist/chunk-6SB7YICQ.js +48 -0
- package/dist/chunk-6W3JIOOR.js +37 -0
- package/dist/chunk-CTGFMK2J.js +335 -0
- package/dist/chunk-G4CF5ZWV.js +1319 -0
- package/dist/chunk-KQOE5CT6.js +21 -0
- package/dist/chunk-MWSCSCCU.js +67 -0
- package/dist/chunk-Q66BLMNJ.js +33 -0
- package/dist/chunk-TZ5F24BG.js +149 -0
- package/dist/chunk-VL6MMF2P.js +21 -0
- package/dist/chunk-VSS5FWSR.js +334 -0
- package/dist/{chunk-KKM2MCM4.js → chunk-WL4IBW2D.js} +121 -43
- package/dist/chunk-YFWHKIVH.js +1187 -0
- package/dist/components/admin-dashboard.d.ts +10 -0
- package/dist/components/admin-dashboard.js +9 -0
- package/dist/components/edit-post-view.d.ts +9 -0
- package/dist/components/edit-post-view.js +12 -0
- package/dist/components/index.d.ts +14 -42
- package/dist/components/index.js +22 -33
- package/dist/components/login-view.d.ts +5 -0
- package/dist/components/login-view.js +9 -0
- package/dist/components/mcp-tokens-view.d.ts +16 -0
- package/dist/components/mcp-tokens-view.js +9 -0
- package/dist/components/media-view.d.ts +5 -0
- package/dist/components/media-view.js +12 -0
- package/dist/components/new-post-view.d.ts +5 -0
- package/dist/components/new-post-view.js +12 -0
- package/dist/components/posts-list-view.d.ts +5 -0
- package/dist/components/posts-list-view.js +9 -0
- package/dist/components/users-list-view.d.ts +7 -0
- package/dist/components/users-list-view.js +9 -0
- package/dist/{i18n-DzXXcIQQ.d.ts → i18n-BhMBRfio.d.ts} +179 -1
- package/dist/index.d.ts +18 -18
- package/dist/index.js +17 -38
- package/dist/lib/theme-actions.d.ts +3 -3
- package/dist/lib/theme-actions.js +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/pages/index.d.ts +35 -16
- package/dist/pages/index.js +90 -257
- package/package.json +19 -8
- package/dist/chunk-QDPB5W35.js +0 -3251
- package/dist/login-view-BKrSZLJu.d.ts +0 -24
|
@@ -0,0 +1,1187 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import {
|
|
3
|
+
invalidateSiteSettingsCache
|
|
4
|
+
} from "./chunk-6LQGVDCW.js";
|
|
5
|
+
import {
|
|
6
|
+
setAdminCmsConfigClient
|
|
7
|
+
} from "./chunk-CTGFMK2J.js";
|
|
8
|
+
import {
|
|
9
|
+
setAdminMediaContext
|
|
10
|
+
} from "./chunk-2ITWLRYF.js";
|
|
11
|
+
import {
|
|
12
|
+
useLocale,
|
|
13
|
+
useT
|
|
14
|
+
} from "./chunk-Q66BLMNJ.js";
|
|
15
|
+
|
|
16
|
+
// src/lib/amplify-client.ts
|
|
17
|
+
import { Amplify } from "aws-amplify";
|
|
18
|
+
var configured = false;
|
|
19
|
+
function configureAmplify(outputs) {
|
|
20
|
+
if (configured) return;
|
|
21
|
+
Amplify.configure(outputs, { ssr: true });
|
|
22
|
+
configured = true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/lib/posts-provider.ts
|
|
26
|
+
import { generateClient } from "aws-amplify/api";
|
|
27
|
+
import {
|
|
28
|
+
setPostsProvider,
|
|
29
|
+
decodeAwsJson,
|
|
30
|
+
encodeAwsJson
|
|
31
|
+
} from "ampless";
|
|
32
|
+
function decodeMetadata(value) {
|
|
33
|
+
if (value === null || value === void 0) return void 0;
|
|
34
|
+
const parsed = decodeAwsJson(value);
|
|
35
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
36
|
+
}
|
|
37
|
+
function toCorePost(p) {
|
|
38
|
+
return {
|
|
39
|
+
postId: p.postId,
|
|
40
|
+
slug: p.slug,
|
|
41
|
+
title: p.title,
|
|
42
|
+
excerpt: p.excerpt ?? void 0,
|
|
43
|
+
format: p.format ?? "markdown",
|
|
44
|
+
body: decodeAwsJson(p.body),
|
|
45
|
+
status: p.status ?? "draft",
|
|
46
|
+
publishedAt: p.publishedAt ?? void 0,
|
|
47
|
+
tags: (p.tags ?? []).filter((t) => typeof t === "string"),
|
|
48
|
+
metadata: decodeMetadata(p.metadata)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function postTagEntries(post) {
|
|
52
|
+
if (post.status !== "published" || !post.publishedAt || !post.tags?.length) return [];
|
|
53
|
+
return post.tags.map((tag) => ({
|
|
54
|
+
tag,
|
|
55
|
+
publishedAtPostId: `${post.publishedAt}#${post.postId}`
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
function entryKey(e) {
|
|
59
|
+
return `${e.tag}|${e.publishedAtPostId}`;
|
|
60
|
+
}
|
|
61
|
+
var installed = false;
|
|
62
|
+
function installAdminPostsProvider() {
|
|
63
|
+
if (installed) return;
|
|
64
|
+
installed = true;
|
|
65
|
+
const client = generateClient();
|
|
66
|
+
async function syncPostTags(post, oldPost) {
|
|
67
|
+
const oldEntries = oldPost ? postTagEntries(oldPost) : [];
|
|
68
|
+
const newEntries = postTagEntries(post);
|
|
69
|
+
const oldKeys = new Set(oldEntries.map(entryKey));
|
|
70
|
+
const newKeys = new Set(newEntries.map(entryKey));
|
|
71
|
+
function fullRow(e) {
|
|
72
|
+
return {
|
|
73
|
+
tag: e.tag,
|
|
74
|
+
publishedAtPostId: e.publishedAtPostId,
|
|
75
|
+
postId: post.postId,
|
|
76
|
+
publishedAt: post.publishedAt,
|
|
77
|
+
slug: post.slug,
|
|
78
|
+
title: post.title,
|
|
79
|
+
excerpt: post.excerpt,
|
|
80
|
+
tags: post.tags ?? []
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function upsertPostTag(e) {
|
|
84
|
+
const row = fullRow(e);
|
|
85
|
+
const upd = await client.models.PostTag.update(row);
|
|
86
|
+
if (!upd.errors) return;
|
|
87
|
+
const cre = await client.models.PostTag.create(row);
|
|
88
|
+
if (cre.errors) {
|
|
89
|
+
throw new Error(upd.errors[0]?.message ?? "PostTag.update failed");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await Promise.all(
|
|
93
|
+
oldEntries.filter((e) => !newKeys.has(entryKey(e))).map((e) => client.models.PostTag.delete(e))
|
|
94
|
+
);
|
|
95
|
+
await Promise.all(
|
|
96
|
+
newEntries.filter((e) => !oldKeys.has(entryKey(e))).map(async (e) => {
|
|
97
|
+
const cre = await client.models.PostTag.create(fullRow(e));
|
|
98
|
+
if (!cre.errors) return;
|
|
99
|
+
const upd = await client.models.PostTag.update(fullRow(e));
|
|
100
|
+
if (upd.errors)
|
|
101
|
+
throw new Error(cre.errors[0]?.message ?? "PostTag.create failed");
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
await Promise.all(
|
|
105
|
+
newEntries.filter((e) => oldKeys.has(entryKey(e))).map(upsertPostTag)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const provider = {
|
|
109
|
+
async list(opts = {}) {
|
|
110
|
+
const status = opts.status ?? "published";
|
|
111
|
+
const filter = {};
|
|
112
|
+
if (status !== "all") filter.status = { eq: status };
|
|
113
|
+
const hasFilter = Object.keys(filter).length > 0;
|
|
114
|
+
const { data } = await client.models.Post.list({
|
|
115
|
+
filter: hasFilter ? filter : void 0,
|
|
116
|
+
limit: opts.limit ?? 100
|
|
117
|
+
});
|
|
118
|
+
return data.map(toCorePost);
|
|
119
|
+
},
|
|
120
|
+
async get(slug) {
|
|
121
|
+
const { data } = await client.models.Post.list({
|
|
122
|
+
filter: { slug: { eq: slug } },
|
|
123
|
+
limit: 1
|
|
124
|
+
});
|
|
125
|
+
return data[0] ? toCorePost(data[0]) : null;
|
|
126
|
+
},
|
|
127
|
+
async getById(postId) {
|
|
128
|
+
const { data } = await client.models.Post.get({ postId });
|
|
129
|
+
return data ? toCorePost(data) : null;
|
|
130
|
+
},
|
|
131
|
+
async create(input) {
|
|
132
|
+
const postId = input.postId ?? `post-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
133
|
+
const { data, errors } = await client.models.Post.create({
|
|
134
|
+
postId,
|
|
135
|
+
slug: input.slug,
|
|
136
|
+
title: input.title,
|
|
137
|
+
excerpt: input.excerpt,
|
|
138
|
+
format: input.format,
|
|
139
|
+
body: encodeAwsJson(input.body),
|
|
140
|
+
status: input.status,
|
|
141
|
+
publishedAt: input.publishedAt,
|
|
142
|
+
tags: input.tags,
|
|
143
|
+
...input.metadata !== void 0 && { metadata: encodeAwsJson(input.metadata) }
|
|
144
|
+
});
|
|
145
|
+
if (errors || !data) throw new Error(errors?.[0]?.message ?? "Failed to create post");
|
|
146
|
+
const created = toCorePost(data);
|
|
147
|
+
await syncPostTags(created, null);
|
|
148
|
+
return created;
|
|
149
|
+
},
|
|
150
|
+
async update(postId, patch) {
|
|
151
|
+
const oldPost = await this.getById(postId);
|
|
152
|
+
const { data, errors } = await client.models.Post.update({
|
|
153
|
+
postId,
|
|
154
|
+
...patch.slug !== void 0 && { slug: patch.slug },
|
|
155
|
+
...patch.title !== void 0 && { title: patch.title },
|
|
156
|
+
...patch.excerpt !== void 0 && { excerpt: patch.excerpt },
|
|
157
|
+
...patch.format !== void 0 && { format: patch.format },
|
|
158
|
+
...patch.body !== void 0 && { body: encodeAwsJson(patch.body) },
|
|
159
|
+
...patch.status !== void 0 && { status: patch.status },
|
|
160
|
+
...patch.publishedAt !== void 0 && { publishedAt: patch.publishedAt },
|
|
161
|
+
...patch.tags !== void 0 && { tags: patch.tags },
|
|
162
|
+
...patch.metadata !== void 0 && { metadata: encodeAwsJson(patch.metadata) }
|
|
163
|
+
});
|
|
164
|
+
if (errors || !data) throw new Error(errors?.[0]?.message ?? "Failed to update post");
|
|
165
|
+
const updated = toCorePost(data);
|
|
166
|
+
await syncPostTags(updated, oldPost);
|
|
167
|
+
return updated;
|
|
168
|
+
},
|
|
169
|
+
async remove(postId) {
|
|
170
|
+
const oldPost = await this.getById(postId);
|
|
171
|
+
if (oldPost) {
|
|
172
|
+
await syncPostTags({ ...oldPost, status: "draft" }, oldPost);
|
|
173
|
+
}
|
|
174
|
+
const { errors } = await client.models.Post.delete({ postId });
|
|
175
|
+
if (errors) throw new Error(errors[0]?.message ?? "Failed to delete post");
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
setPostsProvider(provider);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/lib/kv-provider.ts
|
|
182
|
+
import { generateClient as generateClient2 } from "aws-amplify/api";
|
|
183
|
+
import { decodeAwsJson as decodeAwsJson2, encodeAwsJson as encodeAwsJson2, setKvStore } from "ampless";
|
|
184
|
+
var installed2 = false;
|
|
185
|
+
function installAdminKvProvider() {
|
|
186
|
+
if (installed2) return;
|
|
187
|
+
installed2 = true;
|
|
188
|
+
const client = generateClient2();
|
|
189
|
+
function requireModel() {
|
|
190
|
+
const m = client.models.KvStore;
|
|
191
|
+
if (!m) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
"KvStore model is not available on the AppSync client. Did you redeploy the sandbox? Run `npx ampx sandbox` and wait for it to finish, then reload this page."
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return m;
|
|
197
|
+
}
|
|
198
|
+
const store = {
|
|
199
|
+
async get(pk, sk) {
|
|
200
|
+
const model = requireModel();
|
|
201
|
+
const { data } = await model.get({ pk, sk });
|
|
202
|
+
return data ? decodeAwsJson2(data.value) : null;
|
|
203
|
+
},
|
|
204
|
+
async query(pk) {
|
|
205
|
+
const model = requireModel();
|
|
206
|
+
const { data } = await model.list({
|
|
207
|
+
filter: { pk: { eq: pk } },
|
|
208
|
+
limit: 1e3
|
|
209
|
+
});
|
|
210
|
+
return (data ?? []).map((row) => ({
|
|
211
|
+
pk: row.pk,
|
|
212
|
+
sk: row.sk,
|
|
213
|
+
value: decodeAwsJson2(row.value),
|
|
214
|
+
ttl: row.ttl ?? void 0
|
|
215
|
+
}));
|
|
216
|
+
},
|
|
217
|
+
async put(pk, sk, value, opts) {
|
|
218
|
+
const model = requireModel();
|
|
219
|
+
const ttl = opts?.ttlSeconds ? Math.floor(Date.now() / 1e3) + opts.ttlSeconds : void 0;
|
|
220
|
+
const existing = await model.get({ pk, sk });
|
|
221
|
+
if (existing.data) {
|
|
222
|
+
const { errors } = await model.update({
|
|
223
|
+
pk,
|
|
224
|
+
sk,
|
|
225
|
+
value: encodeAwsJson2(value),
|
|
226
|
+
ttl: ttl ?? null
|
|
227
|
+
});
|
|
228
|
+
if (errors) throw new Error(errors[0]?.message ?? "KvStore.update failed");
|
|
229
|
+
} else {
|
|
230
|
+
const { errors } = await model.create({
|
|
231
|
+
pk,
|
|
232
|
+
sk,
|
|
233
|
+
value: encodeAwsJson2(value),
|
|
234
|
+
ttl
|
|
235
|
+
});
|
|
236
|
+
if (errors) throw new Error(errors[0]?.message ?? "KvStore.create failed");
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
async remove(pk, sk) {
|
|
240
|
+
const model = requireModel();
|
|
241
|
+
const { errors } = await model.delete({ pk, sk });
|
|
242
|
+
if (errors) throw new Error(errors[0]?.message ?? "KvStore.delete failed");
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
setKvStore(store);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/components/admin-providers.tsx
|
|
249
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
250
|
+
function AdminProviders({ outputs, cmsConfig, children }) {
|
|
251
|
+
configureAmplify(outputs);
|
|
252
|
+
setAdminCmsConfigClient(cmsConfig);
|
|
253
|
+
setAdminMediaContext(outputs, cmsConfig);
|
|
254
|
+
installAdminPostsProvider();
|
|
255
|
+
installAdminKvProvider();
|
|
256
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/components/sidebar.tsx
|
|
260
|
+
import { useEffect, useState } from "react";
|
|
261
|
+
import Link from "next/link";
|
|
262
|
+
import { usePathname } from "next/navigation";
|
|
263
|
+
import { signOut } from "aws-amplify/auth";
|
|
264
|
+
import {
|
|
265
|
+
LayoutDashboard,
|
|
266
|
+
FileText,
|
|
267
|
+
Image,
|
|
268
|
+
Globe,
|
|
269
|
+
Users,
|
|
270
|
+
Key,
|
|
271
|
+
LogOut,
|
|
272
|
+
ExternalLink,
|
|
273
|
+
Menu,
|
|
274
|
+
X
|
|
275
|
+
} from "lucide-react";
|
|
276
|
+
import { Button, cn } from "@ampless/runtime/ui";
|
|
277
|
+
import { Fragment as Fragment2, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
278
|
+
var navItems = [
|
|
279
|
+
{ href: "/admin", key: "sidebar.dashboard", icon: LayoutDashboard },
|
|
280
|
+
{ href: "/admin/posts", key: "sidebar.posts", icon: FileText },
|
|
281
|
+
{ href: "/admin/media", key: "sidebar.media", icon: Image },
|
|
282
|
+
// ampless runs one site per deployment, so this links directly to
|
|
283
|
+
// the single site's settings page instead of a list landing.
|
|
284
|
+
{ href: "/admin/sites/default", key: "sidebar.sites", icon: Globe },
|
|
285
|
+
{ href: "/admin/users", key: "sidebar.users", icon: Users, adminOnly: true },
|
|
286
|
+
{ href: "/admin/mcp-tokens", key: "sidebar.mcpTokens", icon: Key, adminOnly: true }
|
|
287
|
+
];
|
|
288
|
+
function Sidebar({
|
|
289
|
+
email,
|
|
290
|
+
isAdmin
|
|
291
|
+
}) {
|
|
292
|
+
const pathname = usePathname();
|
|
293
|
+
const t = useT();
|
|
294
|
+
const [open, setOpen] = useState(false);
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
setOpen(false);
|
|
297
|
+
}, [pathname]);
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
if (!open) return;
|
|
300
|
+
const prev = document.body.style.overflow;
|
|
301
|
+
document.body.style.overflow = "hidden";
|
|
302
|
+
return () => {
|
|
303
|
+
document.body.style.overflow = prev;
|
|
304
|
+
};
|
|
305
|
+
}, [open]);
|
|
306
|
+
return /* @__PURE__ */ jsxs(Fragment2, { children: [
|
|
307
|
+
/* @__PURE__ */ jsxs("header", { className: "sticky top-0 z-30 flex h-14 items-center justify-between border-b bg-background px-4 md:hidden", children: [
|
|
308
|
+
/* @__PURE__ */ jsx2(Link, { href: "/admin", className: "font-semibold", children: t("sidebar.brand") }),
|
|
309
|
+
/* @__PURE__ */ jsx2(
|
|
310
|
+
Button,
|
|
311
|
+
{
|
|
312
|
+
variant: "ghost",
|
|
313
|
+
size: "icon",
|
|
314
|
+
"aria-label": t("sidebar.openMenu"),
|
|
315
|
+
"aria-expanded": open,
|
|
316
|
+
onClick: () => setOpen(true),
|
|
317
|
+
children: /* @__PURE__ */ jsx2(Menu, { className: "h-5 w-5" })
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
] }),
|
|
321
|
+
open && /* @__PURE__ */ jsx2(
|
|
322
|
+
"div",
|
|
323
|
+
{
|
|
324
|
+
className: "fixed inset-0 z-40 bg-black/40 md:hidden",
|
|
325
|
+
"aria-hidden": "true",
|
|
326
|
+
onClick: () => setOpen(false)
|
|
327
|
+
}
|
|
328
|
+
),
|
|
329
|
+
/* @__PURE__ */ jsxs(
|
|
330
|
+
"aside",
|
|
331
|
+
{
|
|
332
|
+
className: cn(
|
|
333
|
+
// Mobile drawer overlays page content (`fixed` + `z-50`), so
|
|
334
|
+
// it must be fully opaque to stay readable. The desktop rail
|
|
335
|
+
// (`md:sticky`) sits in its own column with nothing behind it,
|
|
336
|
+
// so the original subtle muted tint is fine there.
|
|
337
|
+
"fixed inset-y-0 left-0 z-50 flex w-60 flex-col border-r bg-background transition-transform md:sticky md:top-0 md:h-screen md:translate-x-0 md:bg-muted/30",
|
|
338
|
+
open ? "translate-x-0" : "-translate-x-full md:translate-x-0"
|
|
339
|
+
),
|
|
340
|
+
"aria-label": t("sidebar.brand"),
|
|
341
|
+
children: [
|
|
342
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b p-4", children: [
|
|
343
|
+
/* @__PURE__ */ jsx2(Link, { href: "/admin", className: "font-semibold", children: t("sidebar.brand") }),
|
|
344
|
+
/* @__PURE__ */ jsx2(
|
|
345
|
+
Button,
|
|
346
|
+
{
|
|
347
|
+
variant: "ghost",
|
|
348
|
+
size: "icon",
|
|
349
|
+
className: "md:hidden",
|
|
350
|
+
"aria-label": t("sidebar.closeMenu"),
|
|
351
|
+
onClick: () => setOpen(false),
|
|
352
|
+
children: /* @__PURE__ */ jsx2(X, { className: "h-5 w-5" })
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
] }),
|
|
356
|
+
/* @__PURE__ */ jsx2("nav", { className: "flex-1 space-y-1 overflow-y-auto p-2", children: navItems.map((item) => {
|
|
357
|
+
if (item.adminOnly && !isAdmin) return null;
|
|
358
|
+
const Icon = item.icon;
|
|
359
|
+
const isActive = pathname === item.href || item.href !== "/admin" && pathname?.startsWith(item.href);
|
|
360
|
+
return /* @__PURE__ */ jsxs(
|
|
361
|
+
Link,
|
|
362
|
+
{
|
|
363
|
+
href: item.href,
|
|
364
|
+
className: cn(
|
|
365
|
+
"flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
|
|
366
|
+
isActive ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
367
|
+
),
|
|
368
|
+
children: [
|
|
369
|
+
/* @__PURE__ */ jsx2(Icon, { className: "h-4 w-4" }),
|
|
370
|
+
t(item.key)
|
|
371
|
+
]
|
|
372
|
+
},
|
|
373
|
+
item.href
|
|
374
|
+
);
|
|
375
|
+
}) }),
|
|
376
|
+
/* @__PURE__ */ jsxs("div", { className: "border-t p-2 space-y-1", children: [
|
|
377
|
+
/* @__PURE__ */ jsxs(
|
|
378
|
+
Link,
|
|
379
|
+
{
|
|
380
|
+
href: "/",
|
|
381
|
+
target: "_blank",
|
|
382
|
+
className: "flex items-center gap-3 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
|
383
|
+
children: [
|
|
384
|
+
/* @__PURE__ */ jsx2(ExternalLink, { className: "h-4 w-4" }),
|
|
385
|
+
t("sidebar.viewSite")
|
|
386
|
+
]
|
|
387
|
+
}
|
|
388
|
+
),
|
|
389
|
+
/* @__PURE__ */ jsx2("div", { className: "px-3 py-2 text-xs text-muted-foreground truncate", children: email }),
|
|
390
|
+
/* @__PURE__ */ jsxs(
|
|
391
|
+
Button,
|
|
392
|
+
{
|
|
393
|
+
variant: "ghost",
|
|
394
|
+
size: "sm",
|
|
395
|
+
className: "w-full justify-start gap-3",
|
|
396
|
+
onClick: async () => {
|
|
397
|
+
await signOut();
|
|
398
|
+
window.location.href = "/login";
|
|
399
|
+
},
|
|
400
|
+
children: [
|
|
401
|
+
/* @__PURE__ */ jsx2(LogOut, { className: "h-4 w-4" }),
|
|
402
|
+
t("sidebar.signOut")
|
|
403
|
+
]
|
|
404
|
+
}
|
|
405
|
+
)
|
|
406
|
+
] })
|
|
407
|
+
]
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
] });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/components/site-settings-form.tsx
|
|
414
|
+
import { useState as useState2 } from "react";
|
|
415
|
+
import { useRouter } from "next/navigation";
|
|
416
|
+
import { setSiteSetting } from "ampless";
|
|
417
|
+
import { Button as Button2, Input, Label, Textarea } from "@ampless/runtime/ui";
|
|
418
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
419
|
+
var KEYS = [
|
|
420
|
+
"site.name",
|
|
421
|
+
"site.url",
|
|
422
|
+
"site.description",
|
|
423
|
+
"media.imageDisplay",
|
|
424
|
+
"media.imageMaxWidth",
|
|
425
|
+
"dateFormat",
|
|
426
|
+
"timezone"
|
|
427
|
+
];
|
|
428
|
+
function SiteSettingsForm({ initial, fallback }) {
|
|
429
|
+
const router = useRouter();
|
|
430
|
+
const t = useT();
|
|
431
|
+
const [values, setValues] = useState2(initial);
|
|
432
|
+
const [saving, setSaving] = useState2(false);
|
|
433
|
+
const [error, setError] = useState2(null);
|
|
434
|
+
const [info, setInfo] = useState2(null);
|
|
435
|
+
function update(key, value) {
|
|
436
|
+
setValues((prev) => ({ ...prev, [key]: value }));
|
|
437
|
+
}
|
|
438
|
+
async function save(e) {
|
|
439
|
+
e.preventDefault();
|
|
440
|
+
setSaving(true);
|
|
441
|
+
setError(null);
|
|
442
|
+
setInfo(null);
|
|
443
|
+
try {
|
|
444
|
+
await Promise.all(
|
|
445
|
+
KEYS.map((key) => {
|
|
446
|
+
const value = values[key];
|
|
447
|
+
if (value === void 0 || value === "") return Promise.resolve();
|
|
448
|
+
return setSiteSetting(key, value);
|
|
449
|
+
})
|
|
450
|
+
);
|
|
451
|
+
setInfo(t("sites.edit.saved"));
|
|
452
|
+
router.refresh();
|
|
453
|
+
} catch (err) {
|
|
454
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
455
|
+
} finally {
|
|
456
|
+
setSaving(false);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return /* @__PURE__ */ jsxs2("form", { onSubmit: save, className: "space-y-6 max-w-xl", children: [
|
|
460
|
+
/* @__PURE__ */ jsxs2("fieldset", { className: "space-y-4", children: [
|
|
461
|
+
/* @__PURE__ */ jsx3("legend", { className: "text-sm font-medium uppercase tracking-wide text-muted-foreground", children: t("sites.edit.site") }),
|
|
462
|
+
/* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
|
|
463
|
+
/* @__PURE__ */ jsx3(Label, { htmlFor: "name", children: t("common.name") }),
|
|
464
|
+
/* @__PURE__ */ jsx3(
|
|
465
|
+
Input,
|
|
466
|
+
{
|
|
467
|
+
id: "name",
|
|
468
|
+
value: values["site.name"] ?? "",
|
|
469
|
+
placeholder: fallback["site.name"] ?? "",
|
|
470
|
+
onChange: (e) => update("site.name", e.target.value)
|
|
471
|
+
}
|
|
472
|
+
)
|
|
473
|
+
] }),
|
|
474
|
+
/* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
|
|
475
|
+
/* @__PURE__ */ jsx3(Label, { htmlFor: "url", children: t("common.url") }),
|
|
476
|
+
/* @__PURE__ */ jsx3(
|
|
477
|
+
Input,
|
|
478
|
+
{
|
|
479
|
+
id: "url",
|
|
480
|
+
value: values["site.url"] ?? "",
|
|
481
|
+
placeholder: fallback["site.url"] ?? "",
|
|
482
|
+
onChange: (e) => update("site.url", e.target.value)
|
|
483
|
+
}
|
|
484
|
+
)
|
|
485
|
+
] }),
|
|
486
|
+
/* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
|
|
487
|
+
/* @__PURE__ */ jsx3(Label, { htmlFor: "description", children: t("common.description") }),
|
|
488
|
+
/* @__PURE__ */ jsx3(
|
|
489
|
+
Textarea,
|
|
490
|
+
{
|
|
491
|
+
id: "description",
|
|
492
|
+
value: values["site.description"] ?? "",
|
|
493
|
+
placeholder: fallback["site.description"] ?? "",
|
|
494
|
+
rows: 2,
|
|
495
|
+
onChange: (e) => update("site.description", e.target.value)
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
] })
|
|
499
|
+
] }),
|
|
500
|
+
/* @__PURE__ */ jsxs2("fieldset", { className: "space-y-4", children: [
|
|
501
|
+
/* @__PURE__ */ jsx3("legend", { className: "text-sm font-medium uppercase tracking-wide text-muted-foreground", children: t("sites.edit.media") }),
|
|
502
|
+
/* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
|
|
503
|
+
/* @__PURE__ */ jsx3(Label, { htmlFor: "imageDisplay", children: t("sites.edit.imageDisplay") }),
|
|
504
|
+
/* @__PURE__ */ jsxs2(
|
|
505
|
+
"select",
|
|
506
|
+
{
|
|
507
|
+
id: "imageDisplay",
|
|
508
|
+
className: "w-full rounded-md border bg-background px-2 py-1.5 text-sm",
|
|
509
|
+
value: values["media.imageDisplay"] ?? "",
|
|
510
|
+
onChange: (e) => update("media.imageDisplay", e.target.value),
|
|
511
|
+
children: [
|
|
512
|
+
/* @__PURE__ */ jsx3("option", { value: "", children: t("sites.edit.defaultPlaceholder", {
|
|
513
|
+
value: fallback["media.imageDisplay"] ?? "inline"
|
|
514
|
+
}) }),
|
|
515
|
+
/* @__PURE__ */ jsx3("option", { value: "inline", children: t("sites.edit.imageDisplayInline") }),
|
|
516
|
+
/* @__PURE__ */ jsx3("option", { value: "lightbox", children: t("sites.edit.imageDisplayLightbox") })
|
|
517
|
+
]
|
|
518
|
+
}
|
|
519
|
+
)
|
|
520
|
+
] }),
|
|
521
|
+
/* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
|
|
522
|
+
/* @__PURE__ */ jsx3(Label, { htmlFor: "imageMaxWidth", children: t("sites.edit.imageMaxWidth") }),
|
|
523
|
+
/* @__PURE__ */ jsx3(
|
|
524
|
+
Input,
|
|
525
|
+
{
|
|
526
|
+
id: "imageMaxWidth",
|
|
527
|
+
value: values["media.imageMaxWidth"] ?? "",
|
|
528
|
+
placeholder: fallback["media.imageMaxWidth"] ?? "100%",
|
|
529
|
+
onChange: (e) => update("media.imageMaxWidth", e.target.value)
|
|
530
|
+
}
|
|
531
|
+
)
|
|
532
|
+
] })
|
|
533
|
+
] }),
|
|
534
|
+
/* @__PURE__ */ jsxs2("fieldset", { className: "space-y-4", children: [
|
|
535
|
+
/* @__PURE__ */ jsx3("legend", { className: "text-sm font-medium uppercase tracking-wide text-muted-foreground", children: t("sites.edit.dateDisplay") }),
|
|
536
|
+
/* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
|
|
537
|
+
/* @__PURE__ */ jsx3(Label, { htmlFor: "dateFormat", children: t("sites.edit.dateFormat") }),
|
|
538
|
+
/* @__PURE__ */ jsxs2(
|
|
539
|
+
"select",
|
|
540
|
+
{
|
|
541
|
+
id: "dateFormat",
|
|
542
|
+
className: "w-full rounded-md border bg-background px-2 py-1.5 text-sm",
|
|
543
|
+
value: values["dateFormat"] ?? "",
|
|
544
|
+
onChange: (e) => update("dateFormat", e.target.value),
|
|
545
|
+
children: [
|
|
546
|
+
/* @__PURE__ */ jsx3("option", { value: "", children: t("sites.edit.defaultPlaceholder", {
|
|
547
|
+
value: fallback["dateFormat"] ?? "iso"
|
|
548
|
+
}) }),
|
|
549
|
+
/* @__PURE__ */ jsx3("option", { value: "iso", children: t("sites.edit.dateFormatIso") }),
|
|
550
|
+
/* @__PURE__ */ jsx3("option", { value: "long", children: t("sites.edit.dateFormatLong") }),
|
|
551
|
+
/* @__PURE__ */ jsx3("option", { value: "locale", children: t("sites.edit.dateFormatLocale") })
|
|
552
|
+
]
|
|
553
|
+
}
|
|
554
|
+
)
|
|
555
|
+
] }),
|
|
556
|
+
/* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
|
|
557
|
+
/* @__PURE__ */ jsx3(Label, { htmlFor: "timezone", children: t("sites.edit.timezone") }),
|
|
558
|
+
/* @__PURE__ */ jsx3(
|
|
559
|
+
Input,
|
|
560
|
+
{
|
|
561
|
+
id: "timezone",
|
|
562
|
+
value: values["timezone"] ?? "",
|
|
563
|
+
placeholder: fallback["timezone"] ?? "UTC",
|
|
564
|
+
onChange: (e) => update("timezone", e.target.value)
|
|
565
|
+
}
|
|
566
|
+
)
|
|
567
|
+
] })
|
|
568
|
+
] }),
|
|
569
|
+
info && /* @__PURE__ */ jsx3("p", { className: "text-sm text-muted-foreground", children: info }),
|
|
570
|
+
error && /* @__PURE__ */ jsx3("p", { className: "text-sm text-destructive", children: error }),
|
|
571
|
+
/* @__PURE__ */ jsx3(Button2, { type: "submit", disabled: saving, children: saving ? t("common.saving") : t("sites.edit.saveButton") })
|
|
572
|
+
] });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/components/theme-settings-form.tsx
|
|
576
|
+
import { useEffect as useEffect2, useState as useState3 } from "react";
|
|
577
|
+
import { useRouter as useRouter2 } from "next/navigation";
|
|
578
|
+
import {
|
|
579
|
+
setSiteSetting as setSiteSetting2,
|
|
580
|
+
deleteSiteSetting,
|
|
581
|
+
themeSettingKey,
|
|
582
|
+
validateThemeValue,
|
|
583
|
+
resolveLocalized,
|
|
584
|
+
parseLinkList,
|
|
585
|
+
stringifyLinkList,
|
|
586
|
+
parseColorPair,
|
|
587
|
+
formatColorPair
|
|
588
|
+
} from "ampless";
|
|
589
|
+
import {
|
|
590
|
+
COLOR_SCHEME_SETTING_KEY,
|
|
591
|
+
DEFAULT_COLOR_SCHEME,
|
|
592
|
+
validateColorScheme
|
|
593
|
+
} from "@ampless/runtime";
|
|
594
|
+
import { Button as Button3, Input as Input2, Label as Label2 } from "@ampless/runtime/ui";
|
|
595
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
596
|
+
var CACHE_REBUILD_DELAY_MS = 8e3;
|
|
597
|
+
function ThemeSettingsForm({
|
|
598
|
+
manifest,
|
|
599
|
+
activeTheme,
|
|
600
|
+
themeOptions,
|
|
601
|
+
initial,
|
|
602
|
+
initialColorScheme
|
|
603
|
+
}) {
|
|
604
|
+
const router = useRouter2();
|
|
605
|
+
const t = useT();
|
|
606
|
+
const locale = useLocale();
|
|
607
|
+
const [state, setState] = useState3({ values: initial, touched: {} });
|
|
608
|
+
const [pendingTheme, setPendingTheme] = useState3(activeTheme);
|
|
609
|
+
const [optimisticActive, setOptimisticActive] = useState3(activeTheme);
|
|
610
|
+
const [saving, setSaving] = useState3(false);
|
|
611
|
+
const [switching, setSwitching] = useState3(false);
|
|
612
|
+
const [error, setError] = useState3(null);
|
|
613
|
+
const [info, setInfo] = useState3(null);
|
|
614
|
+
const [invalid, setInvalid] = useState3({});
|
|
615
|
+
const [colorScheme, setColorScheme] = useState3(
|
|
616
|
+
initialColorScheme ?? DEFAULT_COLOR_SCHEME
|
|
617
|
+
);
|
|
618
|
+
const [colorSchemeTouched, setColorSchemeTouched] = useState3(false);
|
|
619
|
+
function update(key, value) {
|
|
620
|
+
setState((prev) => ({
|
|
621
|
+
values: { ...prev.values, [key]: value },
|
|
622
|
+
touched: { ...prev.touched, [key]: true }
|
|
623
|
+
}));
|
|
624
|
+
}
|
|
625
|
+
function scheduleCacheInvalidation() {
|
|
626
|
+
setTimeout(async () => {
|
|
627
|
+
try {
|
|
628
|
+
await invalidateSiteSettingsCache();
|
|
629
|
+
} catch (err) {
|
|
630
|
+
console.warn("[theme] cache invalidation failed", err);
|
|
631
|
+
}
|
|
632
|
+
}, CACHE_REBUILD_DELAY_MS);
|
|
633
|
+
}
|
|
634
|
+
function scheduleHardReload() {
|
|
635
|
+
setTimeout(async () => {
|
|
636
|
+
try {
|
|
637
|
+
await invalidateSiteSettingsCache();
|
|
638
|
+
} catch (err) {
|
|
639
|
+
console.warn("[theme] cache invalidation failed", err);
|
|
640
|
+
}
|
|
641
|
+
window.location.reload();
|
|
642
|
+
}, CACHE_REBUILD_DELAY_MS);
|
|
643
|
+
}
|
|
644
|
+
async function switchTheme(e) {
|
|
645
|
+
e.preventDefault();
|
|
646
|
+
if (pendingTheme === optimisticActive) return;
|
|
647
|
+
setSwitching(true);
|
|
648
|
+
setError(null);
|
|
649
|
+
setInfo(null);
|
|
650
|
+
try {
|
|
651
|
+
await setSiteSetting2("theme.active", pendingTheme);
|
|
652
|
+
setOptimisticActive(pendingTheme);
|
|
653
|
+
setInfo(t("theme.switched", { theme: pendingTheme }));
|
|
654
|
+
scheduleHardReload();
|
|
655
|
+
} catch (err) {
|
|
656
|
+
console.error("[theme] switch failed", err);
|
|
657
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
658
|
+
} finally {
|
|
659
|
+
setSwitching(false);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
async function save(e) {
|
|
663
|
+
e.preventDefault();
|
|
664
|
+
setSaving(true);
|
|
665
|
+
setError(null);
|
|
666
|
+
setInfo(null);
|
|
667
|
+
setInvalid({});
|
|
668
|
+
const newInvalid = {};
|
|
669
|
+
const writes = [];
|
|
670
|
+
for (const field of manifest.fields) {
|
|
671
|
+
if (!state.touched[field.key]) continue;
|
|
672
|
+
const raw = (state.values[field.key] ?? "").trim();
|
|
673
|
+
const storeKey = themeSettingKey(field.key);
|
|
674
|
+
if (raw === "") {
|
|
675
|
+
writes.push(deleteSiteSetting(storeKey));
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
const validated = validateThemeValue(field, raw);
|
|
679
|
+
if (validated === null) {
|
|
680
|
+
newInvalid[field.key] = true;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
writes.push(setSiteSetting2(storeKey, validated));
|
|
684
|
+
}
|
|
685
|
+
if (colorSchemeTouched) {
|
|
686
|
+
if (colorScheme === DEFAULT_COLOR_SCHEME) {
|
|
687
|
+
writes.push(deleteSiteSetting(COLOR_SCHEME_SETTING_KEY));
|
|
688
|
+
} else {
|
|
689
|
+
writes.push(setSiteSetting2(COLOR_SCHEME_SETTING_KEY, colorScheme));
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (Object.keys(newInvalid).length > 0) {
|
|
693
|
+
setInvalid(newInvalid);
|
|
694
|
+
setSaving(false);
|
|
695
|
+
setError(t("theme.invalidValues"));
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
await Promise.all(writes);
|
|
700
|
+
setInfo(t("theme.saved"));
|
|
701
|
+
setState((prev) => ({ values: prev.values, touched: {} }));
|
|
702
|
+
setColorSchemeTouched(false);
|
|
703
|
+
scheduleCacheInvalidation();
|
|
704
|
+
} catch (err) {
|
|
705
|
+
console.error("[theme] save failed", err);
|
|
706
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
707
|
+
} finally {
|
|
708
|
+
setSaving(false);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const groups = groupFields(manifest.fields);
|
|
712
|
+
return /* @__PURE__ */ jsxs3("div", { className: "space-y-8", children: [
|
|
713
|
+
/* @__PURE__ */ jsxs3("form", { onSubmit: switchTheme, className: "max-w-xl space-y-3 rounded-md border p-4", children: [
|
|
714
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-1", children: [
|
|
715
|
+
/* @__PURE__ */ jsx4(Label2, { htmlFor: "active-theme", className: "text-sm font-medium", children: t("theme.activeLabel") }),
|
|
716
|
+
/* @__PURE__ */ jsxs3("p", { className: "text-xs text-muted-foreground", children: [
|
|
717
|
+
t("theme.currentlyActive", { theme: optimisticActive }),
|
|
718
|
+
optimisticActive !== activeTheme && t("theme.propagating")
|
|
719
|
+
] })
|
|
720
|
+
] }),
|
|
721
|
+
/* @__PURE__ */ jsx4(
|
|
722
|
+
"select",
|
|
723
|
+
{
|
|
724
|
+
id: "active-theme",
|
|
725
|
+
className: "w-full rounded-md border bg-background px-2 py-1.5 text-sm",
|
|
726
|
+
value: pendingTheme,
|
|
727
|
+
onChange: (e) => setPendingTheme(e.target.value),
|
|
728
|
+
children: themeOptions.map((opt) => /* @__PURE__ */ jsxs3("option", { value: opt.value, children: [
|
|
729
|
+
resolveLocalized(opt.label, locale),
|
|
730
|
+
" (",
|
|
731
|
+
opt.value,
|
|
732
|
+
")"
|
|
733
|
+
] }, opt.value))
|
|
734
|
+
}
|
|
735
|
+
),
|
|
736
|
+
(() => {
|
|
737
|
+
const desc = themeOptions.find((o) => o.value === pendingTheme)?.description;
|
|
738
|
+
return desc ? /* @__PURE__ */ jsx4("p", { className: "text-xs text-muted-foreground", children: resolveLocalized(desc, locale) }) : null;
|
|
739
|
+
})(),
|
|
740
|
+
/* @__PURE__ */ jsx4(
|
|
741
|
+
Button3,
|
|
742
|
+
{
|
|
743
|
+
type: "submit",
|
|
744
|
+
disabled: switching || pendingTheme === optimisticActive,
|
|
745
|
+
variant: pendingTheme === optimisticActive ? "outline" : "default",
|
|
746
|
+
children: switching ? t("theme.switching") : t("theme.switchButton")
|
|
747
|
+
}
|
|
748
|
+
)
|
|
749
|
+
] }),
|
|
750
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
751
|
+
/* @__PURE__ */ jsx4(Label2, { className: "text-sm font-medium", children: t("theme.previewLabel") }),
|
|
752
|
+
/* @__PURE__ */ jsx4(
|
|
753
|
+
"iframe",
|
|
754
|
+
{
|
|
755
|
+
src: `/?previewTheme=${encodeURIComponent(pendingTheme)}&previewColorScheme=${encodeURIComponent(colorScheme)}`,
|
|
756
|
+
title: t("theme.previewLabel"),
|
|
757
|
+
className: "h-[600px] w-full rounded-md border bg-[var(--background)]"
|
|
758
|
+
},
|
|
759
|
+
`${pendingTheme}-${colorScheme}`
|
|
760
|
+
),
|
|
761
|
+
/* @__PURE__ */ jsx4("p", { className: "text-xs text-muted-foreground", children: t("theme.previewHint") })
|
|
762
|
+
] }),
|
|
763
|
+
/* @__PURE__ */ jsxs3("form", { onSubmit: save, className: "max-w-xl space-y-6", children: [
|
|
764
|
+
/* @__PURE__ */ jsxs3("div", { children: [
|
|
765
|
+
/* @__PURE__ */ jsx4("h2", { className: "text-lg font-semibold", children: t("theme.customizationHeading", {
|
|
766
|
+
theme: resolveLocalized(manifest.label, locale)
|
|
767
|
+
}) }),
|
|
768
|
+
manifest.description && /* @__PURE__ */ jsx4("p", { className: "text-sm text-muted-foreground", children: resolveLocalized(manifest.description, locale) }),
|
|
769
|
+
/* @__PURE__ */ jsx4("p", { className: "mt-1 text-xs text-muted-foreground", children: t("theme.customizationHint") })
|
|
770
|
+
] }),
|
|
771
|
+
/* @__PURE__ */ jsxs3("fieldset", { className: "space-y-2", children: [
|
|
772
|
+
/* @__PURE__ */ jsx4(Label2, { htmlFor: "color-scheme", className: "text-sm font-medium", children: t("theme.colorScheme.label") }),
|
|
773
|
+
/* @__PURE__ */ jsxs3(
|
|
774
|
+
"select",
|
|
775
|
+
{
|
|
776
|
+
id: "color-scheme",
|
|
777
|
+
className: "w-full rounded-md border bg-background px-2 py-1.5 text-sm",
|
|
778
|
+
value: colorScheme,
|
|
779
|
+
onChange: (e) => {
|
|
780
|
+
setColorScheme(validateColorScheme(e.target.value));
|
|
781
|
+
setColorSchemeTouched(true);
|
|
782
|
+
},
|
|
783
|
+
children: [
|
|
784
|
+
/* @__PURE__ */ jsx4("option", { value: "auto", children: t("theme.colorScheme.auto") }),
|
|
785
|
+
/* @__PURE__ */ jsx4("option", { value: "light", children: t("theme.colorScheme.light") }),
|
|
786
|
+
/* @__PURE__ */ jsx4("option", { value: "dark", children: t("theme.colorScheme.dark") })
|
|
787
|
+
]
|
|
788
|
+
}
|
|
789
|
+
),
|
|
790
|
+
/* @__PURE__ */ jsx4("p", { className: "text-xs text-muted-foreground", children: t("theme.colorScheme.hint") })
|
|
791
|
+
] }),
|
|
792
|
+
groups.map(({ key, name, fields }) => /* @__PURE__ */ jsxs3("fieldset", { className: "space-y-4", children: [
|
|
793
|
+
/* @__PURE__ */ jsx4("legend", { className: "text-sm font-medium uppercase tracking-wide text-muted-foreground", children: resolveLocalized(name, locale) }),
|
|
794
|
+
fields.map((field) => /* @__PURE__ */ jsx4(
|
|
795
|
+
FieldRow,
|
|
796
|
+
{
|
|
797
|
+
field,
|
|
798
|
+
value: state.values[field.key] ?? "",
|
|
799
|
+
invalid: !!invalid[field.key],
|
|
800
|
+
onChange: (v) => update(field.key, v)
|
|
801
|
+
},
|
|
802
|
+
field.key
|
|
803
|
+
))
|
|
804
|
+
] }, key)),
|
|
805
|
+
info && /* @__PURE__ */ jsx4("p", { className: "text-sm text-muted-foreground", children: info }),
|
|
806
|
+
error && /* @__PURE__ */ jsx4("p", { className: "text-sm text-destructive", children: error }),
|
|
807
|
+
/* @__PURE__ */ jsx4(Button3, { type: "submit", disabled: saving, children: saving ? t("theme.saving") : t("theme.saveButton") })
|
|
808
|
+
] })
|
|
809
|
+
] });
|
|
810
|
+
}
|
|
811
|
+
function groupFields(fields) {
|
|
812
|
+
const map = /* @__PURE__ */ new Map();
|
|
813
|
+
for (const field of fields) {
|
|
814
|
+
const g = field.group ?? "General";
|
|
815
|
+
const k = typeof g === "string" ? g : JSON.stringify(g);
|
|
816
|
+
const existing = map.get(k);
|
|
817
|
+
if (existing) {
|
|
818
|
+
existing.fields.push(field);
|
|
819
|
+
} else {
|
|
820
|
+
map.set(k, { name: g, fields: [field] });
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return Array.from(map.entries()).map(([key, { name, fields: fields2 }]) => ({ key, name, fields: fields2 }));
|
|
824
|
+
}
|
|
825
|
+
function FieldRow({ field, value, invalid, onChange }) {
|
|
826
|
+
const t = useT();
|
|
827
|
+
const locale = useLocale();
|
|
828
|
+
const id = `theme-${field.key}`;
|
|
829
|
+
const labelEl = /* @__PURE__ */ jsx4(Label2, { htmlFor: id, className: invalid ? "text-destructive" : void 0, children: resolveLocalized(field.label, locale) });
|
|
830
|
+
const description = field.description ? /* @__PURE__ */ jsx4("p", { className: "text-xs text-muted-foreground", children: resolveLocalized(field.description, locale) }) : null;
|
|
831
|
+
switch (field.type) {
|
|
832
|
+
case "color":
|
|
833
|
+
return /* @__PURE__ */ jsx4(ColorField, { field, id, labelEl, description, value, invalid, onChange });
|
|
834
|
+
case "length":
|
|
835
|
+
return /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
836
|
+
labelEl,
|
|
837
|
+
description,
|
|
838
|
+
/* @__PURE__ */ jsx4(
|
|
839
|
+
Input2,
|
|
840
|
+
{
|
|
841
|
+
id,
|
|
842
|
+
value,
|
|
843
|
+
placeholder: field.default,
|
|
844
|
+
onChange: (e) => onChange(e.target.value),
|
|
845
|
+
"aria-invalid": invalid,
|
|
846
|
+
className: "font-mono text-xs"
|
|
847
|
+
}
|
|
848
|
+
),
|
|
849
|
+
/* @__PURE__ */ jsx4("p", { className: "text-xs text-muted-foreground", children: t("theme.lengthHelp") })
|
|
850
|
+
] });
|
|
851
|
+
case "select":
|
|
852
|
+
case "fontFamily":
|
|
853
|
+
return /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
854
|
+
labelEl,
|
|
855
|
+
description,
|
|
856
|
+
/* @__PURE__ */ jsxs3(
|
|
857
|
+
"select",
|
|
858
|
+
{
|
|
859
|
+
id,
|
|
860
|
+
className: "w-full rounded-md border bg-background px-2 py-1.5 text-sm",
|
|
861
|
+
value: value || "",
|
|
862
|
+
onChange: (e) => onChange(e.target.value),
|
|
863
|
+
"aria-invalid": invalid,
|
|
864
|
+
children: [
|
|
865
|
+
/* @__PURE__ */ jsx4("option", { value: "", children: t("common.default") }),
|
|
866
|
+
field.options.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt.value, children: resolveLocalized(opt.label, locale) }, opt.value))
|
|
867
|
+
]
|
|
868
|
+
}
|
|
869
|
+
)
|
|
870
|
+
] });
|
|
871
|
+
case "image":
|
|
872
|
+
return /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
873
|
+
labelEl,
|
|
874
|
+
description,
|
|
875
|
+
/* @__PURE__ */ jsx4(
|
|
876
|
+
Input2,
|
|
877
|
+
{
|
|
878
|
+
id,
|
|
879
|
+
value,
|
|
880
|
+
placeholder: field.default || t("theme.imagePlaceholder"),
|
|
881
|
+
onChange: (e) => onChange(e.target.value),
|
|
882
|
+
"aria-invalid": invalid
|
|
883
|
+
}
|
|
884
|
+
)
|
|
885
|
+
] });
|
|
886
|
+
case "text":
|
|
887
|
+
return /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
888
|
+
labelEl,
|
|
889
|
+
description,
|
|
890
|
+
/* @__PURE__ */ jsx4(
|
|
891
|
+
Input2,
|
|
892
|
+
{
|
|
893
|
+
id,
|
|
894
|
+
value,
|
|
895
|
+
placeholder: field.default,
|
|
896
|
+
maxLength: field.maxLength,
|
|
897
|
+
onChange: (e) => onChange(e.target.value),
|
|
898
|
+
"aria-invalid": invalid
|
|
899
|
+
}
|
|
900
|
+
)
|
|
901
|
+
] });
|
|
902
|
+
case "linkList":
|
|
903
|
+
return /* @__PURE__ */ jsx4(
|
|
904
|
+
LinkListField,
|
|
905
|
+
{
|
|
906
|
+
field,
|
|
907
|
+
labelEl,
|
|
908
|
+
description,
|
|
909
|
+
value,
|
|
910
|
+
onChange
|
|
911
|
+
}
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function LinkListField({ field, labelEl, description, value, onChange }) {
|
|
916
|
+
const items = parseLinkList(value);
|
|
917
|
+
const max = field.maxItems ?? 50;
|
|
918
|
+
function commit(next) {
|
|
919
|
+
onChange(stringifyLinkList(next));
|
|
920
|
+
}
|
|
921
|
+
function update(idx, patch) {
|
|
922
|
+
commit(items.map((it, i) => i === idx ? { ...it, ...patch } : it));
|
|
923
|
+
}
|
|
924
|
+
function add() {
|
|
925
|
+
if (items.length >= max) return;
|
|
926
|
+
commit([...items, { label: "", url: "" }]);
|
|
927
|
+
}
|
|
928
|
+
function remove(idx) {
|
|
929
|
+
commit(items.filter((_, i) => i !== idx));
|
|
930
|
+
}
|
|
931
|
+
function move(idx, delta) {
|
|
932
|
+
const target = idx + delta;
|
|
933
|
+
if (target < 0 || target >= items.length) return;
|
|
934
|
+
const next = items.slice();
|
|
935
|
+
const [moved] = next.splice(idx, 1);
|
|
936
|
+
next.splice(target, 0, moved);
|
|
937
|
+
commit(next);
|
|
938
|
+
}
|
|
939
|
+
return /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
940
|
+
labelEl,
|
|
941
|
+
description,
|
|
942
|
+
/* @__PURE__ */ jsxs3("div", { className: "space-y-2 rounded-md border bg-muted/20 p-3", children: [
|
|
943
|
+
items.length === 0 && /* @__PURE__ */ jsx4("p", { className: "text-xs text-muted-foreground", children: "No links yet." }),
|
|
944
|
+
items.map((item, idx) => {
|
|
945
|
+
const isTagRef = /^tag:/.test(item.url.trim());
|
|
946
|
+
return /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-start gap-2", children: [
|
|
947
|
+
/* @__PURE__ */ jsxs3("div", { className: "grid flex-1 grid-cols-1 gap-2 sm:grid-cols-2", children: [
|
|
948
|
+
/* @__PURE__ */ jsx4(
|
|
949
|
+
Input2,
|
|
950
|
+
{
|
|
951
|
+
value: item.label,
|
|
952
|
+
placeholder: "Label",
|
|
953
|
+
onChange: (e) => update(idx, { label: e.target.value })
|
|
954
|
+
}
|
|
955
|
+
),
|
|
956
|
+
/* @__PURE__ */ jsx4(
|
|
957
|
+
Input2,
|
|
958
|
+
{
|
|
959
|
+
value: item.url,
|
|
960
|
+
placeholder: "/path or https://\u2026 or tag:name",
|
|
961
|
+
onChange: (e) => update(idx, { url: e.target.value }),
|
|
962
|
+
className: isTagRef ? "font-mono text-xs" : void 0
|
|
963
|
+
}
|
|
964
|
+
)
|
|
965
|
+
] }),
|
|
966
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex shrink-0 items-center gap-1", children: [
|
|
967
|
+
/* @__PURE__ */ jsx4(
|
|
968
|
+
Button3,
|
|
969
|
+
{
|
|
970
|
+
type: "button",
|
|
971
|
+
variant: "ghost",
|
|
972
|
+
size: "icon",
|
|
973
|
+
onClick: () => move(idx, -1),
|
|
974
|
+
disabled: idx === 0,
|
|
975
|
+
"aria-label": "Move up",
|
|
976
|
+
children: "\u2191"
|
|
977
|
+
}
|
|
978
|
+
),
|
|
979
|
+
/* @__PURE__ */ jsx4(
|
|
980
|
+
Button3,
|
|
981
|
+
{
|
|
982
|
+
type: "button",
|
|
983
|
+
variant: "ghost",
|
|
984
|
+
size: "icon",
|
|
985
|
+
onClick: () => move(idx, 1),
|
|
986
|
+
disabled: idx === items.length - 1,
|
|
987
|
+
"aria-label": "Move down",
|
|
988
|
+
children: "\u2193"
|
|
989
|
+
}
|
|
990
|
+
),
|
|
991
|
+
/* @__PURE__ */ jsx4(
|
|
992
|
+
Button3,
|
|
993
|
+
{
|
|
994
|
+
type: "button",
|
|
995
|
+
variant: "ghost",
|
|
996
|
+
size: "icon",
|
|
997
|
+
onClick: () => remove(idx),
|
|
998
|
+
"aria-label": "Remove",
|
|
999
|
+
children: "\xD7"
|
|
1000
|
+
}
|
|
1001
|
+
)
|
|
1002
|
+
] })
|
|
1003
|
+
] }, idx);
|
|
1004
|
+
}),
|
|
1005
|
+
/* @__PURE__ */ jsx4(
|
|
1006
|
+
Button3,
|
|
1007
|
+
{
|
|
1008
|
+
type: "button",
|
|
1009
|
+
variant: "outline",
|
|
1010
|
+
size: "sm",
|
|
1011
|
+
onClick: add,
|
|
1012
|
+
disabled: items.length >= max,
|
|
1013
|
+
children: "+ Add link"
|
|
1014
|
+
}
|
|
1015
|
+
),
|
|
1016
|
+
/* @__PURE__ */ jsxs3("p", { className: "text-xs text-muted-foreground", children: [
|
|
1017
|
+
"Tip: use ",
|
|
1018
|
+
/* @__PURE__ */ jsx4("code", { children: "tag:<name>" }),
|
|
1019
|
+
" as a URL to render a list of posts with that tag instead of a single link."
|
|
1020
|
+
] })
|
|
1021
|
+
] })
|
|
1022
|
+
] });
|
|
1023
|
+
}
|
|
1024
|
+
function ColorField({
|
|
1025
|
+
field,
|
|
1026
|
+
id,
|
|
1027
|
+
labelEl,
|
|
1028
|
+
description,
|
|
1029
|
+
value,
|
|
1030
|
+
invalid,
|
|
1031
|
+
onChange
|
|
1032
|
+
}) {
|
|
1033
|
+
const parsed = parseColorPair(value);
|
|
1034
|
+
const lightInput = parsed.dark !== null ? parsed.light : value;
|
|
1035
|
+
const darkInput = parsed.dark ?? "";
|
|
1036
|
+
const [showDark, setShowDark] = useState3(parsed.dark !== null);
|
|
1037
|
+
function emit(nextLight, nextDark) {
|
|
1038
|
+
if (!nextDark.trim()) {
|
|
1039
|
+
onChange(nextLight);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
onChange(formatColorPair(nextLight || field.default, nextDark));
|
|
1043
|
+
}
|
|
1044
|
+
const lightEffective = lightInput || field.default;
|
|
1045
|
+
const darkEffective = darkInput || lightEffective;
|
|
1046
|
+
return /* @__PURE__ */ jsxs3("div", { className: "space-y-2", children: [
|
|
1047
|
+
labelEl,
|
|
1048
|
+
description,
|
|
1049
|
+
/* @__PURE__ */ jsx4(
|
|
1050
|
+
ColorRow,
|
|
1051
|
+
{
|
|
1052
|
+
id,
|
|
1053
|
+
label: showDark ? "Light" : void 0,
|
|
1054
|
+
value: lightInput,
|
|
1055
|
+
effective: lightEffective,
|
|
1056
|
+
placeholder: field.default,
|
|
1057
|
+
ariaLabel: `${typeof field.label === "string" ? field.label : id} (light)`,
|
|
1058
|
+
invalid,
|
|
1059
|
+
onChange: (v) => emit(v, darkInput)
|
|
1060
|
+
}
|
|
1061
|
+
),
|
|
1062
|
+
showDark ? /* @__PURE__ */ jsx4(
|
|
1063
|
+
ColorRow,
|
|
1064
|
+
{
|
|
1065
|
+
id: `${id}-dark`,
|
|
1066
|
+
label: "Dark",
|
|
1067
|
+
value: darkInput,
|
|
1068
|
+
effective: darkEffective,
|
|
1069
|
+
placeholder: lightEffective,
|
|
1070
|
+
ariaLabel: `${typeof field.label === "string" ? field.label : id} (dark)`,
|
|
1071
|
+
invalid,
|
|
1072
|
+
onChange: (v) => emit(lightInput, v)
|
|
1073
|
+
}
|
|
1074
|
+
) : null,
|
|
1075
|
+
/* @__PURE__ */ jsx4("div", { className: "flex items-center gap-3 text-xs", children: /* @__PURE__ */ jsx4(
|
|
1076
|
+
"button",
|
|
1077
|
+
{
|
|
1078
|
+
type: "button",
|
|
1079
|
+
className: "text-muted-foreground underline-offset-2 hover:underline",
|
|
1080
|
+
onClick: () => {
|
|
1081
|
+
if (showDark) {
|
|
1082
|
+
setShowDark(false);
|
|
1083
|
+
emit(lightInput, "");
|
|
1084
|
+
} else {
|
|
1085
|
+
setShowDark(true);
|
|
1086
|
+
}
|
|
1087
|
+
},
|
|
1088
|
+
children: showDark ? "\u2212 Remove dark variant" : "+ Add dark variant (optional)"
|
|
1089
|
+
}
|
|
1090
|
+
) })
|
|
1091
|
+
] });
|
|
1092
|
+
}
|
|
1093
|
+
function ColorRow({
|
|
1094
|
+
id,
|
|
1095
|
+
label,
|
|
1096
|
+
value,
|
|
1097
|
+
effective,
|
|
1098
|
+
placeholder,
|
|
1099
|
+
ariaLabel,
|
|
1100
|
+
invalid,
|
|
1101
|
+
onChange
|
|
1102
|
+
}) {
|
|
1103
|
+
const hex = useColorAsHex(effective);
|
|
1104
|
+
return /* @__PURE__ */ jsxs3("div", { className: "space-y-1", children: [
|
|
1105
|
+
label ? /* @__PURE__ */ jsx4(Label2, { htmlFor: id, className: "text-xs uppercase tracking-wide text-muted-foreground", children: label }) : null,
|
|
1106
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
|
|
1107
|
+
/* @__PURE__ */ jsx4(
|
|
1108
|
+
"input",
|
|
1109
|
+
{
|
|
1110
|
+
type: "color",
|
|
1111
|
+
value: hex,
|
|
1112
|
+
onChange: (e) => onChange(e.target.value),
|
|
1113
|
+
className: "h-9 w-12 cursor-pointer rounded border border-input bg-background p-0",
|
|
1114
|
+
"aria-label": `${ariaLabel} swatch`
|
|
1115
|
+
}
|
|
1116
|
+
),
|
|
1117
|
+
/* @__PURE__ */ jsx4(
|
|
1118
|
+
Input2,
|
|
1119
|
+
{
|
|
1120
|
+
id,
|
|
1121
|
+
value,
|
|
1122
|
+
placeholder,
|
|
1123
|
+
onChange: (e) => onChange(e.target.value),
|
|
1124
|
+
"aria-invalid": invalid,
|
|
1125
|
+
className: "font-mono text-xs"
|
|
1126
|
+
}
|
|
1127
|
+
)
|
|
1128
|
+
] }),
|
|
1129
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
|
|
1130
|
+
/* @__PURE__ */ jsx4(
|
|
1131
|
+
"span",
|
|
1132
|
+
{
|
|
1133
|
+
className: "inline-block h-4 w-4 rounded border",
|
|
1134
|
+
style: { background: effective },
|
|
1135
|
+
"aria-hidden": true
|
|
1136
|
+
}
|
|
1137
|
+
),
|
|
1138
|
+
/* @__PURE__ */ jsx4("code", { className: "text-xs text-muted-foreground", children: effective })
|
|
1139
|
+
] })
|
|
1140
|
+
] });
|
|
1141
|
+
}
|
|
1142
|
+
function useColorAsHex(value) {
|
|
1143
|
+
const [hex, setHex] = useState3(() => directHex(value) ?? "#000000");
|
|
1144
|
+
useEffect2(() => {
|
|
1145
|
+
const direct = directHex(value);
|
|
1146
|
+
if (direct) {
|
|
1147
|
+
setHex(direct);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
if (typeof document === "undefined") return;
|
|
1151
|
+
try {
|
|
1152
|
+
const el = document.createElement("span");
|
|
1153
|
+
el.style.color = value;
|
|
1154
|
+
el.style.display = "none";
|
|
1155
|
+
document.body.appendChild(el);
|
|
1156
|
+
const computed = getComputedStyle(el).color;
|
|
1157
|
+
document.body.removeChild(el);
|
|
1158
|
+
const next = rgbStringToHex(computed);
|
|
1159
|
+
if (next) setHex(next);
|
|
1160
|
+
} catch {
|
|
1161
|
+
}
|
|
1162
|
+
}, [value]);
|
|
1163
|
+
return hex;
|
|
1164
|
+
}
|
|
1165
|
+
function directHex(value) {
|
|
1166
|
+
const m = /^#([0-9a-fA-F]{6})$/.exec(value.trim());
|
|
1167
|
+
return m ? value.trim().toLowerCase() : null;
|
|
1168
|
+
}
|
|
1169
|
+
function rgbStringToHex(rgb) {
|
|
1170
|
+
const m = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/.exec(rgb);
|
|
1171
|
+
if (!m) return null;
|
|
1172
|
+
const r = clampToHex(m[1]);
|
|
1173
|
+
const g = clampToHex(m[2]);
|
|
1174
|
+
const b = clampToHex(m[3]);
|
|
1175
|
+
return `#${r}${g}${b}`;
|
|
1176
|
+
}
|
|
1177
|
+
function clampToHex(s) {
|
|
1178
|
+
const n = Math.max(0, Math.min(255, parseInt(s, 10)));
|
|
1179
|
+
return n.toString(16).padStart(2, "0");
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
export {
|
|
1183
|
+
AdminProviders,
|
|
1184
|
+
Sidebar,
|
|
1185
|
+
SiteSettingsForm,
|
|
1186
|
+
ThemeSettingsForm
|
|
1187
|
+
};
|