@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.
Files changed (49) 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-5JKOPRCO.js +41 -0
  6. package/dist/chunk-5Q6KVRZ2.js +250 -0
  7. package/dist/chunk-7IR4F7GA.js +6 -0
  8. package/dist/chunk-A3SWBQA6.js +71 -0
  9. package/dist/chunk-BC4B6DLO.js +21 -0
  10. package/dist/chunk-CQY55RDG.js +48 -0
  11. package/dist/chunk-CVJCMTYB.js +1197 -0
  12. package/dist/chunk-IM5MVZOH.js +1260 -0
  13. package/dist/chunk-JOASK4AM.js +360 -0
  14. package/dist/{chunk-TJR3ALRJ.js → chunk-OSUTPPAU.js} +171 -68
  15. package/dist/chunk-QXJIIBUQ.js +21 -0
  16. package/dist/chunk-S66L5CDS.js +335 -0
  17. package/dist/chunk-SRNH2IVA.js +149 -0
  18. package/dist/chunk-TZWSXAHD.js +32 -0
  19. package/dist/chunk-VXEVLHGL.js +10 -0
  20. package/dist/chunk-W6BXESPW.js +198 -0
  21. package/dist/chunk-XY4JWSMS.js +33 -0
  22. package/dist/components/admin-dashboard.d.ts +10 -0
  23. package/dist/components/admin-dashboard.js +9 -0
  24. package/dist/components/edit-post-view.d.ts +9 -0
  25. package/dist/components/edit-post-view.js +14 -0
  26. package/dist/components/index.d.ts +40 -21
  27. package/dist/components/index.js +32 -15
  28. package/dist/components/login-view.d.ts +5 -0
  29. package/dist/components/login-view.js +9 -0
  30. package/dist/components/mcp-tokens-view.d.ts +22 -0
  31. package/dist/components/mcp-tokens-view.js +9 -0
  32. package/dist/components/media-view.d.ts +5 -0
  33. package/dist/components/media-view.js +12 -0
  34. package/dist/components/new-post-view.d.ts +5 -0
  35. package/dist/components/new-post-view.js +14 -0
  36. package/dist/components/posts-list-view.d.ts +5 -0
  37. package/dist/components/posts-list-view.js +11 -0
  38. package/dist/components/users-list-view.d.ts +7 -0
  39. package/dist/components/users-list-view.js +9 -0
  40. package/dist/{i18n-ByHM_Bho.d.ts → i18n-MWvAMHzn.d.ts} +253 -2
  41. package/dist/index.d.ts +14 -9
  42. package/dist/index.js +18 -10
  43. package/dist/lib/theme-actions.d.ts +17 -0
  44. package/dist/lib/theme-actions.js +7 -0
  45. package/dist/metafile-esm.json +1 -1
  46. package/dist/pages/index.d.ts +67 -15
  47. package/dist/pages/index.js +147 -659
  48. package/package.json +12 -9
  49. package/dist/chunk-T2RSMFOI.js +0 -2074
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+ import {
3
+ MediaUploader
4
+ } from "./chunk-5Q6KVRZ2.js";
5
+ import {
6
+ useT
7
+ } from "./chunk-XY4JWSMS.js";
8
+
9
+ // src/components/media-view.tsx
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ function MediaPage() {
12
+ const t = useT();
13
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-7xl p-4 md:p-8", children: [
14
+ /* @__PURE__ */ jsx("h1", { className: "mb-6 text-2xl font-bold md:mb-8 md:text-3xl", children: t("media.title") }),
15
+ /* @__PURE__ */ jsx(MediaUploader, {})
16
+ ] });
17
+ }
18
+
19
+ export {
20
+ MediaPage
21
+ };
@@ -0,0 +1,335 @@
1
+ 'use client';
2
+ import {
3
+ useT
4
+ } from "./chunk-XY4JWSMS.js";
5
+
6
+ // src/components/image-upload-dialog.tsx
7
+ import { useEffect, useRef, useState } from "react";
8
+ import ReactCrop, { centerCrop, makeAspectCrop } from "react-image-crop";
9
+ import "react-image-crop/dist/ReactCrop.css";
10
+ import { shouldSkipProcessing } from "ampless/media";
11
+ import {
12
+ Dialog,
13
+ DialogContent,
14
+ DialogDescription,
15
+ DialogHeader,
16
+ DialogTitle,
17
+ Button,
18
+ Input,
19
+ Label
20
+ } from "@ampless/runtime/ui";
21
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
22
+ var ASPECTS = {
23
+ free: void 0,
24
+ "1:1": 1,
25
+ "4:3": 4 / 3,
26
+ "16:9": 16 / 9,
27
+ "3:2": 3 / 2
28
+ };
29
+ var ASPECT_CHOICES = ["free", "1:1", "4:3", "16:9", "3:2"];
30
+ var FORMAT_CHOICES = ["auto", "webp", "jpeg"];
31
+ var MAX_DIMENSION_PRESETS = [640, 1024, 1600, 2400, 4e3];
32
+ var MIN_DIMENSION = 100;
33
+ var MAX_DIMENSION_CEILING = 8e3;
34
+ function clampMaxDimension(value, fallback) {
35
+ if (!Number.isFinite(value) || value <= 0) return fallback;
36
+ return Math.min(MAX_DIMENSION_CEILING, Math.max(MIN_DIMENSION, Math.round(value)));
37
+ }
38
+ function clampQuality(value) {
39
+ if (!Number.isFinite(value)) return 0.85;
40
+ return Math.min(1, Math.max(0, value));
41
+ }
42
+ function resolveFormat(choice, inputMime, losslessForPng) {
43
+ if (choice === "auto") {
44
+ return {
45
+ format: "webp",
46
+ lossless: losslessForPng && inputMime === "image/png"
47
+ };
48
+ }
49
+ return { format: choice, lossless: false };
50
+ }
51
+ function buildInitialCrop(naturalWidth, naturalHeight, aspect) {
52
+ if (aspect) {
53
+ return centerCrop(
54
+ makeAspectCrop({ unit: "%", width: 100 }, aspect, naturalWidth, naturalHeight),
55
+ naturalWidth,
56
+ naturalHeight
57
+ );
58
+ }
59
+ return { unit: "%", x: 0, y: 0, width: 100, height: 100 };
60
+ }
61
+ function ImageUploadDialog({
62
+ file,
63
+ remaining,
64
+ busy = false,
65
+ defaults,
66
+ onConfirm,
67
+ onSkip,
68
+ onCancel
69
+ }) {
70
+ const t = useT();
71
+ const defaultMaxDimension = defaults?.maxDimension ?? 2400;
72
+ const defaultQuality = defaults?.quality ?? 0.85;
73
+ const losslessForPng = defaults?.losslessForPng ?? true;
74
+ const [original, setOriginal] = useState(false);
75
+ const [aspect, setAspect] = useState("free");
76
+ const [crop, setCrop] = useState(void 0);
77
+ const [percentCrop, setPercentCrop] = useState(null);
78
+ const [naturalSize, setNaturalSize] = useState(null);
79
+ const [formatChoice, setFormatChoice] = useState("auto");
80
+ const [losslessOverride, setLosslessOverride] = useState(null);
81
+ const [quality, setQuality] = useState(defaultQuality);
82
+ const [maxDimension, setMaxDimension] = useState(defaultMaxDimension);
83
+ const [previewUrl, setPreviewUrl] = useState(null);
84
+ const imgRef = useRef(null);
85
+ useEffect(() => {
86
+ setOriginal(false);
87
+ setAspect("free");
88
+ setCrop(void 0);
89
+ setPercentCrop(null);
90
+ setNaturalSize(null);
91
+ setFormatChoice("auto");
92
+ setLosslessOverride(null);
93
+ setQuality(defaultQuality);
94
+ setMaxDimension(defaultMaxDimension);
95
+ }, [file, defaultQuality, defaultMaxDimension]);
96
+ useEffect(() => {
97
+ if (!naturalSize) return;
98
+ const next = buildInitialCrop(naturalSize.width, naturalSize.height, ASPECTS[aspect]);
99
+ setCrop(next);
100
+ setPercentCrop(next);
101
+ }, [aspect, naturalSize]);
102
+ useEffect(() => {
103
+ if (!file) {
104
+ setPreviewUrl(null);
105
+ return;
106
+ }
107
+ const url = URL.createObjectURL(file);
108
+ setPreviewUrl(url);
109
+ return () => URL.revokeObjectURL(url);
110
+ }, [file]);
111
+ if (!file) return null;
112
+ const isImage = file.type.startsWith("image/");
113
+ const passthrough = !isImage || shouldSkipProcessing(file.type);
114
+ const showCropper = !passthrough && !original;
115
+ const { format, lossless: autoLossless } = resolveFormat(formatChoice, file.type, losslessForPng);
116
+ const lossless = losslessOverride ?? autoLossless;
117
+ const showLosslessToggle = !original && !passthrough && format === "webp";
118
+ const showQualitySlider = !original && !passthrough && (format === "jpeg" || format === "webp" && !lossless);
119
+ function handleImageLoad(e) {
120
+ const { naturalWidth, naturalHeight } = e.currentTarget;
121
+ setNaturalSize({ width: naturalWidth, height: naturalHeight });
122
+ const initial = buildInitialCrop(naturalWidth, naturalHeight, ASPECTS[aspect]);
123
+ setCrop(initial);
124
+ setPercentCrop(initial);
125
+ }
126
+ function handleConfirm() {
127
+ if (!file || busy) return;
128
+ if (original || passthrough) {
129
+ onConfirm(file, { original: true });
130
+ return;
131
+ }
132
+ let cropArea = void 0;
133
+ if (percentCrop && naturalSize) {
134
+ const x = Math.round(percentCrop.x / 100 * naturalSize.width);
135
+ const y = Math.round(percentCrop.y / 100 * naturalSize.height);
136
+ const width = Math.round(percentCrop.width / 100 * naturalSize.width);
137
+ const height = Math.round(percentCrop.height / 100 * naturalSize.height);
138
+ if (width > 0 && height > 0 && (x !== 0 || y !== 0 || width !== naturalSize.width || height !== naturalSize.height)) {
139
+ cropArea = { x, y, width, height };
140
+ }
141
+ }
142
+ onConfirm(file, {
143
+ crop: cropArea,
144
+ maxDimension: clampMaxDimension(maxDimension, defaultMaxDimension),
145
+ format,
146
+ quality: clampQuality(quality),
147
+ lossless: format === "webp" ? lossless : false
148
+ });
149
+ }
150
+ return /* @__PURE__ */ jsx(
151
+ Dialog,
152
+ {
153
+ open: true,
154
+ onOpenChange: (open) => {
155
+ if (open) return;
156
+ onCancel();
157
+ },
158
+ children: /* @__PURE__ */ jsxs(DialogContent, { className: "max-h-[90vh] max-w-4xl overflow-y-auto", children: [
159
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
160
+ /* @__PURE__ */ jsx(DialogTitle, { className: "truncate", children: file.name }),
161
+ /* @__PURE__ */ jsxs(DialogDescription, { children: [
162
+ remaining > 1 ? t("media.dialog.remaining", { count: remaining }) : `${formatBytes(file.size)} \xB7 ${file.type || "unknown"}`,
163
+ busy && t("media.dialog.uploading")
164
+ ] })
165
+ ] }),
166
+ previewUrl && showCropper && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-md bg-black/90 p-2", children: /* @__PURE__ */ jsx(
167
+ ReactCrop,
168
+ {
169
+ crop,
170
+ aspect: ASPECTS[aspect],
171
+ minWidth: 20,
172
+ minHeight: 20,
173
+ onChange: (_pixel, percent) => {
174
+ setCrop(percent);
175
+ setPercentCrop(percent);
176
+ },
177
+ children: /* @__PURE__ */ jsx(
178
+ "img",
179
+ {
180
+ ref: imgRef,
181
+ src: previewUrl,
182
+ alt: "preview",
183
+ className: "block max-h-[60vh] max-w-full",
184
+ onLoad: handleImageLoad
185
+ }
186
+ )
187
+ }
188
+ ) }),
189
+ previewUrl && !showCropper && isImage && /* @__PURE__ */ jsx("div", { className: "flex h-48 items-center justify-center rounded-md bg-muted", children: /* @__PURE__ */ jsx("img", { src: previewUrl, alt: "preview", className: "max-h-full max-w-full object-contain" }) }),
190
+ !isImage && // Non-image upload: skip the broken-img preview. Show the
191
+ // file's name / size / mime so the admin can confirm before
192
+ // committing the bytes to S3.
193
+ /* @__PURE__ */ jsxs("div", { className: "flex h-32 flex-col items-center justify-center gap-1 rounded-md bg-muted text-sm text-muted-foreground", children: [
194
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: file.name }),
195
+ /* @__PURE__ */ jsxs("span", { className: "font-mono text-xs", children: [
196
+ formatBytes(file.size),
197
+ " \xB7 ",
198
+ file.type || "unknown"
199
+ ] })
200
+ ] }),
201
+ /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
202
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm", children: [
203
+ /* @__PURE__ */ jsx(
204
+ "input",
205
+ {
206
+ type: "checkbox",
207
+ checked: original,
208
+ disabled: busy,
209
+ onChange: (e) => setOriginal(e.target.checked)
210
+ }
211
+ ),
212
+ /* @__PURE__ */ jsx("span", { children: t("media.dialog.useOriginal") }),
213
+ passthrough && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("media.dialog.passthroughNote") })
214
+ ] }),
215
+ !original && !passthrough && /* @__PURE__ */ jsxs(Fragment, { children: [
216
+ /* @__PURE__ */ jsxs("div", { children: [
217
+ /* @__PURE__ */ jsx(Label, { children: t("media.dialog.aspectRatio") }),
218
+ /* @__PURE__ */ jsx("div", { className: "mt-2 flex flex-wrap gap-2", children: ASPECT_CHOICES.map((choice) => /* @__PURE__ */ jsx(
219
+ Button,
220
+ {
221
+ type: "button",
222
+ variant: aspect === choice ? "default" : "outline",
223
+ size: "sm",
224
+ disabled: busy,
225
+ onClick: () => setAspect(choice),
226
+ children: choice
227
+ },
228
+ choice
229
+ )) })
230
+ ] }),
231
+ /* @__PURE__ */ jsxs("div", { children: [
232
+ /* @__PURE__ */ jsx(Label, { children: t("media.dialog.outputFormat") }),
233
+ /* @__PURE__ */ jsx("div", { className: "mt-2 flex flex-wrap gap-2", children: FORMAT_CHOICES.map((choice) => /* @__PURE__ */ jsx(
234
+ Button,
235
+ {
236
+ type: "button",
237
+ variant: formatChoice === choice ? "default" : "outline",
238
+ size: "sm",
239
+ disabled: busy,
240
+ onClick: () => {
241
+ setFormatChoice(choice);
242
+ setLosslessOverride(null);
243
+ },
244
+ children: choice
245
+ },
246
+ choice
247
+ )) })
248
+ ] }),
249
+ showLosslessToggle && /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm", children: [
250
+ /* @__PURE__ */ jsx(
251
+ "input",
252
+ {
253
+ type: "checkbox",
254
+ checked: lossless,
255
+ disabled: busy,
256
+ onChange: (e) => setLosslessOverride(e.target.checked)
257
+ }
258
+ ),
259
+ /* @__PURE__ */ jsx("span", { children: t("media.dialog.losslessWebp") })
260
+ ] }),
261
+ showQualitySlider && /* @__PURE__ */ jsxs("div", { children: [
262
+ /* @__PURE__ */ jsx(Label, { children: t("media.dialog.quality", { value: Math.round(quality * 100) }) }),
263
+ /* @__PURE__ */ jsx(
264
+ "input",
265
+ {
266
+ type: "range",
267
+ min: 50,
268
+ max: 100,
269
+ step: 1,
270
+ disabled: busy,
271
+ value: Math.round(quality * 100),
272
+ onChange: (e) => setQuality(Number(e.target.value) / 100),
273
+ className: "mt-2 w-full"
274
+ }
275
+ )
276
+ ] }),
277
+ /* @__PURE__ */ jsxs("div", { className: "max-w-xs", children: [
278
+ /* @__PURE__ */ jsx(Label, { htmlFor: "maxDimension", children: t("media.dialog.maxDimension") }),
279
+ /* @__PURE__ */ jsx("div", { className: "mt-2 flex flex-wrap gap-2", children: MAX_DIMENSION_PRESETS.map((preset) => /* @__PURE__ */ jsx(
280
+ Button,
281
+ {
282
+ type: "button",
283
+ variant: maxDimension === preset ? "default" : "outline",
284
+ size: "sm",
285
+ disabled: busy,
286
+ onClick: () => setMaxDimension(preset),
287
+ children: preset
288
+ },
289
+ preset
290
+ )) }),
291
+ /* @__PURE__ */ jsx(
292
+ Input,
293
+ {
294
+ id: "maxDimension",
295
+ type: "number",
296
+ className: "mt-2",
297
+ min: MIN_DIMENSION,
298
+ max: MAX_DIMENSION_CEILING,
299
+ disabled: busy,
300
+ value: maxDimension,
301
+ onChange: (e) => setMaxDimension(Number(e.target.value) || defaultMaxDimension)
302
+ }
303
+ )
304
+ ] })
305
+ ] })
306
+ ] }),
307
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
308
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", type: "button", onClick: onCancel, children: t("media.dialog.cancelAll") }),
309
+ /* @__PURE__ */ jsx(Button, { variant: "outline", type: "button", disabled: busy, onClick: onSkip, children: t("media.dialog.skip") }),
310
+ /* @__PURE__ */ jsx(Button, { type: "button", disabled: busy, onClick: handleConfirm, children: busy ? t("media.dialog.uploadingButton") : t("media.dialog.upload") })
311
+ ] })
312
+ ] })
313
+ }
314
+ );
315
+ }
316
+ function formatBytes(bytes) {
317
+ if (bytes < 1024) return `${bytes} B`;
318
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
319
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
320
+ }
321
+
322
+ // src/lib/admin-config-client.ts
323
+ var cmsConfig = null;
324
+ function setAdminCmsConfigClient(config) {
325
+ cmsConfig = config;
326
+ }
327
+ function getMediaProcessingDefaults() {
328
+ return cmsConfig?.media?.processing;
329
+ }
330
+
331
+ export {
332
+ ImageUploadDialog,
333
+ setAdminCmsConfigClient,
334
+ getMediaProcessingDefaults
335
+ };
@@ -0,0 +1,149 @@
1
+ 'use client';
2
+ import {
3
+ useT
4
+ } from "./chunk-XY4JWSMS.js";
5
+
6
+ // src/components/users-list-view.tsx
7
+ import { useEffect, useState } from "react";
8
+ import { generateClient } from "aws-amplify/api";
9
+ import {
10
+ Button,
11
+ Table,
12
+ TableBody,
13
+ TableCell,
14
+ TableHead,
15
+ TableHeader,
16
+ TableRow
17
+ } from "@ampless/runtime/ui";
18
+ import { jsx, jsxs } from "react/jsx-runtime";
19
+ function isAdminRole(value) {
20
+ return value === "admin" || value === "editor" || value === "none";
21
+ }
22
+ function UsersListView({ currentUserId }) {
23
+ const t = useT();
24
+ const [users, setUsers] = useState(null);
25
+ const [loading, setLoading] = useState(true);
26
+ const [loadError, setLoadError] = useState(null);
27
+ const [rows, setRows] = useState({});
28
+ useEffect(() => {
29
+ const client = generateClient();
30
+ client.queries.listAdminUsers().then(({ data, errors }) => {
31
+ if (errors && errors.length > 0) {
32
+ const msg = errors[0]?.message ?? "listAdminUsers failed";
33
+ console.error("[users-list-view] listAdminUsers errors:", errors);
34
+ setLoadError(msg);
35
+ return;
36
+ }
37
+ const list = data ?? [];
38
+ setUsers(list);
39
+ setRows(
40
+ Object.fromEntries(
41
+ list.map((u) => [u.userId, { selected: u.role, saving: false, error: null }])
42
+ )
43
+ );
44
+ }).catch((err) => {
45
+ console.error("[users-list-view] listAdminUsers threw:", err);
46
+ setLoadError(err instanceof Error ? err.message : String(err));
47
+ }).finally(() => setLoading(false));
48
+ }, []);
49
+ function updateRow(userId, patch) {
50
+ setRows((prev) => ({
51
+ ...prev,
52
+ [userId]: { ...prev[userId], ...patch }
53
+ }));
54
+ }
55
+ async function save(userId) {
56
+ const row = rows[userId];
57
+ if (!row) return;
58
+ updateRow(userId, { saving: true, error: null });
59
+ try {
60
+ const client = generateClient();
61
+ const { data, errors } = await client.mutations.setAdminUserRole({
62
+ userId,
63
+ role: row.selected
64
+ });
65
+ if (errors && errors.length > 0) {
66
+ const msg = errors[0]?.message ?? "setAdminUserRole failed";
67
+ console.error("[users-list-view] setAdminUserRole errors:", errors);
68
+ updateRow(userId, { saving: false, error: msg });
69
+ return;
70
+ }
71
+ if (data) {
72
+ setUsers(
73
+ (prev) => (prev ?? []).map((u) => u.userId === userId ? data : u)
74
+ );
75
+ updateRow(userId, { saving: false, selected: data.role, error: null });
76
+ } else {
77
+ updateRow(userId, { saving: false });
78
+ }
79
+ } catch (err) {
80
+ console.error("[users-list-view] setAdminUserRole threw:", err);
81
+ updateRow(userId, {
82
+ saving: false,
83
+ error: err instanceof Error ? err.message : String(err)
84
+ });
85
+ }
86
+ }
87
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-7xl p-4 md:p-8", children: [
88
+ /* @__PURE__ */ jsxs("div", { className: "mb-6 md:mb-8", children: [
89
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold md:text-3xl", children: t("users.list.title") }),
90
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: t("users.list.description") })
91
+ ] }),
92
+ loading ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("users.list.loading") }) : loadError ? /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive", children: [
93
+ t("users.list.error"),
94
+ ": ",
95
+ loadError
96
+ ] }) : !users || users.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("users.list.empty") }) : /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-md border", children: /* @__PURE__ */ jsxs(Table, { children: [
97
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
98
+ /* @__PURE__ */ jsx(TableHead, { children: t("users.list.columnEmail") }),
99
+ /* @__PURE__ */ jsx(TableHead, { children: t("users.list.columnRole") }),
100
+ /* @__PURE__ */ jsx(TableHead, { className: "w-[1%] whitespace-nowrap", children: t("users.list.columnActions") })
101
+ ] }) }),
102
+ /* @__PURE__ */ jsx(TableBody, { children: users.map((u) => {
103
+ const row = rows[u.userId];
104
+ if (!row) return null;
105
+ const isSelf = u.userId === currentUserId;
106
+ const dirty = row.selected !== u.role;
107
+ return /* @__PURE__ */ jsxs(TableRow, { children: [
108
+ /* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: u.email || u.userId }),
109
+ /* @__PURE__ */ jsxs(TableCell, { children: [
110
+ /* @__PURE__ */ jsxs(
111
+ "select",
112
+ {
113
+ className: "rounded-md border bg-background px-2 py-1.5 text-sm disabled:cursor-not-allowed disabled:opacity-60",
114
+ value: row.selected,
115
+ disabled: isSelf || row.saving,
116
+ onChange: (e) => {
117
+ const next = e.target.value;
118
+ if (isAdminRole(next)) {
119
+ updateRow(u.userId, { selected: next });
120
+ }
121
+ },
122
+ children: [
123
+ /* @__PURE__ */ jsx("option", { value: "admin", children: t("users.list.roleAdmin") }),
124
+ /* @__PURE__ */ jsx("option", { value: "editor", children: t("users.list.roleEditor") }),
125
+ /* @__PURE__ */ jsx("option", { value: "none", children: t("users.list.roleNone") })
126
+ ]
127
+ }
128
+ ),
129
+ isSelf && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t("users.list.cannotEditSelf") }),
130
+ row.error && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-destructive", children: row.error })
131
+ ] }),
132
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(
133
+ Button,
134
+ {
135
+ size: "sm",
136
+ disabled: isSelf || row.saving || !dirty,
137
+ onClick: () => save(u.userId),
138
+ children: row.saving ? t("users.list.saving") : t("users.list.save")
139
+ }
140
+ ) })
141
+ ] }, u.userId);
142
+ }) })
143
+ ] }) })
144
+ ] });
145
+ }
146
+
147
+ export {
148
+ UsersListView
149
+ };
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import {
3
+ ADMIN_SITE_COOKIE
4
+ } from "./chunk-7IR4F7GA.js";
5
+
6
+ // src/lib/admin-site-client.ts
7
+ import { DEFAULT_SITE_ID, isMultiSite } from "ampless";
8
+ var cmsConfig = null;
9
+ function setAdminCmsConfig(config) {
10
+ cmsConfig = config;
11
+ }
12
+ function readAdminSiteIdFromCookie() {
13
+ if (!cmsConfig) return DEFAULT_SITE_ID;
14
+ if (!isMultiSite(cmsConfig)) return DEFAULT_SITE_ID;
15
+ const sites = cmsConfig.sites ?? {};
16
+ if (typeof document !== "undefined") {
17
+ const match = document.cookie.match(
18
+ new RegExp(`(?:^|;\\s*)${ADMIN_SITE_COOKIE}=([^;]+)`)
19
+ );
20
+ if (match) {
21
+ const v = decodeURIComponent(match[1]);
22
+ if (sites[v]) return v;
23
+ }
24
+ }
25
+ const first = Object.keys(sites)[0];
26
+ return first ?? DEFAULT_SITE_ID;
27
+ }
28
+
29
+ export {
30
+ setAdminCmsConfig,
31
+ readAdminSiteIdFromCookie
32
+ };
@@ -0,0 +1,10 @@
1
+ 'use server';
2
+ // src/lib/theme-actions.ts
3
+ import { updateTag } from "next/cache";
4
+ async function invalidateSiteSettingsCache(siteId) {
5
+ updateTag(`site-settings:${siteId}`);
6
+ }
7
+
8
+ export {
9
+ invalidateSiteSettingsCache
10
+ };