@cero-base/panel 0.0.1

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.
@@ -0,0 +1,34 @@
1
+ import { GripVertical } from 'lucide-react'
2
+ import { PanelGroup, Panel, PanelResizeHandle } from 'react-resizable-panels'
3
+
4
+ import { cn } from '../../lib/utils'
5
+
6
+ function ResizablePanelGroup({ className, direction, ...props }) {
7
+ return (
8
+ <PanelGroup orientation={direction} className={cn('h-full w-full', className)} {...props} />
9
+ )
10
+ }
11
+
12
+ function ResizablePanel({ className, ...props }) {
13
+ return <Panel className={cn('h-full', className)} {...props} />
14
+ }
15
+
16
+ function ResizableHandle({ withHandle, className, ...props }) {
17
+ return (
18
+ <PanelResizeHandle
19
+ className={cn(
20
+ 'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:left-0 data-[orientation=vertical]:after:h-1 data-[orientation=vertical]:after:w-full data-[orientation=vertical]:after:-translate-y-1/2 data-[orientation=vertical]:after:translate-x-0 [&[data-orientation=vertical]>div]:rotate-90',
21
+ className
22
+ )}
23
+ {...props}
24
+ >
25
+ {withHandle && (
26
+ <div className='bg-border z-10 flex h-4 w-3 items-center justify-center rounded-sm border'>
27
+ <GripVertical className='size-2.5' />
28
+ </div>
29
+ )}
30
+ </PanelResizeHandle>
31
+ )
32
+ }
33
+
34
+ export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
@@ -0,0 +1,32 @@
1
+ import { ScrollArea as ScrollAreaPrimitive } from '@base-ui/react/scroll-area'
2
+
3
+ import { cn } from '../../lib/utils'
4
+
5
+ function ScrollArea({ className, children, ...props }) {
6
+ return (
7
+ <ScrollAreaPrimitive.Root
8
+ data-slot='scroll-area'
9
+ className={cn('relative', className)}
10
+ {...props}
11
+ >
12
+ <ScrollAreaPrimitive.Viewport className='size-full overflow-scroll'>
13
+ {children}
14
+ </ScrollAreaPrimitive.Viewport>
15
+ <ScrollAreaPrimitive.Scrollbar
16
+ orientation='vertical'
17
+ className='flex h-full w-2.5 touch-none border-l border-l-transparent p-px select-none transition-colors'
18
+ >
19
+ <ScrollAreaPrimitive.Thumb className='bg-border relative flex-1 rounded-full' />
20
+ </ScrollAreaPrimitive.Scrollbar>
21
+ <ScrollAreaPrimitive.Scrollbar
22
+ orientation='horizontal'
23
+ className='flex h-2.5 touch-none flex-col border-t border-t-transparent p-px select-none transition-colors'
24
+ >
25
+ <ScrollAreaPrimitive.Thumb className='bg-border relative flex-1 rounded-full' />
26
+ </ScrollAreaPrimitive.Scrollbar>
27
+ <ScrollAreaPrimitive.Corner />
28
+ </ScrollAreaPrimitive.Root>
29
+ )
30
+ }
31
+
32
+ export { ScrollArea }
@@ -0,0 +1,169 @@
1
+ import { Select as SelectPrimitive } from '@base-ui/react/select'
2
+
3
+ import { cn } from '../../lib/utils'
4
+ import { ChevronsUpDown, Check, ChevronUp, ChevronDown } from 'lucide-react'
5
+
6
+ const Select = SelectPrimitive.Root
7
+
8
+ function SelectGroup({ className, ...props }) {
9
+ return (
10
+ <SelectPrimitive.Group
11
+ data-slot='select-group'
12
+ className={cn('scroll-my-1 p-1', className)}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function SelectValue({ className, ...props }) {
19
+ return (
20
+ <SelectPrimitive.Value
21
+ data-slot='select-value'
22
+ className={cn('flex flex-1 text-left', className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function SelectTrigger({ className, size = 'default', children, ...props }) {
29
+ return (
30
+ <SelectPrimitive.Trigger
31
+ data-slot='select-trigger'
32
+ data-size={size}
33
+ className={cn(
34
+ "border-input data-[placeholder]:text-muted-foreground bg-input/20 dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-md border px-2 py-1.5 text-xs/relaxed transition-colors focus-visible:ring-[2px] aria-invalid:ring-[2px] data-[size=default]:h-7 data-[size=sm]:h-6 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-3.5 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
35
+ className
36
+ )}
37
+ {...props}
38
+ >
39
+ {children}
40
+ <SelectPrimitive.Icon
41
+ render={<ChevronsUpDown className='text-muted-foreground size-3.5 pointer-events-none' />}
42
+ />
43
+ </SelectPrimitive.Trigger>
44
+ )
45
+ }
46
+
47
+ function SelectContent({
48
+ className,
49
+ children,
50
+ side = 'bottom',
51
+ sideOffset = 4,
52
+ align = 'center',
53
+ alignOffset = 0,
54
+ alignItemWithTrigger = true,
55
+ ...props
56
+ }) {
57
+ return (
58
+ <SelectPrimitive.Portal>
59
+ <SelectPrimitive.Positioner
60
+ side={side}
61
+ sideOffset={sideOffset}
62
+ align={align}
63
+ alignOffset={alignOffset}
64
+ alignItemWithTrigger={alignItemWithTrigger}
65
+ className='isolate z-50'
66
+ >
67
+ <SelectPrimitive.Popup
68
+ data-slot='select-content'
69
+ className={cn(
70
+ 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-32 rounded-lg shadow-md ring-1 duration-100 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto',
71
+ className
72
+ )}
73
+ {...props}
74
+ >
75
+ <SelectScrollUpButton />
76
+ <SelectPrimitive.List>{children}</SelectPrimitive.List>
77
+ <SelectScrollDownButton />
78
+ </SelectPrimitive.Popup>
79
+ </SelectPrimitive.Positioner>
80
+ </SelectPrimitive.Portal>
81
+ )
82
+ }
83
+
84
+ function SelectLabel({ className, ...props }) {
85
+ return (
86
+ <SelectPrimitive.GroupLabel
87
+ data-slot='select-label'
88
+ className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ function SelectItem({ className, children, ...props }) {
95
+ return (
96
+ <SelectPrimitive.Item
97
+ data-slot='select-item'
98
+ className={cn(
99
+ "focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs/relaxed [&_svg:not([class*='size-'])]:size-3.5 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
100
+ className
101
+ )}
102
+ {...props}
103
+ >
104
+ <SelectPrimitive.ItemText className='flex flex-1 gap-2 shrink-0 whitespace-nowrap'>
105
+ {children}
106
+ </SelectPrimitive.ItemText>
107
+ <SelectPrimitive.ItemIndicator
108
+ render={
109
+ <span className='pointer-events-none absolute right-2 flex items-center justify-center' />
110
+ }
111
+ >
112
+ <Check className='pointer-events-none' />
113
+ </SelectPrimitive.ItemIndicator>
114
+ </SelectPrimitive.Item>
115
+ )
116
+ }
117
+
118
+ function SelectSeparator({ className, ...props }) {
119
+ return (
120
+ <SelectPrimitive.Separator
121
+ data-slot='select-separator'
122
+ className={cn('bg-border/50 -mx-1 my-1 h-px pointer-events-none', className)}
123
+ {...props}
124
+ />
125
+ )
126
+ }
127
+
128
+ function SelectScrollUpButton({ className, ...props }) {
129
+ return (
130
+ <SelectPrimitive.ScrollUpArrow
131
+ data-slot='select-scroll-up-button'
132
+ className={cn(
133
+ "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-3.5 top-0 w-full",
134
+ className
135
+ )}
136
+ {...props}
137
+ >
138
+ <ChevronUp />
139
+ </SelectPrimitive.ScrollUpArrow>
140
+ )
141
+ }
142
+
143
+ function SelectScrollDownButton({ className, ...props }) {
144
+ return (
145
+ <SelectPrimitive.ScrollDownArrow
146
+ data-slot='select-scroll-down-button'
147
+ className={cn(
148
+ "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-3.5 bottom-0 w-full",
149
+ className
150
+ )}
151
+ {...props}
152
+ >
153
+ <ChevronDown />
154
+ </SelectPrimitive.ScrollDownArrow>
155
+ )
156
+ }
157
+
158
+ export {
159
+ Select,
160
+ SelectContent,
161
+ SelectGroup,
162
+ SelectItem,
163
+ SelectLabel,
164
+ SelectScrollDownButton,
165
+ SelectScrollUpButton,
166
+ SelectSeparator,
167
+ SelectTrigger,
168
+ SelectValue
169
+ }
@@ -0,0 +1,88 @@
1
+ import { cn } from '../../lib/utils'
2
+
3
+ function Table({ className, ...props }) {
4
+ return (
5
+ <div data-slot='table-container' className='relative w-full overflow-x-auto'>
6
+ <table
7
+ data-slot='table'
8
+ className={cn('w-full caption-bottom text-xs', className)}
9
+ {...props}
10
+ />
11
+ </div>
12
+ )
13
+ }
14
+
15
+ function TableHeader({ className, ...props }) {
16
+ return <thead data-slot='table-header' className={cn('[&_tr]:border-b', className)} {...props} />
17
+ }
18
+
19
+ function TableBody({ className, ...props }) {
20
+ return (
21
+ <tbody
22
+ data-slot='table-body'
23
+ className={cn('[&_tr:last-child]:border-0', className)}
24
+ {...props}
25
+ />
26
+ )
27
+ }
28
+
29
+ function TableFooter({ className, ...props }) {
30
+ return (
31
+ <tfoot
32
+ data-slot='table-footer'
33
+ className={cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className)}
34
+ {...props}
35
+ />
36
+ )
37
+ }
38
+
39
+ function TableRow({ className, ...props }) {
40
+ return (
41
+ <tr
42
+ data-slot='table-row'
43
+ className={cn(
44
+ 'border-b border-border/50 transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
45
+ className
46
+ )}
47
+ {...props}
48
+ />
49
+ )
50
+ }
51
+
52
+ function TableHead({ className, ...props }) {
53
+ return (
54
+ <th
55
+ data-slot='table-head'
56
+ className={cn(
57
+ 'text-muted-foreground h-9 px-3 text-left align-middle text-xs font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
58
+ className
59
+ )}
60
+ {...props}
61
+ />
62
+ )
63
+ }
64
+
65
+ function TableCell({ className, ...props }) {
66
+ return (
67
+ <td
68
+ data-slot='table-cell'
69
+ className={cn(
70
+ 'px-3 py-1.5 align-middle max-w-xs truncate [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
71
+ className
72
+ )}
73
+ {...props}
74
+ />
75
+ )
76
+ }
77
+
78
+ function TableCaption({ className, ...props }) {
79
+ return (
80
+ <caption
81
+ data-slot='table-caption'
82
+ className={cn('text-muted-foreground mt-4 text-xs', className)}
83
+ {...props}
84
+ />
85
+ )
86
+ }
87
+
88
+ export { Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption }
@@ -0,0 +1,43 @@
1
+ import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'
2
+
3
+ import { cn } from '../../lib/utils'
4
+
5
+ const Tabs = TabsPrimitive.Root
6
+
7
+ function TabsList({ className, ...props }) {
8
+ return (
9
+ <TabsPrimitive.List
10
+ data-slot='tabs-list'
11
+ className={cn(
12
+ 'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-1',
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+
20
+ function TabsTrigger({ className, ...props }) {
21
+ return (
22
+ <TabsPrimitive.Tab
23
+ data-slot='tabs-trigger'
24
+ className={cn(
25
+ "inline-flex h-7 flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 text-xs font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-active:border-primary/50 dark:data-active:border-primary/30 data-active:bg-primary/10 data-active:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
26
+ className
27
+ )}
28
+ {...props}
29
+ />
30
+ )
31
+ }
32
+
33
+ function TabsContent({ className, ...props }) {
34
+ return (
35
+ <TabsPrimitive.Panel
36
+ data-slot='tabs-content'
37
+ className={cn('flex-1 outline-none mt-2', className)}
38
+ {...props}
39
+ />
40
+ )
41
+ }
42
+
43
+ export { Tabs, TabsContent, TabsList, TabsTrigger }
@@ -0,0 +1,33 @@
1
+ import { Tooltip as TooltipPrimitive } from '@base-ui/react/tooltip'
2
+ import { cn } from '../../lib/utils'
3
+
4
+ function Tooltip({ children, ...props }) {
5
+ return <TooltipPrimitive.Provider {...props}>{children}</TooltipPrimitive.Provider>
6
+ }
7
+
8
+ function TooltipRoot({ ...props }) {
9
+ return <TooltipPrimitive.Root {...props} />
10
+ }
11
+
12
+ function TooltipTrigger({ className, ...props }) {
13
+ return <TooltipPrimitive.Trigger className={cn('cursor-default', className)} {...props} />
14
+ }
15
+
16
+ function TooltipContent({ className, sideOffset = 4, ...props }) {
17
+ return (
18
+ <TooltipPrimitive.Portal>
19
+ <TooltipPrimitive.Positioner sideOffset={sideOffset}>
20
+ <TooltipPrimitive.Popup
21
+ className={cn(
22
+ 'bg-popover text-popover-foreground z-50 rounded-md border px-2.5 py-1 text-xs shadow-md',
23
+ 'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95',
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ </TooltipPrimitive.Positioner>
29
+ </TooltipPrimitive.Portal>
30
+ )
31
+ }
32
+
33
+ export { Tooltip, TooltipRoot, TooltipTrigger, TooltipContent }
package/src/global.css ADDED
@@ -0,0 +1,86 @@
1
+ @import 'tailwindcss' theme(static);
2
+
3
+ @custom-variant dark (&:is(.dark *));
4
+
5
+ @theme inline {
6
+ --font-sans: system-ui, sans-serif;
7
+ --color-background: var(--background);
8
+ --color-foreground: var(--foreground);
9
+ --color-card: var(--card);
10
+ --color-card-foreground: var(--card-foreground);
11
+ --color-popover: var(--popover);
12
+ --color-popover-foreground: var(--popover-foreground);
13
+ --color-primary: var(--primary);
14
+ --color-primary-foreground: var(--primary-foreground);
15
+ --color-secondary: var(--secondary);
16
+ --color-secondary-foreground: var(--secondary-foreground);
17
+ --color-muted: var(--muted);
18
+ --color-muted-foreground: var(--muted-foreground);
19
+ --color-accent: var(--accent);
20
+ --color-accent-foreground: var(--accent-foreground);
21
+ --color-destructive: var(--destructive);
22
+ --color-destructive-foreground: var(--destructive-foreground);
23
+ --color-border: var(--border);
24
+ --color-input: var(--input);
25
+ --color-ring: var(--ring);
26
+ --radius-sm: calc(var(--radius) - 4px);
27
+ --radius-md: calc(var(--radius) - 2px);
28
+ --radius-lg: var(--radius);
29
+ --radius-xl: calc(var(--radius) + 4px);
30
+ }
31
+
32
+ :root {
33
+ --background: oklch(0.145 0 0);
34
+ --foreground: oklch(0.985 0 0);
35
+ --card: oklch(0.205 0 0);
36
+ --card-foreground: oklch(0.985 0 0);
37
+ --popover: oklch(0.205 0 0);
38
+ --popover-foreground: oklch(0.985 0 0);
39
+ --primary: oklch(0.7 0.12 183);
40
+ --primary-foreground: oklch(0.28 0.04 193);
41
+ --secondary: oklch(0.274 0.006 286.033);
42
+ --secondary-foreground: oklch(0.985 0 0);
43
+ --muted: oklch(0.269 0 0);
44
+ --muted-foreground: oklch(0.708 0 0);
45
+ --accent: oklch(0.7 0.12 183);
46
+ --accent-foreground: oklch(0.28 0.04 193);
47
+ --destructive: oklch(0.604 0.191 22.216);
48
+ --destructive-foreground: oklch(1 0 0);
49
+ --border: oklch(1 0 0 / 10%);
50
+ --input: oklch(1 0 0 / 10%);
51
+ --ring: oklch(0.556 0 0);
52
+ --radius: 0.625rem;
53
+ }
54
+
55
+ @layer base {
56
+ * {
57
+ @apply border-border outline-ring/50;
58
+ }
59
+ body {
60
+ @apply font-sans bg-background text-foreground;
61
+ }
62
+
63
+ /* Scrollbar styling */
64
+ * {
65
+ scrollbar-width: thin;
66
+ scrollbar-color: oklch(0.5 0 0 / 30%) transparent;
67
+ }
68
+
69
+ *::-webkit-scrollbar {
70
+ width: 4px !important;
71
+ height: 4px !important;
72
+ }
73
+
74
+ *::-webkit-scrollbar-track {
75
+ background: transparent;
76
+ }
77
+
78
+ *::-webkit-scrollbar-thumb {
79
+ background: oklch(0.5 0 0 / 30%);
80
+ border-radius: 3px;
81
+ }
82
+
83
+ *::-webkit-scrollbar-thumb:hover {
84
+ background: oklch(0.5 0 0 / 50%);
85
+ }
86
+ }
@@ -0,0 +1,3 @@
1
+ export * from './use-schema.js'
2
+ export * from './use-collection.js'
3
+ export * from './use-rooms.js'
@@ -0,0 +1,27 @@
1
+ import { useMemo, useCallback } from 'react'
2
+ import { useQuery } from '@cero-base/react'
3
+ import { empty } from './util.js'
4
+
5
+ export function useCollection(db, collectionName, room = null) {
6
+ const col = useMemo(() => {
7
+ if (!db || !collectionName) return null
8
+ try {
9
+ const scope = db.schema.getScope(collectionName)
10
+ if (scope === 'shared') return room?.collection(collectionName) ?? null
11
+ return db.collection(collectionName)
12
+ } catch {
13
+ return null
14
+ }
15
+ }, [db, collectionName, room])
16
+
17
+ const { data, busy, error } = useQuery(() => (col ? col.sub() : empty([])), [col])
18
+
19
+ return {
20
+ data: data || [],
21
+ loading: busy,
22
+ error,
23
+ put: useCallback((doc) => col?.put(doc), [col]),
24
+ del: useCallback((query) => col?.del(query), [col]),
25
+ get: useCallback((query) => col?.get(query) ?? null, [col])
26
+ }
27
+ }
@@ -0,0 +1,80 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+
3
+ /**
4
+ * Hook to manage rooms for shared collections
5
+ */
6
+ export function useRooms(db) {
7
+ const [rooms, setRooms] = useState([])
8
+ const [loading, setLoading] = useState(true)
9
+ const [error, setError] = useState(null)
10
+
11
+ useEffect(() => {
12
+ if (!db?.rooms) {
13
+ setRooms([])
14
+ setLoading(false)
15
+ return
16
+ }
17
+
18
+ const loadRooms = async () => {
19
+ setLoading(true)
20
+ try {
21
+ const list = await db.rooms.list()
22
+ setRooms(list || [])
23
+ } catch (err) {
24
+ setError(err)
25
+ } finally {
26
+ setLoading(false)
27
+ }
28
+ }
29
+
30
+ loadRooms()
31
+
32
+ const onUpdate = () => loadRooms()
33
+ if (db.on) db.on('update', onUpdate)
34
+
35
+ return () => {
36
+ if (db.off) db.off('update', onUpdate)
37
+ }
38
+ }, [db])
39
+
40
+ const openRoom = useCallback(
41
+ async (roomKey) => {
42
+ if (!db?.rooms) return null
43
+ return db.rooms.open(roomKey)
44
+ },
45
+ [db]
46
+ )
47
+
48
+ const createRoom = useCallback(
49
+ async (opts = {}) => {
50
+ if (!db?.rooms) return null
51
+ return db.rooms.create(opts)
52
+ },
53
+ [db]
54
+ )
55
+
56
+ return { rooms, loading, error, openRoom, createRoom }
57
+ }
58
+
59
+ /**
60
+ * Hook for room selection state
61
+ */
62
+ export function useRoomSelection(db) {
63
+ const { rooms, loading, error, openRoom, createRoom } = useRooms(db)
64
+ const [selected, setSelected] = useState(null)
65
+
66
+ const select = useCallback(
67
+ async (roomKey) => {
68
+ if (!roomKey) {
69
+ setSelected(null)
70
+ return null
71
+ }
72
+ const room = await openRoom(roomKey)
73
+ setSelected(room)
74
+ return room
75
+ },
76
+ [openRoom]
77
+ )
78
+
79
+ return { rooms, loading, error, selected, select, createRoom }
80
+ }
@@ -0,0 +1,71 @@
1
+ import { useMemo } from 'react'
2
+
3
+ /**
4
+ * Hook to introspect the cero-base schema
5
+ * Returns collection metadata including fields, key, indexes, and scope
6
+ */
7
+ export function useSchema(db) {
8
+ return useMemo(() => {
9
+ if (!db?.schema?.collections) return []
10
+
11
+ const { collections } = db.schema
12
+
13
+ return Object.entries(collections).map(([name, def]) => {
14
+ // Include timestamp fields if timestamps is enabled
15
+ const fields = { ...def.fields }
16
+ if (def.timestamps) {
17
+ fields.createdAt = { type: 'uint', required: true }
18
+ fields.updatedAt = { type: 'uint', required: false }
19
+ }
20
+ return {
21
+ name,
22
+ scope: db.schema.getScope(name),
23
+ fields,
24
+ key: def.key || [],
25
+ indexes: def.indexes || [],
26
+ timestamps: def.timestamps || false
27
+ }
28
+ })
29
+ }, [db])
30
+ }
31
+
32
+ /**
33
+ * Get collections grouped by scope
34
+ */
35
+ export function useSchemaByScope(db) {
36
+ const collections = useSchema(db)
37
+
38
+ return useMemo(
39
+ () => ({
40
+ local: collections.filter((c) => c.scope === 'local'),
41
+ private: collections.filter((c) => c.scope === 'private'),
42
+ shared: collections.filter((c) => c.scope === 'shared')
43
+ }),
44
+ [collections]
45
+ )
46
+ }
47
+
48
+ /**
49
+ * Get schema for a specific collection
50
+ */
51
+ export function useCollectionSchema(db, collectionName) {
52
+ return useMemo(() => {
53
+ if (!db?.schema?.collections?.[collectionName]) return null
54
+
55
+ const def = db.schema.collections[collectionName]
56
+ // Include timestamp fields if timestamps is enabled
57
+ const fields = { ...def.fields }
58
+ if (def.timestamps) {
59
+ fields.createdAt = { type: 'uint', required: true }
60
+ fields.updatedAt = { type: 'uint', required: false }
61
+ }
62
+ return {
63
+ name: collectionName,
64
+ scope: db.schema.getScope(collectionName),
65
+ fields,
66
+ key: def.key || [],
67
+ indexes: def.indexes || [],
68
+ timestamps: def.timestamps || false
69
+ }
70
+ }, [db, collectionName])
71
+ }
@@ -0,0 +1,23 @@
1
+ import { useQuery } from '@cero-base/react'
2
+ import { empty } from './util.js'
3
+
4
+ function toStream(facade) {
5
+ if (!facade) return empty(null)
6
+ if (facade.sub) return facade.sub()
7
+ if (facade.subscribe) return facade.subscribe()
8
+ return empty(null)
9
+ }
10
+
11
+ export function useIdentityData(db, type) {
12
+ const { data, busy } = useQuery(() => {
13
+ const facade = db?.identity?.[type] || db?.[type]
14
+ return toStream(facade)
15
+ }, [db, type])
16
+ return { data, loading: busy }
17
+ }
18
+
19
+ export function useRoomData(db, room, type) {
20
+ const target = room?.room || room
21
+ const { data, busy } = useQuery(() => toStream(target?.[type]), [target, type])
22
+ return { data, loading: busy }
23
+ }