@ampless/admin 1.0.0-alpha.82 → 1.0.0-alpha.84

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 (36) hide show
  1. package/dist/api/index.d.ts +58 -2
  2. package/dist/api/index.js +213 -1
  3. package/dist/{chunk-TBNETGQL.js → chunk-7NQERXNY.js} +1 -1
  4. package/dist/{chunk-7AGYC7JB.js → chunk-BRTC5O3E.js} +15 -4
  5. package/dist/{chunk-MPZ3GVZC.js → chunk-CFIOUXVE.js} +16 -6
  6. package/dist/{chunk-IR4EB5C3.js → chunk-GGHN2RKQ.js} +1 -1
  7. package/dist/{chunk-DDAGBC4N.js → chunk-HN3UFHP7.js} +1 -1
  8. package/dist/chunk-J6R356H3.js +183 -0
  9. package/dist/{chunk-DUSEHGSV.js → chunk-K44QF3MV.js} +1 -1
  10. package/dist/{chunk-HOJJIABL.js → chunk-MKWYNK4J.js} +2 -2
  11. package/dist/{chunk-7TAT4AHW.js → chunk-OFPNBZKP.js} +36 -2
  12. package/dist/{chunk-PHV2FQFV.js → chunk-SMI7Y77B.js} +1 -1
  13. package/dist/{chunk-DSCNWIBN.js → chunk-SQ6LFPXH.js} +2 -2
  14. package/dist/{chunk-4QTNVVKD.js → chunk-V6JNTCPS.js} +2 -2
  15. package/dist/{chunk-NI4JSVXL.js → chunk-W4VHP2JW.js} +1 -1
  16. package/dist/{chunk-AB2VWBDC.js → chunk-WXIYV2PO.js} +45 -3
  17. package/dist/{chunk-OJTHDE7F.js → chunk-YU5MPQXB.js} +2 -2
  18. package/dist/components/admin-dashboard.js +3 -3
  19. package/dist/components/edit-post-view.js +5 -5
  20. package/dist/components/index.d.ts +1 -1
  21. package/dist/components/index.js +6 -6
  22. package/dist/components/login-view.js +3 -3
  23. package/dist/components/mcp-tokens-view.js +3 -3
  24. package/dist/components/media-view.js +5 -5
  25. package/dist/components/new-post-view.js +5 -5
  26. package/dist/components/plugin-settings-form.js +3 -3
  27. package/dist/components/posts-list-view.js +3 -3
  28. package/dist/components/users-list-view.js +3 -3
  29. package/dist/{i18n-DBNV_Fgf.d.ts → i18n-DA-aakRX.d.ts} +52 -1
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +1 -1
  32. package/dist/metafile-esm.json +1 -1
  33. package/dist/pages/index.d.ts +1 -1
  34. package/dist/pages/index.js +15 -15
  35. package/package.json +4 -4
  36. package/dist/chunk-LNX3I35F.js +0 -67
@@ -2,7 +2,7 @@ import { NextRequest } from 'next/server';
2
2
  import { Admin } from '../index.js';
3
3
  import 'ampless';
4
4
  import '@ampless/runtime';
5
- import '../i18n-DBNV_Fgf.js';
5
+ import '../i18n-DA-aakRX.js';
6
6
  import '@aws-amplify/adapter-nextjs';
7
7
 
8
8
  /**
@@ -34,4 +34,60 @@ declare function createMediaProxyRoute(admin: Admin): {
34
34
  }) => Promise<Response>;
35
35
  };
36
36
 
37
- export { createMediaProxyRoute };
37
+ /**
38
+ * Build the `POST /admin/preview` Route Handler.
39
+ *
40
+ * Client-side `<PostForm>` / `<PostHistoryPanel>` POST a draft Post to this
41
+ * endpoint while the preview tab is open; we render the body + page-level
42
+ * scripts via `ampless.renderBody` / `publicPostScriptsForPage` and return
43
+ * a **complete HTML document** that the admin shows in an iframe using
44
+ * `srcDoc`.
45
+ *
46
+ * ## Why a complete document (not a bare fragment)?
47
+ *
48
+ * An iframe loaded via `srcDoc` does NOT inherit the parent page's CSS.
49
+ * Classes on the iframe *element* (e.g. `prose prose-neutral`) style only
50
+ * the element's box model, not its content document. To get typography inside
51
+ * the preview we must inline the styles directly into the document we return.
52
+ * We inject two `<style>` blocks:
53
+ *
54
+ * 1. **Theme CSS vars**: `renderThemeCss(cssVars)` — the same `:root {…}`
55
+ * block the public root layout emits, so the live theme's colour tokens
56
+ * flow into the preview.
57
+ * 2. **`PREVIEW_BASE_CSS`**: a self-contained typography approximation
58
+ * (h1-h6, p, ul/ol/li, a, code, pre, blockquote, img, table, hr, dark
59
+ * mode). This is *not* the actual live theme stylesheet — Next.js App
60
+ * Router emits hashed CSS filenames that are unknowable from a Route
61
+ * Handler — but it's enough for a "readable preview".
62
+ *
63
+ * ## Why a Route Handler (not a Server Action)?
64
+ *
65
+ * Next.js 15+ refuses to compile Client Components that reach
66
+ * `react-dom/server` through a `'use server'` module, because the build
67
+ * traces the import graph from Client Components through Server Action
68
+ * modules. Putting this rendering behind a Route Handler decouples it from
69
+ * that graph entirely — the form fetches a plain HTTP endpoint and the
70
+ * bundler never walks from `<PostForm>` into here. The endpoint also gets an
71
+ * explicit `admin.isEditor()` gate so a future change to the `(admin)`
72
+ * route-group middleware can't silently turn preview into a content-leak
73
+ * vector for unpublished drafts.
74
+ *
75
+ * ## Dynamic import of `react-dom/server`
76
+ *
77
+ * The `react-dom/server` import itself is loaded via dynamic `import()` inside
78
+ * the handler. Next.js 16's Turbopack flags any top-level static
79
+ * `import 'react-dom/server'` reached from the app router build (Route
80
+ * Handlers included), so we deliberately defer the resolution to request time
81
+ * — the module is still pulled from the same Node.js subpath that `next start`
82
+ * ships, just not visible to the build-time import-graph walker.
83
+ *
84
+ * ## Auth
85
+ *
86
+ * Locked to authenticated editors. Anonymous + reader access is 403. This
87
+ * matches the rest of `/admin/**`, which is gated by the `(admin)` route
88
+ * group + middleware, but we add an explicit check here as defence-in-depth
89
+ * against the middleware gate being misconfigured.
90
+ */
91
+ declare function createPreviewRouteHandler(admin: Admin): (req: Request) => Promise<Response>;
92
+
93
+ export { createMediaProxyRoute, createPreviewRouteHandler };
package/dist/api/index.js CHANGED
@@ -55,6 +55,218 @@ async function signMediaUrl({
55
55
  return null;
56
56
  }
57
57
  }
58
+
59
+ // src/api/preview-route.tsx
60
+ import { renderThemeCss } from "@ampless/runtime";
61
+ import { Fragment, jsxs } from "react/jsx-runtime";
62
+ var PREVIEW_BASE_CSS = `
63
+ /* ampless admin preview \u2014 typography approximation */
64
+ *,
65
+ *::before,
66
+ *::after {
67
+ box-sizing: border-box;
68
+ }
69
+
70
+ body {
71
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
72
+ "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
73
+ font-size: 1rem;
74
+ line-height: 1.75;
75
+ color: #1a202c;
76
+ background: #fff;
77
+ margin: 0;
78
+ padding: 1.5rem;
79
+ max-width: 65ch;
80
+ }
81
+
82
+ main {
83
+ width: 100%;
84
+ }
85
+
86
+ /* Headings */
87
+ h1, h2, h3, h4, h5, h6 {
88
+ font-weight: 700;
89
+ line-height: 1.25;
90
+ margin-top: 1.5em;
91
+ margin-bottom: 0.5em;
92
+ }
93
+ h1 { font-size: 2.25em; margin-top: 0; }
94
+ h2 { font-size: 1.5em; }
95
+ h3 { font-size: 1.25em; }
96
+ h4 { font-size: 1em; }
97
+ h5 { font-size: 0.875em; }
98
+ h6 { font-size: 0.875em; font-weight: 600; color: #4a5568; }
99
+
100
+ /* Paragraph */
101
+ p {
102
+ margin-top: 1em;
103
+ margin-bottom: 1em;
104
+ }
105
+
106
+ /* Lists */
107
+ ul, ol {
108
+ padding-left: 1.625em;
109
+ margin-top: 1em;
110
+ margin-bottom: 1em;
111
+ }
112
+ ul { list-style-type: disc; }
113
+ ol { list-style-type: decimal; }
114
+ li {
115
+ margin-top: 0.25em;
116
+ margin-bottom: 0.25em;
117
+ }
118
+ li > ul, li > ol {
119
+ margin-top: 0.25em;
120
+ margin-bottom: 0.25em;
121
+ }
122
+
123
+ /* Links */
124
+ a {
125
+ color: #2563eb;
126
+ text-decoration: underline;
127
+ }
128
+ a:hover {
129
+ color: #1d4ed8;
130
+ }
131
+
132
+ /* Inline code */
133
+ code {
134
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
135
+ "Liberation Mono", "Courier New", monospace;
136
+ font-size: 0.875em;
137
+ background: #f1f5f9;
138
+ padding: 0.125em 0.375em;
139
+ border-radius: 0.25em;
140
+ }
141
+
142
+ /* Code block */
143
+ pre {
144
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
145
+ "Liberation Mono", "Courier New", monospace;
146
+ font-size: 0.875em;
147
+ background: #1e293b;
148
+ color: #e2e8f0;
149
+ padding: 1em 1.25em;
150
+ border-radius: 0.5em;
151
+ overflow-x: auto;
152
+ margin-top: 1.25em;
153
+ margin-bottom: 1.25em;
154
+ line-height: 1.7;
155
+ }
156
+ pre code {
157
+ background: transparent;
158
+ padding: 0;
159
+ font-size: inherit;
160
+ color: inherit;
161
+ border-radius: 0;
162
+ }
163
+
164
+ /* Blockquote */
165
+ blockquote {
166
+ border-left: 0.25rem solid #cbd5e1;
167
+ padding-left: 1em;
168
+ margin-left: 0;
169
+ margin-right: 0;
170
+ color: #64748b;
171
+ font-style: italic;
172
+ }
173
+
174
+ /* Horizontal rule */
175
+ hr {
176
+ border: none;
177
+ border-top: 1px solid #e2e8f0;
178
+ margin: 2em 0;
179
+ }
180
+
181
+ /* Images */
182
+ img {
183
+ max-width: 100%;
184
+ height: auto;
185
+ border-radius: 0.375em;
186
+ }
187
+
188
+ /* Tables */
189
+ table {
190
+ width: 100%;
191
+ border-collapse: collapse;
192
+ font-size: 0.875em;
193
+ margin-top: 1.25em;
194
+ margin-bottom: 1.25em;
195
+ }
196
+ th {
197
+ font-weight: 600;
198
+ text-align: left;
199
+ padding: 0.5em 0.75em;
200
+ border-bottom: 2px solid #e2e8f0;
201
+ }
202
+ td {
203
+ padding: 0.5em 0.75em;
204
+ border-bottom: 1px solid #e2e8f0;
205
+ }
206
+ tr:last-child td {
207
+ border-bottom: none;
208
+ }
209
+
210
+ /* Strong / em */
211
+ strong { font-weight: 700; }
212
+ em { font-style: italic; }
213
+
214
+ /* ------------------------------------------------------------------ */
215
+ /* Dark mode */
216
+ /* ------------------------------------------------------------------ */
217
+ @media (prefers-color-scheme: dark) {
218
+ body {
219
+ color: #e2e8f0;
220
+ background: #0f172a;
221
+ }
222
+ h6 { color: #94a3b8; }
223
+ a { color: #60a5fa; }
224
+ a:hover { color: #93c5fd; }
225
+ code { background: #1e293b; color: #e2e8f0; }
226
+ pre { background: #0f172a; border: 1px solid #334155; color: #e2e8f0; }
227
+ blockquote { border-left-color: #475569; color: #94a3b8; }
228
+ hr { border-top-color: #334155; }
229
+ th { border-bottom-color: #334155; }
230
+ td { border-bottom-color: #1e293b; }
231
+ }
232
+ `.trim();
233
+ function createPreviewRouteHandler(admin) {
234
+ return async function POST(req) {
235
+ const session = await admin.getServerSession();
236
+ if (!admin.isEditor(session)) {
237
+ return new Response("Forbidden", { status: 403 });
238
+ }
239
+ let draft;
240
+ try {
241
+ draft = await req.json();
242
+ } catch {
243
+ return new Response("Bad Request", { status: 400 });
244
+ }
245
+ const ampless = await admin.getAmpless();
246
+ const node = /* @__PURE__ */ jsxs(Fragment, { children: [
247
+ await ampless.renderBody(draft),
248
+ await ampless.publicPostScriptsForPage([draft])
249
+ ] });
250
+ const { renderToStaticMarkup } = await import("react-dom/server");
251
+ const fragment = renderToStaticMarkup(node);
252
+ let themeCssBlock = "";
253
+ try {
254
+ const themeConfig = await admin.loadThemeConfig();
255
+ const css = renderThemeCss(themeConfig.cssVars);
256
+ if (css) themeCssBlock = css;
257
+ } catch (err) {
258
+ console.warn("[ampless admin] preview theme CSS unavailable:", err);
259
+ }
260
+ const html = `<!doctype html><html><head><meta charset="utf-8">` + (themeCssBlock ? `<style>${themeCssBlock}</style>` : "") + `<style>${PREVIEW_BASE_CSS}</style></head><body class="ampless-preview"><main>${fragment}</main></body></html>`;
261
+ return new Response(html, {
262
+ headers: {
263
+ "Content-Type": "text/html; charset=utf-8",
264
+ "Cache-Control": "no-store"
265
+ }
266
+ });
267
+ };
268
+ }
58
269
  export {
59
- createMediaProxyRoute
270
+ createMediaProxyRoute,
271
+ createPreviewRouteHandler
60
272
  };
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import {
3
3
  useT
4
- } from "./chunk-DUSEHGSV.js";
4
+ } from "./chunk-K44QF3MV.js";
5
5
 
6
6
  // src/components/users-list-view.tsx
7
7
  import { useEffect, useState } from "react";
@@ -4,7 +4,7 @@ import {
4
4
  getAdminCmsConfig,
5
5
  getMediaProcessingDefaults,
6
6
  uploadProcessedImage
7
- } from "./chunk-DDAGBC4N.js";
7
+ } from "./chunk-HN3UFHP7.js";
8
8
  import {
9
9
  getAdminEditorExtensions,
10
10
  getAdminTiptapNodeHtml,
@@ -15,7 +15,7 @@ import {
15
15
  } from "./chunk-2ITWLRYF.js";
16
16
  import {
17
17
  useT
18
- } from "./chunk-DUSEHGSV.js";
18
+ } from "./chunk-K44QF3MV.js";
19
19
 
20
20
  // src/components/media-picker.tsx
21
21
  import { useEffect, useRef, useState } from "react";
@@ -839,13 +839,17 @@ function PostHistoryPanel({
839
839
  // this site. A stricter sandbox (= separate-origin preview
840
840
  // route + CSP / COEP / COOP) is parked for v2.0+ if/when
841
841
  // a real plugin marketplace lands.
842
+ // No `prose` classes here: classes on the iframe ELEMENT
843
+ // don't style its srcDoc contents. Typography comes from
844
+ // the full document the preview route now returns
845
+ // (PREVIEW_BASE_CSS + theme vars in createPreviewRouteHandler).
842
846
  /* @__PURE__ */ jsx6(
843
847
  "iframe",
844
848
  {
845
849
  title: "revision-preview",
846
850
  srcDoc: previewHtml,
847
851
  sandbox: "allow-scripts allow-same-origin",
848
- className: "prose prose-neutral dark:prose-invert max-w-none min-h-[300px] w-full rounded-md border text-sm"
852
+ className: "min-h-[300px] w-full rounded-md border"
849
853
  }
850
854
  )
851
855
  )
@@ -1738,13 +1742,20 @@ function PostForm({ post, previewEndpoint = "/admin/preview" }) {
1738
1742
  // trusted editors of this site. A stricter sandbox (= separate-
1739
1743
  // origin preview route + CSP / COEP / COOP) is parked for
1740
1744
  // v2.0+ if/when a real plugin marketplace lands.
1745
+ //
1746
+ // Typography note: `prose prose-neutral dark:prose-invert
1747
+ // max-w-none` were removed from the iframe element. Those classes
1748
+ // style the iframe *element* border-box only, not its content
1749
+ // document — an iframe srcDoc does not inherit the parent page's
1750
+ // CSS. Typography is now applied inside the document returned by
1751
+ // createPreviewRouteHandler (PREVIEW_BASE_CSS + theme cssVars).
1741
1752
  /* @__PURE__ */ jsx8(
1742
1753
  "iframe",
1743
1754
  {
1744
1755
  title: "post-preview",
1745
1756
  srcDoc: previewHtml,
1746
1757
  sandbox: "allow-scripts allow-same-origin",
1747
- className: "prose prose-neutral dark:prose-invert max-w-none min-h-[400px] w-full rounded-md border"
1758
+ className: "min-h-[400px] w-full rounded-md border"
1748
1759
  }
1749
1760
  )
1750
1761
  ),
@@ -1,43 +1,53 @@
1
1
  'use client';
2
2
  import {
3
3
  useT
4
- } from "./chunk-DUSEHGSV.js";
4
+ } from "./chunk-K44QF3MV.js";
5
5
 
6
6
  // src/components/admin-dashboard.tsx
7
7
  import { useEffect, useState } from "react";
8
8
  import Link from "next/link";
9
- import { listPosts } from "ampless";
9
+ import { listPostSummaries } from "ampless";
10
10
  import { Button, Card, CardContent, CardHeader, CardTitle } from "@ampless/runtime/ui";
11
11
  import { jsx, jsxs } from "react/jsx-runtime";
12
12
  function AdminDashboard() {
13
13
  const t = useT();
14
14
  const [posts, setPosts] = useState([]);
15
15
  const [loading, setLoading] = useState(true);
16
+ const [loadError, setLoadError] = useState(null);
16
17
  useEffect(() => {
17
- listPosts({ status: "all" }).then(setPosts).finally(() => setLoading(false));
18
+ listPostSummaries({ status: "all" }).then(setPosts).catch((err) => {
19
+ console.error("[admin-dashboard] listPostSummaries failed:", err);
20
+ setLoadError(err instanceof Error ? err.message : String(err));
21
+ }).finally(() => setLoading(false));
18
22
  }, []);
19
23
  const published = posts.filter((p) => p.status === "published").length;
20
24
  const drafts = posts.filter((p) => p.status === "draft").length;
25
+ const countPending = loading || Boolean(loadError);
21
26
  return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-7xl p-4 md:p-8", children: [
22
27
  /* @__PURE__ */ jsxs("div", { className: "mb-6 flex flex-wrap items-center justify-between gap-3 md:mb-8", children: [
23
28
  /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold md:text-3xl", children: t("dashboard.title") }),
24
29
  /* @__PURE__ */ jsx(Button, { asChild: true, children: /* @__PURE__ */ jsx(Link, { href: "/admin/posts/new", children: t("dashboard.newPost") }) })
25
30
  ] }),
31
+ loadError && /* @__PURE__ */ jsxs("p", { className: "mb-4 text-sm text-destructive", children: [
32
+ t("posts.list.loadError"),
33
+ ": ",
34
+ loadError
35
+ ] }),
26
36
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3", children: [
27
37
  /* @__PURE__ */ jsxs(Card, { children: [
28
38
  /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { children: t("dashboard.totalPosts") }) }),
29
39
  /* @__PURE__ */ jsxs(CardContent, { children: [
30
- /* @__PURE__ */ jsx("p", { className: "text-3xl font-bold", children: loading ? "\u2014" : posts.length }),
40
+ /* @__PURE__ */ jsx("p", { className: "text-3xl font-bold", children: countPending ? "-" : posts.length }),
31
41
  /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("dashboard.totalLabel") })
32
42
  ] })
33
43
  ] }),
34
44
  /* @__PURE__ */ jsxs(Card, { children: [
35
45
  /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { children: t("dashboard.published") }) }),
36
- /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("p", { className: "text-3xl font-bold", children: loading ? "\u2014" : published }) })
46
+ /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("p", { className: "text-3xl font-bold", children: countPending ? "-" : published }) })
37
47
  ] }),
38
48
  /* @__PURE__ */ jsxs(Card, { children: [
39
49
  /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { children: t("dashboard.drafts") }) }),
40
- /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("p", { className: "text-3xl font-bold", children: loading ? "\u2014" : drafts }) })
50
+ /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("p", { className: "text-3xl font-bold", children: countPending ? "-" : drafts }) })
41
51
  ] })
42
52
  ] })
43
53
  ] });
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import {
3
3
  useT
4
- } from "./chunk-DUSEHGSV.js";
4
+ } from "./chunk-K44QF3MV.js";
5
5
 
6
6
  // src/components/login-view.tsx
7
7
  import { useState } from "react";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-2ITWLRYF.js";
5
5
  import {
6
6
  useT
7
- } from "./chunk-DUSEHGSV.js";
7
+ } from "./chunk-K44QF3MV.js";
8
8
 
9
9
  // src/lib/upload.ts
10
10
  import { uploadData } from "aws-amplify/storage";
@@ -0,0 +1,183 @@
1
+ 'use client';
2
+ import {
3
+ useT
4
+ } from "./chunk-K44QF3MV.js";
5
+
6
+ // src/components/posts-list-view.tsx
7
+ import { useEffect, useMemo, useState } from "react";
8
+ import Link from "next/link";
9
+ import { formatDate, listPostSummaries } from "ampless";
10
+ import {
11
+ Button,
12
+ Input,
13
+ Table,
14
+ TableBody,
15
+ TableCell,
16
+ TableHead,
17
+ TableHeader,
18
+ TableRow
19
+ } from "@ampless/runtime/ui";
20
+ import {
21
+ collectTags,
22
+ filterSortPostSummaries
23
+ } from "ampless";
24
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
25
+ var PAGE_SIZE = 100;
26
+ function PostsList() {
27
+ const t = useT();
28
+ const [posts, setPosts] = useState([]);
29
+ const [loading, setLoading] = useState(true);
30
+ const [loadError, setLoadError] = useState(null);
31
+ const [query, setQuery] = useState("");
32
+ const [statusFilter, setStatusFilter] = useState("all");
33
+ const [tagFilter, setTagFilter] = useState("");
34
+ const [sort, setSort] = useState("updated-desc");
35
+ const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
36
+ useEffect(() => {
37
+ listPostSummaries({ status: "all" }).then(setPosts).catch((err) => {
38
+ console.error("[posts-list-view] listPostSummaries failed:", err);
39
+ setLoadError(err instanceof Error ? err.message : String(err));
40
+ }).finally(() => setLoading(false));
41
+ }, []);
42
+ useEffect(() => {
43
+ setVisibleCount(PAGE_SIZE);
44
+ }, [query, statusFilter, tagFilter, sort]);
45
+ const tagCounts = useMemo(() => collectTags(posts), [posts]);
46
+ const filteredPosts = useMemo(
47
+ () => filterSortPostSummaries(posts, {
48
+ query,
49
+ status: statusFilter,
50
+ tag: tagFilter,
51
+ sort
52
+ }),
53
+ [posts, query, statusFilter, tagFilter, sort]
54
+ );
55
+ const visiblePosts = filteredPosts.slice(0, visibleCount);
56
+ const hasMore = visibleCount < filteredPosts.length;
57
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-7xl p-4 md:p-8", children: [
58
+ /* @__PURE__ */ jsxs("div", { className: "mb-6 flex flex-wrap items-center justify-between gap-3 md:mb-8", children: [
59
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold md:text-3xl", children: t("posts.list.title") }),
60
+ /* @__PURE__ */ jsx(Button, { asChild: true, children: /* @__PURE__ */ jsx(Link, { href: "/admin/posts/new", children: t("posts.list.newButton") }) })
61
+ ] }),
62
+ loading ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("common.loading") }) : loadError ? /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive", children: [
63
+ t("posts.list.loadError"),
64
+ ": ",
65
+ loadError
66
+ ] }) : posts.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-md border p-12 text-center", children: [
67
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("posts.list.empty") }),
68
+ /* @__PURE__ */ jsx(Button, { asChild: true, className: "mt-4", children: /* @__PURE__ */ jsx(Link, { href: "/admin/posts/new", children: t("posts.list.createFirst") }) })
69
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
70
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
71
+ /* @__PURE__ */ jsx(
72
+ Input,
73
+ {
74
+ className: "min-w-56 max-w-sm",
75
+ value: query,
76
+ onChange: (e) => setQuery(e.target.value),
77
+ placeholder: t("posts.list.searchPlaceholder")
78
+ }
79
+ ),
80
+ /* @__PURE__ */ jsxs(
81
+ "select",
82
+ {
83
+ className: "h-9 rounded-md border bg-background px-2 py-1.5 text-sm",
84
+ value: statusFilter,
85
+ "aria-label": t("common.status"),
86
+ onChange: (e) => setStatusFilter(e.target.value),
87
+ children: [
88
+ /* @__PURE__ */ jsx("option", { value: "all", children: t("posts.list.filterAll") }),
89
+ /* @__PURE__ */ jsx("option", { value: "published", children: t("posts.list.filterPublished") }),
90
+ /* @__PURE__ */ jsx("option", { value: "draft", children: t("posts.list.filterDraft") })
91
+ ]
92
+ }
93
+ ),
94
+ /* @__PURE__ */ jsxs(
95
+ "select",
96
+ {
97
+ className: "h-9 rounded-md border bg-background px-2 py-1.5 text-sm",
98
+ value: tagFilter,
99
+ "aria-label": t("common.tags"),
100
+ onChange: (e) => setTagFilter(e.target.value),
101
+ children: [
102
+ /* @__PURE__ */ jsx("option", { value: "", children: t("posts.list.filterAllTags") }),
103
+ [...tagCounts.entries()].map(([tag, count]) => /* @__PURE__ */ jsxs("option", { value: tag, children: [
104
+ tag,
105
+ " (",
106
+ count,
107
+ ")"
108
+ ] }, tag))
109
+ ]
110
+ }
111
+ ),
112
+ /* @__PURE__ */ jsxs(
113
+ "select",
114
+ {
115
+ className: "h-9 rounded-md border bg-background px-2 py-1.5 text-sm",
116
+ value: sort,
117
+ "aria-label": t("posts.list.sortLabel"),
118
+ onChange: (e) => setSort(e.target.value),
119
+ children: [
120
+ /* @__PURE__ */ jsx("option", { value: "updated-desc", children: t("posts.list.sortUpdatedDesc") }),
121
+ /* @__PURE__ */ jsx("option", { value: "updated-asc", children: t("posts.list.sortUpdatedAsc") }),
122
+ /* @__PURE__ */ jsx("option", { value: "published-desc", children: t("posts.list.sortPublishedDesc") }),
123
+ /* @__PURE__ */ jsx("option", { value: "published-asc", children: t("posts.list.sortPublishedAsc") }),
124
+ /* @__PURE__ */ jsx("option", { value: "title-asc", children: t("posts.list.sortTitleAsc") }),
125
+ /* @__PURE__ */ jsx("option", { value: "title-desc", children: t("posts.list.sortTitleDesc") })
126
+ ]
127
+ }
128
+ ),
129
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: t("posts.list.resultCount", {
130
+ shown: filteredPosts.length,
131
+ total: posts.length
132
+ }) })
133
+ ] }),
134
+ filteredPosts.length === 0 ? /* @__PURE__ */ jsx("p", { className: "rounded-md border p-8 text-center text-muted-foreground", children: t("posts.list.noMatches") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
135
+ /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-md border", children: /* @__PURE__ */ jsxs(Table, { children: [
136
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
137
+ /* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnTitle") }),
138
+ /* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnStatus") }),
139
+ /* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnSlug") }),
140
+ /* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnPublished") }),
141
+ /* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnUpdated") })
142
+ ] }) }),
143
+ /* @__PURE__ */ jsx(TableBody, { children: visiblePosts.map((post) => /* @__PURE__ */ jsxs(TableRow, { children: [
144
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(
145
+ Link,
146
+ {
147
+ href: `/admin/posts/${post.postId}`,
148
+ className: "font-medium hover:underline",
149
+ children: post.title
150
+ }
151
+ ) }),
152
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(
153
+ "span",
154
+ {
155
+ className: post.status === "published" ? "inline-block rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-700" : "inline-block rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-700",
156
+ children: t(`common.${post.status}`)
157
+ }
158
+ ) }),
159
+ /* @__PURE__ */ jsx(TableCell, { className: "font-mono text-xs text-muted-foreground", children: post.slug }),
160
+ /* @__PURE__ */ jsx(TableCell, { className: "whitespace-nowrap text-sm text-muted-foreground", children: formatPostDate(post.publishedAt) }),
161
+ /* @__PURE__ */ jsx(TableCell, { className: "whitespace-nowrap text-sm text-muted-foreground", children: formatPostDate(post.updatedAt) })
162
+ ] }, post.postId)) })
163
+ ] }) }),
164
+ hasMore && /* @__PURE__ */ jsx(
165
+ Button,
166
+ {
167
+ type: "button",
168
+ variant: "outline",
169
+ onClick: () => setVisibleCount((n) => n + PAGE_SIZE),
170
+ children: t("posts.list.showMore")
171
+ }
172
+ )
173
+ ] })
174
+ ] })
175
+ ] });
176
+ }
177
+ function formatPostDate(value) {
178
+ return value ? formatDate(value) || "-" : "-";
179
+ }
180
+
181
+ export {
182
+ PostsList
183
+ };
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import {
3
3
  translate
4
- } from "./chunk-7TAT4AHW.js";
4
+ } from "./chunk-OFPNBZKP.js";
5
5
 
6
6
  // src/components/i18n-provider.tsx
7
7
  import { createContext, useContext, useMemo } from "react";
@@ -1,10 +1,10 @@
1
1
  'use client';
2
2
  import {
3
3
  MediaUploader
4
- } from "./chunk-DSCNWIBN.js";
4
+ } from "./chunk-SQ6LFPXH.js";
5
5
  import {
6
6
  useT
7
- } from "./chunk-DUSEHGSV.js";
7
+ } from "./chunk-K44QF3MV.js";
8
8
 
9
9
  // src/components/media-view.tsx
10
10
  import { jsx, jsxs } from "react/jsx-runtime";