@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.
- package/README.md +42 -0
- package/package.json +41 -0
- package/src/components/data-table.jsx +245 -0
- package/src/components/document-form.jsx +211 -0
- package/src/components/panel.jsx +1131 -0
- package/src/components/sidebar.jsx +151 -0
- package/src/components/ui/avatar.jsx +38 -0
- package/src/components/ui/badge.jsx +26 -0
- package/src/components/ui/button.jsx +51 -0
- package/src/components/ui/checkbox.jsx +43 -0
- package/src/components/ui/dialog.jsx +126 -0
- package/src/components/ui/dropdown-menu.jsx +213 -0
- package/src/components/ui/input.jsx +19 -0
- package/src/components/ui/resizable.jsx +34 -0
- package/src/components/ui/scroll-area.jsx +32 -0
- package/src/components/ui/select.jsx +169 -0
- package/src/components/ui/table.jsx +88 -0
- package/src/components/ui/tabs.jsx +43 -0
- package/src/components/ui/tooltip.jsx +33 -0
- package/src/global.css +86 -0
- package/src/hooks/index.js +3 -0
- package/src/hooks/use-collection.js +27 -0
- package/src/hooks/use-rooms.js +80 -0
- package/src/hooks/use-schema.js +71 -0
- package/src/hooks/use-system.js +23 -0
- package/src/hooks/util.js +10 -0
- package/src/index.js +5 -0
- package/src/lib/field-types.js +148 -0
- package/src/lib/utils.js +19 -0
- package/src/open-panel.js +30 -0
- package/src/panel-window.html +24 -0
- package/src/panel-window.jsx +195 -0
- package/src/theme.css +57 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { CeroPanel } from './components/panel.jsx'
|
|
2
|
+
export { useSchema, useSchemaByScope, useCollectionSchema } from './hooks/use-schema.js'
|
|
3
|
+
export { useCollection } from './hooks/use-collection.js'
|
|
4
|
+
export { useRooms, useRoomSelection } from './hooks/use-rooms.js'
|
|
5
|
+
export { useIdentityData, useRoomData } from './hooks/use-system.js'
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field type utilities for displaying and editing schema values
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format a value for display based on its schema type
|
|
7
|
+
*/
|
|
8
|
+
export function formatValue(value, type) {
|
|
9
|
+
if (value === null || value === undefined) return '—'
|
|
10
|
+
|
|
11
|
+
const typeName = typeof type === 'string' ? type : type?.type
|
|
12
|
+
|
|
13
|
+
switch (typeName) {
|
|
14
|
+
case 'bool':
|
|
15
|
+
case 'boolean':
|
|
16
|
+
return value ? 'true' : 'false'
|
|
17
|
+
|
|
18
|
+
case 'json':
|
|
19
|
+
case 'object':
|
|
20
|
+
return JSON.stringify(value)
|
|
21
|
+
|
|
22
|
+
case 'array':
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
return `[${value.length} items]`
|
|
25
|
+
}
|
|
26
|
+
return JSON.stringify(value)
|
|
27
|
+
|
|
28
|
+
case 'date':
|
|
29
|
+
case 'timestamp':
|
|
30
|
+
if (value instanceof Date) {
|
|
31
|
+
return value.toISOString()
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === 'number') {
|
|
34
|
+
return new Date(value).toISOString()
|
|
35
|
+
}
|
|
36
|
+
return String(value)
|
|
37
|
+
|
|
38
|
+
case 'buffer':
|
|
39
|
+
case 'binary':
|
|
40
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
41
|
+
return `<Buffer ${value.length} bytes>`
|
|
42
|
+
}
|
|
43
|
+
return String(value)
|
|
44
|
+
|
|
45
|
+
default:
|
|
46
|
+
if (typeof value === 'object') {
|
|
47
|
+
return JSON.stringify(value)
|
|
48
|
+
}
|
|
49
|
+
return String(value)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get input type for form fields based on schema type
|
|
55
|
+
*/
|
|
56
|
+
export function getInputType(type) {
|
|
57
|
+
const typeName = typeof type === 'string' ? type : type?.type
|
|
58
|
+
|
|
59
|
+
switch (typeName) {
|
|
60
|
+
case 'bool':
|
|
61
|
+
case 'boolean':
|
|
62
|
+
return 'checkbox'
|
|
63
|
+
|
|
64
|
+
case 'number':
|
|
65
|
+
case 'int':
|
|
66
|
+
case 'integer':
|
|
67
|
+
case 'float':
|
|
68
|
+
return 'number'
|
|
69
|
+
|
|
70
|
+
case 'json':
|
|
71
|
+
case 'object':
|
|
72
|
+
case 'array':
|
|
73
|
+
return 'textarea'
|
|
74
|
+
|
|
75
|
+
default:
|
|
76
|
+
return 'text'
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse input value to match schema type
|
|
82
|
+
*/
|
|
83
|
+
export function parseValue(value, type) {
|
|
84
|
+
const typeName = typeof type === 'string' ? type : type?.type
|
|
85
|
+
|
|
86
|
+
switch (typeName) {
|
|
87
|
+
case 'bool':
|
|
88
|
+
case 'boolean':
|
|
89
|
+
return Boolean(value)
|
|
90
|
+
|
|
91
|
+
case 'number':
|
|
92
|
+
case 'float':
|
|
93
|
+
return parseFloat(value) || 0
|
|
94
|
+
|
|
95
|
+
case 'int':
|
|
96
|
+
case 'integer':
|
|
97
|
+
return parseInt(value, 10) || 0
|
|
98
|
+
|
|
99
|
+
case 'json':
|
|
100
|
+
case 'object':
|
|
101
|
+
case 'array':
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(value)
|
|
104
|
+
} catch {
|
|
105
|
+
return value
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
default:
|
|
109
|
+
return value
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get default value for a schema type
|
|
115
|
+
*/
|
|
116
|
+
export function getDefaultValue(type) {
|
|
117
|
+
const typeName = typeof type === 'string' ? type : type?.type
|
|
118
|
+
|
|
119
|
+
switch (typeName) {
|
|
120
|
+
case 'bool':
|
|
121
|
+
case 'boolean':
|
|
122
|
+
return false
|
|
123
|
+
|
|
124
|
+
case 'number':
|
|
125
|
+
case 'int':
|
|
126
|
+
case 'integer':
|
|
127
|
+
case 'float':
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
case 'array':
|
|
131
|
+
return []
|
|
132
|
+
|
|
133
|
+
case 'json':
|
|
134
|
+
case 'object':
|
|
135
|
+
return {}
|
|
136
|
+
|
|
137
|
+
default:
|
|
138
|
+
return ''
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Truncate long strings for display
|
|
144
|
+
*/
|
|
145
|
+
export function truncate(str, maxLength = 50) {
|
|
146
|
+
if (!str || str.length <= maxLength) return str
|
|
147
|
+
return str.slice(0, maxLength) + '...'
|
|
148
|
+
}
|
package/src/lib/utils.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import { twMerge } from 'tailwind-merge'
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs) {
|
|
5
|
+
return twMerge(clsx(inputs))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatValue(value, type) {
|
|
9
|
+
if (value === null || value === undefined) return '—'
|
|
10
|
+
if (type?.type === 'bool') return value ? 'true' : 'false'
|
|
11
|
+
if (type?.type === 'json') return JSON.stringify(value)
|
|
12
|
+
if (typeof value === 'object') return JSON.stringify(value)
|
|
13
|
+
return String(value)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function truncate(str, maxLength = 50) {
|
|
17
|
+
if (!str || str.length <= maxLength) return str
|
|
18
|
+
return str.slice(0, maxLength) + '...'
|
|
19
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* global Pear */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Opens the Cero Panel in a separate Pear window.
|
|
5
|
+
* The panel window connects to the same storage path as the main app.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} opts
|
|
8
|
+
* @param {string} opts.storage - Storage path (usually Pear.config.storage)
|
|
9
|
+
* @param {number} [opts.width=900] - Window width
|
|
10
|
+
* @param {number} [opts.height=600] - Window height
|
|
11
|
+
*/
|
|
12
|
+
export async function openPanel({ storage, width = 900, height = 600 }) {
|
|
13
|
+
// Store config for the panel window to read
|
|
14
|
+
sessionStorage.setItem('cero-panel-storage', storage)
|
|
15
|
+
|
|
16
|
+
const win = new Pear.Window('./node_modules/cero-panel/build/src/panel-window.html', {
|
|
17
|
+
width,
|
|
18
|
+
height
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Send storage path to panel
|
|
22
|
+
win.on('message', (msg) => {
|
|
23
|
+
if (msg.type === 'ready') {
|
|
24
|
+
win.send({ type: 'config', storage })
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
await win.open({ show: true })
|
|
29
|
+
return win
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>Cero Panel</title>
|
|
6
|
+
<style>
|
|
7
|
+
* {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
}
|
|
12
|
+
body {
|
|
13
|
+
font-family: system-ui, sans-serif;
|
|
14
|
+
background: #1e293b;
|
|
15
|
+
color: #e2e8f0;
|
|
16
|
+
font-size: 13px;
|
|
17
|
+
}
|
|
18
|
+
</style>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<div id="root"></div>
|
|
22
|
+
<script type="module" src="./panel-window.js"></script>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* global Pear */
|
|
2
|
+
import { useState, useEffect } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
|
|
5
|
+
// Panel receives base data via Pear.messages from parent window
|
|
6
|
+
function PanelApp() {
|
|
7
|
+
const [collections, setCollections] = useState([])
|
|
8
|
+
const [rooms, setRooms] = useState([])
|
|
9
|
+
const [selected, setSelected] = useState(null)
|
|
10
|
+
const [room, setRoom] = useState(null)
|
|
11
|
+
const [data, setData] = useState([])
|
|
12
|
+
const [loading, setLoading] = useState(false)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
// Listen for data from parent window
|
|
16
|
+
const stream = Pear.messages((msg) => {
|
|
17
|
+
if (msg.type === 'init') {
|
|
18
|
+
setCollections(msg.collections)
|
|
19
|
+
setRooms(msg.rooms)
|
|
20
|
+
} else if (msg.type === 'data') {
|
|
21
|
+
setData(msg.data)
|
|
22
|
+
setLoading(false)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Request initial data
|
|
27
|
+
Pear.message({ type: 'ready' })
|
|
28
|
+
|
|
29
|
+
return () => stream.destroy?.()
|
|
30
|
+
}, [])
|
|
31
|
+
|
|
32
|
+
const onSelect = (col) => {
|
|
33
|
+
setSelected(col)
|
|
34
|
+
setLoading(true)
|
|
35
|
+
Pear.message({ type: 'subscribe', collection: col.name, roomId: room?.id })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const onRoomChange = (r) => {
|
|
39
|
+
setRoom(r)
|
|
40
|
+
if (selected?.scope === 'shared') {
|
|
41
|
+
setLoading(true)
|
|
42
|
+
Pear.message({ type: 'subscribe', collection: selected.name, roomId: r?.id })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const fields = selected
|
|
47
|
+
? Object.keys(collections.find((c) => c.name === selected.name)?.fields || {})
|
|
48
|
+
: []
|
|
49
|
+
const grouped = {
|
|
50
|
+
local: collections.filter((c) => c.scope === 'local'),
|
|
51
|
+
private: collections.filter((c) => c.scope === 'private'),
|
|
52
|
+
shared: collections.filter((c) => c.scope === 'shared')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div style={{ display: 'flex', height: '100vh' }}>
|
|
57
|
+
{/* Sidebar */}
|
|
58
|
+
<div style={{ width: 200, borderRight: '1px solid #334155', padding: 12, overflow: 'auto' }}>
|
|
59
|
+
<div style={{ marginBottom: 16, fontWeight: 600, color: '#0ea5e9' }}>Cero Panel</div>
|
|
60
|
+
{Object.entries(grouped).map(
|
|
61
|
+
([scope, cols]) =>
|
|
62
|
+
cols.length > 0 && (
|
|
63
|
+
<div key={scope} style={{ marginBottom: 16 }}>
|
|
64
|
+
<div
|
|
65
|
+
style={{
|
|
66
|
+
fontSize: 10,
|
|
67
|
+
color: '#64748b',
|
|
68
|
+
textTransform: 'uppercase',
|
|
69
|
+
marginBottom: 8
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{scope} ({cols.length})
|
|
73
|
+
</div>
|
|
74
|
+
{cols.map((c) => (
|
|
75
|
+
<button
|
|
76
|
+
key={c.name}
|
|
77
|
+
onClick={() => onSelect(c)}
|
|
78
|
+
style={{
|
|
79
|
+
display: 'block',
|
|
80
|
+
width: '100%',
|
|
81
|
+
padding: '8px 12px',
|
|
82
|
+
marginBottom: 4,
|
|
83
|
+
backgroundColor: selected?.name === c.name ? '#0ea5e9' : 'transparent',
|
|
84
|
+
color: selected?.name === c.name ? 'white' : '#e2e8f0',
|
|
85
|
+
border: 'none',
|
|
86
|
+
borderRadius: 6,
|
|
87
|
+
textAlign: 'left',
|
|
88
|
+
cursor: 'pointer',
|
|
89
|
+
fontSize: 12
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{c.name}
|
|
93
|
+
</button>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Main */}
|
|
101
|
+
<div style={{ flex: 1, padding: 16, overflow: 'auto' }}>
|
|
102
|
+
{!selected ? (
|
|
103
|
+
<div style={{ color: '#64748b', textAlign: 'center', marginTop: 40 }}>
|
|
104
|
+
Select a collection
|
|
105
|
+
</div>
|
|
106
|
+
) : (
|
|
107
|
+
<div>
|
|
108
|
+
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
109
|
+
<span style={{ fontWeight: 600 }}>{selected.name}</span>
|
|
110
|
+
<span
|
|
111
|
+
style={{
|
|
112
|
+
fontSize: 10,
|
|
113
|
+
color: '#64748b',
|
|
114
|
+
padding: '2px 6px',
|
|
115
|
+
backgroundColor: '#334155',
|
|
116
|
+
borderRadius: 4
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{selected.scope}
|
|
120
|
+
</span>
|
|
121
|
+
{selected.scope === 'shared' && (
|
|
122
|
+
<select
|
|
123
|
+
value={room?.id || ''}
|
|
124
|
+
onChange={(e) => onRoomChange(rooms.find((r) => r.id === e.target.value) || null)}
|
|
125
|
+
style={{
|
|
126
|
+
padding: '6px 10px',
|
|
127
|
+
backgroundColor: '#334155',
|
|
128
|
+
color: '#e2e8f0',
|
|
129
|
+
border: '1px solid #475569',
|
|
130
|
+
borderRadius: 4,
|
|
131
|
+
fontSize: 11
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<option value=''>Select room...</option>
|
|
135
|
+
{rooms.map((r) => (
|
|
136
|
+
<option key={r.id} value={r.id}>
|
|
137
|
+
{r.name || r.id?.slice(0, 8)}
|
|
138
|
+
</option>
|
|
139
|
+
))}
|
|
140
|
+
</select>
|
|
141
|
+
)}
|
|
142
|
+
<span style={{ fontSize: 11, color: '#64748b' }}>
|
|
143
|
+
{loading ? 'Loading...' : `${data.length} documents`}
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{selected.scope === 'shared' && !room ? (
|
|
148
|
+
<div style={{ color: '#f59e0b', fontSize: 12 }}>Select a room to view data</div>
|
|
149
|
+
) : data.length === 0 ? (
|
|
150
|
+
<div style={{ color: '#64748b' }}>No documents</div>
|
|
151
|
+
) : (
|
|
152
|
+
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
|
|
153
|
+
<thead>
|
|
154
|
+
<tr>
|
|
155
|
+
{fields.map((f) => (
|
|
156
|
+
<th
|
|
157
|
+
key={f}
|
|
158
|
+
style={{
|
|
159
|
+
textAlign: 'left',
|
|
160
|
+
padding: '8px 12px',
|
|
161
|
+
borderBottom: '1px solid #334155',
|
|
162
|
+
color: '#94a3b8'
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
{f}
|
|
166
|
+
</th>
|
|
167
|
+
))}
|
|
168
|
+
</tr>
|
|
169
|
+
</thead>
|
|
170
|
+
<tbody>
|
|
171
|
+
{data.map((doc, i) => (
|
|
172
|
+
<tr key={i}>
|
|
173
|
+
{fields.map((f) => (
|
|
174
|
+
<td
|
|
175
|
+
key={f}
|
|
176
|
+
style={{ padding: '8px 12px', borderBottom: '1px solid #334155' }}
|
|
177
|
+
>
|
|
178
|
+
{typeof doc[f] === 'object'
|
|
179
|
+
? JSON.stringify(doc[f])
|
|
180
|
+
: String(doc[f] ?? '')}
|
|
181
|
+
</td>
|
|
182
|
+
))}
|
|
183
|
+
</tr>
|
|
184
|
+
))}
|
|
185
|
+
</tbody>
|
|
186
|
+
</table>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
createRoot(document.getElementById('root')).render(<PanelApp />)
|
package/src/theme.css
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
@custom-variant dark (&:is(.dark *));
|
|
2
|
+
|
|
3
|
+
@theme inline {
|
|
4
|
+
--font-sans: system-ui, sans-serif;
|
|
5
|
+
--color-background: var(--background);
|
|
6
|
+
--color-foreground: var(--foreground);
|
|
7
|
+
--color-card: var(--card);
|
|
8
|
+
--color-card-foreground: var(--card-foreground);
|
|
9
|
+
--color-popover: var(--popover);
|
|
10
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
11
|
+
--color-primary: var(--primary);
|
|
12
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
13
|
+
--color-secondary: var(--secondary);
|
|
14
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
15
|
+
--color-muted: var(--muted);
|
|
16
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
17
|
+
--color-accent: var(--accent);
|
|
18
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
19
|
+
--color-destructive: var(--destructive);
|
|
20
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
21
|
+
--color-border: var(--border);
|
|
22
|
+
--color-input: var(--input);
|
|
23
|
+
--color-ring: var(--ring);
|
|
24
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
25
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
26
|
+
--radius-lg: var(--radius);
|
|
27
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:root {
|
|
31
|
+
--background: oklch(0.145 0 0);
|
|
32
|
+
--foreground: oklch(0.985 0 0);
|
|
33
|
+
--card: oklch(0.205 0 0);
|
|
34
|
+
--card-foreground: oklch(0.985 0 0);
|
|
35
|
+
--popover: oklch(0.205 0 0);
|
|
36
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
37
|
+
--primary: oklch(0.7 0.12 183);
|
|
38
|
+
--primary-foreground: oklch(0.28 0.04 193);
|
|
39
|
+
--secondary: oklch(0.274 0.006 286.033);
|
|
40
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
41
|
+
--muted: oklch(0.269 0 0);
|
|
42
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
43
|
+
--accent: oklch(0.7 0.12 183);
|
|
44
|
+
--accent-foreground: oklch(0.28 0.04 193);
|
|
45
|
+
--destructive: oklch(0.604 0.191 22.216);
|
|
46
|
+
--destructive-foreground: oklch(1 0 0);
|
|
47
|
+
--border: oklch(1 0 0 / 10%);
|
|
48
|
+
--input: oklch(1 0 0 / 10%);
|
|
49
|
+
--ring: oklch(0.556 0 0);
|
|
50
|
+
--radius: 0.625rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@layer base {
|
|
54
|
+
* {
|
|
55
|
+
@apply border-border outline-ring/50;
|
|
56
|
+
}
|
|
57
|
+
}
|