@opensaas/stack-ui 0.1.0 → 0.1.2
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/.turbo/turbo-build.log +4 -2
- package/CHANGELOG.md +18 -0
- package/CLAUDE.md +311 -0
- package/LICENSE +21 -0
- package/dist/components/ItemFormClient.d.ts.map +1 -1
- package/dist/components/ItemFormClient.js +3 -1
- package/dist/components/fields/FileField.d.ts +21 -0
- package/dist/components/fields/FileField.d.ts.map +1 -0
- package/dist/components/fields/FileField.js +78 -0
- package/dist/components/fields/ImageField.d.ts +23 -0
- package/dist/components/fields/ImageField.d.ts.map +1 -0
- package/dist/components/fields/ImageField.js +107 -0
- package/dist/components/fields/JsonField.d.ts +15 -0
- package/dist/components/fields/JsonField.d.ts.map +1 -0
- package/dist/components/fields/JsonField.js +57 -0
- package/dist/components/fields/index.d.ts +6 -0
- package/dist/components/fields/index.d.ts.map +1 -1
- package/dist/components/fields/index.js +3 -0
- package/dist/components/fields/registry.d.ts.map +1 -1
- package/dist/components/fields/registry.js +6 -0
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/textarea.d.ts +6 -0
- package/dist/primitives/textarea.d.ts.map +1 -0
- package/dist/primitives/textarea.js +8 -0
- package/dist/styles/globals.css +89 -0
- package/package.json +5 -3
- package/src/components/ItemFormClient.tsx +3 -1
- package/src/components/fields/FileField.tsx +223 -0
- package/src/components/fields/ImageField.tsx +328 -0
- package/src/components/fields/JsonField.tsx +114 -0
- package/src/components/fields/index.ts +6 -0
- package/src/components/fields/registry.ts +6 -0
- package/src/primitives/index.ts +1 -0
- package/src/primitives/textarea.tsx +24 -0
- package/vitest.config.ts +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
> @opensaas/stack-ui@0.1.
|
|
2
|
+
> @opensaas/stack-ui@0.1.2 build /home/runner/work/stack/stack/packages/ui
|
|
3
3
|
> tsc && npm run build:css
|
|
4
4
|
|
|
5
|
+
npm warn Unknown env config "verify-deps-before-run". This will stop working in the next major version of npm.
|
|
6
|
+
npm warn Unknown env config "_jsr-registry". This will stop working in the next major version of npm.
|
|
5
7
|
|
|
6
|
-
> @opensaas/stack-ui@0.1.
|
|
8
|
+
> @opensaas/stack-ui@0.1.2 build:css
|
|
7
9
|
> mkdir -p dist/styles && postcss ./src/styles/globals.css -o ./dist/styles/globals.css
|
|
8
10
|
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @opensaas/stack-ui
|
|
2
|
+
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @opensaas/stack-core@0.1.2
|
|
8
|
+
|
|
9
|
+
## 0.1.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 9a3fda5: Add JSON field
|
|
14
|
+
- 045c071: Add field and image upload
|
|
15
|
+
- Updated dependencies [9a3fda5]
|
|
16
|
+
- Updated dependencies [f8ebc0e]
|
|
17
|
+
- Updated dependencies [045c071]
|
|
18
|
+
- @opensaas/stack-core@0.1.1
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# @opensaas/stack-ui
|
|
2
|
+
|
|
3
|
+
Composable React UI components for OpenSaas Stack admin interfaces, built on shadcn/ui primitives.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Provides multiple levels of UI abstraction:
|
|
8
|
+
|
|
9
|
+
1. **Full AdminUI** - Complete admin interface with routing
|
|
10
|
+
2. **Standalone Components** - Drop-in CRUD components (forms, tables)
|
|
11
|
+
3. **Field Components** - Individual field inputs
|
|
12
|
+
4. **Primitives** - Low-level shadcn/ui components for custom UIs
|
|
13
|
+
|
|
14
|
+
## Key Exports
|
|
15
|
+
|
|
16
|
+
### Main Export (`src/index.ts`)
|
|
17
|
+
|
|
18
|
+
- `AdminUI` - Complete admin interface
|
|
19
|
+
- `registerFieldComponent(type, Component)` - Register custom field components
|
|
20
|
+
- Primitives re-exported
|
|
21
|
+
|
|
22
|
+
### Primitives (`/primitives`)
|
|
23
|
+
|
|
24
|
+
shadcn/ui components:
|
|
25
|
+
|
|
26
|
+
- `Button`, `Input`, `Label`, `Checkbox`, `Select`
|
|
27
|
+
- `Card`, `CardHeader`, `CardContent`, `CardFooter`
|
|
28
|
+
- `Table`, `TableHeader`, `TableBody`, `TableRow`, `TableCell`
|
|
29
|
+
- `Dialog`, `DialogContent`, `DialogHeader`, `DialogFooter`
|
|
30
|
+
- `Popover`, `Calendar`, `DatetimePicker`, `TimePicker`
|
|
31
|
+
- `Combobox` - Search and select component
|
|
32
|
+
|
|
33
|
+
### Fields (`/fields`)
|
|
34
|
+
|
|
35
|
+
Field components for forms:
|
|
36
|
+
|
|
37
|
+
- `TextField`, `IntegerField`, `CheckboxField`, `TimestampField`
|
|
38
|
+
- `PasswordField`, `SelectField`, `RelationshipField`
|
|
39
|
+
- `FieldRenderer` - Renders field based on config (uses registry)
|
|
40
|
+
|
|
41
|
+
### Standalone (`/standalone`)
|
|
42
|
+
|
|
43
|
+
Composable CRUD components:
|
|
44
|
+
|
|
45
|
+
- `ItemCreateForm` - Create new item
|
|
46
|
+
- `ItemEditForm` - Edit existing item
|
|
47
|
+
- `ListTable` - Display list of items
|
|
48
|
+
- `SearchBar` - Search and filter
|
|
49
|
+
- `DeleteButton` - Delete with confirmation
|
|
50
|
+
|
|
51
|
+
### Server (`/server`)
|
|
52
|
+
|
|
53
|
+
- `getAdminContext(headers)` - Get context with session from request headers
|
|
54
|
+
|
|
55
|
+
## Architecture Patterns
|
|
56
|
+
|
|
57
|
+
### Component Registry
|
|
58
|
+
|
|
59
|
+
Field components are registered by type, avoiding switch statements:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// Default registry
|
|
63
|
+
registerFieldComponent('text', TextField)
|
|
64
|
+
registerFieldComponent('integer', IntegerField)
|
|
65
|
+
// etc.
|
|
66
|
+
|
|
67
|
+
// Custom registration
|
|
68
|
+
registerFieldComponent('color', ColorPickerField)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Component Resolution Priority
|
|
72
|
+
|
|
73
|
+
`FieldRenderer` resolves components in order:
|
|
74
|
+
|
|
75
|
+
1. `field.ui.component` - Per-field override (highest priority)
|
|
76
|
+
2. `field.ui.fieldType` - Custom type lookup in registry
|
|
77
|
+
3. `field.type` - Default type lookup in registry
|
|
78
|
+
|
|
79
|
+
### Composability Levels
|
|
80
|
+
|
|
81
|
+
**Level 1: Full AdminUI**
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { AdminUI } from '@opensaas/stack-ui'
|
|
85
|
+
<AdminUI context={context} config={config} />
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Level 2: Standalone Components**
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { ItemCreateForm, ListTable } from '@opensaas/stack-ui/standalone'
|
|
92
|
+
|
|
93
|
+
<ItemCreateForm
|
|
94
|
+
listKey="Post"
|
|
95
|
+
context={context}
|
|
96
|
+
onSuccess={(item) => router.push(`/posts/${item.id}`)}
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
<ListTable
|
|
100
|
+
listKey="Post"
|
|
101
|
+
context={context}
|
|
102
|
+
columns={['title', 'author', 'createdAt']}
|
|
103
|
+
/>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Level 3: Field Components**
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { TextField, SelectField } from '@opensaas/stack-ui/fields'
|
|
110
|
+
|
|
111
|
+
<form>
|
|
112
|
+
<TextField
|
|
113
|
+
name="title"
|
|
114
|
+
value={title}
|
|
115
|
+
onChange={setTitle}
|
|
116
|
+
label="Title"
|
|
117
|
+
required
|
|
118
|
+
/>
|
|
119
|
+
</form>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Level 4: Primitives**
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { Button, Card, Input } from '@opensaas/stack-ui/primitives'
|
|
126
|
+
|
|
127
|
+
<Card>
|
|
128
|
+
<Input placeholder="Custom input" />
|
|
129
|
+
<Button onClick={handleClick}>Submit</Button>
|
|
130
|
+
</Card>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### UI Options Pass-Through
|
|
134
|
+
|
|
135
|
+
Field config `ui` options automatically pass to components:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// Config
|
|
139
|
+
content: richText({
|
|
140
|
+
ui: {
|
|
141
|
+
placeholder: 'Write content...',
|
|
142
|
+
minHeight: 300,
|
|
143
|
+
customOption: 'value',
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Component receives all ui options as props
|
|
148
|
+
export function RichTextField({ placeholder, minHeight, customOption, ...baseProps }) {
|
|
149
|
+
// Use options
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`FieldRenderer` extracts `component` and `fieldType`, passes rest as props.
|
|
154
|
+
|
|
155
|
+
### Server/Client Boundaries
|
|
156
|
+
|
|
157
|
+
- `AdminUI` is server component (uses `getAdminContext`)
|
|
158
|
+
- Forms and interactive components are client components
|
|
159
|
+
- Data serialization via props (no functions, only JSON-serializable data)
|
|
160
|
+
|
|
161
|
+
## Integration Points
|
|
162
|
+
|
|
163
|
+
### With @opensaas/stack-core
|
|
164
|
+
|
|
165
|
+
- Reads config to generate UI
|
|
166
|
+
- Uses context for all data operations
|
|
167
|
+
- Field components map to field types via registry
|
|
168
|
+
|
|
169
|
+
### With @opensaas/stack-auth
|
|
170
|
+
|
|
171
|
+
- `getAdminContext` uses Better-auth to get session
|
|
172
|
+
- Session flows through context to access control
|
|
173
|
+
- Auth UI components imported separately from `@opensaas/stack-auth/ui`
|
|
174
|
+
|
|
175
|
+
### With Third-Party Field Packages
|
|
176
|
+
|
|
177
|
+
Third-party fields register components on client side:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// lib/register-fields.ts
|
|
181
|
+
'use client'
|
|
182
|
+
import { registerFieldComponent } from '@opensaas/stack-ui'
|
|
183
|
+
import { RichTextField } from '@opensaas/stack-tiptap'
|
|
184
|
+
registerFieldComponent('richText', RichTextField)
|
|
185
|
+
|
|
186
|
+
// app/admin/[[...admin]]/page.tsx
|
|
187
|
+
import '../../../lib/register-fields' // Side-effect import
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Common Patterns
|
|
191
|
+
|
|
192
|
+
### Basic Admin Setup
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// app/admin/[[...admin]]/page.tsx
|
|
196
|
+
import { AdminUI } from '@opensaas/stack-ui'
|
|
197
|
+
import { getAdminContext } from '@opensaas/stack-ui/server'
|
|
198
|
+
import config from '@/opensaas.config'
|
|
199
|
+
|
|
200
|
+
export default async function AdminPage() {
|
|
201
|
+
const context = await getAdminContext()
|
|
202
|
+
return <AdminUI context={context} config={config} />
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Custom Field Component (Global Registration)
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// lib/register-fields.ts
|
|
210
|
+
'use client'
|
|
211
|
+
import { registerFieldComponent } from '@opensaas/stack-ui'
|
|
212
|
+
import { ColorPickerField } from './components/ColorPickerField'
|
|
213
|
+
|
|
214
|
+
registerFieldComponent('color', ColorPickerField)
|
|
215
|
+
|
|
216
|
+
// opensaas.config.ts
|
|
217
|
+
fields: {
|
|
218
|
+
themeColor: text({ ui: { fieldType: 'color' } })
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Custom Field Component (Per-Field Override)
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// opensaas.config.ts
|
|
226
|
+
import { SlugField } from './components/SlugField'
|
|
227
|
+
|
|
228
|
+
fields: {
|
|
229
|
+
slug: text({ ui: { component: SlugField } })
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Composable Dashboard
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { ItemCreateForm, ListTable } from '@opensaas/stack-ui/standalone'
|
|
237
|
+
import { Card, Button } from '@opensaas/stack-ui/primitives'
|
|
238
|
+
|
|
239
|
+
export default function CustomDashboard() {
|
|
240
|
+
return (
|
|
241
|
+
<div className="grid gap-4">
|
|
242
|
+
<Card>
|
|
243
|
+
<h2>Recent Posts</h2>
|
|
244
|
+
<ListTable
|
|
245
|
+
listKey="Post"
|
|
246
|
+
context={context}
|
|
247
|
+
columns={['title', 'status', 'createdAt']}
|
|
248
|
+
/>
|
|
249
|
+
</Card>
|
|
250
|
+
|
|
251
|
+
<Card>
|
|
252
|
+
<h2>Create Post</h2>
|
|
253
|
+
<ItemCreateForm
|
|
254
|
+
listKey="Post"
|
|
255
|
+
context={context}
|
|
256
|
+
onSuccess={(item) => router.push(`/posts/${item.id}`)}
|
|
257
|
+
/>
|
|
258
|
+
</Card>
|
|
259
|
+
</div>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Standalone Form with Custom Actions
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { ItemEditForm } from '@opensaas/stack-ui/standalone'
|
|
268
|
+
|
|
269
|
+
<ItemEditForm
|
|
270
|
+
listKey="Post"
|
|
271
|
+
itemId={postId}
|
|
272
|
+
context={context}
|
|
273
|
+
onSuccess={(item) => {
|
|
274
|
+
// Custom success handling
|
|
275
|
+
toast.success('Post updated!')
|
|
276
|
+
router.push('/posts')
|
|
277
|
+
}}
|
|
278
|
+
onError={(error) => {
|
|
279
|
+
// Custom error handling
|
|
280
|
+
toast.error(error.message)
|
|
281
|
+
}}
|
|
282
|
+
/>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Styling
|
|
286
|
+
|
|
287
|
+
Package includes Tailwind v4 styles:
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// app/layout.tsx
|
|
291
|
+
import '@opensaas/stack-ui/styles'
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Custom theming via CSS variables (follows shadcn/ui conventions).
|
|
295
|
+
|
|
296
|
+
## Type Safety
|
|
297
|
+
|
|
298
|
+
All components are fully typed:
|
|
299
|
+
|
|
300
|
+
- Context types inferred from Prisma client
|
|
301
|
+
- Field props typed based on field config
|
|
302
|
+
- Form data validated with react-hook-form + Zod
|
|
303
|
+
|
|
304
|
+
Avoid `any` types - all props are strongly typed for type safety.
|
|
305
|
+
|
|
306
|
+
## Performance
|
|
307
|
+
|
|
308
|
+
- Server components by default (AdminUI, getAdminContext)
|
|
309
|
+
- Client components marked with `'use client'`
|
|
310
|
+
- Minimal client-side JS for interactive features only
|
|
311
|
+
- Data fetching on server reduces client bundle size
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OpenSaas Stack Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1 +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,
|
|
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,2CAwMrB"}
|
|
@@ -36,6 +36,7 @@ export function ItemFormClient({ listKey, urlKey, mode, fields, initialData = {}
|
|
|
36
36
|
try {
|
|
37
37
|
// Transform relationship fields to Prisma format
|
|
38
38
|
// Filter out password fields with isSet objects (unchanged passwords)
|
|
39
|
+
// File/Image fields: pass File objects through (Next.js will serialize them)
|
|
39
40
|
const transformedData = {};
|
|
40
41
|
for (const [fieldName, value] of Object.entries(formData)) {
|
|
41
42
|
const fieldConfig = fields[fieldName];
|
|
@@ -64,7 +65,8 @@ export function ItemFormClient({ listKey, urlKey, mode, fields, initialData = {}
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
else {
|
|
67
|
-
// Non-relationship field: pass through
|
|
68
|
+
// Non-relationship field: pass through (including File objects for file/image fields)
|
|
69
|
+
// File objects will be serialized by Next.js server action
|
|
68
70
|
transformedData[fieldName] = value;
|
|
69
71
|
}
|
|
70
72
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FileMetadata } from '@opensaas/stack-core';
|
|
2
|
+
export interface FileFieldProps {
|
|
3
|
+
name: string;
|
|
4
|
+
value: File | FileMetadata | null;
|
|
5
|
+
onChange: (value: File | FileMetadata | null) => void;
|
|
6
|
+
label?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
mode?: 'read' | 'edit';
|
|
11
|
+
helpText?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* File upload field with drag-and-drop support
|
|
16
|
+
*
|
|
17
|
+
* Stores File objects in form state. The actual upload happens server-side
|
|
18
|
+
* during form submission via field hooks.
|
|
19
|
+
*/
|
|
20
|
+
export declare function FileField({ name, value, onChange, label, error, disabled, required, mode, helpText, placeholder, }: FileFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
//# sourceMappingURL=FileField.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileField.d.ts","sourceRoot":"","sources":["../../../src/components/fields/FileField.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAMxD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,IAAI,GAAG,YAAY,GAAG,IAAI,CAAA;IACjC,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,GAAG,YAAY,GAAG,IAAI,KAAK,IAAI,CAAA;IACrD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAa,EACb,QAAQ,EACR,WAA8C,GAC/C,EAAE,cAAc,2CA+KhB"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
import { Button } from '../../primitives/button.js';
|
|
5
|
+
import { Input } from '../../primitives/input.js';
|
|
6
|
+
import { Label } from '../../primitives/label.js';
|
|
7
|
+
import { Upload, X, File, Check } from 'lucide-react';
|
|
8
|
+
/**
|
|
9
|
+
* File upload field with drag-and-drop support
|
|
10
|
+
*
|
|
11
|
+
* Stores File objects in form state. The actual upload happens server-side
|
|
12
|
+
* during form submission via field hooks.
|
|
13
|
+
*/
|
|
14
|
+
export function FileField({ name, value, onChange, label, error, disabled, required, mode = 'edit', helpText, placeholder = 'Choose a file or drag and drop', }) {
|
|
15
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
16
|
+
const handleFileSelect = useCallback((file) => {
|
|
17
|
+
// Store File object in form state
|
|
18
|
+
// Upload will happen server-side during form submission
|
|
19
|
+
onChange(file);
|
|
20
|
+
}, [onChange]);
|
|
21
|
+
const handleDragOver = useCallback((e) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setIsDragOver(true);
|
|
24
|
+
}, []);
|
|
25
|
+
const handleDragLeave = useCallback((e) => {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
setIsDragOver(false);
|
|
28
|
+
}, []);
|
|
29
|
+
const handleDrop = useCallback((e) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
setIsDragOver(false);
|
|
32
|
+
if (disabled || mode === 'read')
|
|
33
|
+
return;
|
|
34
|
+
const files = Array.from(e.dataTransfer.files);
|
|
35
|
+
if (files.length > 0) {
|
|
36
|
+
handleFileSelect(files[0]);
|
|
37
|
+
}
|
|
38
|
+
}, [disabled, mode, handleFileSelect]);
|
|
39
|
+
const handleInputChange = useCallback((e) => {
|
|
40
|
+
const files = Array.from(e.target.files || []);
|
|
41
|
+
if (files.length > 0) {
|
|
42
|
+
handleFileSelect(files[0]);
|
|
43
|
+
}
|
|
44
|
+
}, [handleFileSelect]);
|
|
45
|
+
const handleRemove = useCallback(() => {
|
|
46
|
+
onChange(null);
|
|
47
|
+
}, [onChange]);
|
|
48
|
+
// Determine if value is File or FileMetadata
|
|
49
|
+
// Use duck typing instead of instanceof to support SSR
|
|
50
|
+
const isFile = value &&
|
|
51
|
+
typeof value === 'object' &&
|
|
52
|
+
'arrayBuffer' in value &&
|
|
53
|
+
typeof value.arrayBuffer === 'function';
|
|
54
|
+
const isFileMetadata = value && !isFile && typeof value === 'object' && 'url' in value;
|
|
55
|
+
// Read-only mode
|
|
56
|
+
if (mode === 'read') {
|
|
57
|
+
return (_jsxs("div", { className: "space-y-2", children: [label && (_jsxs(Label, { htmlFor: name, children: [label, required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] })), isFileMetadata ? (_jsxs("div", { className: "flex items-center gap-2 p-3 border rounded-md bg-muted", children: [_jsx(File, { className: "h-4 w-4" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-medium truncate", children: value.originalFilename }), _jsx("p", { className: "text-xs text-muted-foreground", children: formatFileSize(value.size) })] }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => window.open(value.url, '_blank'), children: "Download" })] })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "No file uploaded" }))] }));
|
|
58
|
+
}
|
|
59
|
+
// Edit mode
|
|
60
|
+
return (_jsxs("div", { className: "space-y-2", children: [label && (_jsxs(Label, { htmlFor: name, children: [label, required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] })), isFile || isFileMetadata ? (
|
|
61
|
+
// File selected/uploaded - show file info
|
|
62
|
+
_jsxs("div", { className: "flex items-center gap-2 p-3 border rounded-md", children: [_jsx(Check, { className: "h-4 w-4 text-green-600" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-medium truncate", children: isFile ? value.name : value.originalFilename }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [formatFileSize(isFile ? value.size : value.size), isFileMetadata && ` • ${value.mimeType}`, isFile && ' • Will upload on save'] })] }), isFileMetadata && (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => window.open(value.url, '_blank'), children: "View" })), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: handleRemove, disabled: disabled, children: _jsx(X, { className: "h-4 w-4" }) })] })) : (
|
|
63
|
+
// No file - show upload area
|
|
64
|
+
_jsx(_Fragment, { children: _jsxs("div", { className: `
|
|
65
|
+
relative border-2 border-dashed rounded-md p-6
|
|
66
|
+
transition-colors cursor-pointer
|
|
67
|
+
${isDragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/25'}
|
|
68
|
+
${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-primary hover:bg-primary/5'}
|
|
69
|
+
`, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, children: [_jsx(Input, { id: name, type: "file", onChange: handleInputChange, disabled: disabled, className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer" }), _jsxs("div", { className: "flex flex-col items-center gap-2 text-center", children: [_jsx(Upload, { className: "h-8 w-8 text-muted-foreground" }), _jsx("p", { className: "text-sm font-medium", children: placeholder }), helpText && _jsx("p", { className: "text-xs text-muted-foreground", children: helpText })] })] }) })), error && _jsx("p", { className: "text-sm text-destructive", children: error })] }));
|
|
70
|
+
}
|
|
71
|
+
function formatFileSize(bytes) {
|
|
72
|
+
if (bytes === 0)
|
|
73
|
+
return '0 Bytes';
|
|
74
|
+
const k = 1024;
|
|
75
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
76
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
77
|
+
return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ImageMetadata } from '@opensaas/stack-core';
|
|
2
|
+
export interface ImageFieldProps {
|
|
3
|
+
name: string;
|
|
4
|
+
value: File | ImageMetadata | null;
|
|
5
|
+
onChange: (value: File | ImageMetadata | null) => void;
|
|
6
|
+
label?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
mode?: 'read' | 'edit';
|
|
11
|
+
helpText?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
showPreview?: boolean;
|
|
14
|
+
previewSize?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Image upload field with preview, drag-and-drop, and transformation support
|
|
18
|
+
*
|
|
19
|
+
* Stores File objects in form state with client-side preview. The actual upload
|
|
20
|
+
* happens server-side during form submission via field hooks.
|
|
21
|
+
*/
|
|
22
|
+
export declare function ImageField({ name, value, onChange, label, error, disabled, required, mode, helpText, placeholder, showPreview, previewSize, }: ImageFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
//# sourceMappingURL=ImageField.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageField.d.ts","sourceRoot":"","sources":["../../../src/components/fields/ImageField.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAOzD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,IAAI,GAAG,aAAa,GAAG,IAAI,CAAA;IAClC,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,GAAG,aAAa,GAAG,IAAI,KAAK,IAAI,CAAA;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAa,EACb,QAAQ,EACR,WAAgD,EAChD,WAAkB,EAClB,WAAiB,GAClB,EAAE,eAAe,2CAmRjB"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
import { Button } from '../../primitives/button.js';
|
|
5
|
+
import { Input } from '../../primitives/input.js';
|
|
6
|
+
import { Label } from '../../primitives/label.js';
|
|
7
|
+
import { Upload, X, Eye, ImageIcon } from 'lucide-react';
|
|
8
|
+
import Image from 'next/image';
|
|
9
|
+
/**
|
|
10
|
+
* Image upload field with preview, drag-and-drop, and transformation support
|
|
11
|
+
*
|
|
12
|
+
* Stores File objects in form state with client-side preview. The actual upload
|
|
13
|
+
* happens server-side during form submission via field hooks.
|
|
14
|
+
*/
|
|
15
|
+
export function ImageField({ name, value, onChange, label, error, disabled, required, mode = 'edit', helpText, placeholder = 'Choose an image or drag and drop', showPreview = true, previewSize = 200, }) {
|
|
16
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
17
|
+
const [previewUrl, setPreviewUrl] = useState(null);
|
|
18
|
+
const handleFileSelect = useCallback((file) => {
|
|
19
|
+
// Validate file is an image
|
|
20
|
+
if (!file.type.startsWith('image/')) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Generate client-side preview
|
|
24
|
+
if (showPreview) {
|
|
25
|
+
const reader = new FileReader();
|
|
26
|
+
reader.onload = (e) => {
|
|
27
|
+
setPreviewUrl(e.target?.result);
|
|
28
|
+
};
|
|
29
|
+
reader.readAsDataURL(file);
|
|
30
|
+
}
|
|
31
|
+
// Store File object in form state
|
|
32
|
+
// Upload will happen server-side during form submission
|
|
33
|
+
onChange(file);
|
|
34
|
+
}, [onChange, showPreview]);
|
|
35
|
+
const handleDragOver = useCallback((e) => {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
setIsDragOver(true);
|
|
38
|
+
}, []);
|
|
39
|
+
const handleDragLeave = useCallback((e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
setIsDragOver(false);
|
|
42
|
+
}, []);
|
|
43
|
+
const handleDrop = useCallback((e) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
setIsDragOver(false);
|
|
46
|
+
if (disabled || mode === 'read')
|
|
47
|
+
return;
|
|
48
|
+
const files = Array.from(e.dataTransfer.files);
|
|
49
|
+
if (files.length > 0) {
|
|
50
|
+
handleFileSelect(files[0]);
|
|
51
|
+
}
|
|
52
|
+
}, [disabled, mode, handleFileSelect]);
|
|
53
|
+
const handleInputChange = useCallback((e) => {
|
|
54
|
+
const files = Array.from(e.target.files || []);
|
|
55
|
+
if (files.length > 0) {
|
|
56
|
+
handleFileSelect(files[0]);
|
|
57
|
+
}
|
|
58
|
+
}, [handleFileSelect]);
|
|
59
|
+
const handleRemove = useCallback(() => {
|
|
60
|
+
onChange(null);
|
|
61
|
+
setPreviewUrl(null);
|
|
62
|
+
}, [onChange]);
|
|
63
|
+
// Determine if value is File or ImageMetadata
|
|
64
|
+
// Use duck typing instead of instanceof to support SSR
|
|
65
|
+
const isFile = value &&
|
|
66
|
+
typeof value === 'object' &&
|
|
67
|
+
'arrayBuffer' in value &&
|
|
68
|
+
typeof value.arrayBuffer === 'function';
|
|
69
|
+
const isImageMetadata = value && !isFile && typeof value === 'object' && 'url' in value;
|
|
70
|
+
// Read-only mode
|
|
71
|
+
if (mode === 'read') {
|
|
72
|
+
return (_jsxs("div", { className: "space-y-2", children: [label && (_jsxs(Label, { htmlFor: name, children: [label, required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] })), isImageMetadata ? (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "relative inline-block", children: _jsx(Image, { src: value.url, alt: value.originalFilename, width: previewSize, height: previewSize, className: "rounded-md object-cover border", style: {
|
|
73
|
+
maxWidth: `${previewSize}px`,
|
|
74
|
+
maxHeight: `${previewSize}px`,
|
|
75
|
+
} }) }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [value.width, " \u00D7 ", value.height, "px \u2022", ' ', formatFileSize(value.size)] }), value.transformations &&
|
|
76
|
+
Object.keys(value.transformations).length > 0 && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium", children: "Transformations:" }), _jsx("div", { className: "flex flex-wrap gap-2", children: Object.entries(value.transformations).map(([name, transform]) => {
|
|
77
|
+
const t = transform;
|
|
78
|
+
return (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => window.open(t.url, '_blank'), children: [name, " (", t.width, "\u00D7", t.height, ")"] }, name));
|
|
79
|
+
}) })] }))] })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "No image uploaded" }))] }));
|
|
80
|
+
}
|
|
81
|
+
// Edit mode
|
|
82
|
+
return (_jsxs("div", { className: "space-y-2", children: [label && (_jsxs(Label, { htmlFor: name, children: [label, required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] })), previewUrl || isImageMetadata ? (
|
|
83
|
+
// Image selected/uploaded or preview available - show preview
|
|
84
|
+
_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "relative inline-block group", children: [_jsx(Image, { src: previewUrl || value.url, alt: isImageMetadata ? value.originalFilename : 'Preview', width: previewSize, height: previewSize, className: "rounded-md object-cover border", style: {
|
|
85
|
+
maxWidth: `${previewSize}px`,
|
|
86
|
+
maxHeight: `${previewSize}px`,
|
|
87
|
+
} }), _jsxs("div", { className: "absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity", children: [isImageMetadata && (_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => window.open(value.url, '_blank'), children: _jsx(Eye, { className: "h-3 w-3" }) })), _jsx(Button, { type: "button", variant: "destructive", size: "sm", onClick: handleRemove, disabled: disabled, children: _jsx(X, { className: "h-3 w-3" }) })] })] }), isFile && (_jsxs("div", { className: "text-xs text-muted-foreground", children: [value.name, " \u2022 ", formatFileSize(value.size), " \u2022 Will upload on save"] })), isImageMetadata && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "text-xs text-muted-foreground", children: [value.originalFilename, " \u2022 ", value.width, " \u00D7", ' ', value.height, "px \u2022", ' ', formatFileSize(value.size)] }), value.transformations &&
|
|
88
|
+
Object.keys(value.transformations).length > 0 && (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-xs font-medium", children: "Transformations:" }), _jsx("div", { className: "flex flex-wrap gap-2", children: Object.entries(value.transformations).map(([name, transform]) => {
|
|
89
|
+
const t = transform;
|
|
90
|
+
return (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => window.open(t.url, '_blank'), children: [name, " (", t.width, "\u00D7", t.height, ")"] }, name));
|
|
91
|
+
}) })] }))] }))] })) : (
|
|
92
|
+
// No image - show upload area
|
|
93
|
+
_jsx(_Fragment, { children: _jsxs("div", { className: `
|
|
94
|
+
relative border-2 border-dashed rounded-md p-6
|
|
95
|
+
transition-colors cursor-pointer
|
|
96
|
+
${isDragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/25'}
|
|
97
|
+
${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-primary hover:bg-primary/5'}
|
|
98
|
+
`, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, children: [_jsx(Input, { id: name, type: "file", accept: "image/*", onChange: handleInputChange, disabled: disabled, className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer" }), _jsxs("div", { className: "flex flex-col items-center gap-2 text-center", children: [showPreview ? (_jsx(ImageIcon, { className: "h-8 w-8 text-muted-foreground" })) : (_jsx(Upload, { className: "h-8 w-8 text-muted-foreground" })), _jsx("p", { className: "text-sm font-medium", children: placeholder }), helpText && _jsx("p", { className: "text-xs text-muted-foreground", children: helpText })] })] }) })), error && _jsx("p", { className: "text-sm text-destructive", children: error })] }));
|
|
99
|
+
}
|
|
100
|
+
function formatFileSize(bytes) {
|
|
101
|
+
if (bytes === 0)
|
|
102
|
+
return '0 Bytes';
|
|
103
|
+
const k = 1024;
|
|
104
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
105
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
106
|
+
return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;
|
|
107
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface JsonFieldProps {
|
|
2
|
+
name: string;
|
|
3
|
+
value: unknown;
|
|
4
|
+
onChange: (value: unknown) => void;
|
|
5
|
+
label: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
mode?: 'read' | 'edit';
|
|
11
|
+
rows?: number;
|
|
12
|
+
formatted?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function JsonField({ name, value, onChange, label, placeholder, error, disabled, required, mode, rows, formatted, }: JsonFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=JsonField.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"JsonField.d.ts","sourceRoot":"","sources":["../../../src/components/fields/JsonField.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,KAAK,EACL,WAAkC,EAClC,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,IAAa,EACb,IAAQ,EACR,SAAgB,GACjB,EAAE,cAAc,2CAgFhB"}
|