@actuate-media/cms-admin 0.36.0 → 0.37.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.
- package/dist/__tests__/views/appearance-settings.render.test.js +4 -4
- package/dist/__tests__/views/updates-tab.render.test.d.ts +2 -0
- package/dist/__tests__/views/updates-tab.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/updates-tab.render.test.js +132 -0
- package/dist/__tests__/views/updates-tab.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/views/ApiKeys.d.ts.map +1 -1
- package/dist/views/ApiKeys.js +14 -13
- package/dist/views/ApiKeys.js.map +1 -1
- package/dist/views/Profile.d.ts.map +1 -1
- package/dist/views/Profile.js +1 -1
- package/dist/views/Profile.js.map +1 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +16 -119
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/Users.js +1 -1
- package/dist/views/Users.js.map +1 -1
- package/dist/views/settings/AISettingsTab.js +40 -42
- package/dist/views/settings/AISettingsTab.js.map +1 -1
- package/dist/views/settings/AdminThemeCard.js +1 -1
- package/dist/views/settings/AiFeaturesCard.d.ts.map +1 -1
- package/dist/views/settings/AiFeaturesCard.js +2 -3
- package/dist/views/settings/AiFeaturesCard.js.map +1 -1
- package/dist/views/settings/BrandPreviewCard.js +1 -1
- package/dist/views/settings/BrandVoiceCard.d.ts.map +1 -1
- package/dist/views/settings/BrandVoiceCard.js +2 -4
- package/dist/views/settings/BrandVoiceCard.js.map +1 -1
- package/dist/views/settings/BrandingCard.d.ts.map +1 -1
- package/dist/views/settings/BrandingCard.js +34 -19
- package/dist/views/settings/BrandingCard.js.map +1 -1
- package/dist/views/settings/GeneralSettingsTab.js +1 -1
- package/dist/views/settings/LanguageRegionCard.d.ts.map +1 -1
- package/dist/views/settings/LanguageRegionCard.js +2 -1
- package/dist/views/settings/LanguageRegionCard.js.map +1 -1
- package/dist/views/settings/MonthlyUsageCard.d.ts.map +1 -1
- package/dist/views/settings/MonthlyUsageCard.js +3 -3
- package/dist/views/settings/MonthlyUsageCard.js.map +1 -1
- package/dist/views/settings/ProviderConnectionCard.js +1 -1
- package/dist/views/settings/ProviderConnectionCard.js.map +1 -1
- package/dist/views/settings/SecurityStatusCard.d.ts.map +1 -1
- package/dist/views/settings/SecurityStatusCard.js +2 -3
- package/dist/views/settings/SecurityStatusCard.js.map +1 -1
- package/dist/views/settings/SecurityTab.js +1 -1
- package/dist/views/settings/SeoRobotsDefaultsCard.d.ts.map +1 -1
- package/dist/views/settings/SeoRobotsDefaultsCard.js +3 -4
- package/dist/views/settings/SeoRobotsDefaultsCard.js.map +1 -1
- package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
- package/dist/views/settings/SeoSettingsTab.js +5 -8
- package/dist/views/settings/SeoSettingsTab.js.map +1 -1
- package/dist/views/settings/SiteIndexStatusCard.d.ts.map +1 -1
- package/dist/views/settings/SiteIndexStatusCard.js +3 -2
- package/dist/views/settings/SiteIndexStatusCard.js.map +1 -1
- package/dist/views/settings/SiteInformationCard.d.ts.map +1 -1
- package/dist/views/settings/SiteInformationCard.js +4 -4
- package/dist/views/settings/SiteInformationCard.js.map +1 -1
- package/dist/views/settings/TypographyMotionCard.js +1 -1
- package/dist/views/settings/UpdatesTab.d.ts +6 -0
- package/dist/views/settings/UpdatesTab.d.ts.map +1 -0
- package/dist/views/settings/UpdatesTab.js +80 -0
- package/dist/views/settings/UpdatesTab.js.map +1 -0
- package/dist/views/settings/components.d.ts +51 -1
- package/dist/views/settings/components.d.ts.map +1 -1
- package/dist/views/settings/components.js +33 -0
- package/dist/views/settings/components.js.map +1 -1
- package/dist/views/settings/securityPrimitives.d.ts +11 -3
- package/dist/views/settings/securityPrimitives.d.ts.map +1 -1
- package/dist/views/settings/securityPrimitives.js +10 -3
- package/dist/views/settings/securityPrimitives.js.map +1 -1
- package/dist/views/settings/useUpdates.d.ts +67 -0
- package/dist/views/settings/useUpdates.d.ts.map +1 -0
- package/dist/views/settings/useUpdates.js +127 -0
- package/dist/views/settings/useUpdates.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/views/appearance-settings.render.test.tsx +4 -4
- package/src/__tests__/views/updates-tab.render.test.tsx +161 -0
- package/src/views/ApiKeys.tsx +190 -154
- package/src/views/Profile.tsx +4 -2
- package/src/views/Settings.tsx +26 -451
- package/src/views/Users.tsx +1 -1
- package/src/views/settings/AISettingsTab.tsx +64 -76
- package/src/views/settings/AdminThemeCard.tsx +1 -1
- package/src/views/settings/AiFeaturesCard.tsx +6 -12
- package/src/views/settings/BrandPreviewCard.tsx +1 -1
- package/src/views/settings/BrandVoiceCard.tsx +6 -19
- package/src/views/settings/BrandingCard.tsx +179 -180
- package/src/views/settings/GeneralSettingsTab.tsx +1 -1
- package/src/views/settings/LanguageRegionCard.tsx +5 -7
- package/src/views/settings/MonthlyUsageCard.tsx +6 -10
- package/src/views/settings/ProviderConnectionCard.tsx +1 -1
- package/src/views/settings/SecurityStatusCard.tsx +8 -11
- package/src/views/settings/SecurityTab.tsx +1 -1
- package/src/views/settings/SeoRobotsDefaultsCard.tsx +7 -14
- package/src/views/settings/SeoSettingsTab.tsx +17 -26
- package/src/views/settings/SiteIndexStatusCard.tsx +10 -22
- package/src/views/settings/SiteInformationCard.tsx +3 -9
- package/src/views/settings/TypographyMotionCard.tsx +1 -1
- package/src/views/settings/UpdatesTab.tsx +545 -0
- package/src/views/settings/components.tsx +123 -1
- package/src/views/settings/securityPrimitives.tsx +14 -9
- package/src/views/settings/useUpdates.ts +183 -0
package/src/views/ApiKeys.tsx
CHANGED
|
@@ -16,6 +16,10 @@ import { type FormEvent, useState } from 'react'
|
|
|
16
16
|
import { toast } from 'sonner'
|
|
17
17
|
import { useApiData } from '../lib/useApiData.js'
|
|
18
18
|
import { cmsApi } from '../lib/api.js'
|
|
19
|
+
import { SettingsCard } from './settings/components.js'
|
|
20
|
+
|
|
21
|
+
const DIALOG_INPUT_CLASS =
|
|
22
|
+
'w-full rounded-md border border-border bg-input-background px-3 py-2 text-base text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none'
|
|
19
23
|
|
|
20
24
|
export interface ApiKeysProps {
|
|
21
25
|
onNavigate?: (path: string) => void
|
|
@@ -78,7 +82,6 @@ export function ApiKeys({ embedded = false }: ApiKeysProps = {}) {
|
|
|
78
82
|
|
|
79
83
|
const keys = data ?? []
|
|
80
84
|
const rootPadding = embedded ? '' : 'p-4 sm:p-6'
|
|
81
|
-
const Heading = embedded ? 'h2' : 'h1'
|
|
82
85
|
|
|
83
86
|
const handleRevoke = async (id: string, name: string) => {
|
|
84
87
|
if (!confirm(`Revoke API key "${name}"? This cannot be undone.`)) return
|
|
@@ -99,138 +102,156 @@ export function ApiKeys({ embedded = false }: ApiKeysProps = {}) {
|
|
|
99
102
|
if (loading) {
|
|
100
103
|
return (
|
|
101
104
|
<div className={`flex h-64 items-center justify-center ${embedded ? '' : 'p-4 sm:p-6'}`}>
|
|
102
|
-
<Loader2 className="h-6 w-6 animate-spin
|
|
105
|
+
<Loader2 className="text-brand h-6 w-6 animate-spin" />
|
|
103
106
|
</div>
|
|
104
107
|
)
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
return (
|
|
108
111
|
<div className={rootPadding}>
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
</Heading>
|
|
115
|
-
<p className="mt-1 text-sm text-gray-500">
|
|
112
|
+
<SettingsCard
|
|
113
|
+
headingLevel="h2"
|
|
114
|
+
title="API Keys"
|
|
115
|
+
description={
|
|
116
|
+
<>
|
|
116
117
|
Long-lived credentials for programmatic access. Use the{' '}
|
|
117
|
-
<code className="
|
|
118
|
+
<code className="bg-muted rounded px-1 py-0.5 text-xs">Authorization: Bearer</code>{' '}
|
|
118
119
|
header instead of session cookies. API key requests skip CSRF.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
type="button"
|
|
123
|
-
onClick={() => setShowCreate(true)}
|
|
124
|
-
className="inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700"
|
|
125
|
-
>
|
|
126
|
-
<Plus className="h-4 w-4" />
|
|
127
|
-
New API Key
|
|
128
|
-
</button>
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
{error && (
|
|
132
|
-
<div className="mb-4 flex items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-3">
|
|
133
|
-
<AlertTriangle className="h-5 w-5 shrink-0 text-red-600" />
|
|
134
|
-
<span className="flex-1 text-sm text-red-800">{error}</span>
|
|
135
|
-
<button
|
|
136
|
-
onClick={refetch}
|
|
137
|
-
className="rounded-lg border border-red-300 px-3 py-1 text-sm text-red-700 transition-colors hover:bg-red-100"
|
|
138
|
-
>
|
|
139
|
-
Retry
|
|
140
|
-
</button>
|
|
141
|
-
</div>
|
|
142
|
-
)}
|
|
143
|
-
|
|
144
|
-
{keys.length === 0 ? (
|
|
145
|
-
<div className="rounded-lg border border-gray-200 bg-white p-10 text-center">
|
|
146
|
-
<KeyRound className="mx-auto mb-3 h-8 w-8 text-gray-300" />
|
|
147
|
-
<p className="mb-1 text-sm text-gray-500">No API keys yet</p>
|
|
148
|
-
<p className="mb-4 text-xs text-gray-400">
|
|
149
|
-
Create one to let AI agents, CI jobs, or external integrations call the CMS API.
|
|
150
|
-
</p>
|
|
120
|
+
</>
|
|
121
|
+
}
|
|
122
|
+
actions={
|
|
151
123
|
<button
|
|
152
124
|
type="button"
|
|
153
125
|
onClick={() => setShowCreate(true)}
|
|
154
|
-
className="inline-flex items-center justify-center gap-2 rounded-
|
|
126
|
+
className="bg-brand text-brand-foreground inline-flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-opacity hover:opacity-90"
|
|
155
127
|
>
|
|
156
|
-
<Plus
|
|
157
|
-
|
|
128
|
+
<Plus size={16} aria-hidden="true" />
|
|
129
|
+
New API Key
|
|
158
130
|
</button>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
<div
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
<th className="px-4 py-2"></th>
|
|
175
|
-
</tr>
|
|
176
|
-
</thead>
|
|
177
|
-
<tbody className="divide-y divide-gray-200">
|
|
178
|
-
{keys.map((k) => {
|
|
179
|
-
const revoked = !!k.revokedAt
|
|
180
|
-
const expired = k.expiresAt && new Date(k.expiresAt).getTime() < Date.now()
|
|
181
|
-
return (
|
|
182
|
-
<tr key={k.id} className={revoked || expired ? 'opacity-60' : undefined}>
|
|
183
|
-
<td className="px-4 py-3 text-sm">
|
|
184
|
-
<div className="font-medium text-gray-900">{k.name}</div>
|
|
185
|
-
{k.user && (
|
|
186
|
-
<div className="text-xs text-gray-500">
|
|
187
|
-
created by {k.user.name ?? k.user.email}
|
|
188
|
-
</div>
|
|
189
|
-
)}
|
|
190
|
-
</td>
|
|
191
|
-
<td className="px-4 py-3 font-mono text-sm text-gray-700">{k.keyPrefix}…</td>
|
|
192
|
-
<td className="max-w-xs px-4 py-3 text-sm text-gray-700">
|
|
193
|
-
{formatScopes(k.scopes)}
|
|
194
|
-
</td>
|
|
195
|
-
<td className="px-4 py-3 text-sm text-gray-600">
|
|
196
|
-
{formatDate(k.lastUsedAt)}
|
|
197
|
-
</td>
|
|
198
|
-
<td className="px-4 py-3 text-sm text-gray-600">{formatDate(k.expiresAt)}</td>
|
|
199
|
-
<td className="px-4 py-3">
|
|
200
|
-
{revoked ? (
|
|
201
|
-
<span className="inline-flex rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">
|
|
202
|
-
Revoked
|
|
203
|
-
</span>
|
|
204
|
-
) : expired ? (
|
|
205
|
-
<span className="inline-flex rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700">
|
|
206
|
-
Expired
|
|
207
|
-
</span>
|
|
208
|
-
) : (
|
|
209
|
-
<span className="inline-flex rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">
|
|
210
|
-
Active
|
|
211
|
-
</span>
|
|
212
|
-
)}
|
|
213
|
-
</td>
|
|
214
|
-
<td className="px-4 py-3 text-right">
|
|
215
|
-
{!revoked && (
|
|
216
|
-
<button
|
|
217
|
-
type="button"
|
|
218
|
-
onClick={() => handleRevoke(k.id, k.name)}
|
|
219
|
-
className="rounded p-1.5 transition-colors hover:bg-red-50"
|
|
220
|
-
title="Revoke"
|
|
221
|
-
>
|
|
222
|
-
<Trash2 className="h-4 w-4 text-red-600" />
|
|
223
|
-
</button>
|
|
224
|
-
)}
|
|
225
|
-
</td>
|
|
226
|
-
</tr>
|
|
227
|
-
)
|
|
228
|
-
})}
|
|
229
|
-
</tbody>
|
|
230
|
-
</table>
|
|
131
|
+
}
|
|
132
|
+
>
|
|
133
|
+
{error && (
|
|
134
|
+
<div
|
|
135
|
+
role="alert"
|
|
136
|
+
className="border-destructive/30 bg-destructive/10 text-destructive mb-4 flex items-center gap-3 rounded-lg border p-3 text-sm"
|
|
137
|
+
>
|
|
138
|
+
<AlertTriangle size={20} className="shrink-0" aria-hidden="true" />
|
|
139
|
+
<span className="flex-1">{error}</span>
|
|
140
|
+
<button
|
|
141
|
+
onClick={refetch}
|
|
142
|
+
className="border-destructive/40 rounded-md border px-3 py-1 transition-colors hover:opacity-80"
|
|
143
|
+
>
|
|
144
|
+
Retry
|
|
145
|
+
</button>
|
|
231
146
|
</div>
|
|
232
|
-
|
|
233
|
-
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{keys.length === 0 ? (
|
|
150
|
+
<div className="py-10 text-center">
|
|
151
|
+
<KeyRound className="text-muted-foreground mx-auto mb-3 h-8 w-8" aria-hidden="true" />
|
|
152
|
+
<p className="text-foreground mb-1 text-sm font-medium">No API keys yet</p>
|
|
153
|
+
<p className="text-muted-foreground mb-4 text-sm">
|
|
154
|
+
Create one to let AI agents, CI jobs, or external integrations call the CMS API.
|
|
155
|
+
</p>
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
onClick={() => setShowCreate(true)}
|
|
159
|
+
className="bg-brand text-brand-foreground inline-flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-opacity hover:opacity-90"
|
|
160
|
+
>
|
|
161
|
+
<Plus size={16} aria-hidden="true" />
|
|
162
|
+
Create your first API key
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
) : (
|
|
166
|
+
<div className="border-border overflow-hidden rounded-lg border">
|
|
167
|
+
<div className="overflow-x-auto">
|
|
168
|
+
<table className="w-full">
|
|
169
|
+
<thead className="border-border bg-muted/50 border-b">
|
|
170
|
+
<tr>
|
|
171
|
+
<th className="text-muted-foreground px-4 py-2 text-left text-xs font-medium">
|
|
172
|
+
Name
|
|
173
|
+
</th>
|
|
174
|
+
<th className="text-muted-foreground px-4 py-2 text-left text-xs font-medium">
|
|
175
|
+
Token
|
|
176
|
+
</th>
|
|
177
|
+
<th className="text-muted-foreground px-4 py-2 text-left text-xs font-medium">
|
|
178
|
+
Scopes
|
|
179
|
+
</th>
|
|
180
|
+
<th className="text-muted-foreground px-4 py-2 text-left text-xs font-medium">
|
|
181
|
+
Last used
|
|
182
|
+
</th>
|
|
183
|
+
<th className="text-muted-foreground px-4 py-2 text-left text-xs font-medium">
|
|
184
|
+
Expires
|
|
185
|
+
</th>
|
|
186
|
+
<th className="text-muted-foreground px-4 py-2 text-left text-xs font-medium">
|
|
187
|
+
Status
|
|
188
|
+
</th>
|
|
189
|
+
<th className="px-4 py-2"></th>
|
|
190
|
+
</tr>
|
|
191
|
+
</thead>
|
|
192
|
+
<tbody className="divide-border divide-y">
|
|
193
|
+
{keys.map((k) => {
|
|
194
|
+
const revoked = !!k.revokedAt
|
|
195
|
+
const expired = k.expiresAt && new Date(k.expiresAt).getTime() < Date.now()
|
|
196
|
+
return (
|
|
197
|
+
<tr key={k.id} className={revoked || expired ? 'opacity-60' : undefined}>
|
|
198
|
+
<td className="px-4 py-3 text-sm">
|
|
199
|
+
<div className="text-foreground font-medium">{k.name}</div>
|
|
200
|
+
{k.user && (
|
|
201
|
+
<div className="text-muted-foreground text-xs">
|
|
202
|
+
created by {k.user.name ?? k.user.email}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</td>
|
|
206
|
+
<td className="text-muted-foreground px-4 py-3 font-mono text-sm">
|
|
207
|
+
{k.keyPrefix}…
|
|
208
|
+
</td>
|
|
209
|
+
<td className="text-foreground max-w-xs px-4 py-3 text-sm">
|
|
210
|
+
{formatScopes(k.scopes)}
|
|
211
|
+
</td>
|
|
212
|
+
<td className="text-muted-foreground px-4 py-3 text-sm">
|
|
213
|
+
{formatDate(k.lastUsedAt)}
|
|
214
|
+
</td>
|
|
215
|
+
<td className="text-muted-foreground px-4 py-3 text-sm">
|
|
216
|
+
{formatDate(k.expiresAt)}
|
|
217
|
+
</td>
|
|
218
|
+
<td className="px-4 py-3">
|
|
219
|
+
{revoked ? (
|
|
220
|
+
<span className="bg-destructive/10 text-destructive inline-flex rounded-full px-2 py-0.5 text-xs font-medium">
|
|
221
|
+
Revoked
|
|
222
|
+
</span>
|
|
223
|
+
) : expired ? (
|
|
224
|
+
<span className="bg-warning/10 text-warning inline-flex rounded-full px-2 py-0.5 text-xs font-medium">
|
|
225
|
+
Expired
|
|
226
|
+
</span>
|
|
227
|
+
) : (
|
|
228
|
+
<span className="bg-success/10 text-success inline-flex rounded-full px-2 py-0.5 text-xs font-medium">
|
|
229
|
+
Active
|
|
230
|
+
</span>
|
|
231
|
+
)}
|
|
232
|
+
</td>
|
|
233
|
+
<td className="px-4 py-3 text-right">
|
|
234
|
+
{!revoked && (
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
onClick={() => handleRevoke(k.id, k.name)}
|
|
238
|
+
className="hover:bg-destructive/10 rounded p-1.5 transition-colors"
|
|
239
|
+
aria-label={`Revoke ${k.name}`}
|
|
240
|
+
title="Revoke"
|
|
241
|
+
>
|
|
242
|
+
<Trash2 className="text-destructive h-4 w-4" aria-hidden="true" />
|
|
243
|
+
</button>
|
|
244
|
+
)}
|
|
245
|
+
</td>
|
|
246
|
+
</tr>
|
|
247
|
+
)
|
|
248
|
+
})}
|
|
249
|
+
</tbody>
|
|
250
|
+
</table>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
</SettingsCard>
|
|
234
255
|
|
|
235
256
|
<CreateApiKeyDialog
|
|
236
257
|
open={showCreate}
|
|
@@ -326,41 +347,48 @@ function CreateApiKeyDialog({ open, onClose, onCreated }: CreateApiKeyDialogProp
|
|
|
326
347
|
<Dialog.Root open={open} onOpenChange={(o) => !o && onClose()}>
|
|
327
348
|
<Dialog.Portal>
|
|
328
349
|
<Dialog.Overlay className="fixed inset-0 z-40 bg-black/40" />
|
|
329
|
-
<Dialog.Content className="fixed top-1/2 left-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg
|
|
330
|
-
<Dialog.Title className="mb-4 flex items-center gap-2 text-lg font-
|
|
331
|
-
<Shield className="h-5 w-5
|
|
350
|
+
<Dialog.Content className="border-border bg-card fixed top-1/2 left-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border p-6 shadow-xl">
|
|
351
|
+
<Dialog.Title className="text-card-foreground mb-4 flex items-center gap-2 text-lg font-medium">
|
|
352
|
+
<Shield className="text-brand h-5 w-5" aria-hidden="true" />
|
|
332
353
|
New API Key
|
|
333
354
|
</Dialog.Title>
|
|
334
355
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
335
356
|
<div>
|
|
336
|
-
<label
|
|
357
|
+
<label
|
|
358
|
+
htmlFor="apikey-name"
|
|
359
|
+
className="text-foreground mb-1 block text-sm font-medium"
|
|
360
|
+
>
|
|
361
|
+
Name
|
|
362
|
+
</label>
|
|
337
363
|
<input
|
|
364
|
+
id="apikey-name"
|
|
338
365
|
type="text"
|
|
339
366
|
value={name}
|
|
340
367
|
onChange={(e) => setName(e.target.value)}
|
|
341
368
|
placeholder="e.g. AI agent — production"
|
|
342
|
-
className=
|
|
369
|
+
className={DIALOG_INPUT_CLASS}
|
|
343
370
|
required
|
|
344
371
|
maxLength={100}
|
|
345
372
|
/>
|
|
346
373
|
</div>
|
|
347
374
|
|
|
348
375
|
<div>
|
|
349
|
-
<
|
|
376
|
+
<span className="text-foreground mb-2 block text-sm font-medium">Preset</span>
|
|
350
377
|
<div className="grid grid-cols-2 gap-2">
|
|
351
378
|
{(['content', 'readonly', 'admin', 'custom'] as const).map((p) => (
|
|
352
379
|
<button
|
|
353
380
|
key={p}
|
|
354
381
|
type="button"
|
|
355
382
|
onClick={() => setPreset(p)}
|
|
356
|
-
|
|
383
|
+
aria-pressed={preset === p}
|
|
384
|
+
className={`rounded-md border px-3 py-2 text-left text-sm transition-colors ${
|
|
357
385
|
preset === p
|
|
358
|
-
? 'border-
|
|
359
|
-
: 'border-
|
|
386
|
+
? 'border-brand bg-brand/5 text-foreground'
|
|
387
|
+
: 'border-border text-foreground hover:bg-accent'
|
|
360
388
|
}`}
|
|
361
389
|
>
|
|
362
390
|
<div className="font-medium capitalize">{p}</div>
|
|
363
|
-
<div className="text-
|
|
391
|
+
<div className="text-muted-foreground text-xs">
|
|
364
392
|
{p === 'content' && 'Read/create/update content'}
|
|
365
393
|
{p === 'readonly' && 'Read-only access'}
|
|
366
394
|
{p === 'admin' && 'Full admin access'}
|
|
@@ -372,22 +400,22 @@ function CreateApiKeyDialog({ open, onClose, onCreated }: CreateApiKeyDialogProp
|
|
|
372
400
|
</div>
|
|
373
401
|
|
|
374
402
|
{(preset === 'content' || preset === 'custom') && (
|
|
375
|
-
<div className="space-y-2 rounded-
|
|
376
|
-
<label className="flex items-center gap-2 text-sm
|
|
403
|
+
<div className="border-border bg-muted/50 space-y-2 rounded-md border p-3">
|
|
404
|
+
<label className="text-foreground flex items-center gap-2 text-sm">
|
|
377
405
|
<input
|
|
378
406
|
type="checkbox"
|
|
379
407
|
checked={media}
|
|
380
408
|
onChange={(e) => setMedia(e.target.checked)}
|
|
381
|
-
className="
|
|
409
|
+
className="accent-brand h-4 w-4"
|
|
382
410
|
/>
|
|
383
411
|
Allow media uploads
|
|
384
412
|
</label>
|
|
385
|
-
<label className="flex items-center gap-2 text-sm
|
|
413
|
+
<label className="text-foreground flex items-center gap-2 text-sm">
|
|
386
414
|
<input
|
|
387
415
|
type="checkbox"
|
|
388
416
|
checked={pageBuilder}
|
|
389
417
|
onChange={(e) => setPageBuilder(e.target.checked)}
|
|
390
|
-
className="
|
|
418
|
+
className="accent-brand h-4 w-4"
|
|
391
419
|
/>
|
|
392
420
|
Allow AI page generation
|
|
393
421
|
</label>
|
|
@@ -395,31 +423,39 @@ function CreateApiKeyDialog({ open, onClose, onCreated }: CreateApiKeyDialogProp
|
|
|
395
423
|
)}
|
|
396
424
|
|
|
397
425
|
<div>
|
|
398
|
-
<label
|
|
426
|
+
<label
|
|
427
|
+
htmlFor="apikey-expiry"
|
|
428
|
+
className="text-foreground mb-1 block text-sm font-medium"
|
|
429
|
+
>
|
|
399
430
|
Expires in (days)
|
|
400
431
|
</label>
|
|
401
432
|
<input
|
|
433
|
+
id="apikey-expiry"
|
|
402
434
|
type="number"
|
|
403
435
|
min={1}
|
|
404
436
|
placeholder="Never (leave blank)"
|
|
405
437
|
value={expiresInDays}
|
|
406
438
|
onChange={(e) => setExpiresInDays(e.target.value)}
|
|
407
|
-
className=
|
|
439
|
+
className={DIALOG_INPUT_CLASS}
|
|
408
440
|
/>
|
|
409
441
|
</div>
|
|
410
442
|
|
|
411
443
|
<div>
|
|
412
|
-
<label
|
|
444
|
+
<label
|
|
445
|
+
htmlFor="apikey-password"
|
|
446
|
+
className="text-foreground mb-1 block text-sm font-medium"
|
|
447
|
+
>
|
|
413
448
|
Confirm password
|
|
414
449
|
</label>
|
|
415
450
|
<input
|
|
451
|
+
id="apikey-password"
|
|
416
452
|
type="password"
|
|
417
453
|
value={password}
|
|
418
454
|
onChange={(e) => setPassword(e.target.value)}
|
|
419
|
-
className=
|
|
455
|
+
className={DIALOG_INPUT_CLASS}
|
|
420
456
|
required
|
|
421
457
|
/>
|
|
422
|
-
<p className="mt-1 text-
|
|
458
|
+
<p className="text-muted-foreground mt-1 text-sm">
|
|
423
459
|
Creating an API key is a sensitive action and requires re-authentication.
|
|
424
460
|
</p>
|
|
425
461
|
</div>
|
|
@@ -428,14 +464,14 @@ function CreateApiKeyDialog({ open, onClose, onCreated }: CreateApiKeyDialogProp
|
|
|
428
464
|
<button
|
|
429
465
|
type="button"
|
|
430
466
|
onClick={onClose}
|
|
431
|
-
className="
|
|
467
|
+
className="border-border text-foreground hover:bg-accent rounded-md border px-4 py-2 text-sm transition-colors"
|
|
432
468
|
>
|
|
433
469
|
Cancel
|
|
434
470
|
</button>
|
|
435
471
|
<button
|
|
436
472
|
type="submit"
|
|
437
473
|
disabled={submitting}
|
|
438
|
-
className="
|
|
474
|
+
className="bg-brand text-brand-foreground rounded-md px-4 py-2 text-sm font-medium transition-opacity hover:opacity-90 disabled:opacity-50"
|
|
439
475
|
>
|
|
440
476
|
{submitting ? 'Creating…' : 'Create key'}
|
|
441
477
|
</button>
|
|
@@ -459,32 +495,32 @@ function RevealKeyDialog({
|
|
|
459
495
|
<Dialog.Root open onOpenChange={(o) => !o && onClose()}>
|
|
460
496
|
<Dialog.Portal>
|
|
461
497
|
<Dialog.Overlay className="fixed inset-0 z-40 bg-black/40" />
|
|
462
|
-
<Dialog.Content className="fixed top-1/2 left-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg
|
|
463
|
-
<Dialog.Title className="mb-2 flex items-center gap-2 text-lg font-
|
|
464
|
-
<Shield className="h-5 w-5
|
|
498
|
+
<Dialog.Content className="border-border bg-card fixed top-1/2 left-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border p-6 shadow-xl">
|
|
499
|
+
<Dialog.Title className="text-card-foreground mb-2 flex items-center gap-2 text-lg font-medium">
|
|
500
|
+
<Shield className="text-success h-5 w-5" aria-hidden="true" />
|
|
465
501
|
API key created
|
|
466
502
|
</Dialog.Title>
|
|
467
|
-
<Dialog.Description className="mb-4 text-sm
|
|
503
|
+
<Dialog.Description className="text-muted-foreground mb-4 text-sm">
|
|
468
504
|
Copy this key now — it will not be shown again. Treat it like a password.
|
|
469
505
|
</Dialog.Description>
|
|
470
506
|
|
|
471
|
-
<div className="mb-4 flex items-start gap-2 rounded-lg border
|
|
472
|
-
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0
|
|
473
|
-
<p className="text-
|
|
507
|
+
<div className="border-warning/30 bg-warning/10 mb-4 flex items-start gap-2 rounded-lg border p-3">
|
|
508
|
+
<AlertTriangle className="text-warning mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
|
509
|
+
<p className="text-foreground text-xs">
|
|
474
510
|
We only store a hash of this key. If you lose it, you'll need to revoke and
|
|
475
511
|
create a new one.
|
|
476
512
|
</p>
|
|
477
513
|
</div>
|
|
478
514
|
|
|
479
515
|
<div className="mb-4 flex items-center gap-2">
|
|
480
|
-
<code className="flex-1 rounded
|
|
516
|
+
<code className="bg-muted text-foreground flex-1 rounded px-3 py-2 font-mono text-sm break-all">
|
|
481
517
|
{shown ? created.key : '•'.repeat(Math.min(48, created.key.length))}
|
|
482
518
|
</code>
|
|
483
519
|
<button
|
|
484
520
|
type="button"
|
|
485
521
|
onClick={() => setShown((s) => !s)}
|
|
486
|
-
className="
|
|
487
|
-
|
|
522
|
+
className="border-border text-foreground hover:bg-accent rounded-md border p-2 transition-colors"
|
|
523
|
+
aria-label={shown ? 'Hide key' : 'Show key'}
|
|
488
524
|
>
|
|
489
525
|
{shown ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
490
526
|
</button>
|
|
@@ -494,8 +530,8 @@ function RevealKeyDialog({
|
|
|
494
530
|
navigator.clipboard.writeText(created.key)
|
|
495
531
|
toast.success('Copied to clipboard')
|
|
496
532
|
}}
|
|
497
|
-
className="
|
|
498
|
-
|
|
533
|
+
className="border-border text-foreground hover:bg-accent rounded-md border p-2 transition-colors"
|
|
534
|
+
aria-label="Copy key"
|
|
499
535
|
>
|
|
500
536
|
<Copy className="h-4 w-4" />
|
|
501
537
|
</button>
|
|
@@ -505,7 +541,7 @@ function RevealKeyDialog({
|
|
|
505
541
|
<button
|
|
506
542
|
type="button"
|
|
507
543
|
onClick={onClose}
|
|
508
|
-
className="
|
|
544
|
+
className="bg-brand text-brand-foreground rounded-md px-4 py-2 text-sm font-medium transition-opacity hover:opacity-90"
|
|
509
545
|
>
|
|
510
546
|
I've saved it
|
|
511
547
|
</button>
|
package/src/views/Profile.tsx
CHANGED
|
@@ -21,8 +21,10 @@ export function Profile({ session }: ProfileProps = {}) {
|
|
|
21
21
|
return (
|
|
22
22
|
<div className="p-3 pr-6 sm:p-4 sm:pr-8">
|
|
23
23
|
<div className="mb-4">
|
|
24
|
-
<h1 className="mb-1 text-2xl font-
|
|
25
|
-
<p className="text-
|
|
24
|
+
<h1 className="text-foreground mb-1 text-2xl font-medium">Your profile</h1>
|
|
25
|
+
<p className="text-muted-foreground text-sm">
|
|
26
|
+
Manage your account details and sign-in security.
|
|
27
|
+
</p>
|
|
26
28
|
</div>
|
|
27
29
|
|
|
28
30
|
<div className="grid max-w-3xl gap-4">
|