flarecms 0.1.0 → 0.1.2

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 (113) hide show
  1. package/LICENSE +21 -0
  2. package/dist/auth/index.js +201 -1
  3. package/dist/cli/commands.js +5554 -55
  4. package/dist/cli/index.js +5554 -55
  5. package/dist/cli/mcp.js +30 -0
  6. package/dist/client/index.js +23576 -0
  7. package/dist/db/index.js +10392 -25
  8. package/dist/index.js +56776 -7582
  9. package/dist/server/index.js +43280 -0
  10. package/dist/style.css +5536 -0
  11. package/package.json +33 -30
  12. package/scripts/fix-api-paths.mjs +0 -32
  13. package/scripts/fix-imports.mjs +0 -38
  14. package/scripts/prefix-css.mjs +0 -45
  15. package/src/api/lib/cache.ts +0 -45
  16. package/src/api/lib/response.ts +0 -40
  17. package/src/api/middlewares/auth.ts +0 -186
  18. package/src/api/middlewares/cors.ts +0 -10
  19. package/src/api/middlewares/rbac.ts +0 -85
  20. package/src/api/routes/auth.ts +0 -377
  21. package/src/api/routes/collections.ts +0 -205
  22. package/src/api/routes/content.ts +0 -175
  23. package/src/api/routes/device.ts +0 -160
  24. package/src/api/routes/magic.ts +0 -150
  25. package/src/api/routes/mcp.ts +0 -273
  26. package/src/api/routes/oauth.ts +0 -160
  27. package/src/api/routes/settings.ts +0 -43
  28. package/src/api/routes/setup.ts +0 -307
  29. package/src/api/routes/tokens.ts +0 -80
  30. package/src/api/schemas/auth.ts +0 -15
  31. package/src/api/schemas/index.ts +0 -51
  32. package/src/api/schemas/tokens.ts +0 -24
  33. package/src/auth/index.ts +0 -28
  34. package/src/cli/commands.ts +0 -217
  35. package/src/cli/index.ts +0 -21
  36. package/src/cli/mcp.ts +0 -210
  37. package/src/cli/tests/cli.test.ts +0 -40
  38. package/src/cli/tests/create.test.ts +0 -87
  39. package/src/client/FlareAdminRouter.tsx +0 -47
  40. package/src/client/app.tsx +0 -175
  41. package/src/client/components/app-sidebar.tsx +0 -227
  42. package/src/client/components/collection-modal.tsx +0 -215
  43. package/src/client/components/content-list.tsx +0 -247
  44. package/src/client/components/dynamic-form.tsx +0 -190
  45. package/src/client/components/field-modal.tsx +0 -221
  46. package/src/client/components/settings/api-token-section.tsx +0 -400
  47. package/src/client/components/settings/general-section.tsx +0 -224
  48. package/src/client/components/settings/security-section.tsx +0 -154
  49. package/src/client/components/settings/seo-section.tsx +0 -200
  50. package/src/client/components/settings/signup-section.tsx +0 -257
  51. package/src/client/components/ui/accordion.tsx +0 -78
  52. package/src/client/components/ui/avatar.tsx +0 -107
  53. package/src/client/components/ui/badge.tsx +0 -52
  54. package/src/client/components/ui/button.tsx +0 -60
  55. package/src/client/components/ui/card.tsx +0 -103
  56. package/src/client/components/ui/checkbox.tsx +0 -27
  57. package/src/client/components/ui/collapsible.tsx +0 -19
  58. package/src/client/components/ui/dialog.tsx +0 -162
  59. package/src/client/components/ui/icon-picker.tsx +0 -485
  60. package/src/client/components/ui/icons-data.ts +0 -8476
  61. package/src/client/components/ui/input.tsx +0 -20
  62. package/src/client/components/ui/label.tsx +0 -20
  63. package/src/client/components/ui/popover.tsx +0 -91
  64. package/src/client/components/ui/select.tsx +0 -204
  65. package/src/client/components/ui/separator.tsx +0 -23
  66. package/src/client/components/ui/sheet.tsx +0 -141
  67. package/src/client/components/ui/sidebar.tsx +0 -722
  68. package/src/client/components/ui/skeleton.tsx +0 -13
  69. package/src/client/components/ui/sonner.tsx +0 -47
  70. package/src/client/components/ui/switch.tsx +0 -30
  71. package/src/client/components/ui/table.tsx +0 -116
  72. package/src/client/components/ui/tabs.tsx +0 -80
  73. package/src/client/components/ui/textarea.tsx +0 -18
  74. package/src/client/components/ui/tooltip.tsx +0 -68
  75. package/src/client/hooks/use-mobile.ts +0 -19
  76. package/src/client/index.css +0 -149
  77. package/src/client/index.ts +0 -7
  78. package/src/client/layouts/admin-layout.tsx +0 -93
  79. package/src/client/layouts/settings-layout.tsx +0 -104
  80. package/src/client/lib/api.ts +0 -72
  81. package/src/client/lib/utils.ts +0 -6
  82. package/src/client/main.tsx +0 -10
  83. package/src/client/pages/collection-detail.tsx +0 -634
  84. package/src/client/pages/collections.tsx +0 -180
  85. package/src/client/pages/dashboard.tsx +0 -133
  86. package/src/client/pages/device.tsx +0 -66
  87. package/src/client/pages/document-detail-page.tsx +0 -139
  88. package/src/client/pages/documents-page.tsx +0 -103
  89. package/src/client/pages/login.tsx +0 -345
  90. package/src/client/pages/settings.tsx +0 -65
  91. package/src/client/pages/setup.tsx +0 -129
  92. package/src/client/pages/signup.tsx +0 -188
  93. package/src/client/store/auth.ts +0 -30
  94. package/src/client/store/collections.ts +0 -13
  95. package/src/client/store/config.ts +0 -12
  96. package/src/client/store/fetcher.ts +0 -30
  97. package/src/client/store/router.ts +0 -95
  98. package/src/client/store/schema.ts +0 -39
  99. package/src/client/store/settings.ts +0 -31
  100. package/src/client/types.ts +0 -34
  101. package/src/db/dynamic.ts +0 -70
  102. package/src/db/index.ts +0 -16
  103. package/src/db/migrations/001_initial_schema.ts +0 -57
  104. package/src/db/migrations/002_auth_tables.ts +0 -84
  105. package/src/db/migrator.ts +0 -61
  106. package/src/db/schema.ts +0 -142
  107. package/src/index.ts +0 -12
  108. package/src/server/index.ts +0 -66
  109. package/src/types.ts +0 -20
  110. package/tests/css.test.ts +0 -21
  111. package/tests/modular.test.ts +0 -29
  112. package/tsconfig.json +0 -10
  113. /package/{style.css.d.ts → dist/style.css.d.ts} +0 -0
@@ -1,400 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { api } from '../../lib/api';
3
- import { Button } from '../ui/button';
4
- import { Input } from '../ui/input';
5
- import { Badge } from '../ui/badge';
6
- import { Checkbox } from '../ui/checkbox';
7
- import { Label } from '../ui/label';
8
- import {
9
- Loader2Icon,
10
- PlusIcon,
11
- KeyIcon,
12
- Trash2Icon,
13
- CopyIcon,
14
- CalendarIcon,
15
- ShieldIcon,
16
- } from 'lucide-react';
17
- import {
18
- Accordion,
19
- AccordionContent,
20
- AccordionItem,
21
- AccordionTrigger,
22
- } from '../ui/accordion';
23
-
24
- export function APITokenSection() {
25
- const [tokens, setTokens] = useState<any[]>([]);
26
- const [collections, setCollections] = useState<any[]>([]);
27
- const [name, setName] = useState('');
28
- const [granularScopes, setGranularScopes] = useState<any[]>([]); // Array of { resource, actions }
29
- const [newToken, setNewToken] = useState('');
30
- const [loading, setLoading] = useState(false);
31
- const [fetching, setFetching] = useState(true);
32
-
33
- const systemResources = [
34
- {
35
- id: 'system.users',
36
- label: 'Users & Roles',
37
- desc: 'Manage administrative users',
38
- },
39
- {
40
- id: 'system.settings',
41
- label: 'Site Settings',
42
- desc: 'Modify global configurations',
43
- },
44
- {
45
- id: 'system.collections',
46
- label: 'Schema Engine',
47
- desc: 'Create and modify collections',
48
- },
49
- ];
50
-
51
- const fetchTokens = async () => {
52
- try {
53
- const res = await api.get('/tokens');
54
- const data = await res.json<any>();
55
- setTokens(data.data || []);
56
- } catch (err) {}
57
- setFetching(false);
58
- };
59
-
60
- const fetchCollections = async () => {
61
- try {
62
- const res = await api.get('/collections');
63
- const data = await res.json<any>();
64
- setCollections(data.data || []);
65
- } catch (err) {}
66
- };
67
-
68
- useEffect(() => {
69
- fetchTokens();
70
- fetchCollections();
71
- }, []);
72
-
73
- const handleCreate = async (e: React.FormEvent) => {
74
- e.preventDefault();
75
- setLoading(true);
76
- setNewToken('');
77
-
78
- try {
79
- const res = await api
80
- .post('/tokens', {
81
- json: {
82
- name,
83
- scopes: granularScopes,
84
- },
85
- });
86
- const data = await res.json<any>();
87
- setNewToken(data.data.token);
88
- setName('');
89
- setGranularScopes([]);
90
- fetchTokens();
91
- } catch (err) {
92
- console.error('Token creation failed:', err);
93
- alert('Failed to generate token. Check console for details.');
94
- }
95
- setLoading(false);
96
- };
97
-
98
- const handleRevoke = async (id: string) => {
99
- if (
100
- !confirm(
101
- 'Revoke this access token? This action is immediate and permanent.',
102
- )
103
- )
104
- return;
105
- await api.delete(`/tokens/${id}`);
106
- fetchTokens();
107
- };
108
-
109
- const togglePermission = (resource: string, action: string) => {
110
- setGranularScopes((prev) => {
111
- const existing = prev.find((s) => s.resource === resource);
112
- if (existing) {
113
- const newActions = existing.actions.includes(action)
114
- ? existing.actions.filter((a: string) => a !== action)
115
- : [...existing.actions, action];
116
-
117
- if (newActions.length === 0)
118
- return prev.filter((s) => s.resource !== resource);
119
- return prev.map((s) =>
120
- s.resource === resource ? { ...s, actions: newActions } : s,
121
- );
122
- }
123
- return [...prev, { resource, actions: [action] }];
124
- });
125
- };
126
-
127
- const isChecked = (resource: string, action: string) => {
128
- return granularScopes
129
- .find((s) => s.resource === resource)
130
- ?.actions.includes(action);
131
- };
132
-
133
- if (fetching)
134
- return (
135
- <div className="flex items-center gap-2 text-muted-foreground">
136
- <Loader2Icon className="size-4 animate-spin" /> Retrieving active
137
- tokens...
138
- </div>
139
- );
140
-
141
- return (
142
- <div className="space-y-12">
143
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
144
- <div>
145
- <h3 className="text-sm font-bold uppercase tracking-widest text-foreground">
146
- Issue Credential
147
- </h3>
148
- <p className="text-xs text-muted-foreground mt-2 leading-relaxed">
149
- Personal Access Tokens allow secure programmatic access for external
150
- scripts and AI agents.
151
- </p>
152
- </div>
153
- <div className="md:col-span-2 bg-card border rounded-xl overflow-hidden shadow-sm">
154
- <form onSubmit={handleCreate} className="p-8 space-y-8">
155
- <div className="space-y-4">
156
- <div className="grid gap-2">
157
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
158
- Token Designation
159
- </Label>
160
- <Input
161
- value={name}
162
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
163
- setName(e.target.value)
164
- }
165
- required
166
- placeholder="e.g. CI/CD Pipeline"
167
- className="h-11 font-medium"
168
- />
169
- </div>
170
-
171
- <div className="grid gap-4 mt-6">
172
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
173
- Permissions
174
- </Label>
175
-
176
- <Accordion className="w-full border rounded-lg bg-muted/5">
177
- {/* Content Collections */}
178
- <AccordionItem value="content" className="border-none px-4">
179
- <AccordionTrigger className="hover:no-underline py-4">
180
- <div className="flex items-center gap-3">
181
- <ShieldIcon className="size-4 text-primary" />
182
- <span className="text-xs font-bold uppercase tracking-wider">
183
- Content Collections
184
- </span>
185
- </div>
186
- </AccordionTrigger>
187
- {collections.length === 0 ? (
188
- <AccordionContent className="pb-6 space-y-4">
189
- <p className="text-xs text-center text-muted-foreground font-medium">
190
- No collections found
191
- </p>
192
- </AccordionContent>
193
- ) : (
194
- <AccordionContent className="pb-6 space-y-4">
195
- {collections.map((col) => (
196
- <div
197
- key={col.id}
198
- className="flex items-center justify-between p-3 rounded-lg border bg-background group"
199
- >
200
- <div className="space-y-0.5">
201
- <p className="text-xs font-bold capitalize">
202
- {col.label}
203
- </p>
204
- <p className="text-[9px] text-muted-foreground font-mono uppercase opacity-50">
205
- {col.slug}
206
- </p>
207
- </div>
208
- <div className="flex items-center gap-4">
209
- {['read', 'write', 'update', 'delete'].map(
210
- (action) => (
211
- <label
212
- key={action}
213
- className="flex items-center gap-2 cursor-pointer group/label"
214
- >
215
- <Checkbox
216
- checked={isChecked(col.slug, action)}
217
- onCheckedChange={() =>
218
- togglePermission(col.slug, action)
219
- }
220
- />
221
- <span className="text-[10px] uppercase font-black tracking-tighter opacity-70 group-hover/label:opacity-100">
222
- {action}
223
- </span>
224
- </label>
225
- ),
226
- )}
227
- </div>
228
- </div>
229
- ))}
230
- </AccordionContent>
231
- )}
232
- </AccordionItem>
233
-
234
- {/* System Resources */}
235
- <AccordionItem value="system" className="border-none px-4">
236
- <AccordionTrigger className="hover:no-underline py-4">
237
- <div className="flex items-center gap-3">
238
- <ShieldIcon className="size-4 text-primary" />
239
- <span className="text-xs font-bold uppercase tracking-wider">
240
- System Resources
241
- </span>
242
- </div>
243
- </AccordionTrigger>
244
- <AccordionContent className="pb-6 space-y-4">
245
- {systemResources.map((res) => (
246
- <div
247
- key={res.id}
248
- className="flex items-center justify-between p-3 rounded-lg border bg-background group"
249
- >
250
- <div className="space-y-0.5">
251
- <p className="text-xs font-bold">{res.label}</p>
252
- <p className="text-[9px] text-muted-foreground leading-tight">
253
- {res.desc}
254
- </p>
255
- </div>
256
- <div className="flex items-center gap-4">
257
- {['read', 'write', 'update'].map((action) => (
258
- <label
259
- key={action}
260
- className="flex items-center gap-2 cursor-pointer"
261
- >
262
- <Checkbox
263
- checked={isChecked(res.id, action)}
264
- onCheckedChange={() =>
265
- togglePermission(res.id, action)
266
- }
267
- />
268
- <span className="text-[10px] uppercase font-black tracking-tighter opacity-70">
269
- {action}
270
- </span>
271
- </label>
272
- ))}
273
- </div>
274
- </div>
275
- ))}
276
- </AccordionContent>
277
- </AccordionItem>
278
- </Accordion>
279
- </div>
280
- </div>
281
-
282
- <Button
283
- type="submit"
284
- disabled={loading || !name || granularScopes.length === 0}
285
- className="w-full h-11 gap-2 font-bold uppercase tracking-widest text-[10px]"
286
- >
287
- {loading ? (
288
- <Loader2Icon className="size-3 animate-spin" />
289
- ) : (
290
- <PlusIcon className="size-4" />
291
- )}
292
- Generate Access Secret
293
- </Button>
294
-
295
- {newToken && (
296
- <div className="mt-8 p-6 bg-primary/5 border border-primary/20 rounded-xl">
297
- <div className="flex items-center gap-2 text-primary mb-3">
298
- <ShieldIcon className="size-4" />
299
- <p className="text-[11px] font-black uppercase tracking-widest">
300
- Secret Vaulted Successfully
301
- </p>
302
- </div>
303
- <div className="relative group">
304
- <code className="text-xs break-all text-foreground bg-background p-4 rounded-lg block border font-mono">
305
- {newToken}
306
- </code>
307
- <Button
308
- variant="ghost"
309
- size="icon"
310
- className="absolute top-2 right-2 size-8 text-muted-foreground hover:text-primary"
311
- onClick={() => navigator.clipboard.writeText(newToken)}
312
- >
313
- <CopyIcon className="size-3.5" />
314
- </Button>
315
- </div>
316
- <p className="text-[10px] text-primary/70 font-bold uppercase mt-4 tracking-tighter italic">
317
- Warning: This secret will not be displayed again. Store it
318
- securely.
319
- </p>
320
- </div>
321
- )}
322
- </form>
323
- </div>
324
- </div>
325
-
326
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8 border-t pt-12">
327
- <div>
328
- <h3 className="text-sm font-bold uppercase tracking-widest text-foreground">
329
- Active Ledger
330
- </h3>
331
- <p className="text-xs text-muted-foreground mt-2 leading-relaxed">
332
- Review and manage active credentials for your account.
333
- </p>
334
- </div>
335
- <div className="md:col-span-2 space-y-4">
336
- {tokens.map((t: any) => (
337
- <div
338
- key={t.id}
339
- className="bg-card border rounded-xl p-6 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6 group hover:border-primary/20 transition-colors"
340
- >
341
- <div className="space-y-2">
342
- <div className="flex items-center gap-3">
343
- <div className="size-8 bg-primary/5 rounded flex items-center justify-center text-primary border border-primary/10">
344
- <KeyIcon className="size-4" />
345
- </div>
346
- <p className="text-sm font-bold text-foreground tracking-tight">
347
- {t.name}
348
- </p>
349
- </div>
350
- <div className="flex flex-wrap items-center gap-2 pl-1">
351
- <span className="text-[10px] font-mono text-muted-foreground uppercase opacity-50 px-1 border rounded">
352
- {t.id}
353
- </span>
354
- {Array.isArray(t.scopes) &&
355
- t.scopes.map((s: any, idx: number) => {
356
- const label =
357
- typeof s === 'string'
358
- ? s
359
- : `${s.resource}:${s.actions.join(',')}`;
360
- return (
361
- <Badge
362
- key={idx}
363
- variant="outline"
364
- className="text-[8px] uppercase font-bold tracking-widest px-1.5 h-4 border-primary/20 text-primary/60"
365
- >
366
- {label}
367
- </Badge>
368
- );
369
- })}
370
- </div>
371
- <div className="flex items-center gap-3 pl-1 pt-1 opacity-50">
372
- <div className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-tighter text-muted-foreground">
373
- <CalendarIcon className="size-3" />
374
- Created: {new Date(t.created_at).toLocaleDateString()}
375
- </div>
376
- </div>
377
- </div>
378
- <Button
379
- variant="ghost"
380
- size="sm"
381
- onClick={() => handleRevoke(t.id)}
382
- className="text-[10px] font-black uppercase tracking-widest h-8 px-3 text-muted-foreground hover:bg-destructive/5 hover:text-destructive"
383
- >
384
- Revoke Access
385
- </Button>
386
- </div>
387
- ))}
388
-
389
- {tokens.length === 0 && (
390
- <div className="text-center py-12 border-2 border-dashed rounded-xl opacity-30">
391
- <p className="text-xs font-bold uppercase tracking-widest">
392
- No active credentials recorded
393
- </p>
394
- </div>
395
- )}
396
- </div>
397
- </div>
398
- </div>
399
- );
400
- }
@@ -1,224 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { api } from '../../lib/api';
3
- import { Button } from '../ui/button';
4
- import { Input } from '../ui/input';
5
- import { Label } from '../ui/label';
6
- import { Switch } from '../ui/switch';
7
- import {
8
- Loader2Icon,
9
- SaveIcon,
10
- LayoutIcon,
11
- GlobeIcon,
12
- SettingsIcon,
13
- ShieldAlertIcon,
14
- } from 'lucide-react';
15
- import { updateSettingsLocally } from '../../store/settings';
16
-
17
- export function GeneralSection() {
18
- const [settings, setSettings] = useState<any>({});
19
- const [loading, setLoading] = useState(true);
20
- const [saving, setSaving] = useState(false);
21
-
22
- useEffect(() => {
23
- const fetchSettings = async () => {
24
- try {
25
- const data = await api.get('/settings').json<any>();
26
- setSettings(data);
27
- } catch (err) {}
28
- setLoading(false);
29
- };
30
- fetchSettings();
31
- }, []);
32
-
33
- const handleSave = async (e?: React.FormEvent) => {
34
- if (e) e.preventDefault();
35
- setSaving(true);
36
- try {
37
- await api.patch('/api/settings', { json: settings });
38
- updateSettingsLocally(settings);
39
- } catch (err) {}
40
- setSaving(false);
41
- };
42
-
43
- const update = (key: string, value: string) => {
44
- setSettings((prev: any) => ({ ...prev, [key]: value }));
45
- };
46
-
47
- if (loading)
48
- return (
49
- <div className="flex items-center gap-2 text-muted-foreground">
50
- <Loader2Icon className="size-4 animate-spin" /> Retrieving site
51
- identity...
52
- </div>
53
- );
54
-
55
- return (
56
- <div className="space-y-12">
57
- <form onSubmit={handleSave} className="space-y-12">
58
- {/* Site Identity */}
59
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
60
- <div>
61
- <h3 className="text-sm font-bold uppercase tracking-widest text-foreground">
62
- Site Identity
63
- </h3>
64
- <p className="text-xs text-muted-foreground mt-2 leading-relaxed">
65
- Fundamental information used in headers, metadata, and browser
66
- tabs.
67
- </p>
68
- </div>
69
- <div className="md:col-span-2 space-y-6 bg-card border rounded-xl p-8 shadow-sm">
70
- <div className="grid gap-6 sm:grid-cols-2">
71
- <div className="grid gap-2">
72
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
73
- Site Title
74
- </Label>
75
- <Input
76
- value={settings['flare:site_title'] || ''}
77
- onChange={(e) => update('flare:site_title', e.target.value)}
78
- placeholder="My Awesome Blog"
79
- />
80
- </div>
81
- <div className="grid gap-2">
82
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
83
- Tagline
84
- </Label>
85
- <Input
86
- value={settings['flare:site_tagline'] || ''}
87
- onChange={(e) => update('flare:site_tagline', e.target.value)}
88
- placeholder="Thoughts on the digital world"
89
- />
90
- </div>
91
- </div>
92
- <div className="grid gap-2">
93
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
94
- Base URL
95
- </Label>
96
- <Input
97
- value={settings['flare:site_url'] || ''}
98
- onChange={(e) => update('flare:site_url', e.target.value)}
99
- placeholder="https://flarecms.dev"
100
- />
101
- <p className="text-[9px] text-muted-foreground italic">
102
- Important for canonical links and XML sitemaps.
103
- </p>
104
- </div>
105
- </div>
106
- </div>
107
-
108
- {/* Branding & Assets */}
109
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8 border-t pt-12">
110
- <div>
111
- <h3 className="text-sm font-bold uppercase tracking-widest text-foreground">
112
- Design Assets
113
- </h3>
114
- <p className="text-xs text-muted-foreground mt-2 leading-relaxed">
115
- Visual identity markers for your public site and administrative
116
- panel.
117
- </p>
118
- </div>
119
- <div className="md:col-span-2 space-y-6 bg-card border rounded-xl p-8 shadow-sm">
120
- <div className="grid gap-6">
121
- <div className="grid gap-2">
122
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
123
- Public Logo URL
124
- </Label>
125
- <Input
126
- value={settings['flare:site_logo'] || ''}
127
- onChange={(e) => update('flare:site_logo', e.target.value)}
128
- placeholder="https://cdn.com/logo.png"
129
- />
130
- </div>
131
- <div className="grid gap-2">
132
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
133
- Admin Panel Logo URL
134
- </Label>
135
- <Input
136
- value={settings['flare:admin_logo'] || ''}
137
- onChange={(e) => update('flare:admin_logo', e.target.value)}
138
- placeholder="https://cdn.com/admin-logo.png"
139
- />
140
- </div>
141
- <div className="grid gap-2">
142
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
143
- Favicon Shortcut URL
144
- </Label>
145
- <Input
146
- value={settings['flare:site_favicon'] || ''}
147
- onChange={(e) => update('flare:site_favicon', e.target.value)}
148
- placeholder="https://cdn.com/favicon.ico"
149
- />
150
- </div>
151
- </div>
152
- </div>
153
- </div>
154
-
155
- {/* System Operations */}
156
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8 border-t pt-12">
157
- <div>
158
- <h3 className="text-sm font-bold uppercase tracking-widest text-foreground">
159
- Operations
160
- </h3>
161
- <p className="text-xs text-muted-foreground mt-2 leading-relaxed">
162
- Site-wide status and SEO architecture configuration.
163
- </p>
164
- </div>
165
- <div className="md:col-span-2 space-y-6">
166
- <div className="bg-card border rounded-xl p-8 shadow-sm space-y-6">
167
- <div className="flex items-center justify-between">
168
- <div className="space-y-1">
169
- <p className="text-sm font-bold text-foreground">
170
- Maintenance Mode
171
- </p>
172
- <p className="text-[10px] text-muted-foreground/60">
173
- Take your site offline for users while you work on it.
174
- Admins still have access.
175
- </p>
176
- </div>
177
- <Switch
178
- checked={settings['flare:signup_enabled'] === 'true'}
179
- onCheckedChange={(checked: boolean) =>
180
- update('flare:signup_enabled', String(checked))
181
- }
182
- />
183
- </div>
184
-
185
- <div className="pt-6 border-t space-y-3">
186
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
187
- Site Title SEO Pattern
188
- </Label>
189
- <Input
190
- value={
191
- settings['flare:site_title_pattern'] ||
192
- '%title% | %site_name%'
193
- }
194
- onChange={(e) =>
195
- update('flare:site_title_pattern', e.target.value)
196
- }
197
- placeholder="%title% | %site_name%"
198
- />
199
- <p className="text-[9px] text-muted-foreground/60">
200
- Supported vars: %title%, %site_name%, %tagline%
201
- </p>
202
- </div>
203
- </div>
204
- </div>
205
- </div>
206
-
207
- <div className="flex justify-end sticky bottom-0 py-4 bg-background/80 backdrop-blur-sm border-t mt-12">
208
- <Button
209
- disabled={saving}
210
- size="sm"
211
- className="gap-2 font-bold uppercase tracking-widest text-[10px] h-10 px-6"
212
- >
213
- {saving ? (
214
- <Loader2Icon className="size-3 animate-spin" />
215
- ) : (
216
- <SaveIcon className="size-3" />
217
- )}
218
- Sync Site Identity
219
- </Button>
220
- </div>
221
- </form>
222
- </div>
223
- );
224
- }