flarecms 0.1.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 (110) hide show
  1. package/README.md +73 -0
  2. package/dist/auth/index.js +40 -0
  3. package/dist/cli/commands.js +389 -0
  4. package/dist/cli/index.js +403 -0
  5. package/dist/cli/mcp.js +209 -0
  6. package/dist/db/index.js +164 -0
  7. package/dist/index.js +17626 -0
  8. package/package.json +105 -0
  9. package/scripts/fix-api-paths.mjs +32 -0
  10. package/scripts/fix-imports.mjs +38 -0
  11. package/scripts/prefix-css.mjs +45 -0
  12. package/src/api/lib/cache.ts +45 -0
  13. package/src/api/lib/response.ts +40 -0
  14. package/src/api/middlewares/auth.ts +186 -0
  15. package/src/api/middlewares/cors.ts +10 -0
  16. package/src/api/middlewares/rbac.ts +85 -0
  17. package/src/api/routes/auth.ts +377 -0
  18. package/src/api/routes/collections.ts +205 -0
  19. package/src/api/routes/content.ts +175 -0
  20. package/src/api/routes/device.ts +160 -0
  21. package/src/api/routes/magic.ts +150 -0
  22. package/src/api/routes/mcp.ts +273 -0
  23. package/src/api/routes/oauth.ts +160 -0
  24. package/src/api/routes/settings.ts +43 -0
  25. package/src/api/routes/setup.ts +307 -0
  26. package/src/api/routes/tokens.ts +80 -0
  27. package/src/api/schemas/auth.ts +15 -0
  28. package/src/api/schemas/index.ts +51 -0
  29. package/src/api/schemas/tokens.ts +24 -0
  30. package/src/auth/index.ts +28 -0
  31. package/src/cli/commands.ts +217 -0
  32. package/src/cli/index.ts +21 -0
  33. package/src/cli/mcp.ts +210 -0
  34. package/src/cli/tests/cli.test.ts +40 -0
  35. package/src/cli/tests/create.test.ts +87 -0
  36. package/src/client/FlareAdminRouter.tsx +47 -0
  37. package/src/client/app.tsx +175 -0
  38. package/src/client/components/app-sidebar.tsx +227 -0
  39. package/src/client/components/collection-modal.tsx +215 -0
  40. package/src/client/components/content-list.tsx +247 -0
  41. package/src/client/components/dynamic-form.tsx +190 -0
  42. package/src/client/components/field-modal.tsx +221 -0
  43. package/src/client/components/settings/api-token-section.tsx +400 -0
  44. package/src/client/components/settings/general-section.tsx +224 -0
  45. package/src/client/components/settings/security-section.tsx +154 -0
  46. package/src/client/components/settings/seo-section.tsx +200 -0
  47. package/src/client/components/settings/signup-section.tsx +257 -0
  48. package/src/client/components/ui/accordion.tsx +78 -0
  49. package/src/client/components/ui/avatar.tsx +107 -0
  50. package/src/client/components/ui/badge.tsx +52 -0
  51. package/src/client/components/ui/button.tsx +60 -0
  52. package/src/client/components/ui/card.tsx +103 -0
  53. package/src/client/components/ui/checkbox.tsx +27 -0
  54. package/src/client/components/ui/collapsible.tsx +19 -0
  55. package/src/client/components/ui/dialog.tsx +162 -0
  56. package/src/client/components/ui/icon-picker.tsx +485 -0
  57. package/src/client/components/ui/icons-data.ts +8476 -0
  58. package/src/client/components/ui/input.tsx +20 -0
  59. package/src/client/components/ui/label.tsx +20 -0
  60. package/src/client/components/ui/popover.tsx +91 -0
  61. package/src/client/components/ui/select.tsx +204 -0
  62. package/src/client/components/ui/separator.tsx +23 -0
  63. package/src/client/components/ui/sheet.tsx +141 -0
  64. package/src/client/components/ui/sidebar.tsx +722 -0
  65. package/src/client/components/ui/skeleton.tsx +13 -0
  66. package/src/client/components/ui/sonner.tsx +47 -0
  67. package/src/client/components/ui/switch.tsx +30 -0
  68. package/src/client/components/ui/table.tsx +116 -0
  69. package/src/client/components/ui/tabs.tsx +80 -0
  70. package/src/client/components/ui/textarea.tsx +18 -0
  71. package/src/client/components/ui/tooltip.tsx +68 -0
  72. package/src/client/hooks/use-mobile.ts +19 -0
  73. package/src/client/index.css +149 -0
  74. package/src/client/index.ts +7 -0
  75. package/src/client/layouts/admin-layout.tsx +93 -0
  76. package/src/client/layouts/settings-layout.tsx +104 -0
  77. package/src/client/lib/api.ts +72 -0
  78. package/src/client/lib/utils.ts +6 -0
  79. package/src/client/main.tsx +10 -0
  80. package/src/client/pages/collection-detail.tsx +634 -0
  81. package/src/client/pages/collections.tsx +180 -0
  82. package/src/client/pages/dashboard.tsx +133 -0
  83. package/src/client/pages/device.tsx +66 -0
  84. package/src/client/pages/document-detail-page.tsx +139 -0
  85. package/src/client/pages/documents-page.tsx +103 -0
  86. package/src/client/pages/login.tsx +345 -0
  87. package/src/client/pages/settings.tsx +65 -0
  88. package/src/client/pages/setup.tsx +129 -0
  89. package/src/client/pages/signup.tsx +188 -0
  90. package/src/client/store/auth.ts +30 -0
  91. package/src/client/store/collections.ts +13 -0
  92. package/src/client/store/config.ts +12 -0
  93. package/src/client/store/fetcher.ts +30 -0
  94. package/src/client/store/router.ts +95 -0
  95. package/src/client/store/schema.ts +39 -0
  96. package/src/client/store/settings.ts +31 -0
  97. package/src/client/types.ts +34 -0
  98. package/src/db/dynamic.ts +70 -0
  99. package/src/db/index.ts +16 -0
  100. package/src/db/migrations/001_initial_schema.ts +57 -0
  101. package/src/db/migrations/002_auth_tables.ts +84 -0
  102. package/src/db/migrator.ts +61 -0
  103. package/src/db/schema.ts +142 -0
  104. package/src/index.ts +12 -0
  105. package/src/server/index.ts +66 -0
  106. package/src/types.ts +20 -0
  107. package/style.css.d.ts +8 -0
  108. package/tests/css.test.ts +21 -0
  109. package/tests/modular.test.ts +29 -0
  110. package/tsconfig.json +10 -0
@@ -0,0 +1,175 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useStore } from '@nanostores/react';
3
+ import { $router, navigate } from './store/router';
4
+ import { $auth } from './store/auth';
5
+ import { $basePath } from './store/config';
6
+ import { $collections } from './store/collections';
7
+ import { loadSettings } from './store/settings';
8
+ import { apiFetch } from './lib/api';
9
+
10
+ import { AdminLayout } from './layouts/admin-layout';
11
+ import { LoginPage } from './pages/login';
12
+ import { SetupPage } from './pages/setup';
13
+ import { SignupPage } from './pages/signup';
14
+ import { DashboardPage } from './pages/dashboard';
15
+ import { CollectionsPage } from './pages/collections';
16
+ import { CollectionDetailPage } from './pages/collection-detail';
17
+ import { DevicePage } from './pages/device';
18
+ import { SettingsPage } from './pages/settings';
19
+ import { DocumentsPage } from './pages/documents-page';
20
+ import { DocumentDetailPage } from './pages/document-detail-page';
21
+ import { TooltipProvider } from './components/ui/tooltip';
22
+
23
+ export default function App() {
24
+ const page = useStore($router);
25
+ const auth = useStore($auth);
26
+ const collections = useStore($collections);
27
+ const [booting, setBooting] = useState(true);
28
+
29
+ useEffect(() => {
30
+ async function checkSetup() {
31
+ try {
32
+ // Initial check to trigger Zero-Config redirect if needed
33
+ await apiFetch('/health');
34
+ // Load global settings
35
+ await loadSettings();
36
+ } catch (err) {
37
+ console.error('Initial setup check failed:', err);
38
+ } finally {
39
+ setBooting(false);
40
+ }
41
+ }
42
+ checkSetup();
43
+ }, []);
44
+
45
+ if (booting) {
46
+ return (
47
+ <div className="min-h-screen bg-background flex flex-col items-center justify-center p-6 antialiased">
48
+ <div className="size-16 bg-muted/30 rounded-2xl flex items-center justify-center mb-8 border border-border/50 shadow-sm relative overflow-hidden group">
49
+ <div className="absolute inset-0 bg-gradient-to-br from-primary/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
50
+ <div className="size-8 bg-primary/10 rounded-xl flex items-center justify-center border border-primary/20 shadow-inner animate-pulse">
51
+ <svg
52
+ xmlns="http://www.w3.org/2000/svg"
53
+ width="16"
54
+ height="16"
55
+ viewBox="0 0 24 24"
56
+ fill="none"
57
+ stroke="currentColor"
58
+ strokeWidth="2.5"
59
+ strokeLinecap="round"
60
+ strokeLinejoin="round"
61
+ className="text-primary opacity-60"
62
+ >
63
+ <path d="M12 2v20" />
64
+ <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
65
+ </svg>
66
+ </div>
67
+ </div>
68
+ <div className="text-center space-y-2">
69
+ <h2 className="text-[10px] font-black uppercase tracking-[0.4em] text-foreground/40 animate-pulse">
70
+ System Synchronizing
71
+ </h2>
72
+ <p className="text-[9px] font-semibold text-muted-foreground/30 uppercase tracking-[0.2em]">
73
+ Establishing Secure Link
74
+ </p>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ // If setup route is active, bypass Auth Guard
81
+ if (page?.route === 'setup') {
82
+ return (
83
+ <TooltipProvider>
84
+ <SetupPage />
85
+ </TooltipProvider>
86
+ );
87
+ }
88
+
89
+ // Auth Guard: If no token, show Login or Signup based on route
90
+ if (!auth.token) {
91
+ return (
92
+ <TooltipProvider>
93
+ {page?.route === 'signup' ? <SignupPage /> : <LoginPage />}
94
+ </TooltipProvider>
95
+ );
96
+ }
97
+
98
+ // Router Logic for authenticated users
99
+ if (!page) {
100
+ return (
101
+ <TooltipProvider>
102
+ <div className="min-h-screen bg-background flex items-center justify-center text-muted-foreground p-6">
103
+ <div className="text-center max-w-sm">
104
+ <h1 className="text-6xl font-black mb-4 tracking-tighter text-foreground opacity-10">
105
+ 404
106
+ </h1>
107
+ <p className="text-[10px] font-semibold uppercase tracking-[0.2em] mb-8">
108
+ Document Module Not Found
109
+ </p>
110
+ <button
111
+ onClick={() => navigate('home')}
112
+ className="px-4 py-2 border rounded-md text-xs font-semibold uppercase tracking-widest hover:bg-accent transition-colors"
113
+ >
114
+ Return to Dashboard
115
+ </button>
116
+ </div>
117
+ </div>
118
+ </TooltipProvider>
119
+ );
120
+ }
121
+
122
+ // Admin pages wrapped in shared Layout
123
+ return (
124
+ <TooltipProvider>
125
+ <AdminLayout>
126
+ {page.route === 'home' && <DashboardPage />}
127
+ {page.route === 'collections' && <CollectionsPage />}
128
+ {page.route === 'device' && <DevicePage />}
129
+ {page.route?.startsWith?.('settings') && <SettingsPage />}
130
+
131
+ {page.route === 'collection' && (
132
+ <CollectionDetailPage
133
+ id={page.params.id}
134
+ slug={page.params.slug}
135
+ onBack={() => navigate('collections')}
136
+ />
137
+ )}
138
+
139
+ {page.route === 'document_list' && <DocumentsPage />}
140
+ {page.route === 'document_edit' && <DocumentDetailPage />}
141
+
142
+ {/* Login route while already authenticated -> redirect to home via layout logic or just show dashboard */}
143
+ {page.route === 'login' && <DashboardPage />}
144
+
145
+ {![
146
+ 'home',
147
+ 'collections',
148
+ 'collection',
149
+ 'document_list',
150
+ 'document_edit',
151
+ 'login',
152
+ 'setup',
153
+ 'device',
154
+ 'settings',
155
+ 'settings_general',
156
+ 'settings_seo',
157
+ 'settings_security',
158
+ 'settings_signup',
159
+ ].includes(page.route) && (
160
+ <div className="flex flex-col items-center justify-center py-40">
161
+ <div className="size-16 bg-muted rounded-xl flex items-center justify-center mb-8 border shadow-sm">
162
+ <div className="size-6 bg-primary/20 rounded-lg animate-pulse" />
163
+ </div>
164
+ <h2 className="text-2xl font-bold text-foreground tracking-tight">
165
+ Module Isolation
166
+ </h2>
167
+ <p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-[0.3em] mt-3">
168
+ Feature currently offline
169
+ </p>
170
+ </div>
171
+ )}
172
+ </AdminLayout>
173
+ </TooltipProvider>
174
+ );
175
+ }
@@ -0,0 +1,227 @@
1
+ import {
2
+ LayoutDashboard as LayoutDashboardIcon,
3
+ Database as DatabaseIcon,
4
+ Users as UsersIcon,
5
+ Settings as SettingsIcon,
6
+ FileText as FileTextIcon,
7
+ Layers as LayersIcon,
8
+ MessageSquare as MessageSquareIcon,
9
+ Menu as MenuIcon,
10
+ LogOut as LogOutIcon,
11
+ Sparkles as SparklesIcon,
12
+ } from 'lucide-react';
13
+
14
+ import {
15
+ Sidebar,
16
+ SidebarContent,
17
+ SidebarFooter,
18
+ SidebarHeader,
19
+ SidebarGroup,
20
+ SidebarGroupLabel,
21
+ SidebarGroupContent,
22
+ SidebarMenu,
23
+ SidebarMenuItem,
24
+ SidebarMenuButton,
25
+ SidebarRail,
26
+ } from './ui/sidebar';
27
+ import { $router, navigate } from '../store/router';
28
+ import { useStore } from '@nanostores/react';
29
+ import { $auth, logout } from '../store/auth';
30
+ import { Avatar, AvatarFallback } from './ui/avatar';
31
+ import {
32
+ Tooltip,
33
+ TooltipContent,
34
+ TooltipTrigger
35
+ } from './ui/tooltip';
36
+ import { useSidebar } from './ui/sidebar';
37
+
38
+ import { $settings } from '../store/settings';
39
+
40
+ import {
41
+ Collapsible,
42
+
43
+ CollapsibleContent,
44
+ CollapsibleTrigger,
45
+ } from './ui/collapsible';
46
+ import { ChevronRight } from 'lucide-react';
47
+
48
+ import { $collections } from '../store/collections';
49
+
50
+ import { Icon } from './ui/icon-picker';
51
+
52
+ interface AppSidebarProps {
53
+ variant?: 'sidebar' | 'floating' | 'inset';
54
+ }
55
+
56
+ export function AppSidebar({ variant = 'sidebar' }: AppSidebarProps) {
57
+ const page = useStore($router);
58
+ const auth = useStore($auth);
59
+ const settings = useStore($settings);
60
+ const { data: collections } = useStore($collections);
61
+
62
+ const menuGroups = [
63
+ {
64
+ label: 'Content',
65
+ items: (collections || []).map((col) => ({
66
+ label: col.label,
67
+ icon: col.slug === 'pages' ? FileTextIcon : () => <Icon name={col.icon as any} />,
68
+ routeName: 'document_list',
69
+ params: { slug: col.slug },
70
+ })),
71
+ },
72
+ {
73
+ label: 'Manage',
74
+ items: [
75
+ {
76
+ label: 'Collections',
77
+ icon: DatabaseIcon,
78
+ routeName: 'collections',
79
+ active: page?.route === 'collections',
80
+ },
81
+ ],
82
+ },
83
+ {
84
+ label: 'Admin',
85
+ items: [
86
+ {
87
+ label: 'Users',
88
+ icon: UsersIcon,
89
+ routeName: 'users',
90
+ active: page?.route === 'users',
91
+ },
92
+ {
93
+ label: 'Settings',
94
+ icon: SettingsIcon,
95
+ routeName: 'settings',
96
+ active: page?.route === 'settings',
97
+ },
98
+ ],
99
+ },
100
+ ];
101
+
102
+ return (
103
+ <Sidebar collapsible="icon" variant={variant}>
104
+ <SidebarHeader className="flex flex-row items-center px-4 py-6 gap-3 group-data-[collapsible=icon]:px-0 group-data-[collapsible=icon]:justify-center transition-[padding,justify-content]">
105
+ <Tooltip>
106
+ <TooltipTrigger render={<div className="flex size-8 items-center justify-center rounded-md bg-primary text-primary-foreground shrink-0 shadow-sm cursor-pointer" />}>
107
+ <SparklesIcon className="size-4" />
108
+ </TooltipTrigger>
109
+ <TooltipContent side="right" className="group-data-[collapsible=expanded]:hidden">
110
+ {settings['flare:site_title'] || 'FlareCMS'}
111
+ </TooltipContent>
112
+ </Tooltip>
113
+ <div className="flex flex-col gap-0.5 group-data-[collapsible=icon]:hidden">
114
+ <span className="font-semibold text-sidebar-foreground tracking-tight leading-none">
115
+ {settings['flare:site_title'] || 'FlareCMS'}
116
+ </span>
117
+ <span className="text-[10px] text-muted-foreground font-medium uppercase tracking-wider">
118
+ {settings['flare:site_tagline'] || 'Management'}
119
+ </span>
120
+ </div>
121
+ </SidebarHeader>
122
+
123
+ <SidebarContent className="px-2">
124
+ <SidebarGroup>
125
+ <SidebarMenu>
126
+ <SidebarMenuItem>
127
+ <SidebarMenuButton
128
+ isActive={page?.route === 'home'}
129
+ onClick={() => navigate('home')}
130
+ tooltip="Dashboard"
131
+ >
132
+ <LayoutDashboardIcon />
133
+ <span>Dashboard</span>
134
+ </SidebarMenuButton>
135
+ </SidebarMenuItem>
136
+ </SidebarMenu>
137
+ </SidebarGroup>
138
+
139
+ {menuGroups.map((group) => (
140
+ <Collapsible
141
+ key={group.label}
142
+ defaultOpen
143
+ className="group/collapsible"
144
+ >
145
+ <SidebarGroup>
146
+ <SidebarGroupLabel
147
+ render={
148
+ <CollapsibleTrigger className="group/trigger flex w-full items-center">
149
+ {group.label}
150
+ <ChevronRight className="ml-auto size-3.5 transition-transform duration-200 group-data-[state=open]/trigger:rotate-90" />
151
+ </CollapsibleTrigger>
152
+ }
153
+ className="group/label text-xs font-semibold uppercase tracking-wider text-muted-foreground/70 hover:text-foreground transition-colors cursor-pointer group-data-[collapsible=icon]:hidden"
154
+ />
155
+ <CollapsibleContent>
156
+ <SidebarGroupContent>
157
+ <SidebarMenu>
158
+ {group.items.map((item) => (
159
+ <SidebarMenuItem key={item.label}>
160
+ <SidebarMenuButton
161
+ isActive={
162
+ (item as any).active || page?.route === item.routeName
163
+ }
164
+ onClick={() => navigate(item.routeName, (item as any).params)}
165
+ tooltip={item.label}
166
+ >
167
+ <item.icon />
168
+ <span>{item.label}</span>
169
+ </SidebarMenuButton>
170
+ </SidebarMenuItem>
171
+ ))}
172
+ </SidebarMenu>
173
+ </SidebarGroupContent>
174
+ </CollapsibleContent>
175
+ </SidebarGroup>
176
+ </Collapsible>
177
+ ))}
178
+ </SidebarContent>
179
+
180
+ <SidebarFooter className="p-4 group-data-[collapsible=icon]:px-0 group-data-[collapsible=icon]:py-4 transition-[padding]">
181
+ <div className="flex items-center gap-3 group-data-[collapsible=icon]:flex-col group-data-[collapsible=icon]:gap-4">
182
+ <Tooltip>
183
+ <TooltipTrigger render={<Avatar className="size-8 rounded-lg shrink-0 cursor-pointer" />}>
184
+ <AvatarFallback className="text-[10px] font-semibold">
185
+ {auth.user?.email.substring(0, 2).toUpperCase()}
186
+ </AvatarFallback>
187
+ </TooltipTrigger>
188
+ <TooltipContent side="right" className="group-data-[collapsible=expanded]:hidden">
189
+ {auth.user?.email}
190
+ </TooltipContent>
191
+ </Tooltip>
192
+
193
+ <div className="flex-1 min-w-0 overflow-hidden group-data-[collapsible=icon]:hidden">
194
+ <p className="text-xs font-medium truncate leading-none mb-1 text-sidebar-foreground">
195
+ {auth.user?.email.split('@')[0]}
196
+ </p>
197
+ <button
198
+ onClick={() => logout()}
199
+ className="text-[10px] text-muted-foreground hover:text-primary transition-colors font-medium"
200
+ >
201
+ Sign out
202
+ </button>
203
+ </div>
204
+
205
+ <SidebarMenu className="w-fit group-data-[collapsible=expanded]:block hidden">
206
+ <SidebarMenuItem>
207
+ <SidebarMenuButton size="sm" onClick={() => logout()} tooltip="Sign out" className="text-muted-foreground/50 hover:text-destructive">
208
+ <LogOutIcon />
209
+ </SidebarMenuButton>
210
+ </SidebarMenuItem>
211
+ </SidebarMenu>
212
+
213
+ <div className="group-data-[collapsible=icon]:block hidden">
214
+ <SidebarMenu>
215
+ <SidebarMenuItem>
216
+ <SidebarMenuButton size="sm" onClick={() => logout()} tooltip="Sign out" className="text-muted-foreground/50 hover:text-destructive">
217
+ <LogOutIcon />
218
+ </SidebarMenuButton>
219
+ </SidebarMenuItem>
220
+ </SidebarMenu>
221
+ </div>
222
+ </div>
223
+ </SidebarFooter>
224
+ <SidebarRail />
225
+ </Sidebar>
226
+ );
227
+ }
@@ -0,0 +1,215 @@
1
+ import React, { useState } from 'react';
2
+ import { Loader2Icon, ChevronRightIcon, SparklesIcon } from 'lucide-react';
3
+ import { api } from '../lib/api';
4
+
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ DialogTrigger,
11
+ } from './ui/dialog';
12
+ import { Button } from './ui/button';
13
+ import { Input } from './ui/input';
14
+ import { Label } from './ui/label';
15
+ import { IconPicker, Icon } from './ui/icon-picker';
16
+ import { Switch } from './ui/switch';
17
+ import { cn } from '../lib/utils';
18
+
19
+ interface CollectionModalProps {
20
+ children: React.ReactElement;
21
+ }
22
+
23
+ export function CollectionModal({ children }: CollectionModalProps) {
24
+ const [isOpen, setIsOpen] = useState(false);
25
+ const [loading, setLoading] = useState(false);
26
+ const [data, setData] = useState({
27
+ slug: '',
28
+ label: '',
29
+ labelSingular: '',
30
+ description: '',
31
+ icon: 'package' as any,
32
+ isPublic: false,
33
+ });
34
+
35
+ const [isSlugDirty, setIsSlugDirty] = useState(false);
36
+ const [isSingularDirty, setIsSingularDirty] = useState(false);
37
+
38
+ const handleSubmit = async (e: React.FormEvent) => {
39
+ e.preventDefault();
40
+ setLoading(true);
41
+
42
+ try {
43
+ await api.post('/collections', { json: data });
44
+ setIsOpen(false);
45
+ setData({
46
+ slug: '',
47
+ label: '',
48
+ labelSingular: '',
49
+ description: '',
50
+ icon: 'package',
51
+ isPublic: false,
52
+ });
53
+ window.location.reload();
54
+ } catch (err) {
55
+ console.error('Failed to create collection:', err);
56
+ } finally {
57
+ setLoading(false);
58
+ }
59
+ };
60
+
61
+ const update = (key: string, value: any) => {
62
+ setData((prev) => {
63
+ const newData = { ...prev, [key]: value };
64
+
65
+ if (key === 'label') {
66
+ if (!isSlugDirty) {
67
+ newData.slug = value.toLowerCase().replace(/[^a-z0-9]+/g, '-');
68
+ }
69
+
70
+ if (!isSingularDirty) {
71
+ newData.labelSingular = value.replace(/s$/i, '') || value;
72
+ }
73
+ }
74
+
75
+ return newData;
76
+ });
77
+ };
78
+
79
+ return (
80
+ <Dialog open={isOpen} onOpenChange={setIsOpen}>
81
+ <DialogTrigger render={children} />
82
+ <DialogContent className="sm:max-w-[700px] p-0 overflow-hidden border-none shadow-2xl">
83
+ <form onSubmit={handleSubmit}>
84
+ {/* Header Section */}
85
+ <div className="bg-linear-to-b from-muted/50 to-background px-8 pt-8 pb-6 border-b">
86
+ <div className="flex items-center gap-2 mb-3">
87
+ <div className="bg-primary/10 text-primary px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-widest flex items-center gap-1.5">
88
+ <SparklesIcon className="size-3" />
89
+ Infrastructure
90
+ </div>
91
+ </div>
92
+ <DialogTitle className="text-2xl font-semibold tracking-tight">
93
+ Create Collection
94
+ </DialogTitle>
95
+ <p className="text-muted-foreground text-xs mt-1.5">
96
+ Define the blueprint for your new high-performance data module.
97
+ </p>
98
+ </div>
99
+
100
+ <div className="p-8 space-y-6">
101
+ <div className="grid grid-cols-2 gap-4">
102
+ <div className="space-y-2.5">
103
+ <Label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/70">
104
+ Display Label
105
+ </Label>
106
+ <Input
107
+ value={data.label}
108
+ onChange={(e) => update('label', e.target.value)}
109
+ placeholder="Blog Posts"
110
+ required
111
+ className="h-10 font-medium"
112
+ />
113
+ </div>
114
+ <div className="space-y-2.5">
115
+ <Label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/70">
116
+ Singular Label
117
+ </Label>
118
+ <Input
119
+ value={data.labelSingular}
120
+ onChange={(e) => {
121
+ setIsSingularDirty(true);
122
+ update('labelSingular', e.target.value);
123
+ }}
124
+ placeholder="Post"
125
+ className="h-10 font-medium"
126
+ />
127
+ </div>
128
+ </div>
129
+
130
+ <div className="space-y-2.5">
131
+ <Label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/70">
132
+ Identification Slug
133
+ </Label>
134
+ <div className="relative group">
135
+ <Input
136
+ value={data.slug}
137
+ onChange={(e) => {
138
+ setIsSlugDirty(true);
139
+ update('slug', e.target.value);
140
+ }}
141
+ placeholder="blog-posts"
142
+ required
143
+ className="h-10 font-mono text-xs pl-8 border-dashed group-focus-within:border-solid transition-all"
144
+ />
145
+ <span className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground/40 font-mono text-xs">
146
+ /
147
+ </span>
148
+ </div>
149
+ </div>
150
+
151
+ <div className="grid grid-cols-2 gap-8 pt-2">
152
+ <div className="space-y-2.5">
153
+ <Label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/70">
154
+ Module Icon
155
+ </Label>
156
+ <IconPicker
157
+ value={data.icon}
158
+ onValueChange={(v) => update('icon', v)}
159
+ />
160
+ </div>
161
+ <div className="space-y-2.5 flex flex-col justify-center">
162
+ <Label className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/70 mb-1.5">
163
+ Access Policy
164
+ </Label>
165
+ <div
166
+ className={cn(
167
+ 'flex items-center justify-between gap-3 px-3 py-2 rounded-lg border transition-all duration-300',
168
+ data.isPublic
169
+ ? 'bg-primary/5 border-primary/20'
170
+ : 'bg-muted/30 border-border',
171
+ )}
172
+ >
173
+ <div className="flex flex-col">
174
+ <span className="text-[10px] font-bold uppercase tracking-tighter">
175
+ {data.isPublic ? 'Public API' : 'Private API'}
176
+ </span>
177
+ <span className="text-[9px] text-muted-foreground leading-none">
178
+ {data.isPublic ? 'Open to everyone' : 'Requires key'}
179
+ </span>
180
+ </div>
181
+ <Switch
182
+ checked={data.isPublic}
183
+ onCheckedChange={(checked) => update('isPublic', checked)}
184
+ />
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+
190
+ {/* Footer */}
191
+ <div className="px-8 py-6 bg-background/80 backdrop-blur-md border-t flex justify-end gap-3 sticky bottom-0">
192
+ <Button
193
+ type="button"
194
+ variant="ghost"
195
+ className="font-semibold text-xs h-10 px-6"
196
+ onClick={() => setIsOpen(false)}
197
+ >
198
+ Discard
199
+ </Button>
200
+ <Button
201
+ type="submit"
202
+ disabled={loading}
203
+ className="font-bold text-xs h-10 px-8 shadow-lg shadow-primary/20"
204
+ >
205
+ {loading ? (
206
+ <Loader2Icon className="size-4 animate-spin mr-2" />
207
+ ) : null}
208
+ Initialize Collection
209
+ </Button>
210
+ </div>
211
+ </form>
212
+ </DialogContent>
213
+ </Dialog>
214
+ );
215
+ }