create-restforge-skills 0.2.0 → 0.4.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.
@@ -1,496 +1,623 @@
1
- # Reference: UDF Catalog (UI Definition File)
2
-
3
- > **Offline mirror.** This file mirrors `designer_get_udf_catalog` from the
4
- > installed RESTForge designer. The live tool is authoritative — when this file
5
- > and the tool disagree, trust the tool, then update this file. Field types and
6
- > features depend on the active plugin version; always re-ground with the tool
7
- > (and `designer_list_plugins`) before defining a UDF payload.
8
-
9
- Source: `designer_get_udf_catalog` — installed designer version.
10
- Use as grounding before defining or editing any UDF payload.
11
-
12
- ---
13
-
14
- ## Table of Contents
15
-
16
- 1. [Payload Envelope](#payload-envelope)
17
- 2. [App Config](#app-config)
18
- 3. [Page Anatomy](#page-anatomy)
19
- 4. [Field Types](#field-types)
20
- 5. [Field Attributes](#field-attributes)
21
- 6. [Field Rows (Layout)](#field-rows-layout)
22
- 7. [Field States (Conditional)](#field-states-conditional)
23
- 8. [Features](#features)
24
- 9. [Data Source](#data-source)
25
- 10. [Navigation](#navigation)
26
- 11. [Homepage](#homepage)
27
- 12. [Dashboard Page](#dashboard-page)
28
- 13. [Master-Detail](#master-detail)
29
- 14. [Workflow Actions](#workflow-actions)
30
- 15. [ID Generation](#id-generation)
31
- 16. [Live Sync](#live-sync)
32
- 17. [Naming Conventions](#naming-conventions)
33
- 18. [Plugins](#plugins)
34
-
35
- ---
36
-
37
- ## Payload Envelope
38
-
39
- Root-level UDF structure. All properties at the top level.
40
-
41
- ```json
42
- {
43
- "appConfig": { ... },
44
- "pages": [ ... ],
45
- "navigation": [ ... ],
46
- "homepage": "page-id"
47
- }
48
- ```
49
-
50
- **Multi-file support:**
51
- - `extends`: inherit from a base file — `"extends": "./base.json"`
52
- - `include`: inline another file's pages array — `"include": "./orders-pages.json"`
53
-
54
- ---
55
-
56
- ## App Config
57
-
58
- | Property | Type | Required | Notes |
59
- |---|---|---|---|
60
- | `appName` | string | yes | Display name shown in page title and header |
61
- | `appCode` | string | yes | Unique app identifier, kebab-case, used in file naming |
62
- | `plugin` | string | yes | Output plugin: `vanilla-js-basic`, `vanilla-js-auth`, or custom |
63
- | `apiBaseUrl` | string | yes | Base URL of the RESTForge backend API |
64
- | `port` | integer | no | Local dev server port; default: `3000` |
65
- | `numberFormat` | string | no | Locale for number display; default: `"id-ID"` |
66
-
67
- Plugin-specific properties (for `vanilla-js-auth`):
68
- - `authAppCode` — app code used for the auth API path
69
- - `idleTimeout` — session idle timeout in seconds
70
-
71
- ---
72
-
73
- ## Page Anatomy
74
-
75
- Each entry in `pages[]` represents one page in the application.
76
-
77
- | Property | Type | Required | Notes |
78
- |---|---|---|---|
79
- | `pageId` | string | yes | Unique identifier; CRUD: `^[a-zA-Z0-9_-]+$`; dashboard: `^dash-[a-z0-9-]+$` |
80
- | `pageTitle` | string | yes | Display title shown in header and sidebar |
81
- | `pageSubtitle` | string | no | Subtitle shown below title |
82
- | `pageIcon` | string | no | Icon class (e.g., Tabler Icons: `"ti ti-home"`) |
83
- | `pageGroup` | string | no | Sidebar group name (Title Case); auto-derives navigation |
84
- | `pageType` | string | no | `"crud"` (default) or `"dashboard"` |
85
- | `apiPath` | string | yes (CRUD) | Backend endpoint path; e.g., `"/customer"` |
86
- | `primaryKey` | string | yes (CRUD) | Primary key field name; e.g., `"customer_id"` |
87
- | `displayField` | string | yes (CRUD) | Field used as record label in lookups |
88
- | `fields` | array | yes (CRUD) | Field definitions for the page |
89
- | `fieldRows` | array | no | Layout grouping for multi-column forms |
90
- | `fieldStates` | array | no | Conditional visibility/readonly rules |
91
- | `details` | array | no | Detail pages for master-detail pattern |
92
- | `workflow` | object | no | Workflow state machine config |
93
- | `workflowActions` | array | no | Action buttons for workflow transitions |
94
- | `features` | object | no | Page-level feature flags |
95
-
96
- ---
97
-
98
- ## Field Types
99
-
100
- 8 field types. Each type maps to a specific HTML element and optional library.
101
-
102
- | Type | HTML Element | Library | Key attributes |
103
- |---|---|---|---|
104
- | `text` | `<input type="text">` | — | `maxlength`, `placeholder` |
105
- | `textarea` | `<textarea>` | — | `rows`, `maxlength` |
106
- | `number` | `<input type="number">` | — | `min`, `max`, `step` |
107
- | `checkbox` | toggle switch | — | `defaultValue` (true/false), `checkboxText` |
108
- | `select` | `<select>` | Select2 | `dataSource`, `tableField` |
109
- | `date` | date picker | Flatpickr | `minDate`, `maxDate`, `dateFormat` |
110
- | `timestamp` | datetime picker | Flatpickr | `minDate`, `maxDate`, `dateFormat` |
111
- | `time` | time picker | Flatpickr | `minTime`, `maxTime` |
112
-
113
- **Notes:**
114
- - `select` requires a `dataSource` to populate options.
115
- - `date`/`timestamp`/`time` require Flatpickr assets bundled by the plugin.
116
- - `checkbox` renders as a toggle switch, not a standard checkbox input.
117
-
118
- ---
119
-
120
- ## Field Attributes
121
-
122
- Common attributes for all field types.
123
-
124
- | Attribute | Type | Notes |
125
- |---|---|---|
126
- | `name` | string | Field name matching the RDF payload field; snake_case |
127
- | `label` | string | Display label shown on form and table header |
128
- | `type` | string | One of the 8 field types above |
129
- | `required` | boolean | Frontend validation — marks field as mandatory |
130
- | `readonly` | boolean | Renders as read-only input |
131
- | `inTable` | boolean | Show column in the data table; default: `true` |
132
- | `tableOrder` | integer | Column order in table; lower = leftmost |
133
- | `tableField` | string | Column value to display in table (for `select` — show label, not id) |
134
- | `width` | integer | Column width in pixels |
135
- | `placeholder` | string | Input placeholder text |
136
- | `defaultValue` | any | Default value on new record form |
137
- | `editorMode` | string | Form only (`"form"`) or table only (`"table"`) |
138
- | `maxlength` | integer | Max character length for `text`/`textarea` |
139
- | `rows` | integer | Row height for `textarea` |
140
-
141
- ---
142
-
143
- ## Field Rows (Layout)
144
-
145
- `fieldRows[]` groups fields into a CSS Grid row for multi-column forms.
146
- Without `fieldRows`, fields render in a single column.
147
-
148
- ```json
149
- "fieldRows": [
150
- { "fields": ["first_name", "last_name"] },
151
- { "fields": ["email", "phone", "status"] }
152
- ]
153
- ```
154
-
155
- - Each entry in `fields[]` refers to a field `name` defined in `fields[]`.
156
- - Fields within a row share the available width equally by default.
157
- - Use `colSpan` on a field attribute to span multiple columns:
158
- `"colSpan": 2` makes a field take 2 of the row's grid columns.
159
- - Interacts with `features.fieldLayout`: `"horizontal"` places the label
160
- to the left of the input; `"vertical"` (default) stacks them.
161
-
162
- ---
163
-
164
- ## Field States (Conditional)
165
-
166
- `fieldStates[]` defines conditional visibility and readonly rules based on
167
- field values. Applied at runtime when field values change.
168
-
169
- ```json
170
- "fieldStates": [
171
- {
172
- "when": { "field": "status", "value": "approved" },
173
- "fields": ["approved_by", "approved_at"],
174
- "state": "readonly"
175
- },
176
- {
177
- "when": { "field": "order_type", "value": "internal" },
178
- "fields": ["customer_id"],
179
- "state": "hidden"
180
- }
181
- ]
182
- ```
183
-
184
- | Property | Notes |
185
- |---|---|
186
- | `when.field` | The triggering field name |
187
- | `when.value` | The value that triggers the state change |
188
- | `fields[]` | Fields to affect when the condition is met |
189
- | `state` | `"readonly"` or `"hidden"` |
190
-
191
- Global scope: `fieldStates` at page level applies to all form instances.
192
-
193
- ---
194
-
195
- ## Features
196
-
197
- `features` is an object at page level controlling toolbar and layout behavior.
198
-
199
- | Feature | Type | Notes |
200
- |---|---|---|
201
- | `enableSearch` | boolean | Show full-text search input in toolbar |
202
- | `enableStatusFilter` | object | Dropdown filter for boolean/enum field; requires `field` and `options[]` |
203
- | `enableDataFilter` | array | Dynamic dropdown filters; each entry requires `field` and `dataSource` |
204
- | `enableLiveSync` | boolean | Enable WebSocket live data refresh; requires `liveSync` at root level |
205
- | `fieldLayout` | string | Form field label placement: `"vertical"` (default) or `"horizontal"` |
206
-
207
- `enableStatusFilter` example:
208
- ```json
209
- "enableStatusFilter": {
210
- "field": "is_active",
211
- "options": [
212
- { "label": "Active", "value": "true" },
213
- { "label": "Inactive", "value": "false" }
214
- ]
215
- }
216
- ```
217
-
218
- `enableDataFilter` example:
219
- ```json
220
- "enableDataFilter": [
221
- { "field": "category_id", "dataSource": { "type": "api", "apiPath": "/category" } }
222
- ]
223
- ```
224
-
225
- ---
226
-
227
- ## Data Source
228
-
229
- Used by `select` fields and `enableDataFilter` to populate options.
230
-
231
- **Type: static**
232
- ```json
233
- "dataSource": {
234
- "type": "static",
235
- "options": [
236
- { "id": "active", "text": "Active" },
237
- { "id": "inactive", "text": "Inactive" }
238
- ]
239
- }
240
- ```
241
-
242
- **Type: api**
243
- ```json
244
- "dataSource": {
245
- "type": "api",
246
- "apiPath": "/category",
247
- "id": "category_id",
248
- "text": "category_name"
249
- }
250
- ```
251
-
252
- - `apiPath` calls the backend `/lookup` endpoint of that resource.
253
- - `id` maps to the option value; `text` maps to the display label.
254
- - The `tableField` attribute on the field should reference `text` to show
255
- the label (not the id) in the data table column.
256
-
257
- ---
258
-
259
- ## Navigation
260
-
261
- `navigation[]` defines the sidebar menu. Three item types: `page`, `group`,
262
- `separator`.
263
-
264
- ```json
265
- "navigation": [
266
- { "type": "page", "pageId": "customer", "icon": "ti ti-users" },
267
- { "type": "separator" },
268
- {
269
- "type": "group",
270
- "label": "Master Data",
271
- "icon": "ti ti-database",
272
- "items": [
273
- { "type": "page", "pageId": "category" },
274
- { "type": "page", "pageId": "supplier" }
275
- ]
276
- }
277
- ]
278
- ```
279
-
280
- - Maximum nesting depth: 3 levels.
281
- - When `pageGroup` is set on a page, the sidebar auto-derives navigation —
282
- manual `navigation[]` is optional.
283
- - Manual `navigation[]` overrides auto-derivation completely.
284
- - `icon` uses Tabler Icons class format: `"ti ti-<name>"`.
285
-
286
- ---
287
-
288
- ## Homepage
289
-
290
- `homepage` sets the default landing page when the app loads.
291
-
292
- ```json
293
- "homepage": "dashboard-main"
294
- ```
295
-
296
- - Value must match a `pageId` in `pages[]`.
297
- - Supports both CRUD and dashboard pages.
298
- - When absent, the first page in `navigation[]` is loaded.
299
-
300
- ---
301
-
302
- ## Dashboard Page
303
-
304
- A page with `pageType: "dashboard"` renders a grid-based analytics view.
305
-
306
- ```json
307
- {
308
- "pageId": "dash-overview",
309
- "pageType": "dashboard",
310
- "pageTitle": "Overview",
311
- "dataSources": [
312
- { "id": "ds-orders", "apiPath": "/order", "action": "aggregate" }
313
- ],
314
- "rows": [
315
- {
316
- "columns": [
317
- { "colSpan": { "xl": 4, "lg": 6, "md": 12 }, "widget": { ... } },
318
- { "colSpan": { "xl": 8, "lg": 6, "md": 12 }, "widget": { ... } }
319
- ]
320
- }
321
- ]
322
- }
323
- ```
324
-
325
- **7 widget types:**
326
-
327
- | Widget type | Display | Key properties |
328
- |---|---|---|
329
- | `column` | Vertical bar chart | `dataSource`, `labelField`, `valueField`, `color` |
330
- | `bar` | Horizontal bar chart | `dataSource`, `labelField`, `valueField`, `color` |
331
- | `pie` | Pie / donut chart | `dataSource`, `labelField`, `valueField` |
332
- | `area` | Area chart | `dataSource`, `labelField`, `valueField`, `color` |
333
- | `mini-bar` | Compact bar (KPI card) | `dataSource`, `valueField`, `label`, `icon`, `color` |
334
- | `progress` | Progress bar list | `dataSource`, `labelField`, `valueField`, `maxValue` |
335
- | `list` | Simple data list | `dataSource`, `columns[]` |
336
-
337
- `dataSources[]` at page level defines reusable data pools; widgets reference
338
- them by `id`. Each data source calls the backend `/aggregate` endpoint.
339
-
340
- ---
341
-
342
- ## Master-Detail
343
-
344
- A page with `details[]` renders a master-detail composite form.
345
-
346
- ```json
347
- {
348
- "pageId": "sales-order",
349
- "apiPath": "/order",
350
- "primaryKey": "order_id",
351
- "displayField": "order_no",
352
- "fields": [ ... ],
353
- "details": [
354
- {
355
- "pageId": "order-item",
356
- "apiPath": "/order-item",
357
- "foreignKey": "order_id",
358
- "primaryKey": "item_id",
359
- "displayField": "product_name",
360
- "fields": [ ... ]
361
- }
362
- ]
363
- }
364
- ```
365
-
366
- - Each entry in `details[]` is a sub-page bound to the master via `foreignKey`.
367
- - Multiple detail tabs are supported — one entry per detail entity.
368
- - Generates composite form: master header + detail items grid.
369
- - Backend must have composite endpoints (`/create-composite`, etc.) generated
370
- from the RDF master-detail payload.
371
-
372
- ---
373
-
374
- ## Workflow Actions
375
-
376
- `workflowActions[]` adds status-transition buttons to the form toolbar.
377
-
378
- ```json
379
- "workflow": {
380
- "statusField": "status"
381
- },
382
- "workflowActions": [
383
- {
384
- "action": "approve",
385
- "label": "Approve",
386
- "targetStatus": "approved",
387
- "fromStatus": ["pending"],
388
- "confirm": {
389
- "message": "Approve this request?",
390
- "summary": ["request_no", "requested_by", "amount"]
391
- }
392
- }
393
- ]
394
- ```
395
-
396
- | Property | Notes |
397
- |---|---|
398
- | `workflow.statusField` | Field name that holds the current status |
399
- | `action` | Action identifier sent to backend `/change-status` |
400
- | `label` | Button label |
401
- | `targetStatus` | Status value after transition |
402
- | `fromStatus[]` | Allowed current statuses for this action to appear |
403
- | `confirm.message` | Confirmation dialog message |
404
- | `confirm.summary[]` | Fields to display in the confirmation dialog |
405
-
406
- ---
407
-
408
- ## ID Generation
409
-
410
- Supports auto-generated field values via `defaultValue.source: "idgen"`.
411
- Requires `IDGEN_ENABLED=true` in backend config.
412
-
413
- ```json
414
- {
415
- "name": "order_no",
416
- "type": "text",
417
- "defaultValue": {
418
- "source": "idgen",
419
- "mode": "serial",
420
- "prefix": "ORD",
421
- "format": "XXXX-XXXX"
422
- }
423
- }
424
- ```
425
-
426
- 5 modes: `number` (sequential with prefix/format), `pin` (OTP numeric),
427
- `code` (voucher-style), `serial` (license key pattern), `random`.
428
- The field renders as readonly on the form; value is reserved on focus and
429
- confirmed on submit.
430
-
431
- ---
432
-
433
- ## Live Sync
434
-
435
- WebSocket-based live data refresh. Requires `LIVE_SYNC_ENABLED=true` and
436
- `LIVE_SYNC_PORT` configured in backend.
437
-
438
- Root-level config in UDF:
439
- ```json
440
- "liveSync": {
441
- "url": "ws://localhost:3033",
442
- "apiKey": "your-api-key"
443
- }
444
- ```
445
-
446
- Per-page activation:
447
- ```json
448
- "features": {
449
- "enableLiveSync": true
450
- }
451
- ```
452
-
453
- When active, the page's data table auto-refreshes when the backend pushes
454
- an event over the WebSocket channel.
455
-
456
- ---
457
-
458
- ## Naming Conventions
459
-
460
- | Element | Pattern | Example |
461
- |---|---|---|
462
- | `pageId` (CRUD) | `^[a-zA-Z0-9_-]+$` | `sales-order`, `product_list` |
463
- | `pageId` (dashboard) | `^dash-[a-z0-9-]+$` | `dash-overview`, `dash-monthly` |
464
- | `widgetId` | `^[a-z][a-z0-9-]*$` | `chart-revenue`, `kpi-orders` |
465
- | field `name` | snake_case | `order_no`, `customer_id` |
466
- | `appCode` | kebab-case | `sales-app`, `inventory` |
467
- | `apiPath` | kebab-case with leading `/` | `"/sales-order"`, `"/product"` |
468
- | `pageGroup` | Title Case | `"Master Data"`, `"Reports"` |
469
-
470
- ---
471
-
472
- ## Plugins
473
-
474
- Built-in plugins. Run `designer_list_plugins` to confirm what the installed
475
- version provides — do not hardcode this list.
476
-
477
- | Plugin | Auth | Notes |
478
- |---|---|---|
479
- | `vanilla-js-basic` | None | Standard CRUD app, no login flow |
480
- | `vanilla-js-auth` | Auth + RBAC | Login page, token refresh, role-based access |
481
- | `vanilla-js-custom` | Auth + RBAC | Customisable markup/CSS/JS; auth + RBAC capable (confirm via `designer_list_plugins`) |
482
-
483
- Plugin auth is built into the app at generation time. Disable it with
484
- `noAuth: true` (`--no-auth`) on `designer_init_project` to get the plugin's UI
485
- without auth. **Plugin auth (with RBAC) is distinct from the embedded `rfx_auth`
486
- extension** (`designer_auth_create`, no RBAC) — see references/auth.md.
487
-
488
- `vanilla-js-auth` accepts additional `appConfig` / init properties:
489
- - `authAppCode` — must match the backend auth module's app code
490
- - `idleTimeout` — auto-logout after inactivity (seconds)
491
-
492
- **Custom plugins:**
493
- Use `designer_scaffold_plugin` to generate a plugin template. The plugin
494
- structure contains `plugin.json` (metadata) and Jinja2 templates for each
495
- file type (HTML, JS, CSS). Use `designer_inspect_plugin` to verify capabilities
496
- before referencing a plugin in a UDF payload.
1
+ # Reference: UDF Catalog (UI Definition File)
2
+
3
+ > **Offline mirror.** This file mirrors `designer_get_udf_catalog` (the
4
+ > `npx restforge-designer catalog` output, built from the validator constants) plus
5
+ > the rules in `restforge-handbook/catalogs/udf/`. The live tool is
6
+ > authoritative — when this file and the tool disagree, trust the tool, then
7
+ > update this file. Field types and features depend on the active plugin
8
+ > version; always re-ground with the tool (and `designer_list_plugins`) before
9
+ > defining a UDF payload.
10
+
11
+ Source: `designer_get_udf_catalog` — installed designer version.
12
+ Use as grounding before defining or editing any UDF payload.
13
+
14
+ **Derive, do not hand-write.** When a backend RDF exists, create the UDF with
15
+ `codegen_migrate_payload` and edit its output. Migrate derives field types,
16
+ `required`, `maxlength`, `decimalPlaces`, `format`, `temporalType`, lookups,
17
+ `details[]`, `statusFilter`, `statusBadge`, and `appConfig.dateFormat` /
18
+ `dateTimeFormat` from the RDF and the backend config. Hand-author a page only
19
+ when no RDF exists.
20
+
21
+ ---
22
+
23
+ ## Table of Contents
24
+
25
+ 1. [Payload Envelope](#payload-envelope)
26
+ 2. [App Config](#app-config)
27
+ 3. [Page Anatomy](#page-anatomy)
28
+ 4. [Field Types](#field-types)
29
+ 5. [Field Attributes](#field-attributes)
30
+ 6. [Field Rows (Layout)](#field-rows-layout)
31
+ 7. [Field States (Row Lock)](#field-states-row-lock)
32
+ 8. [Features](#features)
33
+ 9. [Data Source](#data-source)
34
+ 10. [Navigation](#navigation)
35
+ 11. [Homepage](#homepage)
36
+ 12. [Dashboard Page](#dashboard-page)
37
+ 13. [Master-Detail](#master-detail)
38
+ 14. [Workflow Actions](#workflow-actions)
39
+ 15. [ID Generation](#id-generation)
40
+ 16. [Live Sync](#live-sync)
41
+ 17. [Naming Conventions](#naming-conventions)
42
+ 18. [Plugins](#plugins)
43
+
44
+ ---
45
+
46
+ ## Payload Envelope
47
+
48
+ Root-level UDF structure:
49
+
50
+ ```json
51
+ {
52
+ "extends": "app-config.json",
53
+ "appConfig": { ... },
54
+ "pages": [ ... ],
55
+ "navigation": { "items": [ ... ] },
56
+ "homepage": "contact",
57
+ "liveSync": { ... }
58
+ }
59
+ ```
60
+
61
+ | Key | Required | Notes |
62
+ |---|---|---|
63
+ | `extends` | no | Path (relative to this file) to a base UDF. `appConfig` comes entirely from the base; `pages` from this file; other root keys (e.g. `auth`, `navigation`) merge with this file winning |
64
+ | `appConfig` | yes | See App Config |
65
+ | `pages` | yes | At least one entry: an inline page object or `{ "include": "pages/contact.json" }` |
66
+ | `navigation` | no | Object with `items[]` — see Navigation |
67
+ | `homepage` | no | `pageId` loaded at the app root |
68
+ | `liveSync` | no | WebSocket config — see Live Sync |
69
+ | `auth` | no | Read by auth-capable plugins — see Plugins |
70
+
71
+ An `include` file must itself contain a `pages` array (not a bare page object);
72
+ the loader appends its pages in place of the entry, recursively.
73
+
74
+ **Migrate output layout.** `codegen_migrate_payload` writes a split set into the
75
+ output folder (default `frontend/payload/`): `app-config.json`, one file per
76
+ page under `pages/`, an aggregator `<appCode>.json` that `extends` the config
77
+ and `include`s the pages, and snapshots under `.meta/pages/`. Point every
78
+ designer tool at the aggregator.
79
+
80
+ ---
81
+
82
+ ## App Config
83
+
84
+ | Property | Type | Required | Notes |
85
+ |---|---|---|---|
86
+ | `appName` | string | yes | Display name shown in page title and header |
87
+ | `appCode` | string | yes | Unique app identifier, kebab-case |
88
+ | `plugin` | string | yes | Output plugin: `vanilla-js-basic`, `vanilla-js-auth`, `vanilla-js-custom`, or a custom plugin |
89
+ | `apiBaseUrl` | string | yes | Backend base URL, joined with each page `apiPath`. A loopback host (`localhost`, `127.x`, `[::1]`, `0.0.0.0`) is re-resolved in the browser from the page's own host and port; use a relative path such as `/api/myapp` behind a reverse proxy on a non-standard port |
90
+ | `port` | integer | no | Port of the static preview server (`app-start.bat`), 1–65535; a string is rejected |
91
+ | `numberFormat` | object | no | `locale`: `en-US` (default, `12,500,000.00`) or `id-ID` (`12.500.000,00`); no other value is accepted. `currencyPrefix`: text before `format: "currency"` values, e.g. `"Rp "` |
92
+ | `dateFormat` | string | no | Must equal backend `DATEFORMAT`. One of `yyyy-MM-dd` (default), `dd/MM/yyyy`, `dd-MM-yyyy`, `MM/dd/yyyy`, `yyyy/MM/dd` |
93
+ | `dateTimeFormat` | string | no | Must equal backend `DATETIMEFORMAT`: `<date pattern> <time pattern>`, time `HH:mm`, `HH:mm:ss`, or `HH:mm:ss.SSS`. Default `yyyy-MM-dd HH:mm:ss.SSS` |
94
+
95
+ - Decimal places are **not** an app setting: `numberFormat.decimalPlaces` is
96
+ rejected. Put `decimalPlaces` on the field.
97
+ - `dateFormat` / `dateTimeFormat` are copied from the backend config by every
98
+ `codegen_migrate_payload` run and always overwritten there. Never keep a
99
+ locally edited value: the frontend cannot detect a pattern that differs from
100
+ the backend and would misread dates silently.
101
+
102
+ ---
103
+
104
+ ## Page Anatomy
105
+
106
+ Each entry in `pages[]` is one page. CRUD page properties:
107
+
108
+ | Property | Type | Required | Notes |
109
+ |---|---|---|---|
110
+ | `pageId` | string | yes | `^[a-zA-Z0-9_-]+$`; used as the output file name |
111
+ | `pageTitle` | string | yes | Page heading |
112
+ | `pageSubtitle` | string | no | Text under the title |
113
+ | `pageIcon` | string | no | Icon name |
114
+ | `pageGroup` | array of string | no | Sidebar group path, max 2 levels (e.g. `["Master", "People"]`); drives auto-derived navigation |
115
+ | `pageType` | string | no | `"crud"` (default) or `"dashboard"` |
116
+ | `pageSubject` | string | no | Singular object name used in `{addVerb} {pageSubject}`, `Edit {pageSubject}`, and success messages. Default `pageTitle` |
117
+ | `addVerb` | string | no | `Add` (default), `Create`, `Upload`, or `Invite`; anything else is rejected |
118
+ | `showNewBadge` | boolean | no | `New` badge on rows created today, default `true`; needs `created_at` in the list data |
119
+ | `apiPath` | string | yes | Backend resource path, joined with `apiBaseUrl` |
120
+ | `primaryKey` | string | yes | Primary key column; need not be in `fields` |
121
+ | `displayField` | string | yes | Field used as the record label (dialogs, breadcrumb) |
122
+ | `versionField` | string | no | Row-version column sent as `options.expectedVersion` on edit; the backend RDF needs a `concurrency` block for it to be checked |
123
+ | `actions` | object | no | `{ "create", "read", "update", "delete" }` booleans, default all `true`. Hides basic CRUD controls when there is no `auth` block; with `auth`, permissions decide and `false` only produces a warning |
124
+ | `fields` | array | yes | Field definitions (min 1) |
125
+ | `fieldRows` | array | no | Form grid layout |
126
+ | `fieldStates` | array | no | Status values that lock a row |
127
+ | `features` | object | no | Toolbar and form features |
128
+ | `details` | array | no | Master-detail grids |
129
+ | `workflow` | object | no | `statusField` and `transitions` |
130
+ | `workflowActions` | array | no | Status transition buttons |
131
+
132
+ ---
133
+
134
+ ## Field Types
135
+
136
+ 8 field types:
137
+
138
+ | Type | Renders as | Type-specific attributes |
139
+ |---|---|---|
140
+ | `text` | `<input type="text">` | `maxlength`, `placeholder` |
141
+ | `textarea` | `<textarea>` | `rows`, `maxlength`, `placeholder` |
142
+ | `number` | number input | `min`, `max`, `step`, `format`, `decimalPlaces` |
143
+ | `checkbox` | toggle switch | `defaultValue`, `checkboxText.checked`, `checkboxText.unchecked`, `statusBadge` |
144
+ | `select` | Select2 dropdown | `dataSource` (required), `tableField`, `dependsOn`, `statusBadge` (static only) |
145
+ | `date` | Flatpickr | `dateFormat` |
146
+ | `timestamp` | Flatpickr with time | `dateFormat`, `temporalType` |
147
+ | `time` | Flatpickr time only | — |
148
+
149
+ - `date` is a calendar date; it never shifts with the reader's time zone.
150
+ - `timestamp` with `temporalType: "timestamp"` (default) is wall-clock time shown
151
+ as stored. `temporalType: "timestamptz"` is an absolute moment converted to
152
+ the reader's time zone and sent as ISO UTC; migrate sets it for `timestamptz`
153
+ columns.
154
+ - A `select` without `dataSource` renders empty (warning).
155
+
156
+ ---
157
+
158
+ ## Field Attributes
159
+
160
+ Required on every field: `name`, `label`, `type`.
161
+
162
+ | Attribute | Type | Default | Notes |
163
+ |---|---|---|---|
164
+ | `name` | string | — | Column name, snake_case |
165
+ | `label` | string | — | Form label and table header |
166
+ | `type` | string | — | One of the 8 types |
167
+ | `required` | boolean | `false` | Client-side required check |
168
+ | `readonly` | boolean | `false` | Read-only in every mode; not validated, not sent on save |
169
+ | `readonlyOnEdit` | boolean | `false` | Read-only in edit mode only (e.g. a code set once on create). Cannot be combined with `readonly` or `editorMode` |
170
+ | `editorMode` | string | — | `"hidden"`: rendered but invisible, filled from `defaultValue` on add and from the record on edit, then sent like a normal field. `"readonly"`: same as `readonly: true` |
171
+ | `inTable` | boolean | `false` | Show as a list column |
172
+ | `tableOrder` | integer | — | Column order (from 1); a warning when `inTable` is set without it |
173
+ | `tableField` | string | — | Column to display in the list instead of `name`, e.g. `city_name` for `city_id` |
174
+ | `width` | string | auto | CSS width of the list column, e.g. `"200px"`, `"30%"` |
175
+ | `placeholder` | string | — | Input placeholder |
176
+ | `maxlength` | integer | — | `text` / `textarea` |
177
+ | `defaultValue` | any | — | Value on add: a literal, `today`/`now`, or an idgen object |
178
+ | `format` | string | — | `number` only: `"number"` (thousand separators) or `"currency"` (prefix from `numberFormat.currencyPrefix`). Without `format`, the value is shown raw |
179
+ | `decimalPlaces` | integer | `0` | `number` only, 0–20; migrate derives it from the column scale |
180
+ | `dateFormat` | string | `appConfig.dateFormat` | `date` / `timestamp`: display pattern of the date part for this field only; parsing always uses the app patterns |
181
+ | `temporalType` | string | `timestamp` | `timestamp` fields only: `timestamp` or `timestamptz` |
182
+ | `statusBadge` | boolean | `false` | Show as the fixed-palette Status column. `checkbox` or static `select` only; max one per page. A `checkbox` with `statusBadge` also adds `Deactivate` / `Activate` to the row Actions menu |
183
+ | `checkboxText` | object | — | `{ "checked": "Active", "unchecked": "Inactive" }` |
184
+
185
+ `valueFormat` is no longer supported and is rejected. `min`/`max` on `date`
186
+ fields are not read by the validator.
187
+
188
+ ---
189
+
190
+ ## Field Rows (Layout)
191
+
192
+ `fieldRows[]` places several fields side by side in one CSS grid row. Without
193
+ it, fields render one per row.
194
+
195
+ ```json
196
+ "fieldRows": [
197
+ { "fields": ["first_name", "last_name"] },
198
+ { "fields": ["email", "phone", "status"] }
199
+ ]
200
+ ```
201
+
202
+ - Each name refers to a `name` in `fields[]`; the column count equals the number
203
+ of fields in the row.
204
+ - A field in `fields` that is not referenced in `fieldRows` is not rendered
205
+ (warning), except `editorMode: "hidden"` fields.
206
+ - `features.fieldLayout` `"horizontal"` puts the label left of the input;
207
+ `"vertical"` (default) stacks them.
208
+
209
+ ---
210
+
211
+ ## Field States (Row Lock)
212
+
213
+ `fieldStates[]` marks status values that make a row read-only: `Edit`,
214
+ `Delete`, and `Deactivate`/`Activate` are shown disabled with an info tooltip.
215
+ It does not hide or lock individual form fields.
216
+
217
+ ```json
218
+ "fieldStates": [
219
+ { "when": { "status": ["posted", "cancelled"] }, "state": "readonly" }
220
+ ]
221
+ ```
222
+
223
+ | Property | Notes |
224
+ |---|---|
225
+ | `when` | Key = the status field name (`workflow.statusField`, default `status`); value = array of status values |
226
+ | `state` | `"readonly"` |
227
+
228
+ Takes effect only with `vanilla-js-auth` or `vanilla-js-custom`, and only on a
229
+ page with at least one `workflowActions` item. The validator also accepts
230
+ `state: "hidden"`, a `fields` list, and a string `when`, but the generator
231
+ ignores them. `fieldStates` is frontend only: enforce the same rule in the
232
+ backend with a component hook (e.g. `onBeforeUpdate`, `onBeforeDelete`).
233
+
234
+ ---
235
+
236
+ ## Features
237
+
238
+ `features` is an object on a CRUD page.
239
+
240
+ | Feature | Type | Notes |
241
+ |---|---|---|
242
+ | `enableSearch` | boolean | Search box; server-side search |
243
+ | `enableStatusFilter` | boolean | Status dropdown in the toolbar; configured by `statusFilter` |
244
+ | `statusFilter` | object | `{ field, label, options: [{ value, text }] }`; required when `enableStatusFilter` is `true` |
245
+ | `enableDataFilter` | boolean | Extra filters; configured by `dataFilters` |
246
+ | `dataFilters` | array | Entries with keys `name`, `field`, `label`, `dataSource`, `dependsOn`, `display` (`inline` or `dropdown`), `width`. `field` is required for `vanilla-js-auth` and `vanilla-js-custom` |
247
+ | `enableLiveSync` | boolean | WebSocket refresh; needs the root `liveSync` block |
248
+ | `autoRefresh` | object | `{ "interval": <seconds, min 10> }` polling; not together with `enableLiveSync`; not in `vanilla-js-basic` |
249
+ | `fieldLayout` | string | `"vertical"` (default) or `"horizontal"` |
250
+
251
+ ```json
252
+ "features": {
253
+ "enableSearch": true,
254
+ "enableStatusFilter": true,
255
+ "statusFilter": {
256
+ "field": "is_active",
257
+ "label": "Status",
258
+ "options": [ { "value": "true", "text": "Active" }, { "value": "false", "text": "Inactive" } ]
259
+ },
260
+ "enableDataFilter": true,
261
+ "dataFilters": [
262
+ { "name": "city_id", "field": "city_id", "label": "City",
263
+ "dataSource": { "type": "api", "resource": "city", "select": ["city_id", "city_name"] } }
264
+ ]
265
+ }
266
+ ```
267
+
268
+ At most 2 filters (status + data filters) are shown inline; the rest move to a
269
+ "More Filters" popup unless `display` overrides it. Migrate generates
270
+ `statusFilter` for the main status column.
271
+
272
+ ---
273
+
274
+ ## Data Source
275
+
276
+ Used by `select` fields and `dataFilters[]`.
277
+
278
+ **Type: static**
279
+ ```json
280
+ "dataSource": {
281
+ "type": "static",
282
+ "options": [
283
+ { "value": "active", "text": "Active" },
284
+ { "value": "inactive", "text": "Inactive" }
285
+ ]
286
+ }
287
+ ```
288
+
289
+ With `required: true` the first option is preselected; otherwise a
290
+ `-- Select --` placeholder comes first.
291
+
292
+ **Type: api**
293
+ ```json
294
+ "dataSource": {
295
+ "type": "api",
296
+ "resource": "category",
297
+ "select": ["category_id", "category_name"]
298
+ }
299
+ ```
300
+
301
+ | Key | Notes |
302
+ |---|---|
303
+ | `resource` | Backend resource; the request goes to `POST <apiBaseUrl>/<resource>/lookup`, which answers `{ id, text }` rows (configured by RDF `fieldNameLookup`) |
304
+ | `select` | Columns requested from `/lookup` |
305
+ | `url` | Explicit lookup path; used only in master-detail grids, ignored on master pages |
306
+ | `lookupMode` | `"static"` (load once, default) or `"dynamic"` (AJAX search while typing) |
307
+ | `lookupDisplay` | `"select2"` (default) or `"modal"` (search dialog with a table) |
308
+ | `optionColumns` | `[{ column, format: "number", decimalPlaces, align }]`; columns shown in the dropdown or modal, each must be in `select` |
309
+ | `autofill` | Copies columns of the chosen row into other fields of the same form or grid row |
310
+
311
+ `dependsOn` (a sibling `select` field name) turns a field into a cascade. Set
312
+ `tableField` on the field so the list shows the label column, not the id.
313
+
314
+ ---
315
+
316
+ ## Navigation
317
+
318
+ `navigation` is an **object** with `items[]`. Without it, the sidebar is
319
+ derived from `pageGroup`; with it, `pageGroup` is ignored (warning).
320
+
321
+ ```json
322
+ "navigation": {
323
+ "items": [
324
+ { "type": "page", "pageRef": "dashboard", "icon": "home" },
325
+ { "type": "separator" },
326
+ {
327
+ "type": "group",
328
+ "label": "Master Data",
329
+ "icon": "data",
330
+ "children": [
331
+ { "type": "page", "pageRef": "category" },
332
+ { "type": "page", "pageRef": "supplier", "badgeColor": "info" }
333
+ ]
334
+ },
335
+ { "type": "link", "label": "Payroll Report", "href": "report-payroll.html", "permission": "REPORT_PAYROLL_READ" }
336
+ ]
337
+ }
338
+ ```
339
+
340
+ | Item type | Keys |
341
+ |---|---|
342
+ | `page` | `pageRef` (required, a `pageId`), `label` (default `pageTitle`), `icon`, `badgeColor` |
343
+ | `group` | `label` (required), `children` (required, non-empty), `icon` |
344
+ | `link` | `label` and `href` (required; a hand-written HTML file), `permission` (string or array; used only with an `auth` block), `icon`, `badgeColor` |
345
+ | `separator` | — |
346
+
347
+ - `badgeColor`: `danger`, `info`, `primary`, `success`, `warning`.
348
+ - Maximum nesting depth: 3. `icon` renders at depth 1 only.
349
+ - Icons are Keenicons (`vanilla-js-auth`): one word (`"data"`) renders
350
+ `ki-outline ki-data fs-2`; a value with spaces is used as the class string.
351
+ Do not use Tabler classes (`ti ti-*`).
352
+ - With an `auth` block, a `page` item is hidden when the user lacks
353
+ `permissions.<pageRef>.read`; groups whose items are all hidden disappear.
354
+ - Re-running migrate keeps a hand-built `navigation` and adds new pages at the
355
+ top level.
356
+
357
+ ---
358
+
359
+ ## Homepage
360
+
361
+ `homepage` sets the page loaded at the app root.
362
+
363
+ ```json
364
+ "homepage": "dashboard-main"
365
+ ```
366
+
367
+ - Value must match a `pageId` in `pages[]`; CRUD and dashboard pages both work.
368
+ - When absent, the first page in `pages[]` is the default.
369
+
370
+ ---
371
+
372
+ ## Dashboard Page
373
+
374
+ A page with `pageType: "dashboard"` renders a widget grid. It reads data from
375
+ URLs you declare; it does not call `/aggregate` by itself.
376
+
377
+ ```json
378
+ {
379
+ "pageId": "overview",
380
+ "pageType": "dashboard",
381
+ "pageTitle": "Overview",
382
+ "refreshInterval": 300,
383
+ "dataSources": {
384
+ "revenueMonthly": { "url": "/api/myapp/dash-sales/dashboard", "method": "POST", "body": {} }
385
+ },
386
+ "rows": [
387
+ {
388
+ "columns": [
389
+ {
390
+ "colSpan": { "base": 12, "md": 6, "lg": 4 },
391
+ "widgets": [
392
+ {
393
+ "widgetId": "revenue-chart",
394
+ "widgetType": "column",
395
+ "title": "Revenue per Month",
396
+ "dataSource": "revenueMonthly",
397
+ "chartEngine": "apexcharts",
398
+ "chart": { "xField": "month", "yField": "revenue" }
399
+ }
400
+ ]
401
+ }
402
+ ]
403
+ }
404
+ ]
405
+ }
406
+ ```
407
+
408
+ | Part | Rules |
409
+ |---|---|
410
+ | `pageId` | `^[a-z][a-z0-9-]*$` (no underscore, no uppercase); no `dash-` prefix needed |
411
+ | Allowed page keys | `dataSources`, `pageGroup`, `pageIcon`, `pageId`, `pageSubject`, `pageSubtitle`, `pageTitle`, `pageType`, `refreshInterval`, `rows` |
412
+ | Forbidden keys (error) | `apiPath`, `primaryKey`, `displayField`, `fields`, `fieldRows`, `details`, `workflowActions`, `workflow`, `fieldStates`, `features` |
413
+ | `dataSources` | Required object `name → { url, method, body }`; `method` is `GET` (default) or `POST`, `body` is sent with `POST` only |
414
+ | `refreshInterval` | Seconds, 15–3600, default 300 |
415
+ | `colSpan` | Per breakpoint `base`, `sm`, `md`, `lg`, `xl`, `xxl`, values 1–12 |
416
+ | `widgets[]` | `widgetId` (`^[a-z][a-z0-9_-]*$`, unique per page), `widgetType`, `title`, `subtitle`, `dataSource` (a `dataSources` key), `chartEngine`, `chart`, `display` |
417
+
418
+ 7 widget types:
419
+
420
+ | `widgetType` | Needs `chart` + `chartEngine` | `chart` keys |
421
+ |---|---|---|
422
+ | `column`, `bar`, `area` | yes | `xField`, `yField`, `format` |
423
+ | `pie` | yes | `xField`, `yField`, `shape` (`pie`, `donut`, `semi-pie`, `semi-donut`), `format` |
424
+ | `mini-bar` | yes | `xField`, `yField`, `format` |
425
+ | `progress`, `list` | no — `chart`/`chartEngine` are rejected | — |
426
+
427
+ `chartEngine`: `apexcharts` or `amcharts`. `chart.format.<xAxis|yAxis|tooltip|dataLabel>`
428
+ accepts `prefix`, `suffix`, `decimalPlaces`. `chart.library` and
429
+ `chart.orientation` are deprecated.
430
+
431
+ ---
432
+
433
+ ## Master-Detail
434
+
435
+ A CRUD page with `details[]` renders the master form plus one editable grid per
436
+ detail. Migrate builds `details[]` from the RDF `masterDetail` block.
437
+
438
+ ```json
439
+ "details": [
440
+ {
441
+ "detailId": "items",
442
+ "detailTitle": "Order Items",
443
+ "primaryKey": "item_id",
444
+ "fields": [
445
+ { "name": "product_id", "label": "Product", "type": "select",
446
+ "dataSource": { "type": "api", "resource": "product", "select": ["product_id", "product_name", "price"],
447
+ "autofill": { "price": "price" } } },
448
+ { "name": "qty", "label": "Qty", "type": "number", "defaultValue": 1 },
449
+ { "name": "price", "label": "Price", "type": "number", "format": "number", "decimalPlaces": 2 },
450
+ { "name": "subtotal", "label": "Subtotal", "type": "number", "readonly": true,
451
+ "calculated": { "formula": "qty * price" }, "format": "number", "decimalPlaces": 2 }
452
+ ],
453
+ "summary": { "totalItems": true, "totalQtyField": "qty", "grandTotalField": "subtotal" }
454
+ }
455
+ ]
456
+ ```
457
+
458
+ | Property | Required | Notes |
459
+ |---|---|---|
460
+ | `detailId` | yes | Detail key in the composite payload |
461
+ | `detailTitle` | no | Grid heading; default = `detailId` in Title Case |
462
+ | `primaryKey` | yes | Detail row primary key |
463
+ | `fields` | yes | Same field structure as the master |
464
+ | `summary` | no | `totalItems`, `totalQtyField`, `grandTotalField` |
465
+
466
+ - `calculated.formula` supports only `"<fieldA> * <fieldB>"`.
467
+ - Only one detail level; no nested details.
468
+ - The backend RDF needs `masterDetail` and the composite actions.
469
+ - To keep a user-defined row order, store it in a `line_number` column the user
470
+ fills in; the server does not number rows.
471
+
472
+ ---
473
+
474
+ ## Workflow Actions
475
+
476
+ `workflowActions[]` adds a **Change Status** item to each row's Actions menu.
477
+ The dialog shows one button per allowed target status.
478
+
479
+ ```json
480
+ "workflow": {
481
+ "statusField": "status",
482
+ "transitions": {
483
+ "pending": ["paid", "cancelled"],
484
+ "paid": ["shipped"],
485
+ "shipped": [],
486
+ "cancelled": []
487
+ }
488
+ },
489
+ "workflowActions": [
490
+ {
491
+ "actionId": "paid",
492
+ "label": "Paid",
493
+ "icon": "check",
494
+ "style": "success",
495
+ "confirm": { "title": "Mark this order as paid?", "message": "Stock will be deducted.", "confirmButton": "Yes, mark as paid", "cancelButton": "Cancel" },
496
+ "api": { "endpoint": "sales-order/change-status", "payload": { "sales_order_id": "$primaryKey", "status": "paid" } },
497
+ "onSuccess": { "notification": { "type": "success", "message": "Order marked as paid." } },
498
+ "onError": { "display": "modal", "title": "Payment rejected" }
499
+ }
500
+ ]
501
+ ```
502
+
503
+ | Property | Notes |
504
+ |---|---|
505
+ | `workflow.statusField` | Required when `workflowActions` exists; the column must be returned by `/datatables` |
506
+ | `workflow.transitions` | `current → [targets]`. A button renders only for a target listed here **and** having an action with the same `actionId`. Without `transitions` no button ever shows. Keep it identical to the RDF `workflow.transitions` |
507
+ | `actionId` | Required, unique per page, **equal to the target status** |
508
+ | `label` | Required |
509
+ | `icon`, `style` | Optional; `style` = `primary` (default), `success`, `danger`, `secondary`, `info`, `warning` |
510
+ | `confirm` | Optional: `title`, `message`, `confirmButton`, `cancelButton` |
511
+ | `api` | Required: `endpoint` (default `<apiPath>/change-status`, no leading slash) and/or `payload` (`"$primaryKey"` is replaced) |
512
+ | `onSuccess.notification` | `{ type, message }` |
513
+ | `onError` | `{ display: "modal" \| "toast", title }` |
514
+
515
+ `confirm.summary[]` is accepted by the validator but not rendered. A backend
516
+ rejection (422 invalid transition, 400 hook rejection, 502 blocking hook failure)
517
+ is shown with the response `message`.
518
+
519
+ ---
520
+
521
+ ## ID Generation
522
+
523
+ `defaultValue.source: "idgen"` fills a field from the backend ID generator.
524
+ Requires `IDGEN_ENABLED=true` in the backend config.
525
+
526
+ ```json
527
+ {
528
+ "name": "invoice_no",
529
+ "type": "text",
530
+ "defaultValue": {
531
+ "source": "idgen",
532
+ "mode": "number",
533
+ "resource": "invoice",
534
+ "format": "yyyymm",
535
+ "numDigits": 5,
536
+ "separator": "-",
537
+ "reserve": true,
538
+ "ttl": 60
539
+ }
540
+ }
541
+ ```
542
+
543
+ | Key | Notes |
544
+ |---|---|
545
+ | `mode` | `number`, `pin`, `code`, `serial`, `random` |
546
+ | `resource` | Required namespace; must not contain `:` |
547
+ | `format` | `number` mode: `text`, `yyyy`, `yyyymm`, `yyyymmdd` |
548
+ | `numDigits`, `separator` | `number` mode |
549
+ | `digits` | `pin` mode (default 6) |
550
+ | `pattern` | `code`, `serial`, `random` modes |
551
+ | `reserve`, `ttl` | Reserve the value while the form is open; max one `reserve: true` field per page |
552
+
553
+ Allowed field types: `number`, `text`, `textarea`.
554
+
555
+ ---
556
+
557
+ ## Live Sync
558
+
559
+ WebSocket-based list refresh. Requires `LIVE_SYNC_ENABLED=true` and
560
+ `LIVE_SYNC_PORT` in the backend config.
561
+
562
+ Root-level config:
563
+ ```json
564
+ "liveSync": {
565
+ "url": "ws://localhost:3033",
566
+ "apiKey": "your-api-key"
567
+ }
568
+ ```
569
+
570
+ Per-page activation: `"features": { "enableLiveSync": true }`. `url` must start
571
+ with `ws://` or `wss://`. A page with `enableLiveSync` and no root `liveSync` is
572
+ an error; a `liveSync` block no page uses is a warning.
573
+
574
+ ---
575
+
576
+ ## Naming Conventions
577
+
578
+ | Element | Pattern | Example |
579
+ |---|---|---|
580
+ | `pageId` (CRUD) | `^[a-zA-Z0-9_-]+$` | `sales-order`, `product_list` |
581
+ | `pageId` (dashboard) | `^[a-z][a-z0-9-]*$` | `overview`, `monthly-sales` |
582
+ | `widgetId` | `^[a-z][a-z0-9_-]*$` | `chart-revenue`, `kpi_orders` |
583
+ | field `name` | snake_case | `order_no`, `customer_id` |
584
+ | `appCode` | kebab-case | `sales-app`, `inventory` |
585
+ | `pageGroup` entries | consistent casing | `["Master Data"]` |
586
+
587
+ ---
588
+
589
+ ## Plugins
590
+
591
+ Built-in plugins. Run `designer_list_plugins` to confirm what the installed
592
+ version provides — do not hardcode this list.
593
+
594
+ | Plugin | Auth | Notes |
595
+ |---|---|---|
596
+ | `vanilla-js-basic` | None | Standard CRUD app, no login flow |
597
+ | `vanilla-js-auth` | Auth + RBAC | Login page, token refresh, role-based access |
598
+ | `vanilla-js-custom` | Auth + RBAC | Customisable markup/CSS/JS; auth + RBAC capable (confirm via `designer_list_plugins`) |
599
+
600
+ Plugin auth is built into the app at generation time. Disable it with
601
+ `noAuth: true` (`--no-auth`) on `designer_init_project` to get the plugin's UI
602
+ without auth. **Plugin auth (with RBAC) is distinct from the embedded `rfx_auth`
603
+ extension** (`designer_auth_create`, no RBAC) — see references/auth.md.
604
+
605
+ Auth-capable plugins read a root-level `auth` block, not `appConfig` keys:
606
+
607
+ ```json
608
+ "auth": {
609
+ "appCode": "MY_APP",
610
+ "authApiUrl": "http://localhost:3000/api/auth",
611
+ "idleTimeoutMinutes": 30
612
+ }
613
+ ```
614
+
615
+ `designer_init_project` fills this block from its auth app code and idle
616
+ timeout options. It also leaves an `appConfig.authAppCode` copy, which no plugin
617
+ reads; edit `auth.appCode`, never `appConfig.authAppCode`.
618
+
619
+ **Custom plugins:**
620
+ Use `designer_scaffold_plugin` to generate a plugin template. The plugin
621
+ structure contains `plugin.json` (metadata) and Jinja2 templates for each
622
+ file type (HTML, JS, CSS). Use `designer_inspect_plugin` to verify capabilities
623
+ before referencing a plugin in a UDF payload.