@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.
Files changed (100) hide show
  1. package/dist/__tests__/views/appearance-settings.render.test.js +4 -4
  2. package/dist/__tests__/views/updates-tab.render.test.d.ts +2 -0
  3. package/dist/__tests__/views/updates-tab.render.test.d.ts.map +1 -0
  4. package/dist/__tests__/views/updates-tab.render.test.js +132 -0
  5. package/dist/__tests__/views/updates-tab.render.test.js.map +1 -0
  6. package/dist/actuate-admin.css +1 -1
  7. package/dist/views/ApiKeys.d.ts.map +1 -1
  8. package/dist/views/ApiKeys.js +14 -13
  9. package/dist/views/ApiKeys.js.map +1 -1
  10. package/dist/views/Profile.d.ts.map +1 -1
  11. package/dist/views/Profile.js +1 -1
  12. package/dist/views/Profile.js.map +1 -1
  13. package/dist/views/Settings.d.ts.map +1 -1
  14. package/dist/views/Settings.js +16 -119
  15. package/dist/views/Settings.js.map +1 -1
  16. package/dist/views/Users.js +1 -1
  17. package/dist/views/Users.js.map +1 -1
  18. package/dist/views/settings/AISettingsTab.js +40 -42
  19. package/dist/views/settings/AISettingsTab.js.map +1 -1
  20. package/dist/views/settings/AdminThemeCard.js +1 -1
  21. package/dist/views/settings/AiFeaturesCard.d.ts.map +1 -1
  22. package/dist/views/settings/AiFeaturesCard.js +2 -3
  23. package/dist/views/settings/AiFeaturesCard.js.map +1 -1
  24. package/dist/views/settings/BrandPreviewCard.js +1 -1
  25. package/dist/views/settings/BrandVoiceCard.d.ts.map +1 -1
  26. package/dist/views/settings/BrandVoiceCard.js +2 -4
  27. package/dist/views/settings/BrandVoiceCard.js.map +1 -1
  28. package/dist/views/settings/BrandingCard.d.ts.map +1 -1
  29. package/dist/views/settings/BrandingCard.js +34 -19
  30. package/dist/views/settings/BrandingCard.js.map +1 -1
  31. package/dist/views/settings/GeneralSettingsTab.js +1 -1
  32. package/dist/views/settings/LanguageRegionCard.d.ts.map +1 -1
  33. package/dist/views/settings/LanguageRegionCard.js +2 -1
  34. package/dist/views/settings/LanguageRegionCard.js.map +1 -1
  35. package/dist/views/settings/MonthlyUsageCard.d.ts.map +1 -1
  36. package/dist/views/settings/MonthlyUsageCard.js +3 -3
  37. package/dist/views/settings/MonthlyUsageCard.js.map +1 -1
  38. package/dist/views/settings/ProviderConnectionCard.js +1 -1
  39. package/dist/views/settings/ProviderConnectionCard.js.map +1 -1
  40. package/dist/views/settings/SecurityStatusCard.d.ts.map +1 -1
  41. package/dist/views/settings/SecurityStatusCard.js +2 -3
  42. package/dist/views/settings/SecurityStatusCard.js.map +1 -1
  43. package/dist/views/settings/SecurityTab.js +1 -1
  44. package/dist/views/settings/SeoRobotsDefaultsCard.d.ts.map +1 -1
  45. package/dist/views/settings/SeoRobotsDefaultsCard.js +3 -4
  46. package/dist/views/settings/SeoRobotsDefaultsCard.js.map +1 -1
  47. package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
  48. package/dist/views/settings/SeoSettingsTab.js +5 -8
  49. package/dist/views/settings/SeoSettingsTab.js.map +1 -1
  50. package/dist/views/settings/SiteIndexStatusCard.d.ts.map +1 -1
  51. package/dist/views/settings/SiteIndexStatusCard.js +3 -2
  52. package/dist/views/settings/SiteIndexStatusCard.js.map +1 -1
  53. package/dist/views/settings/SiteInformationCard.d.ts.map +1 -1
  54. package/dist/views/settings/SiteInformationCard.js +4 -4
  55. package/dist/views/settings/SiteInformationCard.js.map +1 -1
  56. package/dist/views/settings/TypographyMotionCard.js +1 -1
  57. package/dist/views/settings/UpdatesTab.d.ts +6 -0
  58. package/dist/views/settings/UpdatesTab.d.ts.map +1 -0
  59. package/dist/views/settings/UpdatesTab.js +80 -0
  60. package/dist/views/settings/UpdatesTab.js.map +1 -0
  61. package/dist/views/settings/components.d.ts +51 -1
  62. package/dist/views/settings/components.d.ts.map +1 -1
  63. package/dist/views/settings/components.js +33 -0
  64. package/dist/views/settings/components.js.map +1 -1
  65. package/dist/views/settings/securityPrimitives.d.ts +11 -3
  66. package/dist/views/settings/securityPrimitives.d.ts.map +1 -1
  67. package/dist/views/settings/securityPrimitives.js +10 -3
  68. package/dist/views/settings/securityPrimitives.js.map +1 -1
  69. package/dist/views/settings/useUpdates.d.ts +67 -0
  70. package/dist/views/settings/useUpdates.d.ts.map +1 -0
  71. package/dist/views/settings/useUpdates.js +127 -0
  72. package/dist/views/settings/useUpdates.js.map +1 -0
  73. package/package.json +2 -2
  74. package/src/__tests__/views/appearance-settings.render.test.tsx +4 -4
  75. package/src/__tests__/views/updates-tab.render.test.tsx +161 -0
  76. package/src/views/ApiKeys.tsx +190 -154
  77. package/src/views/Profile.tsx +4 -2
  78. package/src/views/Settings.tsx +26 -451
  79. package/src/views/Users.tsx +1 -1
  80. package/src/views/settings/AISettingsTab.tsx +64 -76
  81. package/src/views/settings/AdminThemeCard.tsx +1 -1
  82. package/src/views/settings/AiFeaturesCard.tsx +6 -12
  83. package/src/views/settings/BrandPreviewCard.tsx +1 -1
  84. package/src/views/settings/BrandVoiceCard.tsx +6 -19
  85. package/src/views/settings/BrandingCard.tsx +179 -180
  86. package/src/views/settings/GeneralSettingsTab.tsx +1 -1
  87. package/src/views/settings/LanguageRegionCard.tsx +5 -7
  88. package/src/views/settings/MonthlyUsageCard.tsx +6 -10
  89. package/src/views/settings/ProviderConnectionCard.tsx +1 -1
  90. package/src/views/settings/SecurityStatusCard.tsx +8 -11
  91. package/src/views/settings/SecurityTab.tsx +1 -1
  92. package/src/views/settings/SeoRobotsDefaultsCard.tsx +7 -14
  93. package/src/views/settings/SeoSettingsTab.tsx +17 -26
  94. package/src/views/settings/SiteIndexStatusCard.tsx +10 -22
  95. package/src/views/settings/SiteInformationCard.tsx +3 -9
  96. package/src/views/settings/TypographyMotionCard.tsx +1 -1
  97. package/src/views/settings/UpdatesTab.tsx +545 -0
  98. package/src/views/settings/components.tsx +123 -1
  99. package/src/views/settings/securityPrimitives.tsx +14 -9
  100. package/src/views/settings/useUpdates.ts +183 -0
@@ -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 text-blue-600" />
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
- <div className="mb-6 flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
110
- <div>
111
- <Heading className="flex items-center gap-2 text-xl font-semibold text-gray-900 sm:text-2xl">
112
- <KeyRound className="h-5 w-5 text-gray-500" />
113
- API Keys
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="rounded bg-gray-100 px-1 py-0.5 text-xs">Authorization: Bearer</code>{' '}
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
- </p>
120
- </div>
121
- <button
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-lg bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700"
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 className="h-4 w-4" />
157
- Create your first API key
128
+ <Plus size={16} aria-hidden="true" />
129
+ New API Key
158
130
  </button>
159
- </div>
160
- ) : (
161
- <div className="overflow-hidden rounded-lg border border-gray-200 bg-white">
162
- <div className="overflow-x-auto">
163
- <table className="w-full">
164
- <thead className="border-b border-gray-200 bg-gray-50">
165
- <tr>
166
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-700">Name</th>
167
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-700">Token</th>
168
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-700">Scopes</th>
169
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-700">
170
- Last used
171
- </th>
172
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-700">Expires</th>
173
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-700">Status</th>
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
- </div>
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 bg-white p-6 shadow-xl">
330
- <Dialog.Title className="mb-4 flex items-center gap-2 text-lg font-semibold text-gray-900">
331
- <Shield className="h-5 w-5 text-blue-600" />
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 className="mb-1 block text-sm font-medium text-gray-700">Name</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="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
369
+ className={DIALOG_INPUT_CLASS}
343
370
  required
344
371
  maxLength={100}
345
372
  />
346
373
  </div>
347
374
 
348
375
  <div>
349
- <label className="mb-2 block text-sm font-medium text-gray-700">Preset</label>
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
- className={`rounded-lg border px-3 py-2 text-left text-sm transition-colors ${
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-blue-500 bg-blue-50 text-blue-900'
359
- : 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
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-xs text-gray-500">
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-lg border border-gray-200 bg-gray-50 p-3">
376
- <label className="flex items-center gap-2 text-sm text-gray-700">
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="rounded border-gray-300"
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 text-gray-700">
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="rounded border-gray-300"
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 className="mb-1 block text-sm font-medium text-gray-700">
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="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
439
+ className={DIALOG_INPUT_CLASS}
408
440
  />
409
441
  </div>
410
442
 
411
443
  <div>
412
- <label className="mb-1 block text-sm font-medium text-gray-700">
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="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
455
+ className={DIALOG_INPUT_CLASS}
420
456
  required
421
457
  />
422
- <p className="mt-1 text-xs text-gray-500">
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="rounded-lg border border-gray-300 px-4 py-2 text-sm transition-colors hover:bg-gray-50"
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="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
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 bg-white p-6 shadow-xl">
463
- <Dialog.Title className="mb-2 flex items-center gap-2 text-lg font-semibold text-gray-900">
464
- <Shield className="h-5 w-5 text-green-600" />
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 text-gray-600">
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 border-amber-200 bg-amber-50 p-3">
472
- <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-600" />
473
- <p className="text-xs text-amber-900">
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&apos;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 bg-gray-100 px-3 py-2 font-mono text-sm break-all">
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="rounded border border-gray-300 p-2 hover:bg-gray-50"
487
- title={shown ? 'Hide' : 'Show'}
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="rounded border border-gray-300 p-2 hover:bg-gray-50"
498
- title="Copy"
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="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700"
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&apos;ve saved it
511
547
  </button>
@@ -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-semibold text-gray-900">Your profile</h1>
25
- <p className="text-sm text-gray-600">Manage your account details and sign-in security.</p>
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">