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,485 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { useState, useMemo, useCallback, useEffect } from 'react';
5
+ import {
6
+ Popover,
7
+ PopoverContent,
8
+ PopoverTrigger,
9
+ } from './popover';
10
+ import { Button } from './button';
11
+ import { Input } from './input';
12
+ import { cn } from '../../lib/utils';
13
+ import type { LucideProps, LucideIcon } from 'lucide-react';
14
+ import {
15
+ DynamicIcon,
16
+ dynamicIconImports,
17
+ type IconName,
18
+ } from 'lucide-react/dynamic';
19
+ import {
20
+ Tooltip,
21
+ TooltipContent,
22
+ TooltipProvider,
23
+ TooltipTrigger,
24
+ } from './tooltip';
25
+ import { iconsData } from './icons-data';
26
+ import { useVirtualizer, type VirtualItem } from '@tanstack/react-virtual';
27
+ import { Skeleton } from './skeleton';
28
+ import Fuse from 'fuse.js';
29
+ import { useDebounceValue } from 'usehooks-ts';
30
+
31
+ export type IconData = (typeof iconsData)[number];
32
+
33
+ interface IconPickerProps extends Omit<
34
+ React.ComponentPropsWithoutRef<typeof PopoverTrigger>,
35
+ 'onSelect' | 'onOpenChange'
36
+ > {
37
+ value?: IconName;
38
+ defaultValue?: IconName;
39
+ onValueChange?: (value: IconName) => void;
40
+ open?: boolean;
41
+ defaultOpen?: boolean;
42
+ onOpenChange?: (open: boolean) => void;
43
+ searchable?: boolean;
44
+ searchPlaceholder?: string;
45
+ triggerPlaceholder?: string;
46
+ iconsList?: IconData[];
47
+ categorized?: boolean;
48
+ modal?: boolean;
49
+ }
50
+
51
+ const IconRenderer = React.memo(({ name }: { name: IconName }) => {
52
+ return <Icon name={name} />;
53
+ });
54
+ IconRenderer.displayName = 'IconRenderer';
55
+
56
+ const IconsColumnSkeleton = () => {
57
+ return (
58
+ <div className="flex flex-col gap-2 w-full">
59
+ <Skeleton className="h-4 w-1/2 rounded-md" />
60
+ <div className="grid grid-cols-5 gap-2 w-full">
61
+ {Array.from({ length: 40 }).map((_, i) => (
62
+ <Skeleton key={i} className="h-10 w-10 rounded-md" />
63
+ ))}
64
+ </div>
65
+ </div>
66
+ );
67
+ };
68
+
69
+ const useIconsData = () => {
70
+ const [icons, setIcons] = useState<IconData[]>([]);
71
+ const [isLoading, setIsLoading] = useState(true);
72
+
73
+ useEffect(() => {
74
+ let isMounted = true;
75
+
76
+ const loadIcons = async () => {
77
+ setIsLoading(true);
78
+
79
+ const { iconsData } = await import('./icons-data');
80
+ if (isMounted) {
81
+ setIcons(
82
+ iconsData.filter((icon: IconData) => {
83
+ return icon.name in dynamicIconImports;
84
+ }),
85
+ );
86
+ setIsLoading(false);
87
+ }
88
+ };
89
+
90
+ loadIcons();
91
+
92
+ return () => {
93
+ isMounted = false;
94
+ };
95
+ }, []);
96
+
97
+ return { icons, isLoading };
98
+ };
99
+
100
+ const IconPicker = React.forwardRef<
101
+ React.ComponentRef<typeof PopoverTrigger>,
102
+ IconPickerProps
103
+ >(
104
+ (
105
+ {
106
+ value,
107
+ defaultValue,
108
+ onValueChange,
109
+ open,
110
+ defaultOpen,
111
+ onOpenChange,
112
+ children,
113
+ searchable = true,
114
+ searchPlaceholder = 'Search for an icon...',
115
+ triggerPlaceholder = 'Select an icon',
116
+ iconsList,
117
+ categorized = true,
118
+ modal = false,
119
+ ...props
120
+ },
121
+ ref,
122
+ ) => {
123
+ const [selectedIcon, setSelectedIcon] = useState<IconName | undefined>(
124
+ defaultValue,
125
+ );
126
+ const [isOpen, setIsOpen] = useState(defaultOpen || false);
127
+ const [search, setSearch] = useDebounceValue('', 100);
128
+ const [isPopoverVisible, setIsPopoverVisible] = useState(false);
129
+ const { icons } = useIconsData();
130
+ const [isLoading, setIsLoading] = useState(true);
131
+
132
+ const iconsToUse = useMemo(() => iconsList || icons, [iconsList, icons]);
133
+
134
+ const fuseInstance = useMemo(() => {
135
+ return new Fuse(iconsToUse, {
136
+ keys: ['name', 'tags', 'categories'],
137
+ threshold: 0.3,
138
+ ignoreLocation: true,
139
+ includeScore: true,
140
+ });
141
+ }, [iconsToUse]);
142
+
143
+ const filteredIcons = useMemo(() => {
144
+ if (search.trim() === '') {
145
+ return iconsToUse;
146
+ }
147
+
148
+ const results = fuseInstance.search(search.toLowerCase().trim());
149
+ return results.map((result) => result.item);
150
+ }, [search, iconsToUse, fuseInstance]);
151
+
152
+ const categorizedIcons = useMemo(() => {
153
+ if (!categorized || search.trim() !== '') {
154
+ return [{ name: 'All Icons', icons: filteredIcons }];
155
+ }
156
+
157
+ const categories = new Map<string, IconData[]>();
158
+
159
+ filteredIcons.forEach((icon) => {
160
+ if (icon.categories && icon.categories.length > 0) {
161
+ icon.categories.forEach((category) => {
162
+ if (!categories.has(category)) {
163
+ categories.set(category, []);
164
+ }
165
+ categories.get(category)!.push(icon);
166
+ });
167
+ } else {
168
+ const category = 'Other';
169
+ if (!categories.has(category)) {
170
+ categories.set(category, []);
171
+ }
172
+ categories.get(category)!.push(icon);
173
+ }
174
+ });
175
+
176
+ return Array.from(categories.entries())
177
+ .map(([name, icons]) => ({ name, icons }))
178
+ .sort((a, b) => a.name.localeCompare(b.name));
179
+ }, [filteredIcons, categorized, search]);
180
+
181
+ const virtualItems = useMemo(() => {
182
+ const items: Array<{
183
+ type: 'category' | 'row';
184
+ categoryIndex: number;
185
+ rowIndex?: number;
186
+ icons?: IconData[];
187
+ }> = [];
188
+
189
+ categorizedIcons.forEach((category, categoryIndex) => {
190
+ items.push({ type: 'category', categoryIndex });
191
+
192
+ const rows = [];
193
+ for (let i = 0; i < category.icons.length; i += 5) {
194
+ rows.push(category.icons.slice(i, i + 5));
195
+ }
196
+
197
+ rows.forEach((rowIcons, rowIndex) => {
198
+ items.push({
199
+ type: 'row',
200
+ categoryIndex,
201
+ rowIndex,
202
+ icons: rowIcons,
203
+ });
204
+ });
205
+ });
206
+
207
+ return items;
208
+ }, [categorizedIcons]);
209
+
210
+ const categoryIndices = useMemo(() => {
211
+ const indices: Record<string, number> = {};
212
+
213
+ virtualItems.forEach((item, index) => {
214
+ if (item.type === 'category') {
215
+ indices[categorizedIcons[item.categoryIndex]!.name] = index;
216
+ }
217
+ });
218
+
219
+ return indices;
220
+ }, [virtualItems, categorizedIcons]);
221
+
222
+ const parentRef = React.useRef<HTMLDivElement>(null);
223
+
224
+ const virtualizer = useVirtualizer({
225
+ count: virtualItems.length,
226
+ getScrollElement: () => parentRef.current,
227
+ estimateSize: (index) =>
228
+ virtualItems[index]?.type === 'category' ? 25 : 40,
229
+ paddingEnd: 2,
230
+ gap: 10,
231
+ overscan: 5,
232
+ });
233
+
234
+ const handleValueChange = useCallback(
235
+ (icon: IconName) => {
236
+ if (value === undefined) {
237
+ setSelectedIcon(icon);
238
+ }
239
+ onValueChange?.(icon);
240
+ },
241
+ [value, onValueChange],
242
+ );
243
+
244
+ const handleOpenChange = useCallback(
245
+ (newOpen: boolean) => {
246
+ setSearch('');
247
+ if (open === undefined) {
248
+ setIsOpen(newOpen);
249
+ }
250
+ onOpenChange?.(newOpen);
251
+
252
+ setIsPopoverVisible(newOpen);
253
+
254
+ if (newOpen) {
255
+ setTimeout(() => {
256
+ virtualizer.measure();
257
+ setIsLoading(false);
258
+ }, 1);
259
+ }
260
+ },
261
+ [open, onOpenChange, virtualizer],
262
+ );
263
+
264
+ const handleIconClick = useCallback(
265
+ (iconName: IconName) => {
266
+ handleValueChange(iconName);
267
+ setIsOpen(false);
268
+ setSearch('');
269
+ },
270
+ [handleValueChange],
271
+ );
272
+
273
+ const handleSearchChange = useCallback(
274
+ (e: React.ChangeEvent<HTMLInputElement>) => {
275
+ setSearch(e.target.value);
276
+
277
+ if (parentRef.current) {
278
+ parentRef.current.scrollTop = 0;
279
+ }
280
+
281
+ virtualizer.scrollToOffset(0);
282
+ },
283
+ [virtualizer],
284
+ );
285
+
286
+ const scrollToCategory = useCallback(
287
+ (categoryName: string) => {
288
+ const categoryIndex = categoryIndices[categoryName];
289
+
290
+ if (categoryIndex !== undefined && virtualizer) {
291
+ virtualizer.scrollToIndex(categoryIndex, {
292
+ align: 'start',
293
+ behavior: 'smooth',
294
+ });
295
+ }
296
+ },
297
+ [categoryIndices, virtualizer],
298
+ );
299
+
300
+ const categoryButtons = useMemo(() => {
301
+ if (!categorized || search.trim() !== '') return null;
302
+
303
+ return categorizedIcons.map((category) => (
304
+ <Button
305
+ key={category.name}
306
+ variant={'outline'}
307
+ size="sm"
308
+ className="text-xs"
309
+ onClick={(e) => {
310
+ e.stopPropagation();
311
+ scrollToCategory(category.name);
312
+ }}
313
+ >
314
+ {category.name.charAt(0).toUpperCase() + category.name.slice(1)}
315
+ </Button>
316
+ ));
317
+ }, [categorizedIcons, scrollToCategory, categorized, search]);
318
+
319
+ const renderIcon = useCallback(
320
+ (icon: IconData) => (
321
+ <TooltipProvider key={icon.name}>
322
+ <Tooltip>
323
+ <TooltipTrigger
324
+ className={cn(
325
+ 'p-2 rounded-md border hover:bg-foreground/10 transition',
326
+ 'flex items-center justify-center',
327
+ )}
328
+ onClick={() => handleIconClick(icon.name as IconName)}
329
+ >
330
+ <IconRenderer name={icon.name as IconName} />
331
+ </TooltipTrigger>
332
+ <TooltipContent>
333
+ <p>{icon.name}</p>
334
+ </TooltipContent>
335
+ </Tooltip>
336
+ </TooltipProvider>
337
+ ),
338
+ [handleIconClick],
339
+ );
340
+
341
+ const renderVirtualContent = useCallback(() => {
342
+ if (filteredIcons.length === 0) {
343
+ return <div className="text-center text-gray-500">No icon found</div>;
344
+ }
345
+
346
+ return (
347
+ <div
348
+ className="relative w-full overscroll-contain"
349
+ style={{
350
+ height: `${virtualizer.getTotalSize()}px`,
351
+ }}
352
+ >
353
+ {virtualizer.getVirtualItems().map((virtualItem: VirtualItem) => {
354
+ const item = virtualItems[virtualItem.index];
355
+
356
+ if (!item) return null;
357
+
358
+ const itemStyle = {
359
+ position: 'absolute' as const,
360
+ top: 0,
361
+ left: 0,
362
+ width: '100%',
363
+ height: `${virtualItem.size}px`,
364
+ transform: `translateY(${virtualItem.start}px)`,
365
+ };
366
+
367
+ if (item.type === 'category') {
368
+ return (
369
+ <div
370
+ key={virtualItem.key}
371
+ style={itemStyle}
372
+ className="top-0 bg-background z-10"
373
+ >
374
+ <h3 className="font-medium text-sm capitalize">
375
+ {categorizedIcons[item.categoryIndex]!.name}
376
+ </h3>
377
+ <div className="h-[1px] bg-foreground/10 w-full" />
378
+ </div>
379
+ );
380
+ }
381
+
382
+ return (
383
+ <div
384
+ key={virtualItem.key}
385
+ data-index={virtualItem.index}
386
+ style={itemStyle}
387
+ >
388
+ <div className="grid grid-cols-5 gap-2 w-full">
389
+ {item.icons!.map(renderIcon)}
390
+ </div>
391
+ </div>
392
+ );
393
+ })}
394
+ </div>
395
+ );
396
+ }, [
397
+ virtualizer,
398
+ virtualItems,
399
+ categorizedIcons,
400
+ filteredIcons,
401
+ renderIcon,
402
+ ]);
403
+
404
+ React.useEffect(() => {
405
+ if (isPopoverVisible) {
406
+ setIsLoading(true);
407
+ const timer = setTimeout(() => {
408
+ setIsLoading(false);
409
+ virtualizer.measure();
410
+ }, 10);
411
+
412
+ const resizeObserver = new ResizeObserver(() => {
413
+ virtualizer.measure();
414
+ });
415
+
416
+ if (parentRef.current) {
417
+ resizeObserver.observe(parentRef.current);
418
+ }
419
+
420
+ return () => {
421
+ clearTimeout(timer);
422
+ resizeObserver.disconnect();
423
+ };
424
+ }
425
+ }, [isPopoverVisible, virtualizer]);
426
+
427
+ return (
428
+ <Popover
429
+ open={open ?? isOpen}
430
+ onOpenChange={handleOpenChange}
431
+ modal={modal}
432
+ >
433
+ <PopoverTrigger asChild ref={ref} {...props}>
434
+ {children || (
435
+ <Button variant="outline">
436
+ {value || selectedIcon ? (
437
+ <>
438
+ <Icon name={(value || selectedIcon)!} />{' '}
439
+ {value || selectedIcon}
440
+ </>
441
+ ) : (
442
+ triggerPlaceholder
443
+ )}
444
+ </Button>
445
+ )}
446
+ </PopoverTrigger>
447
+ <PopoverContent className="w-64 p-2">
448
+ {searchable && (
449
+ <Input
450
+ placeholder={searchPlaceholder}
451
+ onChange={handleSearchChange}
452
+ className="mb-2"
453
+ />
454
+ )}
455
+ {categorized && search.trim() === '' && (
456
+ <div className="flex flex-row gap-1 mt-2 overflow-x-auto pb-2">
457
+ {categoryButtons}
458
+ </div>
459
+ )}
460
+ <div
461
+ ref={parentRef}
462
+ className="max-h-60 overflow-auto"
463
+ style={{ scrollbarWidth: 'thin' }}
464
+ >
465
+ {isLoading ? <IconsColumnSkeleton /> : renderVirtualContent()}
466
+ </div>
467
+ </PopoverContent>
468
+ </Popover>
469
+ );
470
+ },
471
+ );
472
+ IconPicker.displayName = 'IconPicker';
473
+
474
+ interface IconProps extends Omit<LucideProps, 'ref'> {
475
+ name: IconName;
476
+ }
477
+
478
+ const Icon = React.forwardRef<React.ComponentRef<LucideIcon>, IconProps>(
479
+ ({ name, ...props }, ref) => {
480
+ return <DynamicIcon name={name} {...props} ref={ref} />;
481
+ },
482
+ );
483
+ Icon.displayName = 'Icon';
484
+
485
+ export { IconPicker, Icon, type IconName };