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