@prenta/admin 2.4.0 → 2.6.0

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 (107) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +4 -0
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/views/appearance-settings.render.test.js +3 -0
  6. package/dist/__tests__/views/appearance-settings.render.test.js.map +1 -1
  7. package/dist/__tests__/views/mcp-connect.render.test.d.ts +2 -0
  8. package/dist/__tests__/views/mcp-connect.render.test.d.ts.map +1 -0
  9. package/dist/__tests__/views/mcp-connect.render.test.js +55 -0
  10. package/dist/__tests__/views/mcp-connect.render.test.js.map +1 -0
  11. package/dist/__tests__/views/mcp-connect.test.d.ts +2 -0
  12. package/dist/__tests__/views/mcp-connect.test.d.ts.map +1 -0
  13. package/dist/__tests__/views/mcp-connect.test.js +37 -0
  14. package/dist/__tests__/views/mcp-connect.test.js.map +1 -0
  15. package/dist/lib/appearance-api.d.ts +8 -0
  16. package/dist/lib/appearance-api.d.ts.map +1 -1
  17. package/dist/lib/appearance-api.js.map +1 -1
  18. package/dist/prenta-admin.css +1 -1
  19. package/dist/router/admin-routes.d.ts.map +1 -1
  20. package/dist/router/admin-routes.js +1 -0
  21. package/dist/router/admin-routes.js.map +1 -1
  22. package/dist/views/ApiKeys.d.ts +1 -1
  23. package/dist/views/ApiKeys.d.ts.map +1 -1
  24. package/dist/views/ApiKeys.js +2 -2
  25. package/dist/views/ApiKeys.js.map +1 -1
  26. package/dist/views/McpConsent.d.ts +4 -0
  27. package/dist/views/McpConsent.d.ts.map +1 -0
  28. package/dist/views/McpConsent.js +48 -0
  29. package/dist/views/McpConsent.js.map +1 -0
  30. package/dist/views/Settings.d.ts.map +1 -1
  31. package/dist/views/Settings.js +5 -3
  32. package/dist/views/Settings.js.map +1 -1
  33. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -1
  34. package/dist/views/page-editor/EditorCanvas.js +2 -1
  35. package/dist/views/page-editor/EditorCanvas.js.map +1 -1
  36. package/dist/views/page-editor/SectionThemeRoot.d.ts +10 -0
  37. package/dist/views/page-editor/SectionThemeRoot.d.ts.map +1 -0
  38. package/dist/views/page-editor/SectionThemeRoot.js +27 -0
  39. package/dist/views/page-editor/SectionThemeRoot.js.map +1 -0
  40. package/dist/views/page-editor/sections/AuthorBioSection.js +1 -1
  41. package/dist/views/page-editor/sections/AuthorBioSection.js.map +1 -1
  42. package/dist/views/page-editor/sections/ByTheNumbersSection.js +1 -1
  43. package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -1
  44. package/dist/views/page-editor/sections/CtaBannerSection.js +1 -1
  45. package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -1
  46. package/dist/views/page-editor/sections/FeatureHighlightSection.js +2 -2
  47. package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -1
  48. package/dist/views/page-editor/sections/HeroBannerSection.js +1 -1
  49. package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -1
  50. package/dist/views/page-editor/sections/parts.d.ts.map +1 -1
  51. package/dist/views/page-editor/sections/parts.js +11 -15
  52. package/dist/views/page-editor/sections/parts.js.map +1 -1
  53. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -1
  54. package/dist/views/post-editor/PostEditorCanvas.js +3 -2
  55. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -1
  56. package/dist/views/settings/AppearanceTab.d.ts.map +1 -1
  57. package/dist/views/settings/AppearanceTab.js +2 -1
  58. package/dist/views/settings/AppearanceTab.js.map +1 -1
  59. package/dist/views/settings/McpConnectTab.d.ts +9 -0
  60. package/dist/views/settings/McpConnectTab.d.ts.map +1 -0
  61. package/dist/views/settings/McpConnectTab.js +71 -0
  62. package/dist/views/settings/McpConnectTab.js.map +1 -0
  63. package/dist/views/settings/PhiSafeguardsCard.d.ts +9 -0
  64. package/dist/views/settings/PhiSafeguardsCard.d.ts.map +1 -0
  65. package/dist/views/settings/PhiSafeguardsCard.js +70 -0
  66. package/dist/views/settings/PhiSafeguardsCard.js.map +1 -0
  67. package/dist/views/settings/SecurityTab.d.ts.map +1 -1
  68. package/dist/views/settings/SecurityTab.js +2 -1
  69. package/dist/views/settings/SecurityTab.js.map +1 -1
  70. package/dist/views/settings/SiteThemeCard.d.ts +7 -0
  71. package/dist/views/settings/SiteThemeCard.d.ts.map +1 -0
  72. package/dist/views/settings/SiteThemeCard.js +23 -0
  73. package/dist/views/settings/SiteThemeCard.js.map +1 -0
  74. package/dist/views/settings/mcp-connect.d.ts +25 -0
  75. package/dist/views/settings/mcp-connect.d.ts.map +1 -0
  76. package/dist/views/settings/mcp-connect.js +166 -0
  77. package/dist/views/settings/mcp-connect.js.map +1 -0
  78. package/dist/views/settings/useAppearanceSettings.d.ts +3 -1
  79. package/dist/views/settings/useAppearanceSettings.d.ts.map +1 -1
  80. package/dist/views/settings/useAppearanceSettings.js +27 -1
  81. package/dist/views/settings/useAppearanceSettings.js.map +1 -1
  82. package/package.json +4 -4
  83. package/src/AdminRoot.tsx +5 -0
  84. package/src/__tests__/views/appearance-settings.render.test.tsx +8 -0
  85. package/src/__tests__/views/mcp-connect.render.test.tsx +64 -0
  86. package/src/__tests__/views/mcp-connect.test.ts +42 -0
  87. package/src/lib/appearance-api.ts +8 -0
  88. package/src/router/admin-routes.ts +1 -0
  89. package/src/views/ApiKeys.tsx +5 -4
  90. package/src/views/McpConsent.tsx +101 -0
  91. package/src/views/Settings.tsx +13 -0
  92. package/src/views/page-editor/EditorCanvas.tsx +2 -1
  93. package/src/views/page-editor/SectionThemeRoot.tsx +33 -0
  94. package/src/views/page-editor/sections/AuthorBioSection.tsx +2 -2
  95. package/src/views/page-editor/sections/ByTheNumbersSection.tsx +1 -1
  96. package/src/views/page-editor/sections/CtaBannerSection.tsx +1 -1
  97. package/src/views/page-editor/sections/FeatureHighlightSection.tsx +3 -3
  98. package/src/views/page-editor/sections/HeroBannerSection.tsx +2 -2
  99. package/src/views/page-editor/sections/parts.tsx +12 -15
  100. package/src/views/post-editor/PostEditorCanvas.tsx +21 -18
  101. package/src/views/settings/AppearanceTab.tsx +2 -0
  102. package/src/views/settings/McpConnectTab.tsx +212 -0
  103. package/src/views/settings/PhiSafeguardsCard.tsx +85 -0
  104. package/src/views/settings/SecurityTab.tsx +3 -0
  105. package/src/views/settings/SiteThemeCard.tsx +71 -0
  106. package/src/views/settings/mcp-connect.ts +209 -0
  107. package/src/views/settings/useAppearanceSettings.ts +39 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Connection text for Settings → MCP. Each client gets one thing to copy.
3
+ * OAuth clients receive the remote URL. Editors that cannot sign in receive
4
+ * a stdio config with a placeholder key, never a real secret.
5
+ */
6
+
7
+ export type McpClientId =
8
+ | 'claude-code'
9
+ | 'claude'
10
+ | 'chatgpt'
11
+ | 'grok'
12
+ | 'cursor'
13
+ | 'vscode'
14
+ | 'other'
15
+
16
+ export interface McpClientGuide {
17
+ id: McpClientId
18
+ label: string
19
+ title: string
20
+ /** One sentence under the title. */
21
+ summary: string
22
+ /** Primary button. */
23
+ action: string
24
+ /** Exact text the primary button and the panel copy control place on the clipboard. */
25
+ copyText: string
26
+ /** Shown in the panel. Equals copyText except where the panel adds a label. */
27
+ panel: string
28
+ manual: string[]
29
+ /** This client needs a Content API key created on the API Keys tab. */
30
+ needsApiKey: boolean
31
+ }
32
+
33
+ export function mcpServerName(mcpUrl: string): string {
34
+ try {
35
+ const host = new URL(mcpUrl).hostname.replace(/^www\./, '')
36
+ const label = (host.split('.')[0] || 'site').toLowerCase().replace(/[^a-z0-9]+/g, '-')
37
+ return `${label.replace(/^-|-$/g, '') || 'site'}-prenta`
38
+ } catch {
39
+ return 'site-prenta'
40
+ }
41
+ }
42
+
43
+ function mcpSiteOrigin(mcpUrl: string): string {
44
+ try {
45
+ return new URL(mcpUrl).origin
46
+ } catch {
47
+ return mcpUrl.replace(/\/mcp\/?$/, '')
48
+ }
49
+ }
50
+
51
+ export function mcpClientGuides(mcpUrl: string): McpClientGuide[] {
52
+ const name = mcpServerName(mcpUrl)
53
+ const origin = mcpSiteOrigin(mcpUrl)
54
+ const claudeCodePrompt = [
55
+ 'Add this Prenta site as a remote MCP server.',
56
+ '',
57
+ 'Connection details:',
58
+ `- Server URL: ${mcpUrl}`,
59
+ '- Transport: HTTP',
60
+ `- Server name: ${name}`,
61
+ '',
62
+ 'Do not store an API key. This site signs in with OAuth. After the server is added, start the sign-in flow so I can approve it in the browser.',
63
+ '',
64
+ `Command: claude mcp add --transport http ${name} ${mcpUrl}`,
65
+ ].join('\n')
66
+
67
+ const cursorConfig = JSON.stringify(
68
+ {
69
+ mcpServers: {
70
+ [name]: {
71
+ command: 'npx',
72
+ args: ['-y', '@prenta/mcp-server'],
73
+ env: {
74
+ PRENTA_BASE_URL: origin,
75
+ PRENTA_API_KEY: 'act_sk_...',
76
+ },
77
+ },
78
+ },
79
+ },
80
+ null,
81
+ 2,
82
+ )
83
+
84
+ const vscodeConfig = JSON.stringify(
85
+ {
86
+ servers: {
87
+ [name]: {
88
+ type: 'stdio',
89
+ command: 'npx',
90
+ args: ['-y', '@prenta/mcp-server'],
91
+ env: {
92
+ PRENTA_BASE_URL: origin,
93
+ PRENTA_API_KEY: 'act_sk_...',
94
+ },
95
+ },
96
+ },
97
+ },
98
+ null,
99
+ 2,
100
+ )
101
+
102
+ const bearer = 'Authorization: Bearer act_sk_...'
103
+
104
+ return [
105
+ {
106
+ id: 'claude-code',
107
+ label: 'Claude Code',
108
+ title: 'Connect this site to Claude Code',
109
+ summary:
110
+ 'Paste this prompt into Claude Code. It adds the server, then this site asks you to approve access.',
111
+ action: 'Copy prompt',
112
+ copyText: claudeCodePrompt,
113
+ panel: claudeCodePrompt,
114
+ manual: [
115
+ `Run: claude mcp add --transport http ${name} ${mcpUrl}`,
116
+ 'Approve the sign-in page this site opens. Do not paste an API key into the command.',
117
+ ],
118
+ needsApiKey: false,
119
+ },
120
+ {
121
+ id: 'claude',
122
+ label: 'Claude',
123
+ title: 'Connect this site to Claude',
124
+ summary: 'Claude on the web, Desktop, and Cowork use this URL and sign in here.',
125
+ action: 'Copy URL',
126
+ copyText: mcpUrl,
127
+ panel: mcpUrl,
128
+ manual: [
129
+ 'Open Claude → Settings → Connectors → Add custom connector.',
130
+ 'Paste the URL and continue. This site opens so you can allow access.',
131
+ ],
132
+ needsApiKey: false,
133
+ },
134
+ {
135
+ id: 'chatgpt',
136
+ label: 'ChatGPT',
137
+ title: 'Connect this site to ChatGPT',
138
+ summary: 'ChatGPT signs in here. It cannot use an API key.',
139
+ action: 'Copy URL',
140
+ copyText: mcpUrl,
141
+ panel: mcpUrl,
142
+ manual: [
143
+ 'Open ChatGPT → Settings → Connectors and add a custom connector.',
144
+ 'Paste the URL. ChatGPT sends you back here to approve access.',
145
+ ],
146
+ needsApiKey: false,
147
+ },
148
+ {
149
+ id: 'grok',
150
+ label: 'Grok',
151
+ title: 'Connect this site to Grok',
152
+ summary:
153
+ 'Grok on the web signs in here. The Grok CLI and the xAI API send a Content API key instead.',
154
+ action: 'Copy URL',
155
+ copyText: mcpUrl,
156
+ panel: mcpUrl,
157
+ manual: [
158
+ 'On grok.com, add a connector and paste the URL, then approve access on this site.',
159
+ `For the Grok CLI or the xAI API, create a Content API key and send ${bearer}. Include the word Bearer.`,
160
+ ],
161
+ needsApiKey: true,
162
+ },
163
+ {
164
+ id: 'cursor',
165
+ label: 'Cursor',
166
+ title: 'Connect this site to Cursor',
167
+ summary:
168
+ 'Cursor runs the local server. Create a Content API key and replace the placeholder.',
169
+ action: 'Copy config',
170
+ copyText: cursorConfig,
171
+ panel: cursorConfig,
172
+ manual: [
173
+ 'Open Cursor → Settings → MCP and add a new server, or paste this into .cursor/mcp.json.',
174
+ 'Create a Content API key and replace act_sk_... . Do not commit the file.',
175
+ ],
176
+ needsApiKey: true,
177
+ },
178
+ {
179
+ id: 'vscode',
180
+ label: 'VS Code',
181
+ title: 'Connect this site to VS Code',
182
+ summary:
183
+ 'Save this as .vscode/mcp.json. Create a Content API key and replace the placeholder.',
184
+ action: 'Copy config',
185
+ copyText: vscodeConfig,
186
+ panel: vscodeConfig,
187
+ manual: [
188
+ 'Save the config as .vscode/mcp.json.',
189
+ 'Create a Content API key and replace act_sk_... . Do not commit the file.',
190
+ ],
191
+ needsApiKey: true,
192
+ },
193
+ {
194
+ id: 'other',
195
+ label: 'Other',
196
+ title: 'Connect another client',
197
+ summary:
198
+ 'Clients that can sign in use the URL. Clients that can set a header use a Content API key.',
199
+ action: 'Copy URL',
200
+ copyText: mcpUrl,
201
+ panel: mcpUrl,
202
+ manual: [
203
+ 'Point the client at the server URL.',
204
+ 'If it signs in, approve access on this site. If it sends a header, use a Content API key as Authorization: Bearer act_sk_... . Do not put the key in the query string.',
205
+ ],
206
+ needsApiKey: true,
207
+ },
208
+ ]
209
+ }
@@ -5,6 +5,8 @@ import {
5
5
  logoShapeWarning,
6
6
  resolveAdminBrand,
7
7
  resolvePublicBrand,
8
+ parsePublicTheme,
9
+ publicThemeFieldError,
8
10
  validateAccentColor,
9
11
  type AdminBrandingMode,
10
12
  type BrandAssetLike,
@@ -13,6 +15,7 @@ import {
13
15
  type ColorMode,
14
16
  type LogoShape,
15
17
  type ResolvedAdminBrand,
18
+ type PublicTheme,
16
19
  type ResolvedPublicBrand,
17
20
  type SidebarDensity,
18
21
  } from '@prenta/core/appearance'
@@ -39,6 +42,7 @@ interface AppearanceForm {
39
42
  density: SidebarDensity
40
43
  font: string
41
44
  reduceMotion: boolean
45
+ publicTheme: Required<PublicTheme>
42
46
  }
43
47
 
44
48
  /** Brand asset references + admin branding mode (the `brand` settings blob). */
@@ -136,6 +140,7 @@ export interface UseAppearanceSettings {
136
140
  setDensity: (v: SidebarDensity) => void
137
141
  setFont: (v: string) => void
138
142
  setReduceMotion: (v: boolean) => void
143
+ setPublicTheme: (patch: Partial<PublicTheme>) => void
139
144
  setBrandingMode: (v: AdminBrandingMode) => void
140
145
  setAltText: (v: string) => void
141
146
  uploadAsset: (field: BrandAssetField, file: File) => Promise<void>
@@ -147,12 +152,22 @@ export interface UseAppearanceSettings {
147
152
  reload: () => void
148
153
  }
149
154
 
155
+ const EMPTY_PUBLIC_THEME: Required<PublicTheme> = {
156
+ accent: '',
157
+ background: '',
158
+ foreground: '',
159
+ muted: '',
160
+ fontFamily: '',
161
+ radius: '',
162
+ }
163
+
150
164
  const DEFAULT_FORM: AppearanceForm = {
151
165
  colorMode: 'system',
152
166
  accent: 'purple',
153
167
  density: 'default',
154
168
  font: 'System UI',
155
169
  reduceMotion: false,
170
+ publicTheme: EMPTY_PUBLIC_THEME,
156
171
  }
157
172
 
158
173
  const DEFAULT_BRAND_FORM: BrandForm = { mode: 'inheritPublicBrand' }
@@ -263,12 +278,14 @@ export function useAppearanceSettings(
263
278
  return
264
279
  }
265
280
  const d = res.data
281
+ const storedTheme = d.appearance.publicTheme
266
282
  const nextAppearance: AppearanceForm = {
267
283
  colorMode: d.appearance.adminColorModeDefault,
268
284
  accent: d.appearance.adminAccentColor,
269
285
  density: d.appearance.sidebarDensity,
270
286
  font: d.appearance.contentFont,
271
287
  reduceMotion: d.appearance.reduceMotion,
288
+ publicTheme: { ...EMPTY_PUBLIC_THEME, ...storedTheme },
272
289
  }
273
290
  const nextBrand: BrandForm = { ...d.brand, mode: d.brand?.mode ?? 'inheritPublicBrand' }
274
291
  const nextAssets = parseAppearanceAssets(d.assets ?? [])
@@ -319,6 +336,15 @@ export function useAppearanceSettings(
319
336
  (v: boolean) => update({ ...appearance, reduceMotion: v }, brand, assets),
320
337
  [appearance, brand, assets, update],
321
338
  )
339
+ const setPublicTheme = useCallback(
340
+ (patch: Partial<PublicTheme>) =>
341
+ update(
342
+ { ...appearance, publicTheme: { ...appearance.publicTheme, ...patch } },
343
+ brand,
344
+ assets,
345
+ ),
346
+ [appearance, brand, assets, update],
347
+ )
322
348
  const setBrandingMode = useCallback(
323
349
  (v: AdminBrandingMode) => update(appearance, { ...brand, mode: v }, assets),
324
350
  [appearance, brand, assets, update],
@@ -434,6 +460,17 @@ export function useAppearanceSettings(
434
460
  }
435
461
  // Dark mode default but no dark logo.
436
462
  const hasDark = Boolean(brand.publicDarkLogoAssetId || brand.adminDarkLogoAssetId)
463
+ for (const field of [
464
+ 'accent',
465
+ 'background',
466
+ 'foreground',
467
+ 'muted',
468
+ 'fontFamily',
469
+ 'radius',
470
+ ] as const) {
471
+ const message = publicThemeFieldError(field, appearance.publicTheme[field])
472
+ if (message) out.push({ field: `publicTheme.${field}`, message, severity: 'error' })
473
+ }
437
474
  if (appearance.colorMode === 'dark' && !hasDark) {
438
475
  out.push({
439
476
  field: 'colorMode',
@@ -488,6 +525,7 @@ export function useAppearanceSettings(
488
525
  sidebarDensity: appearance.density,
489
526
  contentFont: appearance.font,
490
527
  reduceMotion: appearance.reduceMotion,
528
+ publicTheme: parsePublicTheme(appearance.publicTheme),
491
529
  updatedAt: now,
492
530
  }
493
531
  const merged = {
@@ -536,6 +574,7 @@ export function useAppearanceSettings(
536
574
  setDensity,
537
575
  setFont,
538
576
  setReduceMotion,
577
+ setPublicTheme,
539
578
  setBrandingMode,
540
579
  setAltText,
541
580
  uploadAsset,