@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,21 @@
1
+ 'use client';
2
+ import {
3
+ MediaUploader
4
+ } from "./chunk-6NPYUTV6.js";
5
+ import {
6
+ useT
7
+ } from "./chunk-Q66BLMNJ.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,67 @@
1
+ 'use client';
2
+ import {
3
+ useT
4
+ } from "./chunk-Q66BLMNJ.js";
5
+
6
+ // src/components/posts-list-view.tsx
7
+ import { useEffect, useState } from "react";
8
+ import Link from "next/link";
9
+ import { listPosts } from "ampless";
10
+ import {
11
+ Button,
12
+ Table,
13
+ TableBody,
14
+ TableCell,
15
+ TableHead,
16
+ TableHeader,
17
+ TableRow
18
+ } from "@ampless/runtime/ui";
19
+ import { jsx, jsxs } from "react/jsx-runtime";
20
+ function PostsList() {
21
+ const t = useT();
22
+ const [posts, setPosts] = useState([]);
23
+ const [loading, setLoading] = useState(true);
24
+ useEffect(() => {
25
+ listPosts({ status: "all" }).then(setPosts).finally(() => setLoading(false));
26
+ }, []);
27
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-7xl p-4 md:p-8", children: [
28
+ /* @__PURE__ */ jsxs("div", { className: "mb-6 flex flex-wrap items-center justify-between gap-3 md:mb-8", children: [
29
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold md:text-3xl", children: t("posts.list.title") }),
30
+ /* @__PURE__ */ jsx(Button, { asChild: true, children: /* @__PURE__ */ jsx(Link, { href: "/admin/posts/new", children: t("posts.list.newButton") }) })
31
+ ] }),
32
+ loading ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("common.loading") }) : posts.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-md border p-12 text-center", children: [
33
+ /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("posts.list.empty") }),
34
+ /* @__PURE__ */ jsx(Button, { asChild: true, className: "mt-4", children: /* @__PURE__ */ jsx(Link, { href: "/admin/posts/new", children: t("posts.list.createFirst") }) })
35
+ ] }) : /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-md border", children: /* @__PURE__ */ jsxs(Table, { children: [
36
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
37
+ /* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnTitle") }),
38
+ /* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnStatus") }),
39
+ /* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnSlug") }),
40
+ /* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnUpdated") })
41
+ ] }) }),
42
+ /* @__PURE__ */ jsx(TableBody, { children: posts.map((post) => /* @__PURE__ */ jsxs(TableRow, { children: [
43
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(
44
+ Link,
45
+ {
46
+ href: `/admin/posts/${post.postId}`,
47
+ className: "font-medium hover:underline",
48
+ children: post.title
49
+ }
50
+ ) }),
51
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(
52
+ "span",
53
+ {
54
+ 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",
55
+ children: t(`common.${post.status}`)
56
+ }
57
+ ) }),
58
+ /* @__PURE__ */ jsx(TableCell, { className: "font-mono text-xs text-muted-foreground", children: post.slug }),
59
+ /* @__PURE__ */ jsx(TableCell, { className: "text-sm text-muted-foreground", children: post.publishedAt ? new Date(post.publishedAt).toLocaleDateString() : "\u2014" })
60
+ ] }, post.postId)) })
61
+ ] }) })
62
+ ] });
63
+ }
64
+
65
+ export {
66
+ PostsList
67
+ };
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+ import {
3
+ translate
4
+ } from "./chunk-WL4IBW2D.js";
5
+
6
+ // src/components/i18n-provider.tsx
7
+ import { createContext, useContext, useMemo } from "react";
8
+ import { jsx } from "react/jsx-runtime";
9
+ var I18nContext = createContext(null);
10
+ function I18nProvider({ locale, dict, children }) {
11
+ const value = useMemo(() => ({ locale, dict }), [locale, dict]);
12
+ return /* @__PURE__ */ jsx(I18nContext.Provider, { value, children });
13
+ }
14
+ function useT() {
15
+ const ctx = useContext(I18nContext);
16
+ if (!ctx) {
17
+ throw new Error(
18
+ "useT() called outside <I18nProvider>. Wrap the admin layout (or root layout) with <I18nProvider locale={...} dict={...}>."
19
+ );
20
+ }
21
+ return (key, vars) => translate(ctx.dict, key, vars);
22
+ }
23
+ function useLocale() {
24
+ const ctx = useContext(I18nContext);
25
+ if (!ctx) throw new Error("useLocale() called outside <I18nProvider>.");
26
+ return ctx.locale;
27
+ }
28
+
29
+ export {
30
+ I18nProvider,
31
+ useT,
32
+ useLocale
33
+ };
@@ -0,0 +1,149 @@
1
+ 'use client';
2
+ import {
3
+ useT
4
+ } from "./chunk-Q66BLMNJ.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,21 @@
1
+ 'use client';
2
+ import {
3
+ PostForm
4
+ } from "./chunk-G4CF5ZWV.js";
5
+ import {
6
+ useT
7
+ } from "./chunk-Q66BLMNJ.js";
8
+
9
+ // src/components/new-post-view.tsx
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ function NewPostPage() {
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("posts.form.newTitle") }),
15
+ /* @__PURE__ */ jsx(PostForm, {})
16
+ ] });
17
+ }
18
+
19
+ export {
20
+ NewPostPage
21
+ };
@@ -0,0 +1,334 @@
1
+ 'use client';
2
+ import {
3
+ useT
4
+ } from "./chunk-Q66BLMNJ.js";
5
+
6
+ // src/components/mcp-tokens-view.tsx
7
+ import { useEffect, useState } from "react";
8
+ import {
9
+ Button,
10
+ Table,
11
+ TableBody,
12
+ TableCell,
13
+ TableHead,
14
+ TableHeader,
15
+ TableRow
16
+ } from "@ampless/runtime/ui";
17
+
18
+ // src/lib/mcp-token-format.ts
19
+ import { randomBytes, createHash } from "crypto";
20
+ function generateToken() {
21
+ const random = toBase64Url(randomBytes(32));
22
+ const plain = `amk_${random}`;
23
+ return {
24
+ plain,
25
+ hash: hashToken(plain),
26
+ prefix: plain.slice(0, 8)
27
+ };
28
+ }
29
+ function hashToken(plain) {
30
+ return createHash("sha256").update(plain).digest("hex");
31
+ }
32
+ function toBase64Url(bytes) {
33
+ return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
34
+ }
35
+
36
+ // src/lib/mcp-token-storage.ts
37
+ import { getKvStore } from "ampless";
38
+ var TOKENS_PK = "mcp-tokens";
39
+ async function listTokens() {
40
+ const items = await getKvStore().query(TOKENS_PK);
41
+ return items.map((item) => item.value);
42
+ }
43
+ async function findByHash(hash) {
44
+ return await getKvStore().get(TOKENS_PK, hash);
45
+ }
46
+ async function createToken(meta) {
47
+ const full = {
48
+ ...meta,
49
+ lastUsedAt: null,
50
+ revokedAt: null
51
+ };
52
+ await getKvStore().put(TOKENS_PK, meta.hash, full);
53
+ return full;
54
+ }
55
+ async function revokeToken(hash) {
56
+ const existing = await findByHash(hash);
57
+ if (!existing) return;
58
+ if (existing.revokedAt) return;
59
+ await getKvStore().put(TOKENS_PK, hash, {
60
+ ...existing,
61
+ revokedAt: (/* @__PURE__ */ new Date()).toISOString()
62
+ });
63
+ }
64
+
65
+ // src/components/mcp-tokens-view.tsx
66
+ import { jsx, jsxs } from "react/jsx-runtime";
67
+ function tokenStatus(tok) {
68
+ if (tok.revokedAt) return "revoked";
69
+ if (tok.expiresAt && new Date(tok.expiresAt) < /* @__PURE__ */ new Date()) return "expired";
70
+ return "active";
71
+ }
72
+ function relativeTime(iso) {
73
+ if (!iso) return "";
74
+ const ms = Date.now() - new Date(iso).getTime();
75
+ const s = Math.floor(ms / 1e3);
76
+ if (s < 60) return `${s}s ago`;
77
+ const m = Math.floor(s / 60);
78
+ if (m < 60) return `${m}m ago`;
79
+ const h = Math.floor(m / 60);
80
+ if (h < 24) return `${h}h ago`;
81
+ const d = Math.floor(h / 24);
82
+ return `${d}d ago`;
83
+ }
84
+ function McpTokensView({ currentUserId, currentUserEmail, mcpEndpoint }) {
85
+ const t = useT();
86
+ const [endpointCopied, setEndpointCopied] = useState(false);
87
+ async function copyEndpoint() {
88
+ if (!mcpEndpoint) return;
89
+ try {
90
+ await navigator.clipboard.writeText(mcpEndpoint);
91
+ setEndpointCopied(true);
92
+ setTimeout(() => setEndpointCopied(false), 2e3);
93
+ } catch (err) {
94
+ console.error("[mcp-tokens-view] copy endpoint failed", err);
95
+ }
96
+ }
97
+ const [tokens, setTokens] = useState(null);
98
+ const [loading, setLoading] = useState(true);
99
+ const [loadError, setLoadError] = useState(null);
100
+ const [showCreateModal, setShowCreateModal] = useState(false);
101
+ const [expPreset, setExpPreset] = useState("never");
102
+ const [customDate, setCustomDate] = useState("");
103
+ const [creating, setCreating] = useState(false);
104
+ const [createError, setCreateError] = useState(null);
105
+ const [revealedPlain, setRevealedPlain] = useState(null);
106
+ const [copied, setCopied] = useState(false);
107
+ async function loadTokens() {
108
+ setLoading(true);
109
+ setLoadError(null);
110
+ try {
111
+ const list = await listTokens();
112
+ setTokens(list);
113
+ } catch (err) {
114
+ console.error("[mcp-tokens-view] listTokens failed", err);
115
+ setLoadError(err instanceof Error ? err.message : String(err));
116
+ } finally {
117
+ setLoading(false);
118
+ }
119
+ }
120
+ useEffect(() => {
121
+ void loadTokens();
122
+ }, []);
123
+ function openCreateModal() {
124
+ setExpPreset("never");
125
+ setCustomDate("");
126
+ setCreateError(null);
127
+ setShowCreateModal(true);
128
+ }
129
+ function expiresAtFromPreset() {
130
+ if (expPreset === "never") return null;
131
+ if (expPreset === "30days") {
132
+ const d = /* @__PURE__ */ new Date();
133
+ d.setDate(d.getDate() + 30);
134
+ return d.toISOString();
135
+ }
136
+ if (expPreset === "90days") {
137
+ const d = /* @__PURE__ */ new Date();
138
+ d.setDate(d.getDate() + 90);
139
+ return d.toISOString();
140
+ }
141
+ if (expPreset === "custom" && customDate) {
142
+ return new Date(customDate).toISOString();
143
+ }
144
+ return null;
145
+ }
146
+ async function handleIssue() {
147
+ setCreating(true);
148
+ setCreateError(null);
149
+ try {
150
+ const { plain, hash, prefix } = generateToken();
151
+ const meta = await createToken({
152
+ hash,
153
+ prefix,
154
+ createdBy: currentUserId,
155
+ createdByEmail: currentUserEmail,
156
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
157
+ expiresAt: expiresAtFromPreset()
158
+ });
159
+ void meta;
160
+ setShowCreateModal(false);
161
+ setRevealedPlain(plain);
162
+ void loadTokens();
163
+ } catch (err) {
164
+ console.error("[mcp-tokens-view] createToken failed", err);
165
+ setCreateError(err instanceof Error ? err.message : String(err));
166
+ } finally {
167
+ setCreating(false);
168
+ }
169
+ }
170
+ async function handleRevoke(hash) {
171
+ if (!confirm(t("mcpTokens.revokeConfirm"))) return;
172
+ try {
173
+ await revokeToken(hash);
174
+ void loadTokens();
175
+ } catch (err) {
176
+ console.error("[mcp-tokens-view] revokeToken failed", err);
177
+ alert(err instanceof Error ? err.message : String(err));
178
+ }
179
+ }
180
+ async function copyToClipboard() {
181
+ if (!revealedPlain) return;
182
+ try {
183
+ await navigator.clipboard.writeText(revealedPlain);
184
+ setCopied(true);
185
+ setTimeout(() => setCopied(false), 1500);
186
+ } catch (err) {
187
+ console.warn("[mcp-tokens-view] clipboard write failed", err);
188
+ }
189
+ }
190
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-4xl space-y-8 p-4 md:p-8", children: [
191
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4", children: [
192
+ /* @__PURE__ */ jsxs("div", { children: [
193
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold md:text-3xl", children: t("mcpTokens.title") }),
194
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: t("mcpTokens.description") })
195
+ ] }),
196
+ /* @__PURE__ */ jsx(Button, { type: "button", onClick: openCreateModal, children: t("mcpTokens.createButton") })
197
+ ] }),
198
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md border bg-card px-4 py-3 text-sm", children: [
199
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: t("mcpTokens.endpointTitle") }),
200
+ mcpEndpoint ? /* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center gap-2", children: [
201
+ /* @__PURE__ */ jsx("code", { className: "flex-1 overflow-x-auto rounded border bg-muted px-2 py-1 font-mono text-xs", children: mcpEndpoint }),
202
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: copyEndpoint, children: endpointCopied ? t("mcpTokens.endpointCopied") : t("mcpTokens.endpointCopy") })
203
+ ] }) : /* @__PURE__ */ jsx("p", { className: "mt-1 text-muted-foreground", children: t("mcpTokens.endpointMissing") })
204
+ ] }),
205
+ /* @__PURE__ */ jsx("div", { className: "rounded-md border border-yellow-300 bg-yellow-50 px-4 py-3 text-sm text-yellow-800 dark:border-yellow-700 dark:bg-yellow-950 dark:text-yellow-200", children: t("mcpTokens.inertBanner") }),
206
+ /* @__PURE__ */ jsx("section", { className: "space-y-3", children: loading ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("mcpTokens.loading") }) : loadError ? /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive", children: [
207
+ t("mcpTokens.error"),
208
+ ": ",
209
+ loadError
210
+ ] }) : !tokens || tokens.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("mcpTokens.listEmpty") }) : /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-md border", children: /* @__PURE__ */ jsxs(Table, { children: [
211
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
212
+ /* @__PURE__ */ jsx(TableHead, { children: t("mcpTokens.columnPrefix") }),
213
+ /* @__PURE__ */ jsx(TableHead, { children: t("mcpTokens.columnCreated") }),
214
+ /* @__PURE__ */ jsx(TableHead, { children: t("mcpTokens.columnLastUsed") }),
215
+ /* @__PURE__ */ jsx(TableHead, { children: t("mcpTokens.columnStatus") }),
216
+ /* @__PURE__ */ jsx(TableHead, { children: t("common.actions") })
217
+ ] }) }),
218
+ /* @__PURE__ */ jsx(TableBody, { children: tokens.map((tok) => {
219
+ const status = tokenStatus(tok);
220
+ return /* @__PURE__ */ jsxs(TableRow, { children: [
221
+ /* @__PURE__ */ jsx(TableCell, { className: "font-mono text-xs", children: tok.prefix }),
222
+ /* @__PURE__ */ jsxs(TableCell, { className: "text-xs text-muted-foreground", children: [
223
+ /* @__PURE__ */ jsx("span", { title: tok.createdAt, children: relativeTime(tok.createdAt) }),
224
+ tok.createdByEmail && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-muted-foreground/70", children: [
225
+ "by ",
226
+ tok.createdByEmail
227
+ ] })
228
+ ] }),
229
+ /* @__PURE__ */ jsx(TableCell, { className: "text-xs text-muted-foreground", children: tok.lastUsedAt ? /* @__PURE__ */ jsx("span", { title: tok.lastUsedAt, children: relativeTime(tok.lastUsedAt) }) : t("mcpTokens.lastUsedNever") }),
230
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(
231
+ "span",
232
+ {
233
+ className: status === "active" ? "text-sm font-medium text-green-700 dark:text-green-400" : "text-sm text-muted-foreground",
234
+ children: status === "active" ? t("mcpTokens.statusActive") : status === "revoked" ? t("mcpTokens.statusRevoked") : t("mcpTokens.statusExpired")
235
+ }
236
+ ) }),
237
+ /* @__PURE__ */ jsx(TableCell, { children: status === "active" && /* @__PURE__ */ jsx(
238
+ Button,
239
+ {
240
+ type: "button",
241
+ variant: "outline",
242
+ size: "sm",
243
+ onClick: () => handleRevoke(tok.hash),
244
+ children: t("mcpTokens.revoke")
245
+ }
246
+ ) })
247
+ ] }, tok.hash);
248
+ }) })
249
+ ] }) }) }),
250
+ showCreateModal && /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-md rounded-md border bg-card p-6 shadow-lg", children: [
251
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: t("mcpTokens.createModalTitle") }),
252
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-4", children: [
253
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
254
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("mcpTokens.expirationLabel") }),
255
+ ["never", "30days", "90days", "custom"].map((preset) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm", children: [
256
+ /* @__PURE__ */ jsx(
257
+ "input",
258
+ {
259
+ type: "radio",
260
+ name: "mcp-expiration",
261
+ value: preset,
262
+ checked: expPreset === preset,
263
+ onChange: () => setExpPreset(preset)
264
+ }
265
+ ),
266
+ t(`mcpTokens.expiration${preset.charAt(0).toUpperCase() + preset.slice(1)}`)
267
+ ] }, preset)),
268
+ expPreset === "custom" && /* @__PURE__ */ jsx(
269
+ "input",
270
+ {
271
+ type: "date",
272
+ className: "mt-1 block rounded-md border bg-background px-2 py-1 text-sm",
273
+ value: customDate,
274
+ min: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
275
+ onChange: (e) => setCustomDate(e.target.value)
276
+ }
277
+ )
278
+ ] }),
279
+ createError && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: createError })
280
+ ] }),
281
+ /* @__PURE__ */ jsxs("div", { className: "mt-6 flex justify-end gap-2", children: [
282
+ /* @__PURE__ */ jsx(
283
+ Button,
284
+ {
285
+ type: "button",
286
+ variant: "outline",
287
+ onClick: () => setShowCreateModal(false),
288
+ children: t("common.cancel")
289
+ }
290
+ ),
291
+ /* @__PURE__ */ jsx(
292
+ Button,
293
+ {
294
+ type: "button",
295
+ disabled: creating || expPreset === "custom" && !customDate,
296
+ onClick: () => void handleIssue(),
297
+ children: creating ? t("mcpTokens.issuing") : t("mcpTokens.issueButton")
298
+ }
299
+ )
300
+ ] })
301
+ ] }) }),
302
+ revealedPlain && /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-2xl rounded-md border bg-card p-6 shadow-lg", children: [
303
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: t("mcpTokens.revealTitle") }),
304
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: t("mcpTokens.revealHint") }),
305
+ /* @__PURE__ */ jsx("div", { className: "mt-4 rounded-md border bg-muted/50 p-3", children: /* @__PURE__ */ jsx("code", { className: "block select-all break-all font-mono text-xs", children: revealedPlain }) }),
306
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [
307
+ /* @__PURE__ */ jsx(
308
+ Button,
309
+ {
310
+ type: "button",
311
+ variant: "outline",
312
+ onClick: () => void copyToClipboard(),
313
+ children: copied ? t("mcpTokens.copied") : t("mcpTokens.copy")
314
+ }
315
+ ),
316
+ /* @__PURE__ */ jsx(
317
+ Button,
318
+ {
319
+ type: "button",
320
+ onClick: () => {
321
+ setRevealedPlain(null);
322
+ setCopied(false);
323
+ },
324
+ children: t("mcpTokens.done")
325
+ }
326
+ )
327
+ ] })
328
+ ] }) })
329
+ ] });
330
+ }
331
+
332
+ export {
333
+ McpTokensView
334
+ };