@burdenoff/microfe-admin 2026.712.1 → 2026.731.1

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.
@@ -0,0 +1,116 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { Button as t } from "./ui.js";
3
+ import { useCallback as n, useRef as r, useState as i } from "react";
4
+ import { Bold as a, Code as o, Italic as s, Link as c, List as l } from "lucide-react";
5
+ import { jsx as u, jsxs as d } from "react/jsx-runtime";
6
+ import { Markdown as f } from "@burdenoff/fe-libs/shared/components";
7
+ //#region src/admin/components/MarkdownEditor.tsx
8
+ var p = "flex w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] focus-visible:ring-offset-2 disabled:opacity-50";
9
+ function m({ icon: e, label: n, onClick: r, disabled: i }) {
10
+ return /* @__PURE__ */ u(t, {
11
+ type: "button",
12
+ variant: "outline",
13
+ size: "sm",
14
+ onClick: r,
15
+ disabled: i,
16
+ "aria-label": n,
17
+ className: "h-8 w-8 p-0",
18
+ children: e
19
+ });
20
+ }
21
+ function h({ value: t, onChange: h, placeholder: g = "Write in markdown...", "aria-label": _, className: v, showToolbar: y = !0, description: b, disabled: x }) {
22
+ let [S, C] = i("write"), w = r(null), T = n((e, n = e) => {
23
+ let r = w.current;
24
+ if (!r) return;
25
+ let i = r.selectionStart ?? 0, a = r.selectionEnd ?? 0, o = t.slice(i, a) || "", s = `${e}${o}${n}`;
26
+ h(t.slice(0, i) + s + t.slice(a)), requestAnimationFrame(() => {
27
+ r.focus();
28
+ let t = i + e.length, a = o ? i + s.length - n.length : t;
29
+ r.setSelectionRange(t, a);
30
+ });
31
+ }, [h, t]), E = [
32
+ {
33
+ icon: /* @__PURE__ */ u(a, { className: "h-4 w-4" }),
34
+ label: "Bold",
35
+ action: () => T("**")
36
+ },
37
+ {
38
+ icon: /* @__PURE__ */ u(s, { className: "h-4 w-4" }),
39
+ label: "Italic",
40
+ action: () => T("_")
41
+ },
42
+ {
43
+ icon: /* @__PURE__ */ u(c, { className: "h-4 w-4" }),
44
+ label: "Link",
45
+ action: () => T("[", "](url)")
46
+ },
47
+ {
48
+ icon: /* @__PURE__ */ u(o, { className: "h-4 w-4" }),
49
+ label: "Inline code",
50
+ action: () => T("`")
51
+ },
52
+ {
53
+ icon: /* @__PURE__ */ u(l, { className: "h-4 w-4" }),
54
+ label: "List",
55
+ action: () => T("- ")
56
+ }
57
+ ];
58
+ return /* @__PURE__ */ d("div", {
59
+ className: e("space-y-2", v),
60
+ children: [
61
+ /* @__PURE__ */ d("div", {
62
+ className: "flex flex-wrap items-center justify-between gap-2",
63
+ children: [/* @__PURE__ */ d("div", {
64
+ className: "inline-flex rounded-md border border-border-default bg-bg-sunken p-1",
65
+ children: [/* @__PURE__ */ u("button", {
66
+ type: "button",
67
+ onClick: () => C("write"),
68
+ disabled: x,
69
+ className: e("rounded px-3 py-1 text-sm font-medium transition-colors", S === "write" ? "bg-bg-surface text-text-primary shadow-sm" : "text-text-secondary hover:text-text-primary"),
70
+ "aria-pressed": S === "write",
71
+ children: "Write"
72
+ }), /* @__PURE__ */ u("button", {
73
+ type: "button",
74
+ onClick: () => C("preview"),
75
+ disabled: x,
76
+ className: e("rounded px-3 py-1 text-sm font-medium transition-colors", S === "preview" ? "bg-bg-surface text-text-primary shadow-sm" : "text-text-secondary hover:text-text-primary"),
77
+ "aria-pressed": S === "preview",
78
+ children: "Preview"
79
+ })]
80
+ }), y && /* @__PURE__ */ u("div", {
81
+ className: "flex items-center gap-1",
82
+ children: E.map((e) => /* @__PURE__ */ u(m, {
83
+ icon: e.icon,
84
+ label: e.label,
85
+ onClick: e.action,
86
+ disabled: x
87
+ }, e.label))
88
+ })]
89
+ }),
90
+ b && /* @__PURE__ */ u("p", {
91
+ className: "text-xs text-text-secondary",
92
+ children: b
93
+ }),
94
+ S === "write" ? /* @__PURE__ */ u("textarea", {
95
+ ref: w,
96
+ "aria-label": _,
97
+ disabled: x,
98
+ className: e(p, "min-h-[16rem] resize-y font-mono text-xs"),
99
+ value: t,
100
+ onChange: (e) => h(e.target.value),
101
+ placeholder: g
102
+ }) : /* @__PURE__ */ u("div", {
103
+ className: e(p, "min-h-[16rem] overflow-auto bg-bg-sunken", !t.trim() && "text-text-secondary"),
104
+ "aria-label": _ ? `${_} preview` : "Preview",
105
+ children: t.trim() ? /* @__PURE__ */ u(f, {
106
+ className: "[&_p]:mb-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_a]:text-action-primary-bg [&_a]:underline [&_code]:rounded [&_code]:bg-bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:overflow-auto [&_pre]:rounded [&_pre]:bg-bg-muted [&_pre]:p-3",
107
+ children: t
108
+ }) : /* @__PURE__ */ u("span", { children: "Nothing to preview" })
109
+ })
110
+ ]
111
+ });
112
+ }
113
+ //#endregion
114
+ export { h as MarkdownEditor };
115
+
116
+ //# sourceMappingURL=MarkdownEditor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MarkdownEditor.js","names":[],"sources":["../../../src/admin/components/MarkdownEditor.tsx"],"sourcesContent":["import { useState, useRef, useCallback } from 'react';\nimport { Bold, Italic, Link, Code, List } from 'lucide-react';\nimport { Markdown } from '@burdenoff/fe-libs/shared/components';\nimport { cn } from '../utils/cn';\nimport { Button } from './ui';\n\ntype Tab = 'write' | 'preview';\n\ninterface MarkdownEditorProps {\n /** Current markdown value. */\n value: string;\n /** Change handler for the markdown source. */\n onChange: (value: string) => void;\n /** Optional placeholder for the write tab textarea. */\n placeholder?: string;\n /** Optional label/aria-label for the textarea. */\n 'aria-label'?: string;\n /** Additional classes for the root element. */\n className?: string;\n /** Whether to show the formatting toolbar. Default true. */\n showToolbar?: boolean;\n /** Additional description shown below the toolbar tabs. */\n description?: string;\n /** Whether the editor is disabled. */\n disabled?: boolean;\n}\n\nconst inputClass =\n 'flex w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] focus-visible:ring-offset-2 disabled:opacity-50';\n\ninterface ToolbarButtonProps {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n disabled?: boolean;\n}\n\nfunction ToolbarButton({ icon, label, onClick, disabled }: ToolbarButtonProps) {\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={onClick}\n disabled={disabled}\n aria-label={label}\n className=\"h-8 w-8 p-0\"\n >\n {icon}\n </Button>\n );\n}\n\n/**\n * Reusable markdown editor with live preview.\n *\n * Provides a write/preview tab pair, a small formatting toolbar, and renders\n * the preview using the shared `@burdenoff/fe-libs/shared/components/Markdown`\n * component (react-markdown + remark-gfm + rehype-sanitize).\n */\nexport function MarkdownEditor({\n value,\n onChange,\n placeholder = 'Write in markdown...',\n 'aria-label': ariaLabel,\n className,\n showToolbar = true,\n description,\n disabled,\n}: MarkdownEditorProps) {\n const [tab, setTab] = useState<Tab>('write');\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n const insertAround = useCallback(\n (before: string, after: string = before) => {\n const el = textareaRef.current;\n if (!el) return;\n\n const start = el.selectionStart ?? 0;\n const end = el.selectionEnd ?? 0;\n const selected = value.slice(start, end) || '';\n const replacement = `${before}${selected}${after}`;\n const nextValue = value.slice(0, start) + replacement + value.slice(end);\n\n onChange(nextValue);\n\n // Restore focus and select the wrapped text (or place cursor inside the\n // wrappers when nothing was selected).\n requestAnimationFrame(() => {\n el.focus();\n const cursorStart = start + before.length;\n const cursorEnd = selected ? start + replacement.length - after.length : cursorStart;\n el.setSelectionRange(cursorStart, cursorEnd);\n });\n },\n [onChange, value]\n );\n\n const toolbarItems = [\n { icon: <Bold className=\"h-4 w-4\" />, label: 'Bold', action: () => insertAround('**') },\n { icon: <Italic className=\"h-4 w-4\" />, label: 'Italic', action: () => insertAround('_') },\n {\n icon: <Link className=\"h-4 w-4\" />,\n label: 'Link',\n action: () => insertAround('[', '](url)'),\n },\n { icon: <Code className=\"h-4 w-4\" />, label: 'Inline code', action: () => insertAround('`') },\n { icon: <List className=\"h-4 w-4\" />, label: 'List', action: () => insertAround('- ') },\n ];\n\n return (\n <div className={cn('space-y-2', className)}>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"inline-flex rounded-md border border-border-default bg-bg-sunken p-1\">\n <button\n type=\"button\"\n onClick={() => setTab('write')}\n disabled={disabled}\n className={cn(\n 'rounded px-3 py-1 text-sm font-medium transition-colors',\n tab === 'write'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n )}\n aria-pressed={tab === 'write'}\n >\n Write\n </button>\n <button\n type=\"button\"\n onClick={() => setTab('preview')}\n disabled={disabled}\n className={cn(\n 'rounded px-3 py-1 text-sm font-medium transition-colors',\n tab === 'preview'\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n )}\n aria-pressed={tab === 'preview'}\n >\n Preview\n </button>\n </div>\n\n {showToolbar && (\n <div className=\"flex items-center gap-1\">\n {toolbarItems.map((item) => (\n <ToolbarButton\n key={item.label}\n icon={item.icon}\n label={item.label}\n onClick={item.action}\n disabled={disabled}\n />\n ))}\n </div>\n )}\n </div>\n\n {description && <p className=\"text-xs text-text-secondary\">{description}</p>}\n\n {tab === 'write' ? (\n <textarea\n ref={textareaRef}\n aria-label={ariaLabel}\n disabled={disabled}\n className={cn(inputClass, 'min-h-[16rem] resize-y font-mono text-xs')}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder={placeholder}\n />\n ) : (\n <div\n className={cn(\n inputClass,\n 'min-h-[16rem] overflow-auto bg-bg-sunken',\n !value.trim() && 'text-text-secondary'\n )}\n aria-label={ariaLabel ? `${ariaLabel} preview` : 'Preview'}\n >\n {value.trim() ? (\n <Markdown className=\"[&_p]:mb-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_a]:text-action-primary-bg [&_a]:underline [&_code]:rounded [&_code]:bg-bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:overflow-auto [&_pre]:rounded [&_pre]:bg-bg-muted [&_pre]:p-3\">\n {value}\n </Markdown>\n ) : (\n <span>Nothing to preview</span>\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -15,3 +15,4 @@ import "./ConfirmDialog.js";
15
15
  import "./RowActions.js";
16
16
  import "./AccessDenied.js";
17
17
  import "./PermissionGuard.js";
18
+ import "./MarkdownEditor.js";
@@ -2,21 +2,22 @@ import { useToast as e } from "../components/Toast.js";
2
2
  import { useAdminPermissions as t } from "../hooks/useAdminPermissions.js";
3
3
  import { Badge as n, Button as r, EmptyState as i, Input as a } from "../components/ui.js";
4
4
  import { PageLayout as ee } from "../components/PageLayout.js";
5
- import { formatDate as o } from "../utils/formatters.js";
6
- import { DataTable as te, Pagination as ne } from "../components/DataTable.js";
7
- import { FormRow as s, FormSection as c, FormSheet as re, FormSheetFooter as ie } from "../components/FormSheet.js";
8
- import { DeleteConfirmDialog as ae } from "../components/ConfirmDialog.js";
9
- import { RowActions as oe, createRowActions as se } from "../components/RowActions.js";
10
- import { PermissionGuard as ce } from "../components/PermissionGuard.js";
5
+ import { formatDate as te } from "../utils/formatters.js";
6
+ import { DataTable as ne, Pagination as re } from "../components/DataTable.js";
7
+ import { FormRow as o, FormSection as s, FormSheet as ie, FormSheetFooter as ae } from "../components/FormSheet.js";
8
+ import { DeleteConfirmDialog as oe } from "../components/ConfirmDialog.js";
9
+ import { RowActions as se, createRowActions as ce } from "../components/RowActions.js";
10
+ import { PermissionGuard as le } from "../components/PermissionGuard.js";
11
+ import { MarkdownEditor as ue } from "../components/MarkdownEditor.js";
11
12
  import "../components/index.js";
12
- import { useCallback as l, useEffect as u, useRef as le, useState as d } from "react";
13
- import { FileText as f, Plus as ue, Search as de } from "lucide-react";
14
- import { jsx as p, jsxs as m } from "react/jsx-runtime";
15
- import { EmphasisPanel as fe, PagePurpose as pe } from "@burdenoff/fe-libs/ui";
16
- import { gql as h } from "@apollo/client";
17
- import { useApolloClient as me } from "@apollo/client/react";
13
+ import { useCallback as c, useEffect as l, useRef as de, useState as u } from "react";
14
+ import { FileText as d, Plus as fe, Search as pe } from "lucide-react";
15
+ import { jsx as f, jsxs as p } from "react/jsx-runtime";
16
+ import { EmphasisPanel as me, PagePurpose as he } from "@burdenoff/fe-libs/ui";
17
+ import { gql as m } from "@apollo/client";
18
+ import { useApolloClient as ge } from "@apollo/client/react";
18
19
  //#region src/admin/pages/ContentPagesPage.tsx
19
- var g = 20, _ = [
20
+ var h = 20, g = [
20
21
  {
21
22
  value: "blog",
22
23
  label: "Blog",
@@ -32,11 +33,11 @@ var g = 20, _ = [
32
33
  label: "Changelog",
33
34
  category: "Changelog"
34
35
  }
35
- ], he = {
36
+ ], _ = {
36
37
  blog: "default",
37
38
  "case-study": "secondary",
38
39
  changelog: "outline"
39
- }, ge = h`
40
+ }, _e = m`
40
41
  query AdminContentListProducts($limit: Int, $offset: Int) {
41
42
  platformProducts(limit: $limit, offset: $offset) {
42
43
  items {
@@ -46,7 +47,7 @@ var g = 20, _ = [
46
47
  }
47
48
  }
48
49
  }
49
- `, _e = h`
50
+ `, ve = m`
50
51
  query AdminContentPages(
51
52
  $productId: String
52
53
  $filter: ContentPageFilterInput
@@ -76,25 +77,25 @@ var g = 20, _ = [
76
77
  total
77
78
  }
78
79
  }
79
- `, ve = h`
80
+ `, ye = m`
80
81
  mutation AdminCreateContentPage($input: CreateContentPageInput!) {
81
82
  createContentPage(input: $input) {
82
83
  id
83
84
  slug
84
85
  }
85
86
  }
86
- `, v = h`
87
+ `, be = m`
87
88
  mutation AdminUpdateContentPage($id: ID!, $input: UpdateContentPageInput!) {
88
89
  updateContentPage(id: $id, input: $input) {
89
90
  id
90
91
  slug
91
92
  }
92
93
  }
93
- `, ye = h`
94
+ `, xe = m`
94
95
  mutation AdminDeleteContentPage($id: ID!) {
95
96
  deleteContentPage(id: $id)
96
97
  }
97
- `, y = {
98
+ `, v = {
98
99
  productId: "",
99
100
  type: "blog",
100
101
  slug: "",
@@ -108,24 +109,24 @@ var g = 20, _ = [
108
109
  featured: !1,
109
110
  isPublished: !0
110
111
  };
111
- function be(e) {
112
+ function Se(e) {
112
113
  return e.includes("changelog") ? "changelog" : e.includes("case-study") ? "case-study" : "blog";
113
114
  }
114
- var b = "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50";
115
- function x() {
116
- let { canViewProducts: h, canManageProducts: x, isLoading: S } = t(), C = me(), w = e(), [T, xe] = d([]), [E, D] = d(""), [O, Se] = d(""), [k, A] = d(""), [j, Ce] = d(""), [M, N] = d(1), P = le(0);
117
- u(() => {
115
+ var y = "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50";
116
+ function b() {
117
+ let { canViewProducts: m, canManageProducts: b, isLoading: x } = t(), S = ge(), C = e(), [w, Ce] = u([]), [T, E] = u(""), [D, we] = u(""), [O, k] = u(""), [A, Te] = u(""), [j, M] = u(1), N = de(0);
118
+ l(() => {
118
119
  let e = setTimeout(() => {
119
- Ce(k), N(1);
120
+ Te(O), M(1);
120
121
  }, 300);
121
122
  return () => clearTimeout(e);
122
- }, [k]);
123
- let [F, we] = d([]), [I, Te] = d(0), [Ee, L] = d(!1), [R, z] = d(null), [De, B] = d(!1), [V, H] = d(null), [U, W] = d(y), [Oe, G] = d(!1), [K, q] = d(null), [J, Y] = d(!1), X = S || !h;
124
- u(() => {
123
+ }, [O]);
124
+ let [P, Ee] = u([]), [F, De] = u(0), [I, L] = u(!1), [R, z] = u(null), [Oe, B] = u(!1), [V, H] = u(null), [U, W] = u(v), [ke, G] = u(!1), [K, q] = u(null), [J, Y] = u(!1), X = x || !m;
125
+ l(() => {
125
126
  if (X) return;
126
127
  let e = !0;
127
- return C.query({
128
- query: ge,
128
+ return S.query({
129
+ query: _e,
129
130
  variables: {
130
131
  limit: 100,
131
132
  offset: 0
@@ -134,60 +135,60 @@ function x() {
134
135
  }).then((t) => {
135
136
  if (!e) return;
136
137
  let n = t.data?.platformProducts?.items ?? [];
137
- xe(n), D((e) => e || n[0]?.id || "");
138
+ Ce(n), E((e) => e || n[0]?.id || "");
138
139
  }).catch((t) => {
139
140
  e && z(t instanceof Error ? t.message : "Failed to load products");
140
141
  }), () => {
141
142
  e = !1;
142
143
  };
143
- }, [C, X]);
144
- let Z = l(async () => {
145
- if (X || !E) return;
146
- let e = ++P.current;
144
+ }, [S, X]);
145
+ let Z = c(async () => {
146
+ if (X || !T) return;
147
+ let e = ++N.current;
147
148
  L(!0);
148
149
  try {
149
- let t = await C.query({
150
- query: _e,
150
+ let t = await S.query({
151
+ query: ve,
151
152
  variables: {
152
- productId: E,
153
+ productId: T,
153
154
  filter: {
154
- ...j ? { search: j } : {},
155
- ...O ? { tag: O } : {}
155
+ ...A ? { search: A } : {},
156
+ ...D ? { tag: D } : {}
156
157
  },
157
- limit: g,
158
- offset: (M - 1) * g
158
+ limit: h,
159
+ offset: (j - 1) * h
159
160
  },
160
161
  fetchPolicy: "network-only"
161
162
  });
162
- if (e !== P.current) return;
163
- we(t.data?.contentPages?.items ?? []), Te(t.data?.contentPages?.total ?? 0), z(null);
163
+ if (e !== N.current) return;
164
+ Ee(t.data?.contentPages?.items ?? []), De(t.data?.contentPages?.total ?? 0), z(null);
164
165
  } catch (t) {
165
- if (e !== P.current) return;
166
+ if (e !== N.current) return;
166
167
  z(t instanceof Error ? t.message : "Failed to load content pages");
167
168
  } finally {
168
- e === P.current && L(!1);
169
+ e === N.current && L(!1);
169
170
  }
170
171
  }, [
171
- C,
172
+ S,
172
173
  X,
173
- E,
174
- j,
175
- O,
176
- M
174
+ T,
175
+ A,
176
+ D,
177
+ j
177
178
  ]);
178
- u(() => {
179
+ l(() => {
179
180
  Z();
180
181
  }, [Z]);
181
- let ke = Math.ceil(I / g), Ae = l(() => {
182
+ let Ae = Math.ceil(F / h), je = c(() => {
182
183
  H(null), W({
183
- ...y,
184
- productId: E,
185
- type: O || "blog"
184
+ ...v,
185
+ productId: T,
186
+ type: D || "blog"
186
187
  }), B(!0);
187
- }, [E, O]), je = l((e) => {
188
+ }, [T, D]), Me = c((e) => {
188
189
  H(e), W({
189
190
  productId: e.productId ?? "",
190
- type: be(e.tags),
191
+ type: Se(e.tags),
191
192
  slug: e.slug,
192
193
  title: e.title,
193
194
  excerpt: e.excerpt ?? "",
@@ -199,23 +200,23 @@ function x() {
199
200
  featured: e.featured,
200
201
  isPublished: e.isPublished
201
202
  }), B(!0);
202
- }, []), Me = l(async () => {
203
+ }, []), Ne = c(async () => {
203
204
  let e = [
204
205
  !U.title.trim() && "Title",
205
206
  !U.slug.trim() && "Slug",
206
207
  !U.content.trim() && "Content"
207
208
  ].filter(Boolean);
208
209
  if (e.length) {
209
- w.error(`${e.join(", ")} ${e.length > 1 ? "are" : "is"} required`);
210
+ C.error(`${e.join(", ")} ${e.length > 1 ? "are" : "is"} required`);
210
211
  return;
211
212
  }
212
213
  let t = U.thumbnail.trim();
213
214
  if (t && !/^(https?:\/\/|\/\/|\/)/i.test(t)) {
214
- w.error("Thumbnail must be an http(s) URL or an absolute path");
215
+ C.error("Thumbnail must be an http(s) URL or an absolute path");
215
216
  return;
216
217
  }
217
218
  G(!0);
218
- let n = _.find((e) => e.value === U.type), r = V ? null : void 0, i = {
219
+ let n = g.find((e) => e.value === U.type), r = V ? null : void 0, i = {
219
220
  title: U.title.trim(),
220
221
  content: U.content,
221
222
  contentFormat: "markdown",
@@ -228,14 +229,14 @@ function x() {
228
229
  isPublished: U.isPublished
229
230
  };
230
231
  try {
231
- V ? (await C.mutate({
232
- mutation: v,
232
+ V ? (await S.mutate({
233
+ mutation: be,
233
234
  variables: {
234
235
  id: V.id,
235
236
  input: i
236
237
  }
237
- }), w.success("Content page updated")) : (await C.mutate({
238
- mutation: ve,
238
+ }), C.success("Content page updated")) : (await S.mutate({
239
+ mutation: ye,
239
240
  variables: { input: {
240
241
  ...i,
241
242
  visibility: "PUBLIC",
@@ -244,28 +245,28 @@ function x() {
244
245
  productId: U.productId || void 0,
245
246
  slug: U.slug.trim().replace(/-+/g, "-").replace(/^-+|-+$/g, "")
246
247
  } }
247
- }), w.success("Content page created")), B(!1), await Z();
248
+ }), C.success("Content page created")), B(!1), await Z();
248
249
  } catch (e) {
249
- w.error(e instanceof Error ? e.message : "Failed to save content page");
250
+ C.error(e instanceof Error ? e.message : "Failed to save content page");
250
251
  } finally {
251
252
  G(!1);
252
253
  }
253
254
  }, [
254
255
  U,
255
256
  V,
257
+ S,
256
258
  C,
257
- w,
258
259
  Z
259
- ]), Ne = l(async () => {
260
+ ]), Pe = c(async () => {
260
261
  if (!(!K || J)) {
261
262
  Y(!0);
262
263
  try {
263
- await C.mutate({
264
- mutation: ye,
264
+ await S.mutate({
265
+ mutation: xe,
265
266
  variables: { id: K.id }
266
- }), w.success("Content page deleted"), q(null), F.length === 1 && M > 1 ? N((e) => e - 1) : await Z();
267
+ }), C.success("Content page deleted"), q(null), P.length === 1 && j > 1 ? M((e) => e - 1) : await Z();
267
268
  } catch (e) {
268
- w.error(e instanceof Error ? e.message : "Failed to delete content page");
269
+ C.error(e instanceof Error ? e.message : "Failed to delete content page");
269
270
  } finally {
270
271
  Y(!1);
271
272
  }
@@ -273,21 +274,21 @@ function x() {
273
274
  }, [
274
275
  K,
275
276
  J,
277
+ S,
276
278
  C,
277
- w,
278
279
  Z,
279
- F.length,
280
- M
281
- ]), Pe = [
280
+ P.length,
281
+ j
282
+ ]), Fe = [
282
283
  {
283
284
  key: "title",
284
285
  header: "Title",
285
- render: (e) => /* @__PURE__ */ m("div", {
286
+ render: (e) => /* @__PURE__ */ p("div", {
286
287
  className: "min-w-0",
287
- children: [/* @__PURE__ */ p("div", {
288
+ children: [/* @__PURE__ */ f("div", {
288
289
  className: "font-medium text-foreground",
289
290
  children: e.title
290
- }), /* @__PURE__ */ p("div", {
291
+ }), /* @__PURE__ */ f("div", {
291
292
  className: "truncate text-xs text-muted-foreground",
292
293
  children: e.slug
293
294
  })]
@@ -298,9 +299,9 @@ function x() {
298
299
  header: "Type",
299
300
  width: "120px",
300
301
  render: (e) => {
301
- let t = _.find((t) => e.tags.includes(t.value));
302
- return /* @__PURE__ */ p(n, {
303
- variant: t ? he[t.value] : "outline",
302
+ let t = g.find((t) => e.tags.includes(t.value));
303
+ return /* @__PURE__ */ f(n, {
304
+ variant: t ? _[t.value] : "outline",
304
305
  className: "text-xs capitalize",
305
306
  children: t?.label ?? "Other"
306
307
  });
@@ -310,13 +311,13 @@ function x() {
310
311
  key: "status",
311
312
  header: "Status",
312
313
  width: "110px",
313
- render: (e) => /* @__PURE__ */ m("div", {
314
+ render: (e) => /* @__PURE__ */ p("div", {
314
315
  className: "flex items-center gap-1",
315
- children: [/* @__PURE__ */ p(n, {
316
+ children: [/* @__PURE__ */ f(n, {
316
317
  variant: e.isPublished ? "default" : "outline",
317
318
  className: "text-xs",
318
319
  children: e.isPublished ? "Published" : "Draft"
319
- }), e.featured && /* @__PURE__ */ p(n, {
320
+ }), e.featured && /* @__PURE__ */ f(n, {
320
321
  variant: "secondary",
321
322
  className: "text-xs",
322
323
  children: "Featured"
@@ -327,192 +328,192 @@ function x() {
327
328
  key: "updatedAt",
328
329
  header: "Updated",
329
330
  width: "110px",
330
- render: (e) => o(e.updatedAt)
331
+ render: (e) => te(e.updatedAt)
331
332
  },
332
333
  {
333
334
  key: "actions",
334
335
  header: "",
335
336
  width: "60px",
336
337
  align: "right",
337
- render: (e) => /* @__PURE__ */ p(oe, { actions: se({
338
- onEdit: x ? () => je(e) : void 0,
339
- onDelete: x ? () => q(e) : void 0
338
+ render: (e) => /* @__PURE__ */ f(se, { actions: ce({
339
+ onEdit: b ? () => Me(e) : void 0,
340
+ onDelete: b ? () => q(e) : void 0
340
341
  }) })
341
342
  }
342
- ], Q = "content-page-form", $ = !!V && !_.some((e) => V.tags.includes(e.value));
343
- return /* @__PURE__ */ m(ce, {
344
- allowed: h,
345
- isLoading: S,
343
+ ], Q = "content-page-form", $ = !!V && !g.some((e) => V.tags.includes(e.value));
344
+ return /* @__PURE__ */ p(le, {
345
+ allowed: m,
346
+ isLoading: x,
346
347
  pageTitle: "Content",
347
348
  denyMessage: "You don't have permission to manage content pages.",
348
349
  children: [
349
- /* @__PURE__ */ p(ee, {
350
+ /* @__PURE__ */ f(ee, {
350
351
  title: "Content",
351
352
  gradientWord: "Content",
352
- description: `Blog posts, case studies, and changelog entries${I ? ` · ${I} total` : ""}`,
353
- actions: /* @__PURE__ */ m(r, {
354
- onClick: Ae,
355
- disabled: !x || !E,
356
- children: [/* @__PURE__ */ p(ue, { className: "mr-2 h-4 w-4" }), "New Content"]
353
+ description: `Blog posts, case studies, and changelog entries${F ? ` · ${F} total` : ""}`,
354
+ actions: /* @__PURE__ */ p(r, {
355
+ onClick: je,
356
+ disabled: !b || !T,
357
+ children: [/* @__PURE__ */ f(fe, { className: "mr-2 h-4 w-4" }), "New Content"]
357
358
  }),
358
- children: /* @__PURE__ */ m("div", {
359
+ children: /* @__PURE__ */ p("div", {
359
360
  className: "space-y-4",
360
361
  children: [
361
- /* @__PURE__ */ p(pe, { children: "Manage the marketing and product content that powers each product's public site — blog posts, case studies and changelog entries. Pick a product, then draft, edit and publish the stories that show up on its website." }),
362
- /* @__PURE__ */ m(fe, {
362
+ /* @__PURE__ */ f(he, { children: "Manage the marketing and product content that powers each product's public site — blog posts, case studies and changelog entries. Pick a product, then draft, edit and publish the stories that show up on its website." }),
363
+ /* @__PURE__ */ p(me, {
363
364
  className: "flex items-center gap-4 p-5",
364
- children: [/* @__PURE__ */ p("div", {
365
+ children: [/* @__PURE__ */ f("div", {
365
366
  className: "flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg",
366
- children: /* @__PURE__ */ p(f, { className: "h-6 w-6" })
367
- }), /* @__PURE__ */ m("div", { children: [/* @__PURE__ */ p("div", {
367
+ children: /* @__PURE__ */ f(d, { className: "h-6 w-6" })
368
+ }), /* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("div", {
368
369
  className: "text-3xl font-bold tabular-nums text-text-primary",
369
- children: I
370
- }), /* @__PURE__ */ p("p", {
370
+ children: F
371
+ }), /* @__PURE__ */ f("p", {
371
372
  className: "text-sm text-text-secondary",
372
373
  children: "content entries for this product"
373
374
  })] })]
374
375
  }),
375
- /* @__PURE__ */ m("div", {
376
+ /* @__PURE__ */ p("div", {
376
377
  className: "flex flex-wrap items-center gap-3",
377
378
  children: [
378
- /* @__PURE__ */ m("select", {
379
+ /* @__PURE__ */ p("select", {
379
380
  "aria-label": "Filter by product",
380
- className: `${b} max-w-xs`,
381
- value: E,
381
+ className: `${y} max-w-xs`,
382
+ value: T,
382
383
  onChange: (e) => {
383
- N(1), D(e.target.value);
384
+ M(1), E(e.target.value);
384
385
  },
385
- children: [T.length === 0 && /* @__PURE__ */ p("option", {
386
+ children: [w.length === 0 && /* @__PURE__ */ f("option", {
386
387
  value: "",
387
388
  children: "Loading products…"
388
- }), T.map((e) => /* @__PURE__ */ p("option", {
389
+ }), w.map((e) => /* @__PURE__ */ f("option", {
389
390
  value: e.id,
390
391
  children: e.name
391
392
  }, e.id))]
392
393
  }),
393
- /* @__PURE__ */ m("select", {
394
+ /* @__PURE__ */ p("select", {
394
395
  "aria-label": "Filter by content type",
395
- className: `${b} max-w-[180px]`,
396
- value: O,
396
+ className: `${y} max-w-[180px]`,
397
+ value: D,
397
398
  onChange: (e) => {
398
- N(1), Se(e.target.value);
399
+ M(1), we(e.target.value);
399
400
  },
400
- children: [/* @__PURE__ */ p("option", {
401
+ children: [/* @__PURE__ */ f("option", {
401
402
  value: "",
402
403
  children: "All types"
403
- }), _.map((e) => /* @__PURE__ */ p("option", {
404
+ }), g.map((e) => /* @__PURE__ */ f("option", {
404
405
  value: e.value,
405
406
  children: e.label
406
407
  }, e.value))]
407
408
  }),
408
- /* @__PURE__ */ m("div", {
409
+ /* @__PURE__ */ p("div", {
409
410
  className: "relative max-w-sm flex-1",
410
- children: [/* @__PURE__ */ p(de, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), /* @__PURE__ */ p(a, {
411
+ children: [/* @__PURE__ */ f(pe, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), /* @__PURE__ */ f(a, {
411
412
  placeholder: "Search content...",
412
- value: k,
413
- onChange: (e) => A(e.target.value),
413
+ value: O,
414
+ onChange: (e) => k(e.target.value),
414
415
  className: "pl-9"
415
416
  })]
416
417
  })
417
418
  ]
418
419
  }),
419
- R && /* @__PURE__ */ p("div", {
420
+ R && /* @__PURE__ */ f("div", {
420
421
  className: "rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive",
421
422
  children: R
422
423
  }),
423
- /* @__PURE__ */ p(te, {
424
- columns: Pe,
425
- data: F,
424
+ /* @__PURE__ */ f(ne, {
425
+ columns: Fe,
426
+ data: P,
426
427
  keyExtractor: (e) => e.id,
427
- loading: Ee,
428
- emptyState: /* @__PURE__ */ p(i, {
428
+ loading: I,
429
+ emptyState: /* @__PURE__ */ f(i, {
429
430
  illustration: "empty-files",
430
- icon: /* @__PURE__ */ p(f, { className: "h-8 w-8" }),
431
+ icon: /* @__PURE__ */ f(d, { className: "h-8 w-8" }),
431
432
  title: "No content yet",
432
433
  description: "Create a blog post, case study, or changelog entry to get started."
433
434
  })
434
435
  }),
435
- I > g && /* @__PURE__ */ p(ne, {
436
- page: M,
437
- totalPages: ke,
438
- totalItems: I,
439
- pageSize: g,
440
- onPageChange: N
436
+ F > h && /* @__PURE__ */ f(re, {
437
+ page: j,
438
+ totalPages: Ae,
439
+ totalItems: F,
440
+ pageSize: h,
441
+ onPageChange: M
441
442
  })
442
443
  ]
443
444
  })
444
445
  }),
445
- /* @__PURE__ */ p(re, {
446
- open: De,
446
+ /* @__PURE__ */ f(ie, {
447
+ open: Oe,
447
448
  onClose: () => B(!1),
448
449
  title: V ? "Edit content" : "New content",
449
- footer: /* @__PURE__ */ p(ie, {
450
+ footer: /* @__PURE__ */ f(ae, {
450
451
  formId: Q,
451
- isSubmitting: Oe,
452
+ isSubmitting: ke,
452
453
  submitLabel: V ? "Save changes" : "Create",
453
454
  onCancel: () => B(!1)
454
455
  }),
455
- children: /* @__PURE__ */ m("form", {
456
+ children: /* @__PURE__ */ p("form", {
456
457
  id: Q,
457
458
  onSubmit: (e) => {
458
- e.preventDefault(), Me();
459
+ e.preventDefault(), Ne();
459
460
  },
460
461
  className: "space-y-6",
461
462
  children: [
462
- /* @__PURE__ */ m(c, {
463
+ /* @__PURE__ */ p(s, {
463
464
  title: "Basics",
464
465
  children: [
465
- /* @__PURE__ */ m(s, {
466
+ /* @__PURE__ */ p(o, {
466
467
  cols: 2,
467
- children: [/* @__PURE__ */ m("label", {
468
+ children: [/* @__PURE__ */ p("label", {
468
469
  className: "space-y-1.5",
469
- children: [/* @__PURE__ */ p("span", {
470
+ children: [/* @__PURE__ */ f("span", {
470
471
  className: "text-sm font-medium",
471
472
  children: "Type"
472
- }), /* @__PURE__ */ m("select", {
473
- className: b,
473
+ }), /* @__PURE__ */ p("select", {
474
+ className: y,
474
475
  value: $ ? "__other" : U.type,
475
476
  disabled: !!V,
476
477
  onChange: (e) => W((t) => ({
477
478
  ...t,
478
479
  type: e.target.value
479
480
  })),
480
- children: [$ && /* @__PURE__ */ p("option", {
481
+ children: [$ && /* @__PURE__ */ f("option", {
481
482
  value: "__other",
482
483
  children: "Other"
483
- }), _.map((e) => /* @__PURE__ */ p("option", {
484
+ }), g.map((e) => /* @__PURE__ */ f("option", {
484
485
  value: e.value,
485
486
  children: e.label
486
487
  }, e.value))]
487
488
  })]
488
- }), /* @__PURE__ */ m("label", {
489
+ }), /* @__PURE__ */ p("label", {
489
490
  className: "space-y-1.5",
490
- children: [/* @__PURE__ */ p("span", {
491
+ children: [/* @__PURE__ */ f("span", {
491
492
  className: "text-sm font-medium",
492
493
  children: "Product"
493
- }), /* @__PURE__ */ p("select", {
494
- className: b,
494
+ }), /* @__PURE__ */ f("select", {
495
+ className: y,
495
496
  value: U.productId,
496
497
  disabled: !!V,
497
498
  onChange: (e) => W((t) => ({
498
499
  ...t,
499
500
  productId: e.target.value
500
501
  })),
501
- children: T.map((e) => /* @__PURE__ */ p("option", {
502
+ children: w.map((e) => /* @__PURE__ */ f("option", {
502
503
  value: e.id,
503
504
  children: e.name
504
505
  }, e.id))
505
506
  })]
506
507
  })]
507
508
  }),
508
- /* @__PURE__ */ m(s, {
509
+ /* @__PURE__ */ p(o, {
509
510
  cols: 2,
510
- children: [/* @__PURE__ */ m("label", {
511
+ children: [/* @__PURE__ */ p("label", {
511
512
  className: "space-y-1.5",
512
- children: [/* @__PURE__ */ p("span", {
513
+ children: [/* @__PURE__ */ f("span", {
513
514
  className: "text-sm font-medium",
514
515
  children: "Title"
515
- }), /* @__PURE__ */ p(a, {
516
+ }), /* @__PURE__ */ f(a, {
516
517
  value: U.title,
517
518
  onChange: (e) => W((t) => ({
518
519
  ...t,
@@ -520,12 +521,12 @@ function x() {
520
521
  })),
521
522
  placeholder: "A clear, descriptive title"
522
523
  })]
523
- }), /* @__PURE__ */ m("label", {
524
+ }), /* @__PURE__ */ p("label", {
524
525
  className: "space-y-1.5",
525
- children: [/* @__PURE__ */ p("span", {
526
+ children: [/* @__PURE__ */ f("span", {
526
527
  className: "text-sm font-medium",
527
528
  children: "Slug"
528
- }), /* @__PURE__ */ p(a, {
529
+ }), /* @__PURE__ */ f(a, {
529
530
  value: U.slug,
530
531
  disabled: !!V,
531
532
  onChange: (e) => W((t) => ({
@@ -536,13 +537,13 @@ function x() {
536
537
  })]
537
538
  })]
538
539
  }),
539
- /* @__PURE__ */ m("label", {
540
+ /* @__PURE__ */ p("label", {
540
541
  className: "block space-y-1.5",
541
- children: [/* @__PURE__ */ p("span", {
542
+ children: [/* @__PURE__ */ f("span", {
542
543
  className: "text-sm font-medium",
543
544
  children: "Excerpt"
544
- }), /* @__PURE__ */ p("textarea", {
545
- className: `${b} h-16 py-2`,
545
+ }), /* @__PURE__ */ f("textarea", {
546
+ className: `${y} h-16 py-2`,
546
547
  value: U.excerpt,
547
548
  onChange: (e) => W((t) => ({
548
549
  ...t,
@@ -553,32 +554,31 @@ function x() {
553
554
  })
554
555
  ]
555
556
  }),
556
- /* @__PURE__ */ p(c, {
557
+ /* @__PURE__ */ f(s, {
557
558
  title: "Content",
558
- description: "Markdown supported (headings, lists, links, bold).",
559
- children: /* @__PURE__ */ p("textarea", {
559
+ description: "Markdown supported (headings, lists, links, bold). Use the toolbar to insert common syntax.",
560
+ children: /* @__PURE__ */ f(ue, {
560
561
  "aria-label": "Content",
561
- className: `${b} h-64 py-2 font-mono text-xs`,
562
562
  value: U.content,
563
563
  onChange: (e) => W((t) => ({
564
564
  ...t,
565
- content: e.target.value
565
+ content: e
566
566
  })),
567
567
  placeholder: "## Heading\n\nWrite your content in markdown..."
568
568
  })
569
569
  }),
570
- /* @__PURE__ */ m(c, {
570
+ /* @__PURE__ */ p(s, {
571
571
  title: "Meta",
572
572
  children: [
573
- /* @__PURE__ */ m(s, {
573
+ /* @__PURE__ */ p(o, {
574
574
  cols: 3,
575
575
  children: [
576
- /* @__PURE__ */ m("label", {
576
+ /* @__PURE__ */ p("label", {
577
577
  className: "space-y-1.5",
578
- children: [/* @__PURE__ */ p("span", {
578
+ children: [/* @__PURE__ */ f("span", {
579
579
  className: "text-sm font-medium",
580
580
  children: "Author"
581
- }), /* @__PURE__ */ p(a, {
581
+ }), /* @__PURE__ */ f(a, {
582
582
  value: U.author,
583
583
  onChange: (e) => W((t) => ({
584
584
  ...t,
@@ -587,12 +587,12 @@ function x() {
587
587
  placeholder: "The Team"
588
588
  })]
589
589
  }),
590
- /* @__PURE__ */ m("label", {
590
+ /* @__PURE__ */ p("label", {
591
591
  className: "space-y-1.5",
592
- children: [/* @__PURE__ */ p("span", {
592
+ children: [/* @__PURE__ */ f("span", {
593
593
  className: "text-sm font-medium",
594
594
  children: "Published date"
595
- }), /* @__PURE__ */ p(a, {
595
+ }), /* @__PURE__ */ f(a, {
596
596
  type: "date",
597
597
  value: U.publishedAt,
598
598
  onChange: (e) => W((t) => ({
@@ -601,12 +601,12 @@ function x() {
601
601
  }))
602
602
  })]
603
603
  }),
604
- /* @__PURE__ */ m("label", {
604
+ /* @__PURE__ */ p("label", {
605
605
  className: "space-y-1.5",
606
- children: [/* @__PURE__ */ p("span", {
606
+ children: [/* @__PURE__ */ f("span", {
607
607
  className: "text-sm font-medium",
608
608
  children: "Reading time (min)"
609
- }), /* @__PURE__ */ p(a, {
609
+ }), /* @__PURE__ */ f(a, {
610
610
  type: "number",
611
611
  min: 0,
612
612
  value: U.readingTime,
@@ -618,12 +618,12 @@ function x() {
618
618
  })
619
619
  ]
620
620
  }),
621
- /* @__PURE__ */ m("label", {
621
+ /* @__PURE__ */ p("label", {
622
622
  className: "block space-y-1.5",
623
- children: [/* @__PURE__ */ p("span", {
623
+ children: [/* @__PURE__ */ f("span", {
624
624
  className: "text-sm font-medium",
625
625
  children: "Thumbnail URL"
626
- }), /* @__PURE__ */ p(a, {
626
+ }), /* @__PURE__ */ f(a, {
627
627
  value: U.thumbnail,
628
628
  onChange: (e) => W((t) => ({
629
629
  ...t,
@@ -632,11 +632,11 @@ function x() {
632
632
  placeholder: "https://…/image.png"
633
633
  })]
634
634
  }),
635
- /* @__PURE__ */ m("div", {
635
+ /* @__PURE__ */ p("div", {
636
636
  className: "flex items-center gap-6 pt-1",
637
- children: [/* @__PURE__ */ m("label", {
637
+ children: [/* @__PURE__ */ p("label", {
638
638
  className: "flex items-center gap-2 text-sm",
639
- children: [/* @__PURE__ */ p("input", {
639
+ children: [/* @__PURE__ */ f("input", {
640
640
  type: "checkbox",
641
641
  checked: U.featured,
642
642
  onChange: (e) => W((t) => ({
@@ -644,9 +644,9 @@ function x() {
644
644
  featured: e.target.checked
645
645
  }))
646
646
  }), "Featured"]
647
- }), /* @__PURE__ */ m("label", {
647
+ }), /* @__PURE__ */ p("label", {
648
648
  className: "flex items-center gap-2 text-sm",
649
- children: [/* @__PURE__ */ p("input", {
649
+ children: [/* @__PURE__ */ f("input", {
650
650
  type: "checkbox",
651
651
  checked: U.isPublished,
652
652
  onChange: (e) => W((t) => ({
@@ -661,10 +661,10 @@ function x() {
661
661
  ]
662
662
  })
663
663
  }),
664
- /* @__PURE__ */ p(ae, {
664
+ /* @__PURE__ */ f(oe, {
665
665
  open: !!K,
666
666
  onClose: () => q(null),
667
- onConfirm: Ne,
667
+ onConfirm: Pe,
668
668
  itemName: K?.title ?? "this content page",
669
669
  itemType: "content page",
670
670
  loading: J
@@ -673,6 +673,6 @@ function x() {
673
673
  });
674
674
  }
675
675
  //#endregion
676
- export { x as ContentPagesPage };
676
+ export { b as ContentPagesPage };
677
677
 
678
678
  //# sourceMappingURL=ContentPagesPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ContentPagesPage.js","names":[],"sources":["../../../src/admin/pages/ContentPagesPage.tsx"],"sourcesContent":["import { useState, useCallback, useEffect, useRef } from 'react';\nimport { gql } from '@apollo/client';\nimport { useApolloClient } from '@apollo/client/react';\nimport { FileText, Search, Plus } from 'lucide-react';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Input,\n DataTable,\n Pagination,\n EmptyState,\n Badge,\n Button,\n PermissionGuard,\n FormSheet,\n FormSheetFooter,\n FormSection,\n FormRow,\n DeleteConfirmDialog,\n useToast,\n createRowActions,\n RowActions,\n} from '../components';\nimport { formatDate } from '../utils/formatters';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\n\nconst PAGE_SIZE = 20;\n\n// Content \"type\" is stored on the ProductContentPage `tags` array (one tag per\n// type, matching the website SDK filters: BlogListPage → \"blog\",\n// CaseStudiesListPage → \"case-study\", ChangelogPage → \"changelog\").\nconst CONTENT_TYPES = [\n { value: 'blog', label: 'Blog', category: 'Blog' },\n { value: 'case-study', label: 'Case Study', category: 'Case Study' },\n { value: 'changelog', label: 'Changelog', category: 'Changelog' },\n] as const;\n\ntype ContentType = (typeof CONTENT_TYPES)[number]['value'];\n\nconst TYPE_VARIANT: Record<ContentType, 'default' | 'secondary' | 'outline'> = {\n blog: 'default',\n 'case-study': 'secondary',\n changelog: 'outline',\n};\n\n// ---------------------------------------------------------------------------\n// GraphQL\n// ---------------------------------------------------------------------------\n\nconst LIST_PRODUCTS = gql`\n query AdminContentListProducts($limit: Int, $offset: Int) {\n platformProducts(limit: $limit, offset: $offset) {\n items {\n id\n name\n slug\n }\n }\n }\n`;\n\nconst LIST_CONTENT_PAGES = gql`\n query AdminContentPages(\n $productId: String\n $filter: ContentPageFilterInput\n $limit: Int\n $offset: Int\n ) {\n contentPages(productId: $productId, filter: $filter, limit: $limit, offset: $offset) {\n items {\n id\n productId\n slug\n title\n content\n contentFormat\n tags\n category\n excerpt\n thumbnail\n author\n publishedAt\n readingTime\n featured\n isPublished\n visibility\n updatedAt\n }\n total\n }\n }\n`;\n\nconst CREATE_CONTENT_PAGE = gql`\n mutation AdminCreateContentPage($input: CreateContentPageInput!) {\n createContentPage(input: $input) {\n id\n slug\n }\n }\n`;\n\nconst UPDATE_CONTENT_PAGE = gql`\n mutation AdminUpdateContentPage($id: ID!, $input: UpdateContentPageInput!) {\n updateContentPage(id: $id, input: $input) {\n id\n slug\n }\n }\n`;\n\nconst DELETE_CONTENT_PAGE = gql`\n mutation AdminDeleteContentPage($id: ID!) {\n deleteContentPage(id: $id)\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface ProductOption {\n id: string;\n name: string;\n slug: string;\n}\n\ninterface ContentPageItem {\n id: string;\n productId: string | null;\n slug: string;\n title: string;\n content: string;\n contentFormat: string | null;\n tags: string[];\n category: string | null;\n excerpt: string | null;\n thumbnail: string | null;\n author: string | null;\n publishedAt: string | null;\n readingTime: number | null;\n featured: boolean;\n isPublished: boolean;\n visibility: string;\n updatedAt: string;\n}\n\ninterface FormState {\n productId: string;\n type: ContentType;\n slug: string;\n title: string;\n excerpt: string;\n content: string;\n author: string;\n publishedAt: string;\n thumbnail: string;\n readingTime: string;\n featured: boolean;\n isPublished: boolean;\n}\n\nconst EMPTY_FORM: FormState = {\n productId: '',\n type: 'blog',\n slug: '',\n title: '',\n excerpt: '',\n content: '',\n author: '',\n publishedAt: '',\n thumbnail: '',\n readingTime: '',\n featured: false,\n isPublished: true,\n};\n\nfunction typeOf(tags: string[]): ContentType {\n if (tags.includes('changelog')) return 'changelog';\n if (tags.includes('case-study')) return 'case-study';\n return 'blog';\n}\n\nconst inputClass =\n 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50';\n\n// ---------------------------------------------------------------------------\n// Page\n// ---------------------------------------------------------------------------\n\nexport function ContentPagesPage() {\n const {\n canViewProducts,\n canManageProducts,\n isLoading: permissionsLoading,\n } = useAdminPermissions();\n const apolloClient = useApolloClient();\n const toast = useToast();\n\n const [products, setProducts] = useState<ProductOption[]>([]);\n const [productId, setProductId] = useState('');\n const [typeFilter, setTypeFilter] = useState<'' | ContentType>('');\n const [searchInput, setSearchInput] = useState('');\n const [search, setSearch] = useState(''); // debounced value that actually drives the query\n const [page, setPage] = useState(1);\n\n // Monotonic request id so a slow earlier fetch can't overwrite a newer one.\n const fetchId = useRef(0);\n\n // Debounce the search box: only the settled value triggers a fetch, and a\n // new search resets to page 1.\n useEffect(() => {\n const t = setTimeout(() => {\n setSearch(searchInput);\n setPage(1);\n }, 300);\n return () => clearTimeout(t);\n }, [searchInput]);\n\n const [items, setItems] = useState<ContentPageItem[]>([]);\n const [total, setTotal] = useState(0);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const [sheetOpen, setSheetOpen] = useState(false);\n const [editing, setEditing] = useState<ContentPageItem | null>(null);\n const [form, setForm] = useState<FormState>(EMPTY_FORM);\n const [submitting, setSubmitting] = useState(false);\n const [deleteTarget, setDeleteTarget] = useState<ContentPageItem | null>(null);\n const [deleting, setDeleting] = useState(false);\n\n const skip = permissionsLoading || !canViewProducts;\n\n // Load product list for the selector. Capped at 100 — the platform has well\n // under that many products, and this drives a simple filter dropdown, not an\n // exhaustive picker. Revisit with pagination/search if the catalogue grows.\n useEffect(() => {\n if (skip) return;\n // Guard against setting state after unmount if the user navigates away\n // before this resolves.\n let active = true;\n apolloClient\n .query<{ platformProducts: { items: ProductOption[] } }>({\n query: LIST_PRODUCTS,\n variables: { limit: 100, offset: 0 },\n fetchPolicy: 'network-only',\n })\n .then((res) => {\n if (!active) return;\n const list = res.data?.platformProducts?.items ?? [];\n setProducts(list);\n setProductId((prev) => prev || list[0]?.id || '');\n })\n .catch((e) => {\n if (!active) return;\n setError(e instanceof Error ? e.message : 'Failed to load products');\n });\n return () => {\n active = false;\n };\n }, [apolloClient, skip]);\n\n const fetchPages = useCallback(async () => {\n if (skip || !productId) return;\n const id = ++fetchId.current;\n setLoading(true);\n try {\n const res = await apolloClient.query<{\n contentPages: { items: ContentPageItem[]; total: number };\n }>({\n query: LIST_CONTENT_PAGES,\n variables: {\n productId,\n filter: {\n ...(search ? { search } : {}),\n ...(typeFilter ? { tag: typeFilter } : {}),\n },\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'network-only',\n });\n if (id !== fetchId.current) return; // a newer fetch superseded this one\n setItems(res.data?.contentPages?.items ?? []);\n setTotal(res.data?.contentPages?.total ?? 0);\n setError(null);\n } catch (e) {\n if (id !== fetchId.current) return;\n setError(e instanceof Error ? e.message : 'Failed to load content pages');\n } finally {\n if (id === fetchId.current) setLoading(false);\n }\n }, [apolloClient, skip, productId, search, typeFilter, page]);\n\n useEffect(() => {\n fetchPages();\n }, [fetchPages]);\n\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const openCreate = useCallback(() => {\n setEditing(null);\n setForm({ ...EMPTY_FORM, productId, type: typeFilter || 'blog' });\n setSheetOpen(true);\n }, [productId, typeFilter]);\n\n const openEdit = useCallback((item: ContentPageItem) => {\n setEditing(item);\n setForm({\n productId: item.productId ?? '',\n type: typeOf(item.tags),\n slug: item.slug,\n title: item.title,\n excerpt: item.excerpt ?? '',\n content: item.content,\n author: item.author ?? '',\n publishedAt: item.publishedAt ? item.publishedAt.slice(0, 10) : '',\n thumbnail: item.thumbnail ?? '',\n readingTime: item.readingTime != null ? String(item.readingTime) : '',\n featured: item.featured,\n isPublished: item.isPublished,\n });\n setSheetOpen(true);\n }, []);\n\n const handleSubmit = useCallback(async () => {\n const missing = [\n !form.title.trim() && 'Title',\n !form.slug.trim() && 'Slug',\n !form.content.trim() && 'Content',\n ].filter(Boolean);\n if (missing.length) {\n toast.error(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required`);\n return;\n }\n // The thumbnail is rendered as an <img src> on the public sites. Only accept\n // http(s) URLs, protocol-relative (`//…`) or site-absolute (`/…`) paths so a\n // `javascript:`/`data:` scheme can't be stored and later become an XSS vector.\n const thumb = form.thumbnail.trim();\n if (thumb && !/^(https?:\\/\\/|\\/\\/|\\/)/i.test(thumb)) {\n toast.error('Thumbnail must be an http(s) URL or an absolute path');\n return;\n }\n setSubmitting(true);\n const typeMeta = CONTENT_TYPES.find((t) => t.value === form.type)!;\n // On EDIT, an emptied optional field must send `null` so the backend clears\n // the previous value (an omitted/`undefined` field is dropped by JSON\n // serialization and leaves the old value untouched). On CREATE we omit.\n const empty = editing ? null : undefined;\n // tags/category are NOT in the shared (update) payload: the type selector is\n // locked on edit, so an edit must NOT overwrite the row's tags/category.\n // That preserves pages whose tags aren't one of blog/case-study/changelog\n // (e.g. legal/privacy) which can surface in the \"All types\" list — editing\n // their title/content must not silently retag them as a blog post.\n const shared = {\n title: form.title.trim(),\n content: form.content,\n contentFormat: 'markdown',\n excerpt: form.excerpt.trim() || empty,\n author: form.author.trim() || empty,\n publishedAt: form.publishedAt ? new Date(form.publishedAt).toISOString() : empty,\n thumbnail: form.thumbnail.trim() || empty,\n readingTime: form.readingTime !== '' ? Number(form.readingTime) : empty,\n featured: form.featured,\n isPublished: form.isPublished,\n // NOTE: `visibility` is intentionally NOT in the shared (update) payload.\n // The form has no visibility control, so an edit must not clobber a page\n // whose visibility was set to INTERNAL/AUTHENTICATED elsewhere back to\n // PUBLIC. It is only defaulted on CREATE below.\n };\n try {\n if (editing) {\n await apolloClient.mutate({\n mutation: UPDATE_CONTENT_PAGE,\n variables: { id: editing.id, input: shared },\n });\n toast.success('Content page updated');\n } else {\n await apolloClient.mutate({\n mutation: CREATE_CONTENT_PAGE,\n variables: {\n input: {\n ...shared,\n visibility: 'PUBLIC',\n tags: [form.type],\n category: typeMeta.category,\n productId: form.productId || undefined,\n // Collapse internal hyphen runs (a `--` can arise from adjacent\n // invalid chars) and trim leading/trailing hyphens — done once on\n // submit so it never fights the user mid-typing.\n slug: form.slug\n .trim()\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, ''),\n },\n },\n });\n toast.success('Content page created');\n }\n setSheetOpen(false);\n await fetchPages();\n } catch (e) {\n toast.error(e instanceof Error ? e.message : 'Failed to save content page');\n } finally {\n setSubmitting(false);\n }\n }, [form, editing, apolloClient, toast, fetchPages]);\n\n const handleDelete = useCallback(async () => {\n // Guard against a double-click firing two DELETE mutations for the same row\n // (the second would NOT_FOUND once the first commits).\n if (!deleteTarget || deleting) return;\n setDeleting(true);\n try {\n await apolloClient.mutate({\n mutation: DELETE_CONTENT_PAGE,\n variables: { id: deleteTarget.id },\n });\n toast.success('Content page deleted');\n setDeleteTarget(null);\n // If we just removed the last row on a non-first page, step back a page —\n // the page-change effect refetches at the correct offset. Otherwise the\n // stale offset would land past the new total and strand an empty table.\n if (items.length === 1 && page > 1) {\n setPage((p) => p - 1);\n } else {\n await fetchPages();\n }\n } catch (e) {\n toast.error(e instanceof Error ? e.message : 'Failed to delete content page');\n } finally {\n setDeleting(false);\n }\n }, [deleteTarget, deleting, apolloClient, toast, fetchPages, items.length, page]);\n\n const columns = [\n {\n key: 'title',\n header: 'Title',\n render: (p: ContentPageItem) => (\n <div className=\"min-w-0\">\n <div className=\"font-medium text-foreground\">{p.title}</div>\n <div className=\"truncate text-xs text-muted-foreground\">{p.slug}</div>\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n width: '120px',\n render: (p: ContentPageItem) => {\n // Don't fall back to \"Blog\" for tags outside our set (e.g. legal/privacy\n // pages that surface in the \"All types\" list) — label those \"Other\".\n const known = CONTENT_TYPES.find((x) => p.tags.includes(x.value));\n return (\n <Badge\n variant={known ? TYPE_VARIANT[known.value] : 'outline'}\n className=\"text-xs capitalize\"\n >\n {known?.label ?? 'Other'}\n </Badge>\n );\n },\n },\n {\n key: 'status',\n header: 'Status',\n width: '110px',\n render: (p: ContentPageItem) => (\n <div className=\"flex items-center gap-1\">\n <Badge variant={p.isPublished ? 'default' : 'outline'} className=\"text-xs\">\n {p.isPublished ? 'Published' : 'Draft'}\n </Badge>\n {p.featured && (\n <Badge variant=\"secondary\" className=\"text-xs\">\n Featured\n </Badge>\n )}\n </div>\n ),\n },\n {\n key: 'updatedAt',\n header: 'Updated',\n width: '110px',\n render: (p: ContentPageItem) => formatDate(p.updatedAt),\n },\n {\n key: 'actions',\n header: '',\n width: '60px',\n align: 'right' as const,\n render: (p: ContentPageItem) => (\n <RowActions\n actions={createRowActions({\n onEdit: canManageProducts ? () => openEdit(p) : undefined,\n onDelete: canManageProducts ? () => setDeleteTarget(p) : undefined,\n })}\n />\n ),\n },\n ];\n\n const formId = 'content-page-form';\n\n // True when editing a page whose tags don't map to a managed content type —\n // used to show a disabled \"Other\" in the (edit-locked) type selector.\n const editingUnmanagedType =\n !!editing && !CONTENT_TYPES.some((t) => editing.tags.includes(t.value));\n\n return (\n <PermissionGuard\n allowed={canViewProducts}\n isLoading={permissionsLoading}\n pageTitle=\"Content\"\n denyMessage=\"You don't have permission to manage content pages.\"\n >\n <PageLayout\n title=\"Content\"\n gradientWord=\"Content\"\n description={`Blog posts, case studies, and changelog entries${\n total ? ` · ${total} total` : ''\n }`}\n actions={\n <Button onClick={openCreate} disabled={!canManageProducts || !productId}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New Content\n </Button>\n }\n >\n <div className=\"space-y-4\">\n <PagePurpose>\n Manage the marketing and product content that powers each product&apos;s public site —\n blog posts, case studies and changelog entries. Pick a product, then draft, edit and\n publish the stories that show up on its website.\n </PagePurpose>\n\n {/* Emphasis: how many content entries exist for the selected product. */}\n <EmphasisPanel className=\"flex items-center gap-4 p-5\">\n <div className=\"flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg\">\n <FileText className=\"h-6 w-6\" />\n </div>\n <div>\n <div className=\"text-3xl font-bold tabular-nums text-text-primary\">{total}</div>\n <p className=\"text-sm text-text-secondary\">content entries for this product</p>\n </div>\n </EmphasisPanel>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n aria-label=\"Filter by product\"\n className={`${inputClass} max-w-xs`}\n value={productId}\n onChange={(e) => {\n setPage(1);\n setProductId(e.target.value);\n }}\n >\n {products.length === 0 && <option value=\"\">Loading products…</option>}\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n\n <select\n aria-label=\"Filter by content type\"\n className={`${inputClass} max-w-[180px]`}\n value={typeFilter}\n onChange={(e) => {\n setPage(1);\n setTypeFilter(e.target.value as '' | ContentType);\n }}\n >\n <option value=\"\">All types</option>\n {CONTENT_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground\" />\n <Input\n placeholder=\"Search content...\"\n value={searchInput}\n onChange={(e) => setSearchInput(e.target.value)}\n className=\"pl-9\"\n />\n </div>\n </div>\n\n {error && (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n <DataTable\n columns={columns}\n data={items}\n keyExtractor={(p) => p.id}\n loading={loading}\n emptyState={\n <EmptyState\n illustration=\"empty-files\"\n icon={<FileText className=\"h-8 w-8\" />}\n title=\"No content yet\"\n description=\"Create a blog post, case study, or changelog entry to get started.\"\n />\n }\n />\n\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n </PageLayout>\n\n {/* Create / Edit sheet */}\n <FormSheet\n open={sheetOpen}\n onClose={() => setSheetOpen(false)}\n title={editing ? 'Edit content' : 'New content'}\n footer={\n <FormSheetFooter\n formId={formId}\n isSubmitting={submitting}\n submitLabel={editing ? 'Save changes' : 'Create'}\n onCancel={() => setSheetOpen(false)}\n />\n }\n >\n <form\n id={formId}\n onSubmit={(e) => {\n e.preventDefault();\n handleSubmit();\n }}\n className=\"space-y-6\"\n >\n <FormSection title=\"Basics\">\n <FormRow cols={2}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Type</span>\n <select\n className={inputClass}\n // When editing a page whose tags fall outside the managed set\n // (e.g. legal/privacy), show a disabled \"Other\" rather than\n // mislabelling it as the typeOf() fallback \"Blog\". The control\n // is disabled on edit and tags are never written on update, so\n // this is display-only.\n value={editingUnmanagedType ? '__other' : form.type}\n disabled={!!editing}\n onChange={(e) => setForm((f) => ({ ...f, type: e.target.value as ContentType }))}\n >\n {editingUnmanagedType && <option value=\"__other\">Other</option>}\n {CONTENT_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Product</span>\n <select\n className={inputClass}\n value={form.productId}\n disabled={!!editing}\n onChange={(e) => setForm((f) => ({ ...f, productId: e.target.value }))}\n >\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n </label>\n </FormRow>\n <FormRow cols={2}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Title</span>\n <Input\n value={form.title}\n onChange={(e) => setForm((f) => ({ ...f, title: e.target.value }))}\n placeholder=\"A clear, descriptive title\"\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Slug</span>\n <Input\n value={form.slug}\n disabled={!!editing}\n onChange={(e) =>\n setForm((f) => ({\n ...f,\n // Don't strip leading/trailing hyphens here — that breaks\n // typing \"my-slug\" (the hyphen would vanish mid-type).\n // Leading/trailing hyphens are trimmed on submit instead.\n slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]+/g, '-'),\n }))\n }\n placeholder=\"url-friendly-slug\"\n />\n </label>\n </FormRow>\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium\">Excerpt</span>\n <textarea\n className={`${inputClass} h-16 py-2`}\n value={form.excerpt}\n onChange={(e) => setForm((f) => ({ ...f, excerpt: e.target.value }))}\n placeholder=\"Short summary shown in cards and previews\"\n />\n </label>\n </FormSection>\n\n <FormSection\n title=\"Content\"\n description=\"Markdown supported (headings, lists, links, bold).\"\n >\n <textarea\n aria-label=\"Content\"\n className={`${inputClass} h-64 py-2 font-mono text-xs`}\n value={form.content}\n onChange={(e) => setForm((f) => ({ ...f, content: e.target.value }))}\n placeholder={'## Heading\\n\\nWrite your content in markdown...'}\n />\n </FormSection>\n\n <FormSection title=\"Meta\">\n <FormRow cols={3}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Author</span>\n <Input\n value={form.author}\n onChange={(e) => setForm((f) => ({ ...f, author: e.target.value }))}\n placeholder=\"The Team\"\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Published date</span>\n <Input\n type=\"date\"\n value={form.publishedAt}\n onChange={(e) => setForm((f) => ({ ...f, publishedAt: e.target.value }))}\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Reading time (min)</span>\n <Input\n type=\"number\"\n min={0}\n value={form.readingTime}\n onChange={(e) => setForm((f) => ({ ...f, readingTime: e.target.value }))}\n />\n </label>\n </FormRow>\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium\">Thumbnail URL</span>\n <Input\n value={form.thumbnail}\n onChange={(e) => setForm((f) => ({ ...f, thumbnail: e.target.value }))}\n placeholder=\"https://…/image.png\"\n />\n </label>\n <div className=\"flex items-center gap-6 pt-1\">\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={form.featured}\n onChange={(e) => setForm((f) => ({ ...f, featured: e.target.checked }))}\n />\n Featured\n </label>\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={form.isPublished}\n onChange={(e) => setForm((f) => ({ ...f, isPublished: e.target.checked }))}\n />\n Published\n </label>\n </div>\n </FormSection>\n </form>\n </FormSheet>\n\n <DeleteConfirmDialog\n open={!!deleteTarget}\n onClose={() => setDeleteTarget(null)}\n onConfirm={handleDelete}\n itemName={deleteTarget?.title ?? 'this content page'}\n itemType=\"content page\"\n loading={deleting}\n />\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ContentPagesPage.js","names":[],"sources":["../../../src/admin/pages/ContentPagesPage.tsx"],"sourcesContent":["import { useState, useCallback, useEffect, useRef } from 'react';\nimport { gql } from '@apollo/client';\nimport { useApolloClient } from '@apollo/client/react';\nimport { FileText, Search, Plus } from 'lucide-react';\nimport { useAdminPermissions } from '../hooks/useAdminPermissions';\nimport {\n PageLayout,\n Input,\n DataTable,\n Pagination,\n EmptyState,\n Badge,\n Button,\n PermissionGuard,\n FormSheet,\n FormSheetFooter,\n FormSection,\n FormRow,\n DeleteConfirmDialog,\n useToast,\n createRowActions,\n RowActions,\n MarkdownEditor,\n} from '../components';\nimport { formatDate } from '../utils/formatters';\nimport { PagePurpose, EmphasisPanel } from '@burdenoff/fe-libs/ui';\n\nconst PAGE_SIZE = 20;\n\n// Content \"type\" is stored on the ProductContentPage `tags` array (one tag per\n// type, matching the website SDK filters: BlogListPage → \"blog\",\n// CaseStudiesListPage → \"case-study\", ChangelogPage → \"changelog\").\nconst CONTENT_TYPES = [\n { value: 'blog', label: 'Blog', category: 'Blog' },\n { value: 'case-study', label: 'Case Study', category: 'Case Study' },\n { value: 'changelog', label: 'Changelog', category: 'Changelog' },\n] as const;\n\ntype ContentType = (typeof CONTENT_TYPES)[number]['value'];\n\nconst TYPE_VARIANT: Record<ContentType, 'default' | 'secondary' | 'outline'> = {\n blog: 'default',\n 'case-study': 'secondary',\n changelog: 'outline',\n};\n\n// ---------------------------------------------------------------------------\n// GraphQL\n// ---------------------------------------------------------------------------\n\nconst LIST_PRODUCTS = gql`\n query AdminContentListProducts($limit: Int, $offset: Int) {\n platformProducts(limit: $limit, offset: $offset) {\n items {\n id\n name\n slug\n }\n }\n }\n`;\n\nconst LIST_CONTENT_PAGES = gql`\n query AdminContentPages(\n $productId: String\n $filter: ContentPageFilterInput\n $limit: Int\n $offset: Int\n ) {\n contentPages(productId: $productId, filter: $filter, limit: $limit, offset: $offset) {\n items {\n id\n productId\n slug\n title\n content\n contentFormat\n tags\n category\n excerpt\n thumbnail\n author\n publishedAt\n readingTime\n featured\n isPublished\n visibility\n updatedAt\n }\n total\n }\n }\n`;\n\nconst CREATE_CONTENT_PAGE = gql`\n mutation AdminCreateContentPage($input: CreateContentPageInput!) {\n createContentPage(input: $input) {\n id\n slug\n }\n }\n`;\n\nconst UPDATE_CONTENT_PAGE = gql`\n mutation AdminUpdateContentPage($id: ID!, $input: UpdateContentPageInput!) {\n updateContentPage(id: $id, input: $input) {\n id\n slug\n }\n }\n`;\n\nconst DELETE_CONTENT_PAGE = gql`\n mutation AdminDeleteContentPage($id: ID!) {\n deleteContentPage(id: $id)\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ninterface ProductOption {\n id: string;\n name: string;\n slug: string;\n}\n\ninterface ContentPageItem {\n id: string;\n productId: string | null;\n slug: string;\n title: string;\n content: string;\n contentFormat: string | null;\n tags: string[];\n category: string | null;\n excerpt: string | null;\n thumbnail: string | null;\n author: string | null;\n publishedAt: string | null;\n readingTime: number | null;\n featured: boolean;\n isPublished: boolean;\n visibility: string;\n updatedAt: string;\n}\n\ninterface FormState {\n productId: string;\n type: ContentType;\n slug: string;\n title: string;\n excerpt: string;\n content: string;\n author: string;\n publishedAt: string;\n thumbnail: string;\n readingTime: string;\n featured: boolean;\n isPublished: boolean;\n}\n\nconst EMPTY_FORM: FormState = {\n productId: '',\n type: 'blog',\n slug: '',\n title: '',\n excerpt: '',\n content: '',\n author: '',\n publishedAt: '',\n thumbnail: '',\n readingTime: '',\n featured: false,\n isPublished: true,\n};\n\nfunction typeOf(tags: string[]): ContentType {\n if (tags.includes('changelog')) return 'changelog';\n if (tags.includes('case-study')) return 'case-study';\n return 'blog';\n}\n\nconst inputClass =\n 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50';\n\n// ---------------------------------------------------------------------------\n// Page\n// ---------------------------------------------------------------------------\n\nexport function ContentPagesPage() {\n const {\n canViewProducts,\n canManageProducts,\n isLoading: permissionsLoading,\n } = useAdminPermissions();\n const apolloClient = useApolloClient();\n const toast = useToast();\n\n const [products, setProducts] = useState<ProductOption[]>([]);\n const [productId, setProductId] = useState('');\n const [typeFilter, setTypeFilter] = useState<'' | ContentType>('');\n const [searchInput, setSearchInput] = useState('');\n const [search, setSearch] = useState(''); // debounced value that actually drives the query\n const [page, setPage] = useState(1);\n\n // Monotonic request id so a slow earlier fetch can't overwrite a newer one.\n const fetchId = useRef(0);\n\n // Debounce the search box: only the settled value triggers a fetch, and a\n // new search resets to page 1.\n useEffect(() => {\n const t = setTimeout(() => {\n setSearch(searchInput);\n setPage(1);\n }, 300);\n return () => clearTimeout(t);\n }, [searchInput]);\n\n const [items, setItems] = useState<ContentPageItem[]>([]);\n const [total, setTotal] = useState(0);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const [sheetOpen, setSheetOpen] = useState(false);\n const [editing, setEditing] = useState<ContentPageItem | null>(null);\n const [form, setForm] = useState<FormState>(EMPTY_FORM);\n const [submitting, setSubmitting] = useState(false);\n const [deleteTarget, setDeleteTarget] = useState<ContentPageItem | null>(null);\n const [deleting, setDeleting] = useState(false);\n\n const skip = permissionsLoading || !canViewProducts;\n\n // Load product list for the selector. Capped at 100 — the platform has well\n // under that many products, and this drives a simple filter dropdown, not an\n // exhaustive picker. Revisit with pagination/search if the catalogue grows.\n useEffect(() => {\n if (skip) return;\n // Guard against setting state after unmount if the user navigates away\n // before this resolves.\n let active = true;\n apolloClient\n .query<{ platformProducts: { items: ProductOption[] } }>({\n query: LIST_PRODUCTS,\n variables: { limit: 100, offset: 0 },\n fetchPolicy: 'network-only',\n })\n .then((res) => {\n if (!active) return;\n const list = res.data?.platformProducts?.items ?? [];\n setProducts(list);\n setProductId((prev) => prev || list[0]?.id || '');\n })\n .catch((e) => {\n if (!active) return;\n setError(e instanceof Error ? e.message : 'Failed to load products');\n });\n return () => {\n active = false;\n };\n }, [apolloClient, skip]);\n\n const fetchPages = useCallback(async () => {\n if (skip || !productId) return;\n const id = ++fetchId.current;\n setLoading(true);\n try {\n const res = await apolloClient.query<{\n contentPages: { items: ContentPageItem[]; total: number };\n }>({\n query: LIST_CONTENT_PAGES,\n variables: {\n productId,\n filter: {\n ...(search ? { search } : {}),\n ...(typeFilter ? { tag: typeFilter } : {}),\n },\n limit: PAGE_SIZE,\n offset: (page - 1) * PAGE_SIZE,\n },\n fetchPolicy: 'network-only',\n });\n if (id !== fetchId.current) return; // a newer fetch superseded this one\n setItems(res.data?.contentPages?.items ?? []);\n setTotal(res.data?.contentPages?.total ?? 0);\n setError(null);\n } catch (e) {\n if (id !== fetchId.current) return;\n setError(e instanceof Error ? e.message : 'Failed to load content pages');\n } finally {\n if (id === fetchId.current) setLoading(false);\n }\n }, [apolloClient, skip, productId, search, typeFilter, page]);\n\n useEffect(() => {\n fetchPages();\n }, [fetchPages]);\n\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n const openCreate = useCallback(() => {\n setEditing(null);\n setForm({ ...EMPTY_FORM, productId, type: typeFilter || 'blog' });\n setSheetOpen(true);\n }, [productId, typeFilter]);\n\n const openEdit = useCallback((item: ContentPageItem) => {\n setEditing(item);\n setForm({\n productId: item.productId ?? '',\n type: typeOf(item.tags),\n slug: item.slug,\n title: item.title,\n excerpt: item.excerpt ?? '',\n content: item.content,\n author: item.author ?? '',\n publishedAt: item.publishedAt ? item.publishedAt.slice(0, 10) : '',\n thumbnail: item.thumbnail ?? '',\n readingTime: item.readingTime != null ? String(item.readingTime) : '',\n featured: item.featured,\n isPublished: item.isPublished,\n });\n setSheetOpen(true);\n }, []);\n\n const handleSubmit = useCallback(async () => {\n const missing = [\n !form.title.trim() && 'Title',\n !form.slug.trim() && 'Slug',\n !form.content.trim() && 'Content',\n ].filter(Boolean);\n if (missing.length) {\n toast.error(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required`);\n return;\n }\n // The thumbnail is rendered as an <img src> on the public sites. Only accept\n // http(s) URLs, protocol-relative (`//…`) or site-absolute (`/…`) paths so a\n // `javascript:`/`data:` scheme can't be stored and later become an XSS vector.\n const thumb = form.thumbnail.trim();\n if (thumb && !/^(https?:\\/\\/|\\/\\/|\\/)/i.test(thumb)) {\n toast.error('Thumbnail must be an http(s) URL or an absolute path');\n return;\n }\n setSubmitting(true);\n const typeMeta = CONTENT_TYPES.find((t) => t.value === form.type)!;\n // On EDIT, an emptied optional field must send `null` so the backend clears\n // the previous value (an omitted/`undefined` field is dropped by JSON\n // serialization and leaves the old value untouched). On CREATE we omit.\n const empty = editing ? null : undefined;\n // tags/category are NOT in the shared (update) payload: the type selector is\n // locked on edit, so an edit must NOT overwrite the row's tags/category.\n // That preserves pages whose tags aren't one of blog/case-study/changelog\n // (e.g. legal/privacy) which can surface in the \"All types\" list — editing\n // their title/content must not silently retag them as a blog post.\n const shared = {\n title: form.title.trim(),\n content: form.content,\n contentFormat: 'markdown',\n excerpt: form.excerpt.trim() || empty,\n author: form.author.trim() || empty,\n publishedAt: form.publishedAt ? new Date(form.publishedAt).toISOString() : empty,\n thumbnail: form.thumbnail.trim() || empty,\n readingTime: form.readingTime !== '' ? Number(form.readingTime) : empty,\n featured: form.featured,\n isPublished: form.isPublished,\n // NOTE: `visibility` is intentionally NOT in the shared (update) payload.\n // The form has no visibility control, so an edit must not clobber a page\n // whose visibility was set to INTERNAL/AUTHENTICATED elsewhere back to\n // PUBLIC. It is only defaulted on CREATE below.\n };\n try {\n if (editing) {\n await apolloClient.mutate({\n mutation: UPDATE_CONTENT_PAGE,\n variables: { id: editing.id, input: shared },\n });\n toast.success('Content page updated');\n } else {\n await apolloClient.mutate({\n mutation: CREATE_CONTENT_PAGE,\n variables: {\n input: {\n ...shared,\n visibility: 'PUBLIC',\n tags: [form.type],\n category: typeMeta.category,\n productId: form.productId || undefined,\n // Collapse internal hyphen runs (a `--` can arise from adjacent\n // invalid chars) and trim leading/trailing hyphens — done once on\n // submit so it never fights the user mid-typing.\n slug: form.slug\n .trim()\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, ''),\n },\n },\n });\n toast.success('Content page created');\n }\n setSheetOpen(false);\n await fetchPages();\n } catch (e) {\n toast.error(e instanceof Error ? e.message : 'Failed to save content page');\n } finally {\n setSubmitting(false);\n }\n }, [form, editing, apolloClient, toast, fetchPages]);\n\n const handleDelete = useCallback(async () => {\n // Guard against a double-click firing two DELETE mutations for the same row\n // (the second would NOT_FOUND once the first commits).\n if (!deleteTarget || deleting) return;\n setDeleting(true);\n try {\n await apolloClient.mutate({\n mutation: DELETE_CONTENT_PAGE,\n variables: { id: deleteTarget.id },\n });\n toast.success('Content page deleted');\n setDeleteTarget(null);\n // If we just removed the last row on a non-first page, step back a page —\n // the page-change effect refetches at the correct offset. Otherwise the\n // stale offset would land past the new total and strand an empty table.\n if (items.length === 1 && page > 1) {\n setPage((p) => p - 1);\n } else {\n await fetchPages();\n }\n } catch (e) {\n toast.error(e instanceof Error ? e.message : 'Failed to delete content page');\n } finally {\n setDeleting(false);\n }\n }, [deleteTarget, deleting, apolloClient, toast, fetchPages, items.length, page]);\n\n const columns = [\n {\n key: 'title',\n header: 'Title',\n render: (p: ContentPageItem) => (\n <div className=\"min-w-0\">\n <div className=\"font-medium text-foreground\">{p.title}</div>\n <div className=\"truncate text-xs text-muted-foreground\">{p.slug}</div>\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n width: '120px',\n render: (p: ContentPageItem) => {\n // Don't fall back to \"Blog\" for tags outside our set (e.g. legal/privacy\n // pages that surface in the \"All types\" list) — label those \"Other\".\n const known = CONTENT_TYPES.find((x) => p.tags.includes(x.value));\n return (\n <Badge\n variant={known ? TYPE_VARIANT[known.value] : 'outline'}\n className=\"text-xs capitalize\"\n >\n {known?.label ?? 'Other'}\n </Badge>\n );\n },\n },\n {\n key: 'status',\n header: 'Status',\n width: '110px',\n render: (p: ContentPageItem) => (\n <div className=\"flex items-center gap-1\">\n <Badge variant={p.isPublished ? 'default' : 'outline'} className=\"text-xs\">\n {p.isPublished ? 'Published' : 'Draft'}\n </Badge>\n {p.featured && (\n <Badge variant=\"secondary\" className=\"text-xs\">\n Featured\n </Badge>\n )}\n </div>\n ),\n },\n {\n key: 'updatedAt',\n header: 'Updated',\n width: '110px',\n render: (p: ContentPageItem) => formatDate(p.updatedAt),\n },\n {\n key: 'actions',\n header: '',\n width: '60px',\n align: 'right' as const,\n render: (p: ContentPageItem) => (\n <RowActions\n actions={createRowActions({\n onEdit: canManageProducts ? () => openEdit(p) : undefined,\n onDelete: canManageProducts ? () => setDeleteTarget(p) : undefined,\n })}\n />\n ),\n },\n ];\n\n const formId = 'content-page-form';\n\n // True when editing a page whose tags don't map to a managed content type —\n // used to show a disabled \"Other\" in the (edit-locked) type selector.\n const editingUnmanagedType =\n !!editing && !CONTENT_TYPES.some((t) => editing.tags.includes(t.value));\n\n return (\n <PermissionGuard\n allowed={canViewProducts}\n isLoading={permissionsLoading}\n pageTitle=\"Content\"\n denyMessage=\"You don't have permission to manage content pages.\"\n >\n <PageLayout\n title=\"Content\"\n gradientWord=\"Content\"\n description={`Blog posts, case studies, and changelog entries${\n total ? ` · ${total} total` : ''\n }`}\n actions={\n <Button onClick={openCreate} disabled={!canManageProducts || !productId}>\n <Plus className=\"mr-2 h-4 w-4\" />\n New Content\n </Button>\n }\n >\n <div className=\"space-y-4\">\n <PagePurpose>\n Manage the marketing and product content that powers each product&apos;s public site —\n blog posts, case studies and changelog entries. Pick a product, then draft, edit and\n publish the stories that show up on its website.\n </PagePurpose>\n\n {/* Emphasis: how many content entries exist for the selected product. */}\n <EmphasisPanel className=\"flex items-center gap-4 p-5\">\n <div className=\"flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-action-primary-bg\">\n <FileText className=\"h-6 w-6\" />\n </div>\n <div>\n <div className=\"text-3xl font-bold tabular-nums text-text-primary\">{total}</div>\n <p className=\"text-sm text-text-secondary\">content entries for this product</p>\n </div>\n </EmphasisPanel>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n aria-label=\"Filter by product\"\n className={`${inputClass} max-w-xs`}\n value={productId}\n onChange={(e) => {\n setPage(1);\n setProductId(e.target.value);\n }}\n >\n {products.length === 0 && <option value=\"\">Loading products…</option>}\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n\n <select\n aria-label=\"Filter by content type\"\n className={`${inputClass} max-w-[180px]`}\n value={typeFilter}\n onChange={(e) => {\n setPage(1);\n setTypeFilter(e.target.value as '' | ContentType);\n }}\n >\n <option value=\"\">All types</option>\n {CONTENT_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n\n <div className=\"relative max-w-sm flex-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground\" />\n <Input\n placeholder=\"Search content...\"\n value={searchInput}\n onChange={(e) => setSearchInput(e.target.value)}\n className=\"pl-9\"\n />\n </div>\n </div>\n\n {error && (\n <div className=\"rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive\">\n {error}\n </div>\n )}\n\n <DataTable\n columns={columns}\n data={items}\n keyExtractor={(p) => p.id}\n loading={loading}\n emptyState={\n <EmptyState\n illustration=\"empty-files\"\n icon={<FileText className=\"h-8 w-8\" />}\n title=\"No content yet\"\n description=\"Create a blog post, case study, or changelog entry to get started.\"\n />\n }\n />\n\n {total > PAGE_SIZE && (\n <Pagination\n page={page}\n totalPages={totalPages}\n totalItems={total}\n pageSize={PAGE_SIZE}\n onPageChange={setPage}\n />\n )}\n </div>\n </PageLayout>\n\n {/* Create / Edit sheet */}\n <FormSheet\n open={sheetOpen}\n onClose={() => setSheetOpen(false)}\n title={editing ? 'Edit content' : 'New content'}\n footer={\n <FormSheetFooter\n formId={formId}\n isSubmitting={submitting}\n submitLabel={editing ? 'Save changes' : 'Create'}\n onCancel={() => setSheetOpen(false)}\n />\n }\n >\n <form\n id={formId}\n onSubmit={(e) => {\n e.preventDefault();\n handleSubmit();\n }}\n className=\"space-y-6\"\n >\n <FormSection title=\"Basics\">\n <FormRow cols={2}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Type</span>\n <select\n className={inputClass}\n // When editing a page whose tags fall outside the managed set\n // (e.g. legal/privacy), show a disabled \"Other\" rather than\n // mislabelling it as the typeOf() fallback \"Blog\". The control\n // is disabled on edit and tags are never written on update, so\n // this is display-only.\n value={editingUnmanagedType ? '__other' : form.type}\n disabled={!!editing}\n onChange={(e) => setForm((f) => ({ ...f, type: e.target.value as ContentType }))}\n >\n {editingUnmanagedType && <option value=\"__other\">Other</option>}\n {CONTENT_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Product</span>\n <select\n className={inputClass}\n value={form.productId}\n disabled={!!editing}\n onChange={(e) => setForm((f) => ({ ...f, productId: e.target.value }))}\n >\n {products.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n </label>\n </FormRow>\n <FormRow cols={2}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Title</span>\n <Input\n value={form.title}\n onChange={(e) => setForm((f) => ({ ...f, title: e.target.value }))}\n placeholder=\"A clear, descriptive title\"\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Slug</span>\n <Input\n value={form.slug}\n disabled={!!editing}\n onChange={(e) =>\n setForm((f) => ({\n ...f,\n // Don't strip leading/trailing hyphens here — that breaks\n // typing \"my-slug\" (the hyphen would vanish mid-type).\n // Leading/trailing hyphens are trimmed on submit instead.\n slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]+/g, '-'),\n }))\n }\n placeholder=\"url-friendly-slug\"\n />\n </label>\n </FormRow>\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium\">Excerpt</span>\n <textarea\n className={`${inputClass} h-16 py-2`}\n value={form.excerpt}\n onChange={(e) => setForm((f) => ({ ...f, excerpt: e.target.value }))}\n placeholder=\"Short summary shown in cards and previews\"\n />\n </label>\n </FormSection>\n\n <FormSection\n title=\"Content\"\n description=\"Markdown supported (headings, lists, links, bold). Use the toolbar to insert common syntax.\"\n >\n <MarkdownEditor\n aria-label=\"Content\"\n value={form.content}\n onChange={(value) => setForm((f) => ({ ...f, content: value }))}\n placeholder={'## Heading\\n\\nWrite your content in markdown...'}\n />\n </FormSection>\n\n <FormSection title=\"Meta\">\n <FormRow cols={3}>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Author</span>\n <Input\n value={form.author}\n onChange={(e) => setForm((f) => ({ ...f, author: e.target.value }))}\n placeholder=\"The Team\"\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Published date</span>\n <Input\n type=\"date\"\n value={form.publishedAt}\n onChange={(e) => setForm((f) => ({ ...f, publishedAt: e.target.value }))}\n />\n </label>\n <label className=\"space-y-1.5\">\n <span className=\"text-sm font-medium\">Reading time (min)</span>\n <Input\n type=\"number\"\n min={0}\n value={form.readingTime}\n onChange={(e) => setForm((f) => ({ ...f, readingTime: e.target.value }))}\n />\n </label>\n </FormRow>\n <label className=\"block space-y-1.5\">\n <span className=\"text-sm font-medium\">Thumbnail URL</span>\n <Input\n value={form.thumbnail}\n onChange={(e) => setForm((f) => ({ ...f, thumbnail: e.target.value }))}\n placeholder=\"https://…/image.png\"\n />\n </label>\n <div className=\"flex items-center gap-6 pt-1\">\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={form.featured}\n onChange={(e) => setForm((f) => ({ ...f, featured: e.target.checked }))}\n />\n Featured\n </label>\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={form.isPublished}\n onChange={(e) => setForm((f) => ({ ...f, isPublished: e.target.checked }))}\n />\n Published\n </label>\n </div>\n </FormSection>\n </form>\n </FormSheet>\n\n <DeleteConfirmDialog\n open={!!deleteTarget}\n onClose={() => setDeleteTarget(null)}\n onConfirm={handleDelete}\n itemName={deleteTarget?.title ?? 'this content page'}\n itemType=\"content page\"\n loading={deleting}\n />\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-admin",
3
- "version": "2026.712.1",
3
+ "version": "2026.731.1",
4
4
  "description": "Burdenoff Admin Console Microfrontend - Platform administration and management interface",
5
5
  "type": "module",
6
6
  "files": [
@@ -22,27 +22,27 @@
22
22
  "types": "./dist/index.d.ts",
23
23
  "scripts": {
24
24
  "dev": "vite",
25
- "build": "tsc && vite build",
25
+ "build": "tsc && bash scripts/build-limits.sh --gate vite build",
26
26
  "schema:pull": "graphql-codegen --project schema-wspace && graphql-codegen --project schema-global",
27
27
  "codegen": "graphql-codegen --project wspace && graphql-codegen --project global",
28
28
  "codegen:all": "bun run schema:pull && bun run codegen",
29
29
  "lint": "eslint .",
30
30
  "lint:sanity": "if [ \"${CI:-}\" = \"true\" ]; then bun run lint; else bun run lint -- --cache --cache-location .eslintcache; fi",
31
31
  "format": "prettier --write \"src/**/*.{ts,tsx}\"",
32
- "type:check": "tsc --noEmit",
33
- "sanity": "bun run codegen && bun run lint:sanity && bun run format && bun run type:check && bun run test && bun run build",
32
+ "type:check": "bash scripts/build-limits.sh tsc --noEmit",
33
+ "sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run lint:sanity && bun run format && bun run type:check && bun run test && bun run build'",
34
34
  "switch:status": "bun scripts/deps-mode.ts status",
35
35
  "switch:remote": "bun scripts/deps-mode.ts remote",
36
36
  "switch:local": "bun scripts/deps-mode.ts local",
37
37
  "switch:check": "bun scripts/deps-mode.ts check",
38
38
  "prepare": "lefthook install || true",
39
39
  "format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
40
- "test": "vitest run",
41
- "test:coverage": "vitest run --coverage"
40
+ "test": "bash scripts/build-limits.sh vitest run",
41
+ "test:coverage": "bash scripts/build-limits.sh vitest run --coverage"
42
42
  },
43
43
  "dependencies": {
44
44
  "@apollo/client": "^4.0.11",
45
- "@burdenoff/fe-libs": "2026.625.5",
45
+ "@burdenoff/fe-libs": "2026.720.3",
46
46
  "@hookform/resolvers": "^5.1.1",
47
47
  "@tanstack/react-query": "^5.90.16",
48
48
  "clsx": "^2.1.1",