@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.
Files changed (48) hide show
  1. package/README.ja.md +73 -0
  2. package/README.md +3 -0
  3. package/dist/api/index.d.ts +1 -1
  4. package/dist/chunk-2ITWLRYF.js +38 -0
  5. package/dist/chunk-2U3POKAZ.js +198 -0
  6. package/dist/{chunk-VXEVLHGL.js → chunk-6LQGVDCW.js} +2 -2
  7. package/dist/chunk-6NPYUTV6.js +250 -0
  8. package/dist/chunk-6SB7YICQ.js +48 -0
  9. package/dist/chunk-6W3JIOOR.js +37 -0
  10. package/dist/chunk-CTGFMK2J.js +335 -0
  11. package/dist/chunk-G4CF5ZWV.js +1319 -0
  12. package/dist/chunk-KQOE5CT6.js +21 -0
  13. package/dist/chunk-MWSCSCCU.js +67 -0
  14. package/dist/chunk-Q66BLMNJ.js +33 -0
  15. package/dist/chunk-TZ5F24BG.js +149 -0
  16. package/dist/chunk-VL6MMF2P.js +21 -0
  17. package/dist/chunk-VSS5FWSR.js +334 -0
  18. package/dist/{chunk-KKM2MCM4.js → chunk-WL4IBW2D.js} +121 -43
  19. package/dist/chunk-YFWHKIVH.js +1187 -0
  20. package/dist/components/admin-dashboard.d.ts +10 -0
  21. package/dist/components/admin-dashboard.js +9 -0
  22. package/dist/components/edit-post-view.d.ts +9 -0
  23. package/dist/components/edit-post-view.js +12 -0
  24. package/dist/components/index.d.ts +14 -42
  25. package/dist/components/index.js +22 -33
  26. package/dist/components/login-view.d.ts +5 -0
  27. package/dist/components/login-view.js +9 -0
  28. package/dist/components/mcp-tokens-view.d.ts +16 -0
  29. package/dist/components/mcp-tokens-view.js +9 -0
  30. package/dist/components/media-view.d.ts +5 -0
  31. package/dist/components/media-view.js +12 -0
  32. package/dist/components/new-post-view.d.ts +5 -0
  33. package/dist/components/new-post-view.js +12 -0
  34. package/dist/components/posts-list-view.d.ts +5 -0
  35. package/dist/components/posts-list-view.js +9 -0
  36. package/dist/components/users-list-view.d.ts +7 -0
  37. package/dist/components/users-list-view.js +9 -0
  38. package/dist/{i18n-DzXXcIQQ.d.ts → i18n-BhMBRfio.d.ts} +179 -1
  39. package/dist/index.d.ts +18 -18
  40. package/dist/index.js +17 -38
  41. package/dist/lib/theme-actions.d.ts +3 -3
  42. package/dist/lib/theme-actions.js +1 -1
  43. package/dist/metafile-esm.json +1 -1
  44. package/dist/pages/index.d.ts +35 -16
  45. package/dist/pages/index.js +90 -257
  46. package/package.json +19 -8
  47. package/dist/chunk-QDPB5W35.js +0 -3251
  48. 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
+ };