@opensaas/stack-ui 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 (203) hide show
  1. package/.turbo/turbo-build.log +8 -0
  2. package/README.md +286 -0
  3. package/dist/components/AdminUI.d.ts +24 -0
  4. package/dist/components/AdminUI.d.ts.map +1 -0
  5. package/dist/components/AdminUI.js +48 -0
  6. package/dist/components/ConfirmDialog.d.ts +16 -0
  7. package/dist/components/ConfirmDialog.d.ts.map +1 -0
  8. package/dist/components/ConfirmDialog.js +11 -0
  9. package/dist/components/Dashboard.d.ts +12 -0
  10. package/dist/components/Dashboard.d.ts.map +1 -0
  11. package/dist/components/Dashboard.js +30 -0
  12. package/dist/components/ItemForm.d.ts +17 -0
  13. package/dist/components/ItemForm.d.ts.map +1 -0
  14. package/dist/components/ItemForm.js +97 -0
  15. package/dist/components/ItemFormClient.d.ts +22 -0
  16. package/dist/components/ItemFormClient.d.ts.map +1 -0
  17. package/dist/components/ItemFormClient.js +127 -0
  18. package/dist/components/ListView.d.ts +17 -0
  19. package/dist/components/ListView.d.ts.map +1 -0
  20. package/dist/components/ListView.js +76 -0
  21. package/dist/components/ListViewClient.d.ts +19 -0
  22. package/dist/components/ListViewClient.d.ts.map +1 -0
  23. package/dist/components/ListViewClient.js +108 -0
  24. package/dist/components/LoadingSpinner.d.ts +10 -0
  25. package/dist/components/LoadingSpinner.d.ts.map +1 -0
  26. package/dist/components/LoadingSpinner.js +14 -0
  27. package/dist/components/Navigation.d.ts +13 -0
  28. package/dist/components/Navigation.d.ts.map +1 -0
  29. package/dist/components/Navigation.js +20 -0
  30. package/dist/components/SkeletonLoader.d.ts +22 -0
  31. package/dist/components/SkeletonLoader.d.ts.map +1 -0
  32. package/dist/components/SkeletonLoader.js +25 -0
  33. package/dist/components/fields/CheckboxField.d.ts +11 -0
  34. package/dist/components/fields/CheckboxField.d.ts.map +1 -0
  35. package/dist/components/fields/CheckboxField.js +10 -0
  36. package/dist/components/fields/ComboboxField.d.ts +18 -0
  37. package/dist/components/fields/ComboboxField.d.ts.map +1 -0
  38. package/dist/components/fields/ComboboxField.js +32 -0
  39. package/dist/components/fields/FieldRenderer.d.ts +22 -0
  40. package/dist/components/fields/FieldRenderer.d.ts.map +1 -0
  41. package/dist/components/fields/FieldRenderer.js +81 -0
  42. package/dist/components/fields/IntegerField.d.ts +15 -0
  43. package/dist/components/fields/IntegerField.d.ts.map +1 -0
  44. package/dist/components/fields/IntegerField.js +14 -0
  45. package/dist/components/fields/PasswordField.d.ts +18 -0
  46. package/dist/components/fields/PasswordField.d.ts.map +1 -0
  47. package/dist/components/fields/PasswordField.js +42 -0
  48. package/dist/components/fields/RelationshipField.d.ts +20 -0
  49. package/dist/components/fields/RelationshipField.d.ts.map +1 -0
  50. package/dist/components/fields/RelationshipField.js +11 -0
  51. package/dist/components/fields/RelationshipManager.d.ts +19 -0
  52. package/dist/components/fields/RelationshipManager.d.ts.map +1 -0
  53. package/dist/components/fields/RelationshipManager.js +37 -0
  54. package/dist/components/fields/SelectField.d.ts +16 -0
  55. package/dist/components/fields/SelectField.d.ts.map +1 -0
  56. package/dist/components/fields/SelectField.js +11 -0
  57. package/dist/components/fields/TextField.d.ts +13 -0
  58. package/dist/components/fields/TextField.d.ts.map +1 -0
  59. package/dist/components/fields/TextField.js +11 -0
  60. package/dist/components/fields/TimestampField.d.ts +12 -0
  61. package/dist/components/fields/TimestampField.d.ts.map +1 -0
  62. package/dist/components/fields/TimestampField.js +12 -0
  63. package/dist/components/fields/index.d.ts +23 -0
  64. package/dist/components/fields/index.d.ts.map +1 -0
  65. package/dist/components/fields/index.js +13 -0
  66. package/dist/components/fields/registry.d.ts +43 -0
  67. package/dist/components/fields/registry.d.ts.map +1 -0
  68. package/dist/components/fields/registry.js +42 -0
  69. package/dist/components/standalone/DeleteButton.d.ts +35 -0
  70. package/dist/components/standalone/DeleteButton.d.ts.map +1 -0
  71. package/dist/components/standalone/DeleteButton.js +46 -0
  72. package/dist/components/standalone/ItemCreateForm.d.ts +34 -0
  73. package/dist/components/standalone/ItemCreateForm.d.ts.map +1 -0
  74. package/dist/components/standalone/ItemCreateForm.js +91 -0
  75. package/dist/components/standalone/ItemEditForm.d.ts +37 -0
  76. package/dist/components/standalone/ItemEditForm.d.ts.map +1 -0
  77. package/dist/components/standalone/ItemEditForm.js +112 -0
  78. package/dist/components/standalone/ListTable.d.ts +33 -0
  79. package/dist/components/standalone/ListTable.d.ts.map +1 -0
  80. package/dist/components/standalone/ListTable.js +94 -0
  81. package/dist/components/standalone/SearchBar.d.ts +29 -0
  82. package/dist/components/standalone/SearchBar.d.ts.map +1 -0
  83. package/dist/components/standalone/SearchBar.js +43 -0
  84. package/dist/components/standalone/index.d.ts +11 -0
  85. package/dist/components/standalone/index.d.ts.map +1 -0
  86. package/dist/components/standalone/index.js +6 -0
  87. package/dist/index.d.ts +27 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +19 -0
  90. package/dist/lib/serializeFieldConfig.d.ts +43 -0
  91. package/dist/lib/serializeFieldConfig.d.ts.map +1 -0
  92. package/dist/lib/serializeFieldConfig.js +48 -0
  93. package/dist/lib/theme.d.ts +17 -0
  94. package/dist/lib/theme.d.ts.map +1 -0
  95. package/dist/lib/theme.js +192 -0
  96. package/dist/lib/utils.d.ts +18 -0
  97. package/dist/lib/utils.d.ts.map +1 -0
  98. package/dist/lib/utils.js +76 -0
  99. package/dist/primitives/button.d.ts +12 -0
  100. package/dist/primitives/button.d.ts.map +1 -0
  101. package/dist/primitives/button.js +33 -0
  102. package/dist/primitives/calendar.d.ts +9 -0
  103. package/dist/primitives/calendar.d.ts.map +1 -0
  104. package/dist/primitives/calendar.js +48 -0
  105. package/dist/primitives/card.d.ts +9 -0
  106. package/dist/primitives/card.d.ts.map +1 -0
  107. package/dist/primitives/card.js +16 -0
  108. package/dist/primitives/checkbox.d.ts +5 -0
  109. package/dist/primitives/checkbox.d.ts.map +1 -0
  110. package/dist/primitives/checkbox.js +7 -0
  111. package/dist/primitives/combobox.d.ts +14 -0
  112. package/dist/primitives/combobox.d.ts.map +1 -0
  113. package/dist/primitives/combobox.js +20 -0
  114. package/dist/primitives/datetime-picker.d.ts +9 -0
  115. package/dist/primitives/datetime-picker.d.ts.map +1 -0
  116. package/dist/primitives/datetime-picker.js +42 -0
  117. package/dist/primitives/dialog.d.ts +20 -0
  118. package/dist/primitives/dialog.d.ts.map +1 -0
  119. package/dist/primitives/dialog.js +21 -0
  120. package/dist/primitives/index.d.ts +14 -0
  121. package/dist/primitives/index.d.ts.map +1 -0
  122. package/dist/primitives/index.js +14 -0
  123. package/dist/primitives/input.d.ts +5 -0
  124. package/dist/primitives/input.d.ts.map +1 -0
  125. package/dist/primitives/input.js +8 -0
  126. package/dist/primitives/label.d.ts +6 -0
  127. package/dist/primitives/label.d.ts.map +1 -0
  128. package/dist/primitives/label.js +9 -0
  129. package/dist/primitives/popover.d.ts +7 -0
  130. package/dist/primitives/popover.d.ts.map +1 -0
  131. package/dist/primitives/popover.js +10 -0
  132. package/dist/primitives/select.d.ts +14 -0
  133. package/dist/primitives/select.d.ts.map +1 -0
  134. package/dist/primitives/select.js +24 -0
  135. package/dist/primitives/table.d.ts +11 -0
  136. package/dist/primitives/table.d.ts.map +1 -0
  137. package/dist/primitives/table.js +20 -0
  138. package/dist/primitives/time-picker.d.ts +8 -0
  139. package/dist/primitives/time-picker.d.ts.map +1 -0
  140. package/dist/primitives/time-picker.js +27 -0
  141. package/dist/server/index.d.ts +2 -0
  142. package/dist/server/index.d.ts.map +1 -0
  143. package/dist/server/index.js +2 -0
  144. package/dist/server/types.d.ts +15 -0
  145. package/dist/server/types.d.ts.map +1 -0
  146. package/dist/server/types.js +1 -0
  147. package/dist/styles/globals.css +1896 -0
  148. package/package.json +91 -0
  149. package/postcss.config.cjs +5 -0
  150. package/src/components/AdminUI.tsx +112 -0
  151. package/src/components/ConfirmDialog.tsx +56 -0
  152. package/src/components/Dashboard.tsx +134 -0
  153. package/src/components/ItemForm.tsx +195 -0
  154. package/src/components/ItemFormClient.tsx +237 -0
  155. package/src/components/ListView.tsx +153 -0
  156. package/src/components/ListViewClient.tsx +282 -0
  157. package/src/components/LoadingSpinner.tsx +32 -0
  158. package/src/components/Navigation.tsx +117 -0
  159. package/src/components/SkeletonLoader.tsx +82 -0
  160. package/src/components/fields/CheckboxField.tsx +54 -0
  161. package/src/components/fields/ComboboxField.tsx +127 -0
  162. package/src/components/fields/FieldRenderer.tsx +132 -0
  163. package/src/components/fields/IntegerField.tsx +68 -0
  164. package/src/components/fields/PasswordField.tsx +159 -0
  165. package/src/components/fields/RelationshipField.tsx +71 -0
  166. package/src/components/fields/RelationshipManager.tsx +189 -0
  167. package/src/components/fields/SelectField.tsx +71 -0
  168. package/src/components/fields/TextField.tsx +59 -0
  169. package/src/components/fields/TimestampField.tsx +49 -0
  170. package/src/components/fields/index.ts +27 -0
  171. package/src/components/fields/registry.ts +72 -0
  172. package/src/components/standalone/DeleteButton.tsx +114 -0
  173. package/src/components/standalone/ItemCreateForm.tsx +161 -0
  174. package/src/components/standalone/ItemEditForm.tsx +193 -0
  175. package/src/components/standalone/ListTable.tsx +211 -0
  176. package/src/components/standalone/SearchBar.tsx +86 -0
  177. package/src/components/standalone/index.ts +13 -0
  178. package/src/index.ts +74 -0
  179. package/src/lib/serializeFieldConfig.ts +88 -0
  180. package/src/lib/theme.ts +202 -0
  181. package/src/lib/utils.ts +81 -0
  182. package/src/primitives/button.tsx +49 -0
  183. package/src/primitives/calendar.tsx +160 -0
  184. package/src/primitives/card.tsx +58 -0
  185. package/src/primitives/checkbox.tsx +27 -0
  186. package/src/primitives/combobox.tsx +159 -0
  187. package/src/primitives/datetime-picker.tsx +130 -0
  188. package/src/primitives/dialog.tsx +108 -0
  189. package/src/primitives/index.ts +54 -0
  190. package/src/primitives/input.tsx +24 -0
  191. package/src/primitives/label.tsx +19 -0
  192. package/src/primitives/popover.tsx +31 -0
  193. package/src/primitives/select.tsx +158 -0
  194. package/src/primitives/table.tsx +91 -0
  195. package/src/primitives/time-picker.tsx +65 -0
  196. package/src/server/index.ts +3 -0
  197. package/src/server/types.ts +15 -0
  198. package/src/styles/globals.css +123 -0
  199. package/tailwind.config.ts +3 -0
  200. package/tests/components/TextField.test.tsx +94 -0
  201. package/tests/setup.ts +11 -0
  202. package/tsconfig.json +26 -0
  203. package/vitest.config.ts +22 -0
@@ -0,0 +1,8 @@
1
+
2
+ > @opensaas/stack-ui@0.1.0 build /home/runner/work/stack/stack/packages/ui
3
+ > tsc && npm run build:css
4
+
5
+
6
+ > @opensaas/stack-ui@0.1.0 build:css
7
+ > mkdir -p dist/styles && postcss ./src/styles/globals.css -o ./dist/styles/globals.css
8
+
package/README.md ADDED
@@ -0,0 +1,286 @@
1
+ # @opensaas/stack-ui
2
+
3
+ Composable React UI components for OpenSaas Stack, built with Radix UI and shadcn/ui.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @opensaas/stack-ui
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - 🎨 **Four Levels of Abstraction** - Primitives, Fields, Standalone Components, Full Admin UI
14
+ - ♿ **Accessible** - Built with Radix UI primitives
15
+ - 🎯 **Type-Safe** - Full TypeScript support
16
+ - 🎨 **Customizable** - Tailwind CSS v4 with CSS variables
17
+ - 📦 **Tree-Shakeable** - Import only what you need
18
+ - 🧩 **Composable** - Mix and match components
19
+
20
+ ## Package Exports
21
+
22
+ ```typescript
23
+ // Primitives (shadcn/ui components)
24
+ import { Button, Input, Card, Table, Dialog } from '@opensaas/stack-ui/primitives'
25
+
26
+ // Field components (OpenSaas-aware)
27
+ import { TextField, SelectField, RelationshipField } from '@opensaas/stack-ui/fields'
28
+
29
+ // Standalone components (complete features)
30
+ import { ItemCreateForm, ListTable, SearchBar } from '@opensaas/stack-ui/standalone'
31
+
32
+ // Full components (page-level)
33
+ import { Dashboard, ListView, ItemForm, AdminUI } from '@opensaas/stack-ui'
34
+
35
+ // Server utilities
36
+ import { getAdminContext } from '@opensaas/stack-ui/server'
37
+
38
+ // Utility functions
39
+ import { cn, formatListName, formatFieldName } from '@opensaas/stack-ui/lib/utils'
40
+
41
+ // Styles
42
+ import '@opensaas/stack-ui/styles'
43
+ ```
44
+
45
+ ## Architecture
46
+
47
+ ### Level 1: Primitives (`@opensaas/stack-ui/primitives`)
48
+
49
+ Low-level UI components based on Radix UI and shadcn/ui.
50
+
51
+ **Available Components:**
52
+
53
+ - Button - Buttons with variants
54
+ - Input - Text inputs
55
+ - Label - Form labels
56
+ - Card - Content containers
57
+ - Table - Data tables
58
+ - Dialog - Modal dialogs
59
+ - Select - Dropdown selects
60
+ - Checkbox - Checkboxes
61
+
62
+ **Example:**
63
+
64
+ ```tsx
65
+ import { Button, Card, CardHeader, CardTitle, CardContent } from '@opensaas/stack-ui/primitives'
66
+ ;<Card>
67
+ <CardHeader>
68
+ <CardTitle>Welcome</CardTitle>
69
+ </CardHeader>
70
+ <CardContent>
71
+ <Button>Get Started</Button>
72
+ </CardContent>
73
+ </Card>
74
+ ```
75
+
76
+ ### Level 2: Fields (`@opensaas/stack-ui/fields`)
77
+
78
+ OpenSaas-aware form fields with validation and access control.
79
+
80
+ **Available Fields:**
81
+
82
+ - TextField
83
+ - IntegerField
84
+ - CheckboxField
85
+ - SelectField
86
+ - PasswordField
87
+ - TimestampField
88
+ - RelationshipField
89
+
90
+ **Example:**
91
+
92
+ ```tsx
93
+ import { TextField, SelectField } from '@opensaas/stack-ui/fields'
94
+ ;<form>
95
+ <TextField name="email" label="Email" value={email} onChange={setEmail} required />
96
+ <SelectField
97
+ name="role"
98
+ label="Role"
99
+ value={role}
100
+ onChange={setRole}
101
+ options={[
102
+ { label: 'Admin', value: 'admin' },
103
+ { label: 'User', value: 'user' },
104
+ ]}
105
+ />
106
+ </form>
107
+ ```
108
+
109
+ ### Level 3: Standalone Components (`@opensaas/stack-ui/standalone`)
110
+
111
+ Complete, reusable components for common admin tasks.
112
+
113
+ **Available Components:**
114
+
115
+ #### ItemCreateForm
116
+
117
+ ```tsx
118
+ import { ItemCreateForm } from '@opensaas/stack-ui/standalone'
119
+ ;<ItemCreateForm
120
+ fields={config.lists.Post.fields}
121
+ onSubmit={async (data) => {
122
+ const post = await createPost(data)
123
+ return { success: !!post }
124
+ }}
125
+ onCancel={() => router.back()}
126
+ />
127
+ ```
128
+
129
+ #### ItemEditForm
130
+
131
+ ```tsx
132
+ import { ItemEditForm } from '@opensaas/stack-ui/standalone'
133
+ ;<ItemEditForm
134
+ fields={config.lists.Post.fields}
135
+ initialData={post}
136
+ onSubmit={async (data) => {
137
+ const updated = await updatePost(post.id, data)
138
+ return { success: !!updated }
139
+ }}
140
+ />
141
+ ```
142
+
143
+ #### ListTable
144
+
145
+ ```tsx
146
+ import { ListTable } from '@opensaas/stack-ui/standalone'
147
+ ;<ListTable
148
+ items={posts}
149
+ fieldTypes={{ title: 'text', status: 'select' }}
150
+ columns={['title', 'status']}
151
+ onRowClick={(post) => router.push(`/posts/${post.id}`)}
152
+ sortable
153
+ />
154
+ ```
155
+
156
+ #### SearchBar
157
+
158
+ ```tsx
159
+ import { SearchBar } from '@opensaas/stack-ui/standalone'
160
+ ;<SearchBar onSearch={(query) => fetchPosts({ search: query })} placeholder="Search posts..." />
161
+ ```
162
+
163
+ #### DeleteButton
164
+
165
+ ```tsx
166
+ import { DeleteButton } from '@opensaas/stack-ui/standalone'
167
+ ;<DeleteButton
168
+ onDelete={async () => {
169
+ await deletePost(postId)
170
+ return { success: true }
171
+ }}
172
+ itemName="post"
173
+ />
174
+ ```
175
+
176
+ ### Level 4: Full Admin UI (`@opensaas/stack-ui`)
177
+
178
+ Complete admin interface with routing and navigation.
179
+
180
+ ```tsx
181
+ import { AdminUI } from '@opensaas/stack-ui'
182
+ ;<AdminUI
183
+ context={context}
184
+ params={params?.admin}
185
+ searchParams={searchParams}
186
+ basePath="/admin"
187
+ serverAction={handleServerAction}
188
+ />
189
+ ```
190
+
191
+ ## Component Registry
192
+
193
+ Extend or override field components:
194
+
195
+ ```tsx
196
+ import { registerFieldComponent } from '@opensaas/stack-ui'
197
+ import { ColorPickerField } from './components/ColorPickerField'
198
+
199
+ // Register globally
200
+ registerFieldComponent('color', ColorPickerField)
201
+
202
+ // Use in config
203
+ fields: {
204
+ themeColor: text({
205
+ ui: { fieldType: 'color' },
206
+ })
207
+ }
208
+
209
+ // Or override per-field
210
+ fields: {
211
+ slug: text({
212
+ ui: { component: SlugField },
213
+ })
214
+ }
215
+ ```
216
+
217
+ ## Theming
218
+
219
+ All components use Tailwind CSS v4 with CSS variables:
220
+
221
+ ```css
222
+ /* app/globals.css */
223
+ @import '@opensaas/stack-ui/styles';
224
+
225
+ :root {
226
+ --background: 0 0% 100%;
227
+ --foreground: 0 0% 3.9%;
228
+ --primary: 0 0% 9%;
229
+ --primary-foreground: 0 0% 98%;
230
+ --destructive: 0 84.2% 60.2%;
231
+ --destructive-foreground: 0 0% 98%;
232
+ --muted: 0 0% 96.1%;
233
+ --muted-foreground: 0 0% 45.1%;
234
+ --accent: 0 0% 96.1%;
235
+ --accent-foreground: 0 0% 9%;
236
+ --border: 0 0% 89.8%;
237
+ --input: 0 0% 89.8%;
238
+ --ring: 0 0% 3.9%;
239
+ }
240
+
241
+ .dark {
242
+ --background: 0 0% 3.9%;
243
+ --foreground: 0 0% 98%;
244
+ /* ... */
245
+ }
246
+ ```
247
+
248
+ ## Accessibility
249
+
250
+ All components follow WAI-ARIA guidelines:
251
+
252
+ - Proper semantic HTML
253
+ - ARIA attributes
254
+ - Keyboard navigation
255
+ - Focus management
256
+ - Screen reader support
257
+
258
+ ## TypeScript
259
+
260
+ Full TypeScript support with exported types:
261
+
262
+ ```typescript
263
+ import type {
264
+ FieldComponent,
265
+ FieldComponentProps,
266
+ ItemCreateFormProps,
267
+ ListTableProps,
268
+ AdminUIProps,
269
+ } from '@opensaas/stack-ui'
270
+ ```
271
+
272
+ ## Examples
273
+
274
+ - [Blog Example](../../examples/blog) - Basic usage with full AdminUI
275
+ - [Custom Field Example](../../examples/custom-field) - Custom field components
276
+ - [Composable Dashboard](../../examples/composable-dashboard) - Using standalone components
277
+
278
+ ## Learn More
279
+
280
+ - [Composability Guide](../../docs/COMPOSABILITY.md) - Complete guide to all four levels
281
+ - [API Reference](../../docs/API.md) - Full API documentation
282
+ - [OpenSaas Stack](../../README.md) - Stack overview
283
+
284
+ ## License
285
+
286
+ MIT
@@ -0,0 +1,24 @@
1
+ import type { ServerActionInput } from '../server/types.js';
2
+ import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
3
+ export interface AdminUIProps<TPrisma> {
4
+ context: AccessContext<TPrisma>;
5
+ config: OpenSaasConfig;
6
+ params?: string[];
7
+ searchParams?: {
8
+ [key: string]: string | string[] | undefined;
9
+ };
10
+ basePath?: string;
11
+ serverAction: (input: ServerActionInput) => Promise<unknown>;
12
+ }
13
+ /**
14
+ * Main AdminUI component - complete admin interface with routing
15
+ * Server Component
16
+ *
17
+ * Handles routing based on params array:
18
+ * - [] → Dashboard
19
+ * - [list] → ListView
20
+ * - [list, 'create'] → ItemForm (create)
21
+ * - [list, id] → ItemForm (edit)
22
+ */
23
+ export declare function AdminUI<TPrisma>({ context, config, params, searchParams, basePath, serverAction, }: AdminUIProps<TPrisma>): import("react/jsx-runtime").JSX.Element;
24
+ //# sourceMappingURL=AdminUI.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AdminUI.d.ts","sourceRoot":"","sources":["../../src/components/AdminUI.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAqB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGvF,MAAM,WAAW,YAAY,CAAC,OAAO;IACnC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,YAAY,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAA;KAAE,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC7D;AAED;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,EAC/B,OAAO,EACP,MAAM,EACN,MAAW,EACX,YAAiB,EACjB,QAAmB,EACnB,YAAY,GACb,EAAE,YAAY,CAAC,OAAO,CAAC,2CA2EvB"}
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Navigation } from './Navigation.js';
3
+ import { Dashboard } from './Dashboard.js';
4
+ import { ListView } from './ListView.js';
5
+ import { ItemForm } from './ItemForm.js';
6
+ import { getListKeyFromUrl } from '@opensaas/stack-core';
7
+ import { generateThemeCSS } from '../lib/theme.js';
8
+ /**
9
+ * Main AdminUI component - complete admin interface with routing
10
+ * Server Component
11
+ *
12
+ * Handles routing based on params array:
13
+ * - [] → Dashboard
14
+ * - [list] → ListView
15
+ * - [list, 'create'] → ItemForm (create)
16
+ * - [list, id] → ItemForm (edit)
17
+ */
18
+ export function AdminUI({ context, config, params = [], searchParams = {}, basePath = '/admin', serverAction, }) {
19
+ // Parse route from params
20
+ const [urlSegment, action] = params;
21
+ // Convert URL segment (kebab-case) to PascalCase listKey
22
+ const listKey = urlSegment ? getListKeyFromUrl(urlSegment) : undefined;
23
+ // Determine current path for navigation highlighting
24
+ const currentPath = params.length > 0 ? `/${params.join('/')}` : '';
25
+ // Route to appropriate component
26
+ let content;
27
+ if (!listKey) {
28
+ // Dashboard
29
+ content = _jsx(Dashboard, { context: context, config: config, basePath: basePath });
30
+ }
31
+ else if (action === 'create') {
32
+ // Create form
33
+ content = (_jsx(ItemForm, { context: context, config: config, listKey: listKey, mode: "create", basePath: basePath, serverAction: serverAction }));
34
+ }
35
+ else if (action && action !== 'create') {
36
+ // Edit form (action is the item ID)
37
+ content = (_jsx(ItemForm, { context: context, config: config, listKey: listKey, mode: "edit", itemId: action, basePath: basePath, serverAction: serverAction }));
38
+ }
39
+ else {
40
+ // List view
41
+ const search = typeof searchParams.search === 'string' ? searchParams.search : undefined;
42
+ const page = typeof searchParams.page === 'string' ? parseInt(searchParams.page, 10) : 1;
43
+ content = (_jsx(ListView, { context: context, config: config, listKey: listKey, basePath: basePath, search: search, page: page }));
44
+ }
45
+ // Generate theme styles if custom theme is configured
46
+ const themeStyles = config.ui?.theme ? generateThemeCSS(config.ui.theme) : null;
47
+ return (_jsxs(_Fragment, { children: [themeStyles && _jsx("style", { dangerouslySetInnerHTML: { __html: themeStyles } }), _jsxs("div", { className: "flex min-h-screen bg-background", children: [_jsx(Navigation, { context: context, config: config, basePath: basePath, currentPath: currentPath }), _jsx("main", { className: "flex-1 overflow-y-auto", children: content })] })] }));
48
+ }
@@ -0,0 +1,16 @@
1
+ export interface ConfirmDialogProps {
2
+ isOpen: boolean;
3
+ title: string;
4
+ message: string;
5
+ confirmLabel?: string;
6
+ cancelLabel?: string;
7
+ onConfirm: () => void;
8
+ onCancel: () => void;
9
+ variant?: 'danger' | 'warning';
10
+ }
11
+ /**
12
+ * Reusable confirmation dialog component
13
+ * Used for destructive actions like delete
14
+ */
15
+ export declare function ConfirmDialog({ isOpen, title, message, confirmLabel, cancelLabel, onConfirm, onCancel, variant, }: ConfirmDialogProps): import("react/jsx-runtime").JSX.Element;
16
+ //# sourceMappingURL=ConfirmDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfirmDialog.d.ts","sourceRoot":"","sources":["../../src/components/ConfirmDialog.tsx"],"names":[],"mappings":"AAYA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,OAAO,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;CAC/B;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,KAAK,EACL,OAAO,EACP,YAAwB,EACxB,WAAsB,EACtB,SAAS,EACT,QAAQ,EACR,OAAkB,GACnB,EAAE,kBAAkB,2CAmBpB"}
@@ -0,0 +1,11 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../primitives/dialog.js';
4
+ import { Button } from '../primitives/button.js';
5
+ /**
6
+ * Reusable confirmation dialog component
7
+ * Used for destructive actions like delete
8
+ */
9
+ export function ConfirmDialog({ isOpen, title, message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', onConfirm, onCancel, variant = 'danger', }) {
10
+ return (_jsx(Dialog, { open: isOpen, onOpenChange: (open) => !open && onCancel(), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: message })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: onCancel, children: cancelLabel }), _jsx(Button, { variant: variant === 'danger' ? 'destructive' : 'default', onClick: onConfirm, children: confirmLabel })] })] }) }));
11
+ }
@@ -0,0 +1,12 @@
1
+ import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
2
+ export interface DashboardProps<TPrisma> {
3
+ context: AccessContext<TPrisma>;
4
+ config: OpenSaasConfig;
5
+ basePath?: string;
6
+ }
7
+ /**
8
+ * Dashboard landing page showing all available lists
9
+ * Server Component
10
+ */
11
+ export declare function Dashboard<TPrisma>({ context, config, basePath, }: DashboardProps<TPrisma>): Promise<import("react/jsx-runtime").JSX.Element>;
12
+ //# sourceMappingURL=Dashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Dashboard.d.ts","sourceRoot":"","sources":["../../src/components/Dashboard.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGzF,MAAM,WAAW,cAAc,CAAC,OAAO;IACrC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,EACvC,OAAO,EACP,MAAM,EACN,QAAmB,GACpB,EAAE,cAAc,CAAC,OAAO,CAAC,oDAkHzB"}
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Link from 'next/link';
3
+ import { formatListName } from '../lib/utils.js';
4
+ import { getDbKey, getUrlKey } from '@opensaas/stack-core';
5
+ import { Card, CardContent, CardHeader, CardTitle } from '../primitives/card.js';
6
+ /**
7
+ * Dashboard landing page showing all available lists
8
+ * Server Component
9
+ */
10
+ export async function Dashboard({ context, config, basePath = '/admin', }) {
11
+ const lists = Object.keys(config.lists || {});
12
+ // Get counts for each list
13
+ const listCounts = await Promise.all(lists.map(async (listKey) => {
14
+ try {
15
+ const count = await context.db[getDbKey(listKey)]?.count();
16
+ return { listKey, count: count || 0 };
17
+ }
18
+ catch (error) {
19
+ console.error(`Failed to get count for ${listKey}:`, error);
20
+ return { listKey, count: 0 };
21
+ }
22
+ }));
23
+ return (_jsxs("div", { className: "p-8", children: [_jsxs("div", { className: "mb-8 relative", children: [_jsx("div", { className: "absolute inset-0 bg-gradient-to-r from-primary/5 to-accent/5 opacity-100 rounded-2xl" }), _jsxs("div", { className: "relative p-6", children: [_jsx("h1", { className: "text-4xl font-bold mb-2 bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent", children: "Dashboard" }), _jsx("p", { className: "text-muted-foreground", children: "Manage your application data" })] })] }), lists.length === 0 ? (_jsxs(Card, { className: "p-12 text-center border-2 border-dashed", children: [_jsx("div", { className: "mb-4 text-4xl", children: "\uD83D\uDCE6" }), _jsx("p", { className: "text-muted-foreground mb-2 font-medium", children: "No lists configured" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Add lists to your opensaas.config.ts to get started." })] })) : (_jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6", children: listCounts.map(({ listKey, count }) => {
24
+ const urlKey = getUrlKey(listKey);
25
+ return (_jsx(Link, { href: `${basePath}/${urlKey}`, children: _jsxs(Card, { className: "group hover:border-primary hover:shadow-lg hover:shadow-primary/20 transition-all duration-200 cursor-pointer h-full relative overflow-hidden", children: [_jsx("div", { className: "absolute inset-0 bg-gradient-to-br from-primary/5 to-accent/5 opacity-0 group-hover:opacity-100 transition-opacity" }), _jsx(CardHeader, { className: "relative", children: _jsxs("div", { className: "flex items-start justify-between", children: [_jsxs("div", { children: [_jsx(CardTitle, { className: "text-xl group-hover:text-primary transition-colors", children: formatListName(listKey) }), _jsxs("p", { className: "text-sm text-muted-foreground mt-1 font-medium", children: [count, " ", count === 1 ? 'item' : 'items'] })] }), _jsx("div", { className: "text-3xl opacity-60 group-hover:opacity-100 transition-opacity", children: "\uD83D\uDCCB" })] }) }), _jsx(CardContent, { className: "relative", children: _jsxs("div", { className: "flex items-center text-sm font-medium text-primary", children: [_jsx("span", { children: "View all" }), _jsx("svg", { className: "ml-1 w-4 h-4 group-hover:translate-x-1 transition-transform", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })] }) })] }) }, listKey));
26
+ }) })), lists.length > 0 && (_jsxs(Card, { className: "mt-12 bg-gradient-to-br from-accent/10 to-primary/10 border-accent/20", children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "text-lg flex items-center gap-2", children: [_jsx("span", { className: "text-xl", children: "\u26A1" }), "Quick Actions"] }) }), _jsx(CardContent, { children: _jsx("div", { className: "flex flex-wrap gap-3", children: lists.map((listKey) => {
27
+ const urlKey = getUrlKey(listKey);
28
+ return (_jsxs(Link, { href: `${basePath}/${urlKey}/create`, className: "inline-flex items-center gap-1 px-4 py-2 rounded-lg bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground font-medium text-sm transition-colors border border-primary/20", children: [_jsx("span", { className: "text-lg", children: "+" }), "Create ", formatListName(listKey)] }, listKey));
29
+ }) }) })] }))] }));
30
+ }
@@ -0,0 +1,17 @@
1
+ import type { ServerActionInput } from '../server/types.js';
2
+ import { AccessContext, OpenSaasConfig } from '@opensaas/stack-core';
3
+ export interface ItemFormProps<TPrisma> {
4
+ context: AccessContext<TPrisma>;
5
+ config: OpenSaasConfig;
6
+ listKey: string;
7
+ mode: 'create' | 'edit';
8
+ itemId?: string;
9
+ basePath?: string;
10
+ serverAction: (input: ServerActionInput) => Promise<unknown>;
11
+ }
12
+ /**
13
+ * Item form component - create or edit an item
14
+ * Server Component that fetches data and sets up actions
15
+ */
16
+ export declare function ItemForm<TPrisma>({ context, config, listKey, mode, itemId, basePath, serverAction, }: ItemFormProps<TPrisma>): Promise<import("react/jsx-runtime").JSX.Element>;
17
+ //# sourceMappingURL=ItemForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ItemForm.d.ts","sourceRoot":"","sources":["../../src/components/ItemForm.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAuB,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAGzF,MAAM,WAAW,aAAa,CAAC,OAAO;IACpC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,EAAE,cAAc,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAC7D;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,EACtC,OAAO,EACP,MAAM,EACN,OAAO,EACP,IAAI,EACJ,MAAM,EACN,QAAmB,EACnB,YAAY,GACb,EAAE,aAAa,CAAC,OAAO,CAAC,oDAmKxB"}
@@ -0,0 +1,97 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Link from 'next/link';
3
+ import { ItemFormClient } from './ItemFormClient.js';
4
+ import { formatListName } from '../lib/utils.js';
5
+ import { getDbKey, getUrlKey } from '@opensaas/stack-core';
6
+ import { serializeFieldConfigs } from '../lib/serializeFieldConfig.js';
7
+ /**
8
+ * Item form component - create or edit an item
9
+ * Server Component that fetches data and sets up actions
10
+ */
11
+ export async function ItemForm({ context, config, listKey, mode, itemId, basePath = '/admin', serverAction, }) {
12
+ const listConfig = config.lists[listKey];
13
+ const urlKey = getUrlKey(listKey);
14
+ if (!listConfig) {
15
+ return (_jsx("div", { className: "p-8", children: _jsxs("div", { className: "bg-destructive/10 border border-destructive text-destructive rounded-lg p-6", children: [_jsx("h2", { className: "text-lg font-semibold mb-2", children: "List not found" }), _jsxs("p", { children: ["The list \"", listKey, "\" does not exist in your configuration."] })] }) }));
16
+ }
17
+ // Fetch item data if in edit mode
18
+ let itemData = {};
19
+ if (mode === 'edit' && itemId) {
20
+ try {
21
+ // Build include object for relationships
22
+ const includeRelationships = {};
23
+ for (const [fieldName, fieldConfig] of Object.entries(listConfig.fields)) {
24
+ const fieldConfigAny = fieldConfig;
25
+ if (fieldConfigAny.type === 'relationship') {
26
+ includeRelationships[fieldName] = true;
27
+ }
28
+ }
29
+ // Fetch item with relationships included
30
+ itemData = await context.db[getDbKey(listKey)].findUnique({
31
+ where: { id: itemId },
32
+ ...(Object.keys(includeRelationships).length > 0 && { include: includeRelationships }),
33
+ });
34
+ }
35
+ catch (error) {
36
+ console.error(`Failed to fetch item ${itemId}:`, error);
37
+ }
38
+ if (!itemData) {
39
+ return (_jsx("div", { className: "p-8", children: _jsxs("div", { className: "bg-destructive/10 border border-destructive text-destructive rounded-lg p-6", children: [_jsx("h2", { className: "text-lg font-semibold mb-2", children: "Item not found" }), _jsx("p", { children: "The item you're trying to edit doesn't exist or you don't have access to it." }), _jsxs(Link, { href: `${basePath}/${urlKey}`, className: "inline-block mt-4 text-primary hover:underline", children: ["\u2190 Back to ", formatListName(listKey)] })] }) }));
40
+ }
41
+ }
42
+ // Fetch relationship options for all relationship fields
43
+ const relationshipData = {};
44
+ for (const [fieldName, fieldConfig] of Object.entries(listConfig.fields)) {
45
+ // Check if field is a relationship type by checking the discriminated union
46
+ const fieldConfigAny = fieldConfig;
47
+ if (fieldConfigAny.type === 'relationship') {
48
+ const ref = fieldConfigAny.ref;
49
+ if (ref) {
50
+ // Parse ref format: "ListName.fieldName"
51
+ const relatedListName = ref.split('.')[0];
52
+ const relatedListConfig = config.lists[relatedListName];
53
+ if (relatedListConfig) {
54
+ try {
55
+ const dbContext = context.db;
56
+ const relatedItems = await dbContext[getDbKey(relatedListName)].findMany({});
57
+ // Use 'name' field as label if it exists, otherwise use 'id'
58
+ relationshipData[fieldName] = relatedItems.map((item) => ({
59
+ id: item.id,
60
+ label: (item.name || item.title || item.id) || '',
61
+ }));
62
+ }
63
+ catch (error) {
64
+ console.error(`Failed to fetch relationship items for ${fieldName}:`, error);
65
+ relationshipData[fieldName] = [];
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ // Serialize field configs to remove non-serializable properties
72
+ const serializableFields = serializeFieldConfigs(listConfig.fields);
73
+ // Transform relationship data in itemData from objects to IDs for form
74
+ // Also apply valueForClientSerialization transformation
75
+ const formData = { ...itemData };
76
+ for (const [fieldName, fieldConfig] of Object.entries(listConfig.fields)) {
77
+ const fieldConfigAny = fieldConfig;
78
+ if (fieldConfigAny.type === 'relationship' && formData[fieldName]) {
79
+ const value = formData[fieldName];
80
+ if (fieldConfigAny.many && Array.isArray(value)) {
81
+ // Many relationship: extract IDs from array of objects
82
+ formData[fieldName] = value.map((item) => item.id);
83
+ }
84
+ else if (value && typeof value === 'object' && 'id' in value) {
85
+ // Single relationship: extract ID from object
86
+ formData[fieldName] = value.id;
87
+ }
88
+ }
89
+ // Apply valueForClientSerialization if defined
90
+ if (fieldConfigAny.ui?.valueForClientSerialization &&
91
+ typeof fieldConfigAny.ui.valueForClientSerialization === 'function') {
92
+ const transformer = fieldConfigAny.ui.valueForClientSerialization;
93
+ formData[fieldName] = transformer({ value: formData[fieldName] });
94
+ }
95
+ }
96
+ return (_jsxs("div", { className: "p-8 max-w-4xl", children: [_jsxs("div", { className: "mb-8", children: [_jsxs(Link, { href: `${basePath}/${urlKey}`, className: "inline-flex items-center text-sm text-muted-foreground hover:text-foreground mb-4", children: [_jsx("svg", { className: "w-4 h-4 mr-1", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }), "Back to ", formatListName(listKey)] }), _jsxs("h1", { className: "text-3xl font-bold", children: [mode === 'create' ? 'Create' : 'Edit', " ", formatListName(listKey)] })] }), _jsx("div", { className: "bg-card border border-border rounded-lg p-6", children: _jsx(ItemFormClient, { listKey: listKey, urlKey: urlKey, mode: mode, fields: serializableFields, initialData: JSON.parse(JSON.stringify(formData)), itemId: itemId, basePath: basePath, serverAction: serverAction, relationshipData: relationshipData }) })] }));
97
+ }
@@ -0,0 +1,22 @@
1
+ import type { ServerActionInput } from '../server/types.js';
2
+ import type { SerializableFieldConfig } from '../lib/serializeFieldConfig.js';
3
+ export interface ItemFormClientProps {
4
+ listKey: string;
5
+ urlKey: string;
6
+ mode: 'create' | 'edit';
7
+ fields: Record<string, SerializableFieldConfig>;
8
+ initialData?: Record<string, unknown>;
9
+ itemId?: string;
10
+ basePath: string;
11
+ serverAction: (input: ServerActionInput) => Promise<unknown>;
12
+ relationshipData?: Record<string, Array<{
13
+ id: string;
14
+ label: string;
15
+ }>>;
16
+ }
17
+ /**
18
+ * Client component for interactive form
19
+ * Handles form state, validation, and submission
20
+ */
21
+ export declare function ItemFormClient({ listKey, urlKey, mode, fields, initialData, itemId, basePath, serverAction, relationshipData, }: ItemFormClientProps): import("react/jsx-runtime").JSX.Element;
22
+ //# sourceMappingURL=ItemFormClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ItemFormClient.d.ts","sourceRoot":"","sources":["../../src/components/ItemFormClient.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAA;AAE7E,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAA;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CACxE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,MAAM,EACN,IAAI,EACJ,MAAM,EACN,WAAgB,EAChB,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,gBAAqB,GACtB,EAAE,mBAAmB,2CAsMrB"}