@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,335 @@
1
+ 'use client';
2
+ import {
3
+ useT
4
+ } from "./chunk-Q66BLMNJ.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
+ };