@knaw-huc/panoptes-react-blocks 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +666 -0
- package/dist/components/blocks/external-link/index.d.ts +8 -0
- package/dist/components/blocks/json/JsonBlockRenderer.d.ts +5 -0
- package/dist/components/blocks/json/index.d.ts +18 -0
- package/dist/components/blocks/json/schemaSelectors.d.ts +7 -0
- package/dist/components/blocks/label/index.d.ts +8 -0
- package/dist/components/blocks/link/index.d.ts +13 -0
- package/dist/components/blocks/map/index.d.ts +17 -0
- package/dist/components/blocks/markdown/index.d.ts +8 -0
- package/dist/components/blocks/screen/FormColumn.d.ts +7 -0
- package/dist/components/blocks/screen/FormElement.d.ts +7 -0
- package/dist/components/blocks/screen/FormRow.d.ts +7 -0
- package/dist/components/blocks/screen/ScreenActions.d.ts +1 -0
- package/dist/components/blocks/screen/ScreenForm.d.ts +1 -0
- package/dist/components/blocks/screen/ScreenLinks.d.ts +1 -0
- package/dist/components/blocks/screen/ScreenRenderer.d.ts +1 -0
- package/dist/components/blocks/screen/ScreenSidebar.d.ts +6 -0
- package/dist/components/blocks/screen/ScreenTabs.d.ts +1 -0
- package/dist/components/blocks/screen/context/ItemDataContext.d.ts +7 -0
- package/dist/components/blocks/screen/context/ScreenContext.d.ts +16 -0
- package/dist/components/blocks/screen/hooks/index.d.ts +3 -0
- package/dist/components/blocks/screen/hooks/useElementState.d.ts +8 -0
- package/dist/components/blocks/screen/hooks/useItemData.d.ts +1 -0
- package/dist/components/blocks/screen/hooks/useScreenContext.d.ts +2 -0
- package/dist/components/blocks/screen/hooks/useScreenState.d.ts +3 -0
- package/dist/components/blocks/screen/index.d.ts +6 -0
- package/dist/components/blocks/screen/schema/binding.d.ts +8 -0
- package/dist/components/blocks/screen/schema/index.d.ts +3 -0
- package/dist/components/blocks/screen/schema/types.d.ts +100 -0
- package/dist/components/blocks/toggle/index.d.ts +8 -0
- package/dist/components/ghostline/Ghostline.d.ts +1 -0
- package/dist/index.cjs +37 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +37518 -0
- package/dist/panoptes-react-blocks.css +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KNAW-HuC
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
# @knaw-huc/panoptes-react-blocks (WIP)
|
|
2
|
+
|
|
3
|
+
A React component library providing block renderers and a screen layout system for [Panoptes](https://github.com/knaw-huc/panoptes-react) applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @knaw-huc/panoptes-react-blocks
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Import the styles in your app entry point:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import '@knaw-huc/panoptes-react-blocks/style.css';
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Peer dependencies
|
|
18
|
+
|
|
19
|
+
| Package | Version |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `react` | 19 |
|
|
22
|
+
| `react-dom` | 19 |
|
|
23
|
+
| `@knaw-huc/panoptes-react` | `*` |
|
|
24
|
+
| `@tanstack/react-router` | `*` |
|
|
25
|
+
|
|
26
|
+
## Block renderers
|
|
27
|
+
|
|
28
|
+
Each renderer accepts a `block` prop typed as `Block` from `@knaw-huc/panoptes-react`. The block's `type`, `value`, and optional `config` fields drive the rendering.
|
|
29
|
+
|
|
30
|
+
### `ExternalLinkBlockRenderer`
|
|
31
|
+
|
|
32
|
+
Renders a `value` string as an external anchor (`target="_blank"`).
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
interface ExternalLinkBlock extends Block {
|
|
36
|
+
type: 'link';
|
|
37
|
+
value: string; // URL
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `LabelBlockRenderer`
|
|
42
|
+
|
|
43
|
+
Renders a plain text `value`.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
interface LabelBlock extends Block {
|
|
47
|
+
type: 'label';
|
|
48
|
+
value: string;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `LinkBlockRenderer`
|
|
53
|
+
|
|
54
|
+
Renders an internal navigation link using `@tanstack/react-router`. The URL is built from `config.url` and optional route `model` params.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
interface LinkBlock extends Block {
|
|
58
|
+
type: 'link';
|
|
59
|
+
value: string; // link text
|
|
60
|
+
config?: { url: string }; // router path
|
|
61
|
+
model?: Record<string, unknown>; // route params
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `MarkdownBlockRenderer`
|
|
66
|
+
|
|
67
|
+
Renders `value` as Markdown (GFM + raw HTML). HTML is sanitized via `rehype-sanitize`.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
interface MarkDownBlock extends Block {
|
|
71
|
+
type: 'markdown';
|
|
72
|
+
value: string;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `ToggleBlockRenderer`
|
|
77
|
+
|
|
78
|
+
Renders a boolean `value` as a check or cross icon (using Heroicons). Supports i18n via the Panoptes `translateFn`.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
interface ToggleBlock extends Block {
|
|
82
|
+
type: 'toggle';
|
|
83
|
+
value: boolean;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `MapBlockRenderer`
|
|
88
|
+
|
|
89
|
+
Renders an OpenLayers map centred on a lat/lon coordinate with a marker. Defaults to OpenStreetMap tiles.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
interface MapBlock extends Block {
|
|
93
|
+
type: 'map';
|
|
94
|
+
value: { latitude: number; longitude: number };
|
|
95
|
+
config?: {
|
|
96
|
+
zoom?: number; // default 12
|
|
97
|
+
tileUrl?: string; // custom OSM-compatible tile URL
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `RenderJsonBlock`
|
|
103
|
+
|
|
104
|
+
Renders structured JSON data driven by a JSON Schema `config`.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
interface JsonBlock extends Block {
|
|
108
|
+
type: 'json';
|
|
109
|
+
value: JsonData; // any JSON value
|
|
110
|
+
config: JsonSchema; // JSON Schema object
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `ScreenBlockRenderer`
|
|
115
|
+
|
|
116
|
+
Renders a full data-entry screen defined by a `ScreenDefinition` config. See the [Screen system](#screen-system) section below.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
interface ScreenBlock extends Block {
|
|
120
|
+
type: 'screen';
|
|
121
|
+
value: ScreenBlockValue; // Record<string, unknown>
|
|
122
|
+
config: ScreenDefinition;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Screen system
|
|
127
|
+
|
|
128
|
+
The screen block system (`lib/components/blocks/screen/`) renders structured detail screens driven by a declarative JSON configuration from the Panoptes API. A screen block is registered as a Panoptes block of type `"screen"` and can be used anywhere the framework renders blocks.
|
|
129
|
+
|
|
130
|
+
### Architecture overview
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
RenderScreenBlock # Registered Panoptes block component
|
|
134
|
+
└── ScreenProvider # React context (screenDefinition + data + active tab)
|
|
135
|
+
└── ScreenRenderer # Top-level layout shell
|
|
136
|
+
├── ScreenLinks # Optional navigation links (header area)
|
|
137
|
+
├── ScreenTabs # Optional tab bar (hidden when only one tab)
|
|
138
|
+
├── ScreenSidebar # Optional icon sidebar (Lucide icons)
|
|
139
|
+
├── ScreenForm # Form body
|
|
140
|
+
│ └── FormRow # Recursive row rendering (header / group / footer / row)
|
|
141
|
+
│ └── FormColumn → FormElement # Column + element rendering
|
|
142
|
+
└── ScreenActions # Optional action buttons with confirmation dialogs
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### ScreenBlock config schema
|
|
146
|
+
|
|
147
|
+
A `ScreenBlock` received from the API has the following shape:
|
|
148
|
+
|
|
149
|
+
```jsonc
|
|
150
|
+
{
|
|
151
|
+
"type": "screen",
|
|
152
|
+
"value": { /* flat or nested data object */ },
|
|
153
|
+
"config": { /* ScreenDefinition — see below */ }
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### ScreenDefinition
|
|
158
|
+
|
|
159
|
+
| Field | Type | Required | Description |
|
|
160
|
+
|---|---|----------|--------------------------------------------------------------------------------------------------------------|
|
|
161
|
+
| `id` | `string` | yes | Unique identifier |
|
|
162
|
+
| `label` | `string` | no | Screen heading passed through `translateFn`; omit to use the autokey `screens.{id}` |
|
|
163
|
+
| `screenType` | `"normal"` | yes | Screen layout variant (not used yet - intended for screen variants, eg., mobile, in a popover, confirmation) |
|
|
164
|
+
| `tabs` | `TabDefinition[]` | no | Tab list; a tab bar is shown only when there are more than one |
|
|
165
|
+
| `activeTabId` | `string` | no | Initially active tab (defaults to first tab) |
|
|
166
|
+
| `links` | `LinkDefinition[]` | no | Navigation links rendered above the tabs |
|
|
167
|
+
| `actions` | `ActionDefinition[]` | no | Action buttons rendered in the footer |
|
|
168
|
+
| `form` | `FormDefinition` | yes | Content form (rows of elements) |
|
|
169
|
+
| `sidebar` | `SidebarDefinition` | no | Icon sidebar rendered to the left of the form |
|
|
170
|
+
|
|
171
|
+
#### TabDefinition (not used yet)
|
|
172
|
+
|
|
173
|
+
Tabs are .... tabs, and meant for executing an operation after clicking a tab.
|
|
174
|
+
|
|
175
|
+
| Field | Type | Required | Description |
|
|
176
|
+
|---|---|---|--------------------------------------------------------------|
|
|
177
|
+
| `id` | `string` | yes | Unique tab identifier |
|
|
178
|
+
| `label` | `string` | no | Tab label; omit to use the autokey `screens.{screenId}.tabs.{tabId}` |
|
|
179
|
+
| `operation` | `OperationDefinition` | no | API operation to call when the tab is selected |
|
|
180
|
+
| `operationList` | `OperationListItem[]` | no | Sub-navigation items shown beneath the active tab |
|
|
181
|
+
|
|
182
|
+
Each `OperationListItem` has an `id`, an optional `label` (autokey: `screens.{screenId}.tabs.{tabId}.{itemId}`), and an `operation`.
|
|
183
|
+
|
|
184
|
+
#### LinkDefinition (not used yet)
|
|
185
|
+
|
|
186
|
+
Links are intended to be 'follow-up' operations after fetching data. For example, we could envision the flow as follows:
|
|
187
|
+
|
|
188
|
+
- Fetch item data
|
|
189
|
+
- Link -> Fetch screen definition
|
|
190
|
+
- Link -> Fetch translations
|
|
191
|
+
- Link -> Fetch data from remote system
|
|
192
|
+
|
|
193
|
+
| Field | Type | Required | Description |
|
|
194
|
+
|---|---|---|---|
|
|
195
|
+
| `id` | `string` | yes | Unique link identifier |
|
|
196
|
+
| `label` | `string` | yes | Link label |
|
|
197
|
+
| `operation` | `OperationDefinition` | no | API operation to execute on click |
|
|
198
|
+
| `href` | `string` | no | URL to navigate to on click |
|
|
199
|
+
|
|
200
|
+
#### ActionDefinition (not used yet)
|
|
201
|
+
|
|
202
|
+
Actions are meant to be simple API operations, executed after action button clicks.
|
|
203
|
+
|
|
204
|
+
| Field | Type | Required | Description |
|
|
205
|
+
|---|---|---|---|
|
|
206
|
+
| `id` | `string` | yes | Unique action identifier |
|
|
207
|
+
| `label` | `string` | no | Button label; omit to use the autokey `screens.{screenId}.actions.{actionId}` |
|
|
208
|
+
| `activate` | `"always" \| "onDirty" \| "onValid" \| "onDirtyAndValid"` | yes | When the button is enabled |
|
|
209
|
+
| `confirmation` | `ConfirmationDefinition` | yes | Confirmation dialog settings |
|
|
210
|
+
| `operation` | `OperationDefinition` | yes | API operation to call on confirm |
|
|
211
|
+
|
|
212
|
+
**ConfirmationDefinition**
|
|
213
|
+
|
|
214
|
+
| Field | Type | Description |
|
|
215
|
+
|---|---|---|
|
|
216
|
+
| `askConfirmation` | `"always" \| "never" \| "onDirty"` | When to show a confirmation dialog |
|
|
217
|
+
| `labels.title` | `string` | Dialog title; omit to use the autokey `screens.{screenId}.actions.{actionId}.title` |
|
|
218
|
+
| `labels.message` | `string` | Dialog message; omit to use the autokey `screens.{screenId}.actions.{actionId}.message` |
|
|
219
|
+
| `labels.ok` | `string` | Confirm button label; omit to use the autokey `screens.{screenId}.actions.{actionId}.ok` |
|
|
220
|
+
| `labels.cancel` | `string` | Cancel button label; omit to use the autokey `screens.{screenId}.actions.{actionId}.cancel` |
|
|
221
|
+
|
|
222
|
+
#### FormDefinition and rows
|
|
223
|
+
|
|
224
|
+
```jsonc
|
|
225
|
+
{
|
|
226
|
+
"form": {
|
|
227
|
+
"rows": [ /* RowDefinition[] */ ]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**RowDefinition**
|
|
233
|
+
|
|
234
|
+
| Field | Type | Description |
|
|
235
|
+
|---|---|---|
|
|
236
|
+
| `displayType` | `"header" \| "group" \| "footer" \| "row"` | Styling variant (default: `"row"`) |
|
|
237
|
+
| `label` | `string` | Optional fieldset legend; omit to use the autokey `screens.{screenId}.{groupId}` (requires `groupId`) |
|
|
238
|
+
| `groupId` | `string` | Used as the React key and `data-group-id` attribute |
|
|
239
|
+
| `elements` | `ElementDefinition[]` | Direct child elements (mutually exclusive with `columns`/`rows`) |
|
|
240
|
+
| `columns` | `ColumnDefinition[]` | Multi-column layout; each column holds its own elements |
|
|
241
|
+
| `rows` | `RowDefinition[]` | Nested rows (recursive) |
|
|
242
|
+
|
|
243
|
+
Row content is resolved in order of priority: nested `rows` → `columns` → `elements`.
|
|
244
|
+
|
|
245
|
+
#### ElementDefinition
|
|
246
|
+
|
|
247
|
+
| Field | Type | Description |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| `value` | `string \| string[] \| Record<string, string>` | Binding expression(s) — see [Bindings](#bindings) |
|
|
250
|
+
| `type` | `string` | Element type (see [Element types](#element-types)); inferred from data when omitted |
|
|
251
|
+
| `label` | `string` | Field label rendered above the element; omit to use the autokey `screens.{screenId}.{groupId}.{field}` (only available for single-string `value`) |
|
|
252
|
+
| `infoLabel` | `string` | Secondary info text rendered below the element; omit to use the autokey `screens.{screenId}.{groupId}.{field}.info` (only available for single-string `value`) |
|
|
253
|
+
| `hidden` | `boolean` | Hides the element when `true` |
|
|
254
|
+
| `config` | `object` | Type-specific configuration (e.g. `options` for `select`, `itemTemplate` for `array`) |
|
|
255
|
+
|
|
256
|
+
#### SidebarDefinition
|
|
257
|
+
|
|
258
|
+
| Field | Type | Description |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| `id` | `string` | Unique sidebar identifier |
|
|
261
|
+
| `width` | `string` | Optional CSS width for the sidebar (sets `--sidebar-width`) |
|
|
262
|
+
| `sections` | `SidebarSectionDefinition[]` | Groups of navigation items separated by a divider |
|
|
263
|
+
|
|
264
|
+
Each `SidebarNavItemDefinition` has an `icon` (Lucide icon name in kebab-case, e.g. `"book-open"`), an optional `label` (autokey: `screens.{screenId}.sidebar.{sectionId}.{itemId}`), an `operation`, and an optional `active` flag.
|
|
265
|
+
|
|
266
|
+
### Bindings
|
|
267
|
+
|
|
268
|
+
Element `value` fields and `itemTemplate` field values use binding expressions to pull data from the block's payload:
|
|
269
|
+
|
|
270
|
+
| Expression | Source |
|
|
271
|
+
|---|---|
|
|
272
|
+
| `$data#/field/subfield` | The `value` object of the `ScreenBlock` |
|
|
273
|
+
| `$itemData#/field` | The current item object when rendering inside an `array` element |
|
|
274
|
+
|
|
275
|
+
Path segments are separated by `/`.
|
|
276
|
+
|
|
277
|
+
The `value` field on an `ElementDefinition` supports three forms:
|
|
278
|
+
|
|
279
|
+
**Single string** — resolves to a single value and enables autokey label generation:
|
|
280
|
+
```jsonc
|
|
281
|
+
{ "value": "$data#/title", "type": "text" }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Array of strings** — each binding is resolved independently; the block receives an array of resolved values. Useful for blocks that combine multiple data fields:
|
|
285
|
+
```jsonc
|
|
286
|
+
{ "value": ["$data#/firstName", "$data#/lastName"], "type": "full-name" }
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Object with string values** — each property's binding expression is resolved independently; the block receives a plain object with the resolved values. Useful for blocks that need named inputs (e.g. a map block needing separate latitude/longitude fields). No autokey is generated in this form:
|
|
290
|
+
```jsonc
|
|
291
|
+
{
|
|
292
|
+
"value": {
|
|
293
|
+
"latitude": "$data#/plaatsBreedtegraad",
|
|
294
|
+
"longitude": "$data#/plaatsLengtegraad"
|
|
295
|
+
},
|
|
296
|
+
"type": "map",
|
|
297
|
+
"config": { "zoom": 6 }
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Autokey label generation
|
|
302
|
+
|
|
303
|
+
All `label` (and `infoLabel`) fields are optional. When omitted, a translation key is derived automatically and passed through `translateFn`. The keys follow a hierarchical pattern based on the screen ID, group ID, and field path:
|
|
304
|
+
|
|
305
|
+
| Context | Autokey pattern | Example |
|
|
306
|
+
|---|---|---|
|
|
307
|
+
| Screen heading | `screens.{screenId}` | `screens.journal-detail` |
|
|
308
|
+
| Group legend | `screens.{screenId}.{groupId}` | `screens.journal-detail.metadata` |
|
|
309
|
+
| Element label | `screens.{screenId}.{groupId}.{field}` | `screens.journal-detail.metadata.title` |
|
|
310
|
+
| Element info label | `screens.{screenId}.{groupId}.{field}.info` | `screens.journal-detail.metadata.title.info` |
|
|
311
|
+
| Tab label | `screens.{screenId}.tabs.{tabId}` | `screens.journal-detail.tabs.general` |
|
|
312
|
+
| Tab operation list item | `screens.{screenId}.tabs.{tabId}.{itemId}` | `screens.journal-detail.tabs.general.volume-1` |
|
|
313
|
+
| Sidebar nav item | `screens.{screenId}.sidebar.{sectionId}.{itemId}` | `screens.journal-detail.sidebar.main.home` |
|
|
314
|
+
| Action button | `screens.{screenId}.actions.{actionId}` | `screens.journal-detail.actions.save` |
|
|
315
|
+
| Action confirmation title | `screens.{screenId}.actions.{actionId}.title` | `screens.journal-detail.actions.save.title` |
|
|
316
|
+
| Action confirmation message | `screens.{screenId}.actions.{actionId}.message` | `screens.journal-detail.actions.save.message` |
|
|
317
|
+
| Action confirmation ok | `screens.{screenId}.actions.{actionId}.ok` | `screens.journal-detail.actions.save.ok` |
|
|
318
|
+
| Action confirmation cancel | `screens.{screenId}.actions.{actionId}.cancel` | `screens.journal-detail.actions.save.cancel` |
|
|
319
|
+
|
|
320
|
+
The `{field}` segment is the path from the binding expression — e.g. `$data#/title` produces `title`, and `$data#/address/city` produces `address.city`.
|
|
321
|
+
|
|
322
|
+
When a `label` is provided explicitly it is used as-is (also passed through `translateFn`), which allows overriding the autokey with a custom translation key or a literal string.
|
|
323
|
+
|
|
324
|
+
### Element types
|
|
325
|
+
|
|
326
|
+
The element type can be an unspecified type, or a type present in the collection of
|
|
327
|
+
Panoptes-known blocks (list, cmdi) and/or application-specific custom blocks.
|
|
328
|
+
|
|
329
|
+
When `type` is not specified on an `ElementDefinition` the type is inferred from the resolved value:
|
|
330
|
+
|
|
331
|
+
| Inferred condition | Type |
|
|
332
|
+
|---|---|
|
|
333
|
+
| Array | `array` |
|
|
334
|
+
| Boolean | `checkbox` |
|
|
335
|
+
| Number | `number` |
|
|
336
|
+
| String matching `YYYY-MM-DD…` | `date` |
|
|
337
|
+
| String containing `\n` | `textarea` |
|
|
338
|
+
| Anything else | `text` |
|
|
339
|
+
|
|
340
|
+
Explicit types available:
|
|
341
|
+
|
|
342
|
+
| Type | Rendered as | Config options |
|
|
343
|
+
|---|---|---|
|
|
344
|
+
| `text` | `<input type="text">` (read-only) | — |
|
|
345
|
+
| `textarea` | `<textarea>` (read-only) | — |
|
|
346
|
+
| `number` | `<input type="number">` (read-only) | — |
|
|
347
|
+
| `date` | `<input type="date">` (read-only) | — |
|
|
348
|
+
| `checkbox` | `<input type="checkbox">` (read-only) | — |
|
|
349
|
+
| `prose` | Inline `<span>` | — |
|
|
350
|
+
| `select` | Resolved option label in `<input type="text">` | `config.options: { value, label }[]` |
|
|
351
|
+
| `array` | List of text inputs, or templated item rows | `config.itemTemplate`: map of field name → `ElementDefinition` |
|
|
352
|
+
|
|
353
|
+
Any `type` that matches a registered Panoptes block is rendered using that block component. If no matching block is found (or the block component throws), the element falls back to the native HTML renderer above.
|
|
354
|
+
|
|
355
|
+
### Example of a full screen definition
|
|
356
|
+
|
|
357
|
+
```jsonc
|
|
358
|
+
{
|
|
359
|
+
"id": "tijdschrift-detail",
|
|
360
|
+
"screenType": "normal",
|
|
361
|
+
"globals": {
|
|
362
|
+
},
|
|
363
|
+
"tabs": [
|
|
364
|
+
],
|
|
365
|
+
"links": [
|
|
366
|
+
],
|
|
367
|
+
"actions": [
|
|
368
|
+
],
|
|
369
|
+
"sidebar": {
|
|
370
|
+
"id": "tijdschrift-sidebar",
|
|
371
|
+
"sections": [
|
|
372
|
+
{
|
|
373
|
+
"id": "main",
|
|
374
|
+
"items": [
|
|
375
|
+
{
|
|
376
|
+
"id": "tijdschriften",
|
|
377
|
+
"icon": "newspaper",
|
|
378
|
+
"label": "tijdschrift-detail.tijdschrift-sidebar.label.publications"
|
|
379
|
+
}
|
|
380
|
+
]
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
"id": "util",
|
|
384
|
+
"items": [
|
|
385
|
+
{
|
|
386
|
+
"id": "instellingen",
|
|
387
|
+
"icon": "settings",
|
|
388
|
+
"label": "tijdschrift-detail.tijdschrift-sidebar.label.settings"
|
|
389
|
+
}
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
]
|
|
393
|
+
},
|
|
394
|
+
"form": {
|
|
395
|
+
"rows": [
|
|
396
|
+
{
|
|
397
|
+
"displayType": "group",
|
|
398
|
+
"groupId": "titel",
|
|
399
|
+
"columns": [
|
|
400
|
+
{
|
|
401
|
+
"elements": [
|
|
402
|
+
{
|
|
403
|
+
"value": "$data#/lidwoordTitel",
|
|
404
|
+
"type": "label"
|
|
405
|
+
}
|
|
406
|
+
]
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
"elements": [
|
|
410
|
+
{
|
|
411
|
+
"value": "$data#/titelVanTijdschrift",
|
|
412
|
+
"type": "label"
|
|
413
|
+
}
|
|
414
|
+
]
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
"elements": [
|
|
418
|
+
{
|
|
419
|
+
"value": "$data#/onderTitel",
|
|
420
|
+
"type": "label"
|
|
421
|
+
}
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
]
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
"displayType": "group",
|
|
428
|
+
"groupId": "publicatie",
|
|
429
|
+
"rows": [
|
|
430
|
+
{
|
|
431
|
+
"columns": [
|
|
432
|
+
{
|
|
433
|
+
"elements": [
|
|
434
|
+
{
|
|
435
|
+
"value": "$data#/uitgever",
|
|
436
|
+
"type": "link",
|
|
437
|
+
"config": {
|
|
438
|
+
"url": "/politieke-tijdschriften-uitgever_drukker/details/$uitgeverId"
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
]
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
"elements": [
|
|
445
|
+
{
|
|
446
|
+
"value": "$data#/drukker",
|
|
447
|
+
"type": "link",
|
|
448
|
+
"config": {
|
|
449
|
+
"url": "/politieke-tijdschriften-uitgever_drukker/details/$drukkerId"
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
]
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
"elements": [
|
|
456
|
+
{
|
|
457
|
+
"value": "$data#/plaats",
|
|
458
|
+
"type": "link",
|
|
459
|
+
"config": {
|
|
460
|
+
"url": "/politieke-tijdschriften-plaatsnaam/details/$plaatsId"
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
]
|
|
464
|
+
}
|
|
465
|
+
]
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
"columns": [
|
|
469
|
+
{
|
|
470
|
+
"elements": [
|
|
471
|
+
{
|
|
472
|
+
"value": "$data#/uitgeverZeker",
|
|
473
|
+
"type": "toggle"
|
|
474
|
+
}
|
|
475
|
+
]
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
"elements": [
|
|
479
|
+
{
|
|
480
|
+
"value": "$data#/drukkerZeker",
|
|
481
|
+
"type": "toggle"
|
|
482
|
+
}
|
|
483
|
+
]
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
"elements": [
|
|
487
|
+
{
|
|
488
|
+
"value": "$data#/nietBewaard",
|
|
489
|
+
"type": "toggle"
|
|
490
|
+
}
|
|
491
|
+
]
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
"elements": [
|
|
495
|
+
{
|
|
496
|
+
"value": "$data#/vrijheidGelijkheidBroederschap",
|
|
497
|
+
"type": "toggle"
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
}
|
|
501
|
+
]
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
"displayType": "group",
|
|
507
|
+
"groupId": "periode",
|
|
508
|
+
"rows": [
|
|
509
|
+
{
|
|
510
|
+
"columns": [
|
|
511
|
+
{
|
|
512
|
+
"elements": [
|
|
513
|
+
{
|
|
514
|
+
"value": "$data#/eersteNummer",
|
|
515
|
+
"type": "label"
|
|
516
|
+
}
|
|
517
|
+
]
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
"elements": [
|
|
521
|
+
{
|
|
522
|
+
"value": "$data#/laatsteNummer",
|
|
523
|
+
"type": "label"
|
|
524
|
+
}
|
|
525
|
+
]
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
"elements": [
|
|
529
|
+
{
|
|
530
|
+
"value": "$data#/prijsDuiten",
|
|
531
|
+
"type": "label"
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
"elements": [
|
|
537
|
+
{
|
|
538
|
+
"value": "$data#/afleveringen",
|
|
539
|
+
"type": "label"
|
|
540
|
+
}
|
|
541
|
+
]
|
|
542
|
+
}
|
|
543
|
+
]
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
"elements": [
|
|
547
|
+
{
|
|
548
|
+
"value": "$data#/formaat",
|
|
549
|
+
"type": "label"
|
|
550
|
+
}
|
|
551
|
+
]
|
|
552
|
+
}
|
|
553
|
+
]
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
"displayType": "group",
|
|
557
|
+
"groupId": "classificatie",
|
|
558
|
+
"columns": [
|
|
559
|
+
{
|
|
560
|
+
"elements": [
|
|
561
|
+
{
|
|
562
|
+
"value": "$data#/vormTijdschrift",
|
|
563
|
+
"type": "label"
|
|
564
|
+
}
|
|
565
|
+
]
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
"elements": [
|
|
569
|
+
{
|
|
570
|
+
"value": "$data#/typeTijdschrift",
|
|
571
|
+
"type": "label"
|
|
572
|
+
}
|
|
573
|
+
]
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
"elements": [
|
|
577
|
+
{
|
|
578
|
+
"value": "$data#/politiekePositie",
|
|
579
|
+
"type": "label"
|
|
580
|
+
}
|
|
581
|
+
]
|
|
582
|
+
}
|
|
583
|
+
]
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
"displayType": "group",
|
|
587
|
+
"groupId": "inhoud",
|
|
588
|
+
"elements": [
|
|
589
|
+
{
|
|
590
|
+
"value": "$data#/korteOmschrijvingInhoud",
|
|
591
|
+
"type": "markdown",
|
|
592
|
+
"config": {
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
"value": "$data#/verantwoordingSelectie",
|
|
597
|
+
"type": "markdown",
|
|
598
|
+
"config": {
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
"value": "$data#/toelichtingRedacteurAuteur",
|
|
603
|
+
"type": "markdown",
|
|
604
|
+
"config": {
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
"value": "$data#/advertenties_en_andere_verwijsplaatsen",
|
|
609
|
+
"type": "markdown"
|
|
610
|
+
}
|
|
611
|
+
]
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
"displayType": "group",
|
|
615
|
+
"groupId": "aanvullende-titels",
|
|
616
|
+
"elements": [
|
|
617
|
+
{
|
|
618
|
+
"value": "$data#/aanvullendeTitels",
|
|
619
|
+
"type": "list"
|
|
620
|
+
}
|
|
621
|
+
]
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
"displayType": "group",
|
|
625
|
+
"groupId": "artikel-types",
|
|
626
|
+
"elements": [
|
|
627
|
+
{
|
|
628
|
+
"value": "$data#/artikelType",
|
|
629
|
+
"type": "list"
|
|
630
|
+
}
|
|
631
|
+
]
|
|
632
|
+
}
|
|
633
|
+
]
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
## Utility components
|
|
639
|
+
|
|
640
|
+
### `GhostLine`
|
|
641
|
+
|
|
642
|
+
A decorative horizontal line placeholder used for loading states.
|
|
643
|
+
|
|
644
|
+
```tsx
|
|
645
|
+
import { GhostLine } from '@knaw-huc/panoptes-react-blocks';
|
|
646
|
+
|
|
647
|
+
<GhostLine />
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
## Repository structure
|
|
651
|
+
|
|
652
|
+
| Path | Purpose |
|
|
653
|
+
|---|---|
|
|
654
|
+
| `lib/` | Library source — compiled and published as `@knaw-huc/panoptes-react-blocks` |
|
|
655
|
+
| `src/` | Example show app for local development and visual testing |
|
|
656
|
+
|
|
657
|
+
Run the example app with `npm run dev` to preview components in the browser.
|
|
658
|
+
|
|
659
|
+
## Building
|
|
660
|
+
|
|
661
|
+
```bash
|
|
662
|
+
npm run build # compile once
|
|
663
|
+
npm run dev # watch mode (starts the example app)
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Output is written to `dist/` as ES module (`index.js`) and CommonJS (`index.cjs`) bundles, with a bundled `style.css`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Block } from '@knaw-huc/panoptes-react';
|
|
2
|
+
export interface ExternalLinkBlock extends Block {
|
|
3
|
+
type: 'link';
|
|
4
|
+
value: string;
|
|
5
|
+
}
|
|
6
|
+
export default function ExternalLinkBlockRenderer({ block }: {
|
|
7
|
+
block: Block;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|