@lodado/sdui-template 1.0.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.
- package/LICENCE +8 -0
- package/README.md +462 -0
- package/dist/cjs/client/index.cjs +1 -0
- package/dist/es/client/src/components/componentMap.mjs +2 -0
- package/dist/es/client/src/index.mjs +1 -0
- package/dist/es/client/src/react-wrapper/components/SduiLayoutRenderer.mjs +2 -0
- package/dist/es/client/src/react-wrapper/context/SduiLayoutContext.mjs +2 -0
- package/dist/es/client/src/react-wrapper/hooks/useRenderNode.mjs +2 -0
- package/dist/es/client/src/react-wrapper/hooks/useSduiLayoutAction.mjs +2 -0
- package/dist/es/client/src/react-wrapper/hooks/useSduiNodeSubscription.mjs +2 -0
- package/dist/es/client/src/store/SduiLayoutStore.mjs +1 -0
- package/dist/es/client/src/store/errors.mjs +1 -0
- package/dist/es/client/src/store/managers/DocumentManager.mjs +1 -0
- package/dist/es/client/src/store/managers/LayoutStateRepository.mjs +1 -0
- package/dist/es/client/src/store/managers/SubscriptionManager.mjs +1 -0
- package/dist/es/client/src/store/managers/VariablesManager.mjs +1 -0
- package/dist/es/client/src/utils/normalize/denormalize.mjs +1 -0
- package/dist/es/client/src/utils/normalize/normalize.mjs +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/setupTests.d.ts +1 -0
- package/dist/types/src/components/componentMap.d.ts +15 -0
- package/dist/types/src/components/index.d.ts +7 -0
- package/dist/types/src/components/types.d.ts +18 -0
- package/dist/types/src/index.d.ts +38 -0
- package/dist/types/src/react-wrapper/components/SduiLayoutRenderer.d.ts +62 -0
- package/dist/types/src/react-wrapper/components/index.d.ts +6 -0
- package/dist/types/src/react-wrapper/context/SduiLayoutContext.d.ts +37 -0
- package/dist/types/src/react-wrapper/context/index.d.ts +6 -0
- package/dist/types/src/react-wrapper/hooks/index.d.ts +8 -0
- package/dist/types/src/react-wrapper/hooks/useRenderNode.d.ts +10 -0
- package/dist/types/src/react-wrapper/hooks/useSduiLayoutAction.d.ts +15 -0
- package/dist/types/src/react-wrapper/hooks/useSduiNodeSubscription.d.ts +45 -0
- package/dist/types/src/react-wrapper/index.d.ts +8 -0
- package/dist/types/src/schema/base.d.ts +44 -0
- package/dist/types/src/schema/document.d.ts +16 -0
- package/dist/types/src/schema/index.d.ts +8 -0
- package/dist/types/src/schema/node.d.ts +19 -0
- package/dist/types/src/store/SduiLayoutStore.d.ts +192 -0
- package/dist/types/src/store/errors.d.ts +37 -0
- package/dist/types/src/store/index.d.ts +8 -0
- package/dist/types/src/store/managers/DocumentManager.d.ts +67 -0
- package/dist/types/src/store/managers/LayoutStateRepository.d.ts +110 -0
- package/dist/types/src/store/managers/SubscriptionManager.d.ts +48 -0
- package/dist/types/src/store/managers/VariablesManager.d.ts +38 -0
- package/dist/types/src/store/managers/index.d.ts +9 -0
- package/dist/types/src/store/types.d.ts +46 -0
- package/dist/types/src/utils/normalize/denormalize.d.ts +24 -0
- package/dist/types/src/utils/normalize/index.d.ts +8 -0
- package/dist/types/src/utils/normalize/normalize.d.ts +28 -0
- package/dist/types/src/utils/normalize/types.d.ts +15 -0
- package/package.json +89 -0
package/LICENCE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright (c) 2024 lodado
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# @lodado/sdui-template
|
|
2
|
+
|
|
3
|
+
Server-Driven UI Template Library for React. A flexible and powerful template system for building server-driven user interfaces with dynamic layouts and components.
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
Many applications require dynamically controlling UI structure and layout from the server. Common use cases include:
|
|
8
|
+
|
|
9
|
+
- **Dashboard Builders**: Users configure dashboards via drag-and-drop, and saved layouts are loaded from the server and rendered
|
|
10
|
+
- **Dynamic Form Generators**: Form structure is defined on the server and dynamically rendered on the client
|
|
11
|
+
- **Content Management Systems**: Administrators configure page layouts, and users see the same layout
|
|
12
|
+
- **A/B Testing**: Server sends different UI layouts for experimentation
|
|
13
|
+
|
|
14
|
+
In these situations, implementing state management, subscription systems, and component rendering logic from scratch for each new project is inefficient and error-prone.
|
|
15
|
+
|
|
16
|
+
## Solution
|
|
17
|
+
|
|
18
|
+
**SDUI (Server-Driven UI)** is a pattern where UI structure is defined on the server and dynamically rendered on the client. This library provides the core logic for implementing the SDUI pattern with:
|
|
19
|
+
|
|
20
|
+
- ✅ **Reusable**: Implement once, use across multiple projects
|
|
21
|
+
- ✅ **Performance Optimized**: Subscription-based re-rendering updates only changed nodes
|
|
22
|
+
- ✅ **Flexible**: Component overrides allow project-specific customization
|
|
23
|
+
- ✅ **Type Safe**: Full TypeScript support with optional Zod schema validation
|
|
24
|
+
- ✅ **Next.js Compatible**: Works seamlessly with Next.js App Router
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- 🎯 **Server-Driven UI**: Define layouts from server-side configuration
|
|
29
|
+
- ⚡ **Performance Optimized**: ID-based subscription system for minimal re-renders
|
|
30
|
+
- 🔄 **Normalize/Denormalize**: Efficient data structure using normalizr
|
|
31
|
+
- 🎨 **Type Safe**: Full TypeScript support with optional Zod schema validation
|
|
32
|
+
- 🧩 **Modular**: Clean architecture with separated concerns
|
|
33
|
+
- 🚀 **Next.js Compatible**: Works seamlessly with Next.js App Router
|
|
34
|
+
- 🔧 **Flexible State Management**: Update component state programmatically
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @lodado/sdui-template
|
|
40
|
+
# or
|
|
41
|
+
pnpm add @lodado/sdui-template
|
|
42
|
+
# or
|
|
43
|
+
yarn add @lodado/sdui-template
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
### Basic Usage
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
'use client'
|
|
52
|
+
|
|
53
|
+
import { SduiLayoutRenderer } from '@lodado/sdui-template'
|
|
54
|
+
import type { SduiLayoutDocument } from '@lodado/sdui-template'
|
|
55
|
+
|
|
56
|
+
// Define your SDUI document (typically received from server)
|
|
57
|
+
const document: SduiLayoutDocument = {
|
|
58
|
+
version: '1.0.0',
|
|
59
|
+
metadata: {
|
|
60
|
+
id: 'my-layout',
|
|
61
|
+
name: 'My Layout',
|
|
62
|
+
},
|
|
63
|
+
root: {
|
|
64
|
+
id: 'root',
|
|
65
|
+
type: 'Container',
|
|
66
|
+
state: {},
|
|
67
|
+
children: [
|
|
68
|
+
{
|
|
69
|
+
id: 'card-1',
|
|
70
|
+
type: 'Card',
|
|
71
|
+
state: {
|
|
72
|
+
title: 'Card 1',
|
|
73
|
+
content: 'First card content',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'card-2',
|
|
78
|
+
type: 'Card',
|
|
79
|
+
state: {
|
|
80
|
+
title: 'Card 2',
|
|
81
|
+
content: 'Second card content',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default function Page() {
|
|
89
|
+
return <SduiLayoutRenderer document={document} />
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Custom Components with State Management
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
'use client'
|
|
97
|
+
|
|
98
|
+
import {
|
|
99
|
+
SduiLayoutRenderer,
|
|
100
|
+
useSduiNodeSubscription,
|
|
101
|
+
useSduiLayoutAction,
|
|
102
|
+
type ComponentFactory,
|
|
103
|
+
} from '@lodado/sdui-template'
|
|
104
|
+
import { z } from 'zod'
|
|
105
|
+
|
|
106
|
+
// Define state schema for type safety
|
|
107
|
+
const toggleStateSchema = z.object({
|
|
108
|
+
checked: z.boolean(),
|
|
109
|
+
label: z.string().optional(),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Create your component
|
|
113
|
+
function Toggle({ id }: { id: string }) {
|
|
114
|
+
const { state } = useSduiNodeSubscription({
|
|
115
|
+
nodeId: id,
|
|
116
|
+
schema: toggleStateSchema, // Optional: validates state structure
|
|
117
|
+
})
|
|
118
|
+
const store = useSduiLayoutAction()
|
|
119
|
+
|
|
120
|
+
const handleToggle = () => {
|
|
121
|
+
store.updateNodeState(id, {
|
|
122
|
+
checked: !state.checked,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div className="flex items-center gap-2">
|
|
128
|
+
{state.label && <label>{state.label}</label>}
|
|
129
|
+
<button onClick={handleToggle}>{state.checked ? 'ON' : 'OFF'}</button>
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Define component factory
|
|
135
|
+
const ToggleFactory: ComponentFactory = (id) => <Toggle id={id} />
|
|
136
|
+
|
|
137
|
+
const document = {
|
|
138
|
+
version: '1.0.0',
|
|
139
|
+
root: {
|
|
140
|
+
id: 'root',
|
|
141
|
+
type: 'Container',
|
|
142
|
+
state: {},
|
|
143
|
+
children: [
|
|
144
|
+
{
|
|
145
|
+
id: 'toggle-1',
|
|
146
|
+
type: 'Toggle',
|
|
147
|
+
state: {
|
|
148
|
+
checked: false,
|
|
149
|
+
label: 'Enable notifications',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export default function Page() {
|
|
157
|
+
return <SduiLayoutRenderer document={document} components={{ Toggle: ToggleFactory }} />
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Complete Example: Toggle Component
|
|
162
|
+
|
|
163
|
+
Here's a complete example showing how to create an interactive component with state management:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
'use client'
|
|
167
|
+
|
|
168
|
+
import {
|
|
169
|
+
SduiLayoutRenderer,
|
|
170
|
+
useSduiNodeSubscription,
|
|
171
|
+
useSduiLayoutAction,
|
|
172
|
+
type ComponentFactory,
|
|
173
|
+
} from '@lodado/sdui-template'
|
|
174
|
+
import { z } from 'zod'
|
|
175
|
+
|
|
176
|
+
// 1. Define state schema
|
|
177
|
+
const toggleStateSchema = z.object({
|
|
178
|
+
checked: z.boolean(),
|
|
179
|
+
label: z.string().optional(),
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// 2. Create component that subscribes to node state
|
|
183
|
+
function Toggle({ id }: { id: string }) {
|
|
184
|
+
const { state } = useSduiNodeSubscription({
|
|
185
|
+
nodeId: id,
|
|
186
|
+
schema: toggleStateSchema, // Validates and types state
|
|
187
|
+
})
|
|
188
|
+
const store = useSduiLayoutAction()
|
|
189
|
+
|
|
190
|
+
const handleToggle = () => {
|
|
191
|
+
// Update state - only this component re-renders
|
|
192
|
+
store.updateNodeState(id, {
|
|
193
|
+
checked: !state.checked,
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div className="flex items-center gap-2 p-3">
|
|
199
|
+
{state.label && <label>{state.label}</label>}
|
|
200
|
+
<button
|
|
201
|
+
onClick={handleToggle}
|
|
202
|
+
className={`w-11 h-6 rounded-full transition-colors ${state.checked ? 'bg-blue-600' : 'bg-gray-400'}`}
|
|
203
|
+
>
|
|
204
|
+
<span
|
|
205
|
+
className={`block w-5 h-5 bg-white rounded-full transition-transform ${
|
|
206
|
+
state.checked ? 'translate-x-5' : 'translate-x-0'
|
|
207
|
+
}`}
|
|
208
|
+
/>
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 3. Create component factory
|
|
215
|
+
const ToggleFactory: ComponentFactory = (id) => <Toggle id={id} />
|
|
216
|
+
|
|
217
|
+
// 4. Define SDUI document
|
|
218
|
+
const document = {
|
|
219
|
+
version: '1.0.0',
|
|
220
|
+
root: {
|
|
221
|
+
id: 'root',
|
|
222
|
+
type: 'Container',
|
|
223
|
+
state: {},
|
|
224
|
+
children: [
|
|
225
|
+
{
|
|
226
|
+
id: 'toggle-1',
|
|
227
|
+
type: 'Toggle',
|
|
228
|
+
state: {
|
|
229
|
+
checked: false,
|
|
230
|
+
label: 'Enable notifications',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: 'toggle-2',
|
|
235
|
+
type: 'Toggle',
|
|
236
|
+
state: {
|
|
237
|
+
checked: true,
|
|
238
|
+
label: 'Dark mode',
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 5. Render with component map
|
|
246
|
+
export default function Page() {
|
|
247
|
+
return (
|
|
248
|
+
<SduiLayoutRenderer
|
|
249
|
+
document={document}
|
|
250
|
+
components={{ Toggle: ToggleFactory }}
|
|
251
|
+
onError={(error) => console.error('SDUI Error:', error)}
|
|
252
|
+
/>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Key Benefits:**
|
|
258
|
+
|
|
259
|
+
- ✅ Only the clicked toggle re-renders (performance optimized)
|
|
260
|
+
- ✅ Type-safe state with Zod validation
|
|
261
|
+
- ✅ Server controls initial state, client handles interactions
|
|
262
|
+
- ✅ Easy to extend with more components
|
|
263
|
+
|
|
264
|
+
### Component Overrides
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
'use client'
|
|
268
|
+
|
|
269
|
+
import { SduiLayoutRenderer, type ComponentFactory } from '@lodado/sdui-template'
|
|
270
|
+
|
|
271
|
+
const SpecialCardFactory: ComponentFactory = (id) => <div className="special-card">Special: {id}</div>
|
|
272
|
+
|
|
273
|
+
export default function Page() {
|
|
274
|
+
return (
|
|
275
|
+
<SduiLayoutRenderer
|
|
276
|
+
document={document}
|
|
277
|
+
componentOverrides={{
|
|
278
|
+
// Override by node ID (highest priority)
|
|
279
|
+
byNodeId: {
|
|
280
|
+
'special-card-1': SpecialCardFactory,
|
|
281
|
+
},
|
|
282
|
+
// Override by node type
|
|
283
|
+
byNodeType: {
|
|
284
|
+
Card: CustomCardFactory,
|
|
285
|
+
},
|
|
286
|
+
}}
|
|
287
|
+
/>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## API Reference
|
|
293
|
+
|
|
294
|
+
### Components
|
|
295
|
+
|
|
296
|
+
#### `SduiLayoutRenderer`
|
|
297
|
+
|
|
298
|
+
Main component for rendering SDUI layouts.
|
|
299
|
+
|
|
300
|
+
**Props:**
|
|
301
|
+
|
|
302
|
+
- `document: SduiLayoutDocument` - SDUI Layout Document (required)
|
|
303
|
+
- `components?: Record<string, ComponentFactory>` - Custom component map
|
|
304
|
+
- `componentOverrides?: { byNodeId?: Record<string, ComponentFactory>, byNodeType?: Record<string, ComponentFactory> }` - Component overrides
|
|
305
|
+
- `onLayoutChange?: (document: SduiLayoutDocument) => void` - Layout change callback
|
|
306
|
+
- `onError?: (error: Error) => void` - Error callback
|
|
307
|
+
|
|
308
|
+
#### `SduiLayoutProvider`
|
|
309
|
+
|
|
310
|
+
Context provider for SDUI Layout Store.
|
|
311
|
+
|
|
312
|
+
**Props:**
|
|
313
|
+
|
|
314
|
+
- `store: SduiLayoutStore` - Store instance
|
|
315
|
+
- `children: React.ReactNode` - Child components
|
|
316
|
+
|
|
317
|
+
### Hooks
|
|
318
|
+
|
|
319
|
+
#### `useSduiLayoutAction(): SduiLayoutStore`
|
|
320
|
+
|
|
321
|
+
Returns store instance for calling actions and accessing store state.
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
const store = useSduiLayoutAction()
|
|
325
|
+
store.updateNodeState(nodeId, { count: 5 })
|
|
326
|
+
|
|
327
|
+
// Access store state directly (if needed)
|
|
328
|
+
const { rootId, nodes } = store.state
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### `useSduiNodeSubscription<T>(params: { nodeId: string, schema?: ZodSchema }): NodeData`
|
|
332
|
+
|
|
333
|
+
Subscribes to a specific node's changes and returns node information.
|
|
334
|
+
|
|
335
|
+
**Parameters:**
|
|
336
|
+
|
|
337
|
+
- `nodeId: string` - Node ID to subscribe to
|
|
338
|
+
- `schema?: ZodSchema` - Optional Zod schema for state validation and type inference
|
|
339
|
+
|
|
340
|
+
**Returns:**
|
|
341
|
+
|
|
342
|
+
- `node: SduiLayoutNode | undefined` - Node entity
|
|
343
|
+
- `type: string | undefined` - Node type
|
|
344
|
+
- `state: T` - Layout state (inferred from schema if provided, otherwise `BaseLayoutState`)
|
|
345
|
+
- `childrenIds: string[]` - Array of child node IDs
|
|
346
|
+
- `attributes: Record<string, unknown> | undefined` - Node attributes
|
|
347
|
+
- `exists: boolean` - Whether the node exists
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
const { node, state, childrenIds, attributes, exists } = useSduiNodeSubscription({
|
|
351
|
+
nodeId: 'node-1',
|
|
352
|
+
schema: baseLayoutStateSchema, // optional - validates and types state
|
|
353
|
+
})
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
#### `useRenderNode(componentMap?: Record<string, ComponentFactory>): RenderNodeFn`
|
|
357
|
+
|
|
358
|
+
Returns a function to render child nodes (internal use).
|
|
359
|
+
|
|
360
|
+
### Store
|
|
361
|
+
|
|
362
|
+
#### `SduiLayoutStore`
|
|
363
|
+
|
|
364
|
+
Main store class for managing SDUI layout state.
|
|
365
|
+
|
|
366
|
+
**Getters:**
|
|
367
|
+
|
|
368
|
+
- `state: SduiLayoutStoreState` - Current store state
|
|
369
|
+
- `nodes: Record<string, SduiLayoutNode>` - Node entities
|
|
370
|
+
- `layoutStates: Record<string, BaseLayoutState>` - Layout states
|
|
371
|
+
- `layoutAttributes: Record<string, Record<string, unknown>>` - Layout attributes
|
|
372
|
+
- `metadata: SduiLayoutDocument['metadata'] | undefined` - Document metadata
|
|
373
|
+
- `getComponentOverrides(): Record<string, ComponentFactory>` - Get component overrides
|
|
374
|
+
|
|
375
|
+
**Query Methods:**
|
|
376
|
+
|
|
377
|
+
- `getNodeById(nodeId: string): SduiLayoutNode | undefined` - Get node by ID
|
|
378
|
+
- `getNodeTypeById(nodeId: string): string | undefined` - Get node type by ID
|
|
379
|
+
- `getChildrenIdsById(nodeId: string): string[]` - Get children IDs by node ID
|
|
380
|
+
- `getLayoutStateById(nodeId: string): BaseLayoutState | undefined` - Get layout state by ID
|
|
381
|
+
- `getAttributesById(nodeId: string): Record<string, unknown> | undefined` - Get attributes by ID
|
|
382
|
+
- `getRootId(): string | undefined` - Get root node ID
|
|
383
|
+
- `getDocument(): SduiLayoutDocument | null` - Convert current state to document
|
|
384
|
+
|
|
385
|
+
**Update Methods:**
|
|
386
|
+
|
|
387
|
+
- `updateLayout(document: SduiLayoutDocument): void` - Update layout document
|
|
388
|
+
- `updateNodeState(nodeId: string, state: Partial<BaseLayoutState>): void` - Update node state
|
|
389
|
+
- `updateNodeAttributes(nodeId: string, attributes: Partial<Record<string, unknown>>): void` - Update node attributes
|
|
390
|
+
- `updateVariables(variables: Record<string, unknown>): void` - Update global variables
|
|
391
|
+
- `updateVariable(key: string, value: unknown): void` - Update single variable
|
|
392
|
+
- `deleteVariable(key: string): void` - Delete variable
|
|
393
|
+
- `cancelEdit(documentId?: string): void` - Cancel edits and restore original document
|
|
394
|
+
|
|
395
|
+
**Selection Methods:**
|
|
396
|
+
|
|
397
|
+
- `setSelectedNodeId(nodeId?: string): void` - Set selected node ID
|
|
398
|
+
|
|
399
|
+
**Subscription Methods:**
|
|
400
|
+
|
|
401
|
+
- `subscribeNode(nodeId: string, callback: () => void): () => void` - Subscribe to node changes
|
|
402
|
+
- `subscribeVersion(callback: () => void): () => void` - Subscribe to global changes
|
|
403
|
+
|
|
404
|
+
**Utility Methods:**
|
|
405
|
+
|
|
406
|
+
- `reset(): void` - Reset store to initial state
|
|
407
|
+
- `clearCache(): void` - Clear cache and reset store
|
|
408
|
+
|
|
409
|
+
## TypeScript Types
|
|
410
|
+
|
|
411
|
+
All types are exported from the main package:
|
|
412
|
+
|
|
413
|
+
```tsx
|
|
414
|
+
import type {
|
|
415
|
+
SduiLayoutDocument,
|
|
416
|
+
SduiLayoutNode,
|
|
417
|
+
BaseLayoutState,
|
|
418
|
+
LayoutPosition,
|
|
419
|
+
SduiDocument,
|
|
420
|
+
SduiNode,
|
|
421
|
+
ComponentFactory,
|
|
422
|
+
RenderNodeFn,
|
|
423
|
+
SduiLayoutStoreState,
|
|
424
|
+
SduiLayoutStoreOptions,
|
|
425
|
+
UseSduiNodeSubscriptionParams,
|
|
426
|
+
NormalizedSduiEntities,
|
|
427
|
+
} from '@lodado/sdui-template'
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Architecture
|
|
431
|
+
|
|
432
|
+
This library uses a clean architecture with separated concerns:
|
|
433
|
+
|
|
434
|
+
- **SubscriptionManager**: Manages observer pattern for state changes
|
|
435
|
+
- **LayoutStateRepository**: Handles state storage and retrieval
|
|
436
|
+
- **DocumentManager**: Manages document caching and serialization
|
|
437
|
+
- **VariablesManager**: Manages global variables
|
|
438
|
+
|
|
439
|
+
## Performance
|
|
440
|
+
|
|
441
|
+
- Subscription-based re-renders ensure only changed nodes update
|
|
442
|
+
- Normalized data structure for efficient lookups
|
|
443
|
+
- Minimal bundle size (< 50KB gzipped)
|
|
444
|
+
|
|
445
|
+
## Next.js App Router
|
|
446
|
+
|
|
447
|
+
This library is designed to work with Next.js App Router. All React components include the `"use client"` directive and should be used in client components.
|
|
448
|
+
|
|
449
|
+
```tsx
|
|
450
|
+
// app/page.tsx
|
|
451
|
+
'use client'
|
|
452
|
+
|
|
453
|
+
import { SduiLayoutRenderer } from '@lodado/sdui-template'
|
|
454
|
+
|
|
455
|
+
export default function Page() {
|
|
456
|
+
return <SduiLayoutRenderer document={document} />
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## License
|
|
461
|
+
|
|
462
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("react/jsx-runtime"),t=require("react"),s=require("lodash-es"),r=require("normalizr");const i=t.createContext(null),o=({store:s,children:r})=>{const o=t.useMemo(()=>({store:s}),[s]);return e.jsx(i.Provider,{value:o,children:r})},n=()=>{const e=t.useContext(i);if(!e)throw new Error("useSduiLayoutContext must be used within SduiLayoutProvider");return e},d=()=>{const{store:e}=n();return e},a=e=>e+1;function c(e){const{nodeId:s,schema:r}=e,[,i]=t.useReducer(a,0),o=d();let n;t.useEffect(()=>o.subscribeNode(s,i),[o,s]);let c,u,h=[];try{n=o.getNodeById(s),h=(null==n?void 0:n.childrenIds)||[]}catch(e){n=void 0,h=[]}try{c=o.getLayoutStateById(s)}catch(e){c=void 0}try{u=o.getAttributesById(s)}catch(e){u=void 0}const l=t.useMemo(()=>{if(!r)return c;if(!c)throw new Error(`State not found for node "${s}". Schema was provided but state is missing.`);const e=r.safeParse(c);if(!e.success)throw new Error(`State validation failed for node "${s}": ${e.error.message}`);return e.data},[c,r,s]);return{node:n,type:null==n?void 0:n.type,state:l,childrenIds:h,attributes:u,exists:!!n}}const u={},h=({id:t,renderNode:s})=>{const{type:r,childrenIds:i}=c({nodeId:t});return r?e.jsxs("div",{"data-sdui-node-id":t,"data-sdui-node-type":r,children:[e.jsxs("div",{children:["Type: ",r]}),e.jsxs("div",{children:["ID: ",t]}),i&&i.length>0&&e.jsx("div",{children:i.map(t=>e.jsx("div",{children:s(t)},t))})]}):null},l=(t,s)=>e.jsx(h,{id:t,renderNode:s});class p extends Error{constructor(e){super(`Node not found: "${e}"`),this.name="NodeNotFoundError",Error.captureStackTrace&&Error.captureStackTrace(this,p)}}class y extends Error{constructor(){super("Root node ID not found"),this.name="RootNotFoundError",Error.captureStackTrace&&Error.captureStackTrace(this,y)}}class g extends Error{constructor(){super("Metadata not found"),this.name="MetadataNotFoundError",Error.captureStackTrace&&Error.captureStackTrace(this,g)}}function b(e,t){var s;const r=null===(s=t.nodes)||void 0===s?void 0:s[e];if(!r)return null;const i=[];r.childrenIds&&i.push(...r.childrenIds);const o=i.map(e=>b(e,t)).filter(e=>null!==e);return Object.assign({id:r.id,type:r.type,state:r.state||{},attributes:r.attributes||{}},o.length>0&&{children:o})}const _=new r.schema.Entity("nodes",{},{idAttribute:"id",processStrategy:e=>({id:e.id,type:e.type,state:e.state||{},attributes:e.attributes||{}})});function v(e){const t=e.children||[],s=t.length>0?r.normalize(t,[_]):{entities:{nodes:{}}},i={id:e.id,type:e.type,state:e.state||{},attributes:e.attributes||{}},o=r.normalize(i,_),n={nodes:Object.assign(Object.assign({},o.entities.nodes),s.entities.nodes)},d=e=>{var t,s;n.nodes&&n.nodes[e.id]?n.nodes[e.id]=Object.assign(Object.assign({},n.nodes[e.id]),{childrenIds:(null===(t=e.children)||void 0===t?void 0:t.map(e=>e.id))||[]}):n.nodes&&(n.nodes[e.id]={id:e.id,type:e.type,state:e.state||{},attributes:e.attributes||{},childrenIds:(null===(s=e.children)||void 0===s?void 0:s.map(e=>e.id))||[]}),e.children&&e.children.forEach(d)};return d(e),{result:o.result,entities:n}}function m(e){const{result:t,entities:s}=v(e.root);return{result:t,entities:s}}_.define({children:[_]});class I{constructor(){this._cached={},this._originalCached={}}cacheDocument(e){var t;const r=(null===(t=e.metadata)||void 0===t?void 0:t.id)||e.root.id;this._cached[r]=e,this._originalCached[r]||(this._originalCached[r]=s.cloneDeep(e))}setMetadata(e){this._metadata=e}getMetadata(){return this._metadata}getOriginalDocument(e){return this._originalCached[e]}getDocumentId(e){var t;return(null===(t=this._metadata)||void 0===t?void 0:t.id)||e}getDocument(e){const t=e.getRootId();if(!t)return null;const s=b(t,{nodes:e.nodes});return s?{version:"1.0.0",metadata:this._metadata,root:s}:null}clearCache(){this._cached={},this._originalCached={}}reset(){this._metadata=void 0,this.clearCache()}}class N{constructor(){this._state={version:0,rootId:void 0,nodes:{},selectedNodeId:void 0,isEdited:!1,variables:{}}}get state(){return this._state}get nodes(){return this._state.nodes}getNodeById(e){return this._state.nodes[e]}getNodeTypeById(e){var t;return null===(t=this._state.nodes[e])||void 0===t?void 0:t.type}getChildrenIdsById(e){var t;return(null===(t=this._state.nodes[e])||void 0===t?void 0:t.childrenIds)||[]}getRootId(){return this._state.rootId}initializeState(e){this._state=Object.assign({version:0,rootId:void 0,nodes:{},selectedNodeId:void 0,isEdited:!1,variables:{}},e)}updateNodes(e){this._state.nodes=e}updateNodeState(e,t){const s=this._state.nodes[e];s&&(this._state.nodes[e]=Object.assign(Object.assign({},s),{state:t}))}updateNodeAttributes(e,t){const s=this._state.nodes[e];s&&(this._state.nodes[e]=Object.assign(Object.assign({},s),{attributes:t}))}setRootId(e){this._state.rootId=e}setSelectedNodeId(e){const t=this._state.selectedNodeId;return this._state.selectedNodeId=e,t}setEdited(e){this._state.isEdited=e}updateVariables(e){this._state.variables=e}incrementVersion(){this._state.version+=1}reset(){this._state.nodes={},this._state.rootId=void 0,this._state.isEdited=!1,this._state.selectedNodeId=void 0,this._state.variables={},this._state.version+=1}}class f{constructor(){this._nodeListeners=new Map,this._versionListeners=new Set}subscribeNode(e,t){return this._nodeListeners.has(e)||this._nodeListeners.set(e,new Set),this._nodeListeners.get(e).add(t),()=>{var s,r;null===(s=this._nodeListeners.get(e))||void 0===s||s.delete(t),0===(null===(r=this._nodeListeners.get(e))||void 0===r?void 0:r.size)&&this._nodeListeners.delete(e)}}subscribeVersion(e){return this._versionListeners.add(e),()=>{this._versionListeners.delete(e)}}notifyNode(e){var t;null===(t=this._nodeListeners.get(e))||void 0===t||t.forEach(e=>e())}notifyNodes(e){[...new Set(e)].forEach(e=>this.notifyNode(e))}notifyVersion(){this._versionListeners.forEach(e=>e())}}class M{constructor(e,t){this.repository=e,this.subscriptionManager=t}updateVariables(e){this.repository.updateVariables(s.cloneDeep(e)),this.repository.setEdited(!0),this.repository.incrementVersion(),this.subscriptionManager.notifyVersion()}updateVariable(e,t){const r=this.repository.state.variables;this.repository.updateVariables(Object.assign(Object.assign({},s.cloneDeep(r)),{[e]:s.cloneDeep(t)})),this.repository.setEdited(!0),this.repository.incrementVersion(),this.subscriptionManager.notifyVersion()}deleteVariable(e){const t=s.cloneDeep(this.repository.state.variables);delete t[e],this.repository.updateVariables(t),this.repository.setEdited(!0),this.repository.incrementVersion(),this.subscriptionManager.notifyVersion()}}class E{constructor(e,t){this._subscriptionManager=new f,this._repository=new N,this._documentManager=new I,this._componentOverrides={},this._repository.initializeState(e),this._variablesManager=new M(this._repository,this._subscriptionManager),this._componentOverrides=(null==t?void 0:t.componentOverrides)||{}}subscribeNode(e,t){return this._subscriptionManager.subscribeNode(e,t)}subscribeVersion(e){return this._subscriptionManager.subscribeVersion(e)}get state(){return this._repository.state}get nodes(){return this._repository.nodes}get metadata(){const e=this._documentManager.getMetadata();if(!e)throw new g;return e}getComponentOverrides(){return this._componentOverrides}getNodeById(e){const t=this._repository.getNodeById(e);if(!t)throw new p(e);return t}getNodeTypeById(e){const t=this._repository.getNodeTypeById(e);if(!t)throw new p(e);return t}getChildrenIdsById(e){if(!this._repository.getNodeById(e))throw new p(e);return this._repository.getChildrenIdsById(e)}getLayoutStateById(e){return this.getNodeById(e).state||{}}getAttributesById(e){return this.getNodeById(e).attributes||{}}getRootId(){const e=this._repository.getRootId();if(!e)throw new y;return e}updateLayout(e){const{entities:t}=m(e);this._repository.updateNodes(t.nodes||{}),this._repository.setRootId(e.root.id),this._repository.setEdited(!1),this._repository.updateVariables(e.variables?s.cloneDeep(e.variables):{}),this._repository.incrementVersion(),this._documentManager.setMetadata(e.metadata),this._documentManager.cacheDocument(e),this._subscriptionManager.notifyVersion()}cancelEdit(e){const t=this._repository.getRootId(),s=e||(t?this._documentManager.getDocumentId(t):void 0);if(!s)return;const r=this._documentManager.getOriginalDocument(s);r&&this.updateLayout(r)}updateNodeState(e,t){const s=this.getNodeById(e);this._repository.updateNodeState(e,Object.assign(Object.assign({},s.state||{}),t)),this._repository.setEdited(!0),this._subscriptionManager.notifyNode(e)}updateNodeAttributes(e,t){const s=this.getNodeById(e);this._repository.updateNodeAttributes(e,Object.assign(Object.assign({},s.attributes||{}),t)),this._repository.setEdited(!0),this._subscriptionManager.notifyNode(e)}updateVariables(e){this._variablesManager.updateVariables(e)}updateVariable(e,t){this._variablesManager.updateVariable(e,t)}deleteVariable(e){this._variablesManager.deleteVariable(e)}setSelectedNodeId(e){const t=this._repository.setSelectedNodeId(e);t&&this._subscriptionManager.notifyNode(t),e&&this._subscriptionManager.notifyNode(e)}getDocument(){return this._documentManager.getDocument(this._repository)}clearCache(){this._documentManager.clearCache(),this.reset()}reset(){this._documentManager.reset(),this._repository.reset(),this._subscriptionManager.notifyVersion()}}const S=e=>{const{store:s}=n(),{nodes:r}=s,i=t.useRef(null),o=t.useCallback(t=>{const o=r[t];if(!o)return null;const n=s.getComponentOverrides(),d=e||{};return(n[t]||n[o.type]||d[o.type]||l)(t,i.current)},[r,s,e]);return i.current=o,o},w=({id:e,componentMap:t})=>S(t)(e);exports.SduiLayoutProvider=o,exports.SduiLayoutRenderer=({document:s,components:r,componentOverrides:i,onLayoutChange:n,onError:d})=>{var a;const c=t.useMemo(()=>{try{if(!s||!s.root)throw new Error("Invalid document: missing root");if(!s.root.id)throw new Error("Invalid document: root.id is required");const e={componentOverrides:Object.assign(Object.assign(Object.assign({},r),null==i?void 0:i.byNodeType),null==i?void 0:i.byNodeId)},t=new E(void 0,e);return t.updateLayout(s),t}catch(e){return d&&d(e instanceof Error?e:new Error(String(e))),new E}},[s,r,i,d]),h=t.useMemo(()=>Object.assign(Object.assign({},u),r),[r]),l=null===(a=null==s?void 0:s.root)||void 0===a?void 0:a.id;return l?e.jsx(o,{store:c,children:e.jsx(w,{id:l,componentMap:h})}):null},exports.SduiLayoutStore=E,exports.denormalizeSduiLayout=function(e,t,s){const r=b(e,t);return r?{version:"1.0.0",metadata:s,root:r}:null},exports.denormalizeSduiNode=b,exports.normalizeSduiLayout=m,exports.normalizeSduiNode=v,exports.useRenderNode=S,exports.useSduiLayoutAction=d,exports.useSduiNodeSubscription=c;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import{jsx as d,jsxs as e}from"react/jsx-runtime";import{useSduiNodeSubscription as r}from"../react-wrapper/hooks/useSduiNodeSubscription.mjs";const i={},n=({id:i,renderNode:n})=>{const{type:o,childrenIds:t}=r({nodeId:i});return o?e("div",{"data-sdui-node-id":i,"data-sdui-node-type":o,children:[e("div",{children:["Type: ",o]}),e("div",{children:["ID: ",i]}),t&&t.length>0&&d("div",{children:t.map(e=>d("div",{children:n(e)},e))})]}):null},o=(e,r)=>d(n,{id:e,renderNode:r});export{i as componentMap,o as defaultComponentFactory};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{SduiLayoutRenderer}from"./react-wrapper/components/SduiLayoutRenderer.mjs";export{SduiLayoutProvider}from"./react-wrapper/context/SduiLayoutContext.mjs";export{useRenderNode}from"./react-wrapper/hooks/useRenderNode.mjs";export{useSduiLayoutAction}from"./react-wrapper/hooks/useSduiLayoutAction.mjs";export{useSduiNodeSubscription}from"./react-wrapper/hooks/useSduiNodeSubscription.mjs";export{SduiLayoutStore}from"./store/SduiLayoutStore.mjs";export{denormalizeSduiLayout,denormalizeSduiNode}from"./utils/normalize/denormalize.mjs";export{normalizeSduiLayout,normalizeSduiNode}from"./utils/normalize/normalize.mjs";
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import{jsx as o}from"react/jsx-runtime";import{useMemo as r}from"react";import{componentMap as t}from"../../components/componentMap.mjs";import{SduiLayoutStore as n}from"../../store/SduiLayoutStore.mjs";import{SduiLayoutProvider as e}from"../context/SduiLayoutContext.mjs";import{useRenderNode as i}from"../hooks/useRenderNode.mjs";const s=({id:o,componentMap:r})=>i(r)(o),d=({document:i,components:d,componentOverrides:m,onLayoutChange:c,onError:a})=>{var u;const p=r(()=>{try{if(!i||!i.root)throw new Error("Invalid document: missing root");if(!i.root.id)throw new Error("Invalid document: root.id is required");const o={componentOverrides:Object.assign(Object.assign(Object.assign({},d),null==m?void 0:m.byNodeType),null==m?void 0:m.byNodeId)},r=new n(void 0,o);return r.updateLayout(i),r}catch(o){return a&&a(o instanceof Error?o:new Error(String(o))),new n}},[i,d,m,a]),l=r(()=>Object.assign(Object.assign({},t),d),[d]),v=null===(u=null==i?void 0:i.root)||void 0===u?void 0:u.id;return v?o(e,{store:p,children:o(s,{id:v,componentMap:l})}):null};export{d as SduiLayoutRenderer,s as SduiLayoutRendererInner};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import{jsx as r}from"react/jsx-runtime";import{createContext as t,useMemo as e,useContext as o}from"react";const i=t(null),n=({store:t,children:o})=>{const n=e(()=>({store:t}),[t]);return r(i.Provider,{value:n,children:o})},u=()=>{const r=o(i);if(!r)throw new Error("useSduiLayoutContext must be used within SduiLayoutProvider");return r};export{n as SduiLayoutProvider,u as useSduiLayoutContext};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import{useRef as t,useCallback as o}from"react";import{defaultComponentFactory as r}from"../../components/componentMap.mjs";import{useSduiLayoutContext as n}from"../context/SduiLayoutContext.mjs";const e=e=>{const{store:m}=n(),{nodes:c}=m,p=t(null),s=o(t=>{const o=c[t];if(!o)return null;const n=m.getComponentOverrides(),s=e||{};return(n[t]||n[o.type]||s[o.type]||r)(t,p.current)},[c,m,e]);return p.current=s,s};export{e as useRenderNode};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import{useReducer as t,useEffect as e,useMemo as o}from"react";import{useSduiLayoutAction as r}from"./useSduiLayoutAction.mjs";const s=t=>t+1;function d(d){const{nodeId:i,schema:n}=d,[,a]=t(s,0),c=r();let u;e(()=>c.subscribeNode(i,a),[c,i]);let f,l,y=[];try{u=c.getNodeById(i),y=(null==u?void 0:u.childrenIds)||[]}catch(t){u=void 0,y=[]}try{f=c.getLayoutStateById(i)}catch(t){f=void 0}try{l=c.getAttributesById(i)}catch(t){l=void 0}const h=o(()=>{if(!n)return f;if(!f)throw new Error(`State not found for node "${i}". Schema was provided but state is missing.`);const t=n.safeParse(f);if(!t.success)throw new Error(`State validation failed for node "${i}": ${t.error.message}`);return t.data},[f,n,i]);return{node:u,type:null==u?void 0:u.type,state:h,childrenIds:y,attributes:l,exists:!!u}}export{d as useSduiNodeSubscription};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cloneDeep as t}from"lodash-es";import{normalizeSduiLayout as e}from"../utils/normalize/normalize.mjs";import{MetadataNotFoundError as r,NodeNotFoundError as s,RootNotFoundError as i}from"./errors.mjs";import{DocumentManager as o}from"./managers/DocumentManager.mjs";import{LayoutStateRepository as a}from"./managers/LayoutStateRepository.mjs";import{SubscriptionManager as n}from"./managers/SubscriptionManager.mjs";import{VariablesManager as d}from"./managers/VariablesManager.mjs";class u{constructor(t,e){this._subscriptionManager=new n,this._repository=new a,this._documentManager=new o,this._componentOverrides={},this._repository.initializeState(t),this._variablesManager=new d(this._repository,this._subscriptionManager),this._componentOverrides=(null==e?void 0:e.componentOverrides)||{}}subscribeNode(t,e){return this._subscriptionManager.subscribeNode(t,e)}subscribeVersion(t){return this._subscriptionManager.subscribeVersion(t)}get state(){return this._repository.state}get nodes(){return this._repository.nodes}get metadata(){const t=this._documentManager.getMetadata();if(!t)throw new r;return t}getComponentOverrides(){return this._componentOverrides}getNodeById(t){const e=this._repository.getNodeById(t);if(!e)throw new s(t);return e}getNodeTypeById(t){const e=this._repository.getNodeTypeById(t);if(!e)throw new s(t);return e}getChildrenIdsById(t){if(!this._repository.getNodeById(t))throw new s(t);return this._repository.getChildrenIdsById(t)}getLayoutStateById(t){return this.getNodeById(t).state||{}}getAttributesById(t){return this.getNodeById(t).attributes||{}}getRootId(){const t=this._repository.getRootId();if(!t)throw new i;return t}updateLayout(r){const{entities:s}=e(r);this._repository.updateNodes(s.nodes||{}),this._repository.setRootId(r.root.id),this._repository.setEdited(!1),this._repository.updateVariables(r.variables?t(r.variables):{}),this._repository.incrementVersion(),this._documentManager.setMetadata(r.metadata),this._documentManager.cacheDocument(r),this._subscriptionManager.notifyVersion()}cancelEdit(t){const e=this._repository.getRootId(),r=t||(e?this._documentManager.getDocumentId(e):void 0);if(!r)return;const s=this._documentManager.getOriginalDocument(r);s&&this.updateLayout(s)}updateNodeState(t,e){const r=this.getNodeById(t);this._repository.updateNodeState(t,Object.assign(Object.assign({},r.state||{}),e)),this._repository.setEdited(!0),this._subscriptionManager.notifyNode(t)}updateNodeAttributes(t,e){const r=this.getNodeById(t);this._repository.updateNodeAttributes(t,Object.assign(Object.assign({},r.attributes||{}),e)),this._repository.setEdited(!0),this._subscriptionManager.notifyNode(t)}updateVariables(t){this._variablesManager.updateVariables(t)}updateVariable(t,e){this._variablesManager.updateVariable(t,e)}deleteVariable(t){this._variablesManager.deleteVariable(t)}setSelectedNodeId(t){const e=this._repository.setSelectedNodeId(t);e&&this._subscriptionManager.notifyNode(e),t&&this._subscriptionManager.notifyNode(t)}getDocument(){return this._documentManager.getDocument(this._repository)}clearCache(){this._documentManager.clearCache(),this.reset()}reset(){this._documentManager.reset(),this._repository.reset(),this._subscriptionManager.notifyVersion()}}export{u as SduiLayoutStore};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class r extends Error{constructor(t){super(`Node not found: "${t}"`),this.name="NodeNotFoundError",Error.captureStackTrace&&Error.captureStackTrace(this,r)}}class t extends Error{constructor(){super("Root node ID not found"),this.name="RootNotFoundError",Error.captureStackTrace&&Error.captureStackTrace(this,t)}}class o extends Error{constructor(){super("Metadata not found"),this.name="MetadataNotFoundError",Error.captureStackTrace&&Error.captureStackTrace(this,o)}}export{o as MetadataNotFoundError,r as NodeNotFoundError,t as RootNotFoundError};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cloneDeep as t}from"lodash-es";import{denormalizeSduiNode as a}from"../../utils/normalize/denormalize.mjs";import"../../utils/normalize/normalize.mjs";class e{constructor(){this._cached={},this._originalCached={}}cacheDocument(a){var e;const i=(null===(e=a.metadata)||void 0===e?void 0:e.id)||a.root.id;this._cached[i]=a,this._originalCached[i]||(this._originalCached[i]=t(a))}setMetadata(t){this._metadata=t}getMetadata(){return this._metadata}getOriginalDocument(t){return this._originalCached[t]}getDocumentId(t){var a;return(null===(a=this._metadata)||void 0===a?void 0:a.id)||t}getDocument(t){const e=t.getRootId();if(!e)return null;const i=a(e,{nodes:t.nodes});return i?{version:"1.0.0",metadata:this._metadata,root:i}:null}clearCache(){this._cached={},this._originalCached={}}reset(){this._metadata=void 0,this.clearCache()}}export{e as DocumentManager};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class t{constructor(){this._state={version:0,rootId:void 0,nodes:{},selectedNodeId:void 0,isEdited:!1,variables:{}}}get state(){return this._state}get nodes(){return this._state.nodes}getNodeById(t){return this._state.nodes[t]}getNodeTypeById(t){var e;return null===(e=this._state.nodes[t])||void 0===e?void 0:e.type}getChildrenIdsById(t){var e;return(null===(e=this._state.nodes[t])||void 0===e?void 0:e.childrenIds)||[]}getRootId(){return this._state.rootId}initializeState(t){this._state=Object.assign({version:0,rootId:void 0,nodes:{},selectedNodeId:void 0,isEdited:!1,variables:{}},t)}updateNodes(t){this._state.nodes=t}updateNodeState(t,e){const s=this._state.nodes[t];s&&(this._state.nodes[t]=Object.assign(Object.assign({},s),{state:e}))}updateNodeAttributes(t,e){const s=this._state.nodes[t];s&&(this._state.nodes[t]=Object.assign(Object.assign({},s),{attributes:e}))}setRootId(t){this._state.rootId=t}setSelectedNodeId(t){const e=this._state.selectedNodeId;return this._state.selectedNodeId=t,e}setEdited(t){this._state.isEdited=t}updateVariables(t){this._state.variables=t}incrementVersion(){this._state.version+=1}reset(){this._state.nodes={},this._state.rootId=void 0,this._state.isEdited=!1,this._state.selectedNodeId=void 0,this._state.variables={},this._state.version+=1}}export{t as LayoutStateRepository};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class e{constructor(){this._nodeListeners=new Map,this._versionListeners=new Set}subscribeNode(e,s){return this._nodeListeners.has(e)||this._nodeListeners.set(e,new Set),this._nodeListeners.get(e).add(s),()=>{var t,i;null===(t=this._nodeListeners.get(e))||void 0===t||t.delete(s),0===(null===(i=this._nodeListeners.get(e))||void 0===i?void 0:i.size)&&this._nodeListeners.delete(e)}}subscribeVersion(e){return this._versionListeners.add(e),()=>{this._versionListeners.delete(e)}}notifyNode(e){var s;null===(s=this._nodeListeners.get(e))||void 0===s||s.forEach(e=>e())}notifyNodes(e){[...new Set(e)].forEach(e=>this.notifyNode(e))}notifyVersion(){this._versionListeners.forEach(e=>e())}}export{e as SubscriptionManager};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cloneDeep as s}from"lodash-es";class t{constructor(s,t){this.repository=s,this.subscriptionManager=t}updateVariables(t){this.repository.updateVariables(s(t)),this.repository.setEdited(!0),this.repository.incrementVersion(),this.subscriptionManager.notifyVersion()}updateVariable(t,i){const e=this.repository.state.variables;this.repository.updateVariables(Object.assign(Object.assign({},s(e)),{[t]:s(i)})),this.repository.setEdited(!0),this.repository.incrementVersion(),this.subscriptionManager.notifyVersion()}deleteVariable(t){const i=s(this.repository.state.variables);delete i[t],this.repository.updateVariables(i),this.repository.setEdited(!0),this.repository.incrementVersion(),this.subscriptionManager.notifyVersion()}}export{t as VariablesManager};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function t(n,e){var r;const i=null===(r=e.nodes)||void 0===r?void 0:r[n];if(!i)return null;const s=[];i.childrenIds&&s.push(...i.childrenIds);const l=s.map(n=>t(n,e)).filter(t=>null!==t);return Object.assign({id:i.id,type:i.type,state:i.state||{},attributes:i.attributes||{}},l.length>0&&{children:l})}function n(n,e,r){const i=t(n,e);return i?{version:"1.0.0",metadata:r,root:i}:null}export{n as denormalizeSduiLayout,t as denormalizeSduiNode};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{schema as t,normalize as e}from"normalizr";const i=new t.Entity("nodes",{},{idAttribute:"id",processStrategy:t=>({id:t.id,type:t.type,state:t.state||{},attributes:t.attributes||{}})});function s(t){const s=t.children||[],n=s.length>0?e(s,[i]):{entities:{nodes:{}}},d={id:t.id,type:t.type,state:t.state||{},attributes:t.attributes||{}},r=e(d,i),o={nodes:Object.assign(Object.assign({},r.entities.nodes),n.entities.nodes)},a=t=>{var e,i;o.nodes&&o.nodes[t.id]?o.nodes[t.id]=Object.assign(Object.assign({},o.nodes[t.id]),{childrenIds:(null===(e=t.children)||void 0===e?void 0:e.map(t=>t.id))||[]}):o.nodes&&(o.nodes[t.id]={id:t.id,type:t.type,state:t.state||{},attributes:t.attributes||{},childrenIds:(null===(i=t.children)||void 0===i?void 0:i.map(t=>t.id))||[]}),t.children&&t.children.forEach(a)};return a(t),{result:r.result,entities:o}}function n(t){const{result:e,entities:i}=s(t.root);return{result:e,entities:i}}i.define({children:[i]});export{n as normalizeSduiLayout,s as normalizeSduiNode};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './src';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ComponentFactory } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* 컴포넌트 맵
|
|
4
|
+
*
|
|
5
|
+
* 노드 타입별로 컴포넌트 팩토리를 매핑합니다.
|
|
6
|
+
* 기본적으로 비어있으며, consumers가 components prop을 통해 제공합니다.
|
|
7
|
+
*/
|
|
8
|
+
export declare const componentMap: Record<string, ComponentFactory>;
|
|
9
|
+
/**
|
|
10
|
+
* 기본 컴포넌트 팩토리
|
|
11
|
+
*
|
|
12
|
+
* 노드 타입이 componentMap에 없을 때 사용되는 기본 팩토리입니다.
|
|
13
|
+
* 노드 정보를 표시하고 자식을 렌더링합니다.
|
|
14
|
+
*/
|
|
15
|
+
export declare const defaultComponentFactory: ComponentFactory;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-Driven UI - Component System Types
|
|
3
|
+
*
|
|
4
|
+
* 컴포넌트 팩토리 및 렌더링 함수 타입 정의
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
/**
|
|
8
|
+
* 자식 노드 렌더링 함수 타입 (Render Props)
|
|
9
|
+
*
|
|
10
|
+
* 상위에서 주입되어 자식 노드를 렌더링할 때 사용합니다.
|
|
11
|
+
*/
|
|
12
|
+
export type RenderNodeFn = (childId: string) => ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* 컴포넌트 팩토리 타입
|
|
15
|
+
*
|
|
16
|
+
* id, renderNode를 받아서 컴포넌트를 렌더링합니다.
|
|
17
|
+
*/
|
|
18
|
+
export type ComponentFactory = (id: string, renderNode: RenderNodeFn) => ReactNode;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lodado/sdui-template
|
|
3
|
+
*
|
|
4
|
+
* Server-Driven UI Template Library for React
|
|
5
|
+
*
|
|
6
|
+
* A flexible and powerful template system for building server-driven user interfaces
|
|
7
|
+
* with dynamic layouts and components.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { SduiLayoutRenderer } from '@lodado/sdui-template';
|
|
12
|
+
*
|
|
13
|
+
* function App() {
|
|
14
|
+
* const document = {
|
|
15
|
+
* version: "1.0.0",
|
|
16
|
+
* root: {
|
|
17
|
+
* id: "root",
|
|
18
|
+
* type: "Container",
|
|
19
|
+
* state: {
|
|
20
|
+
* layout: { x: 0, y: 0, w: 12, h: 1 }
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* };
|
|
24
|
+
*
|
|
25
|
+
* return <SduiLayoutRenderer document={document} />;
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export { SduiLayoutRenderer } from './react-wrapper/components/SduiLayoutRenderer';
|
|
30
|
+
export { SduiLayoutProvider } from './react-wrapper/context/SduiLayoutContext';
|
|
31
|
+
export { useRenderNode, useSduiLayoutAction, useSduiNodeSubscription } from './react-wrapper/hooks';
|
|
32
|
+
export type { UseSduiNodeSubscriptionParams } from './react-wrapper/hooks/useSduiNodeSubscription';
|
|
33
|
+
export { SduiLayoutStore } from './store/SduiLayoutStore';
|
|
34
|
+
export type { SduiLayoutStoreOptions, SduiLayoutStoreState } from './store/types';
|
|
35
|
+
export type { SduiDocument, SduiLayoutDocument, SduiLayoutNode, SduiNode, } from './schema';
|
|
36
|
+
export type { ComponentFactory, RenderNodeFn } from './components/types';
|
|
37
|
+
export { denormalizeSduiLayout, denormalizeSduiNode, normalizeSduiLayout, normalizeSduiNode } from './utils/normalize';
|
|
38
|
+
export type { NormalizedSduiEntities } from './utils/normalize/types';
|