@xeplr/ui-factory 1.0.1 → 1.0.3
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/AUTHORING.md +101 -12
- package/README.md +73 -22
- package/bin/xeplr-factory.js +72 -1
- package/package.json +6 -2
- package/src/controls.js +142 -45
- package/src/designs/BuilderSample.jsx +80 -36
- package/src/designs/ControlView.jsx +43 -36
- package/src/designs/ListView.jsx +95 -0
- package/src/designs/PropertyPanel.jsx +187 -13
- package/src/designs/ScreenModal.jsx +42 -0
- package/src/designs/ScreenSample.jsx +49 -9
- package/src/designs/factory.css +72 -20
- package/src/designs/index.js +2 -0
- package/src/designs/styles.js +59 -0
- package/src/document.js +31 -4
- package/src/generate.js +136 -25
- package/src/index.js +2 -2
- package/src/model.js +12 -5
- package/src/pages.jsx +62 -10
- package/src/remote.js +66 -0
- package/src/scaffold.js +116 -0
- package/src/tableSchema.js +329 -0
- package/src/useFactoryBuilder.js +102 -17
- package/src/useFactoryScreen.js +290 -78
- package/src/validateDocument.js +73 -5
- package/src/values.js +61 -0
package/AUTHORING.md
CHANGED
|
@@ -4,26 +4,75 @@ This guide is for whoever **writes** a screen definition — usually Claude, fro
|
|
|
4
4
|
|
|
5
5
|
## The workflow
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
A request for "a form for employees" is an **entity**, and an entity is **two screens**: a list, and an add / edit form the list opens in a popup.
|
|
8
|
+
|
|
9
|
+
1. **Write an entity spec** — the entity's name and its fields, in reading order. Do not write coordinates.
|
|
10
|
+
2. **Generate** both screens and their pages:
|
|
9
11
|
```sh
|
|
10
|
-
npx xeplr-factory
|
|
12
|
+
npx xeplr-factory screens employee.entity.json -o src/screens/employee
|
|
11
13
|
```
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
This writes `employee-list.screen.json`, `employee-edit.screen.json`, `EmployeeList.jsx`, `EditEmployee.jsx` and an empty `employee.hooks.js`. Wire the two pages into the app's menu next to two `<FactoryBuilder>` pages for designing them (Screen · List, Screen · Edit, Designer · List, Designer · Edit). It refuses to overwrite existing files — they may have been refined in the designer — unless given `--force`.
|
|
15
|
+
|
|
16
|
+
A single screen that is not an entity (a settings form, say) uses `npx xeplr-factory generate spec.json -o screen.json` with the screen spec below.
|
|
17
|
+
3. **The table comes from Publish.** Records live in a **real table** — `employees`, one column per field — never in JSON, and there are no migration files to write: **publishing** the edit screen creates its table (`CREATE TABLE`, a column per field, real foreign keys for table dropdowns, the standard `id` / `isActive` / `mtId1–4` / audit columns), and publishing a later version changes it:
|
|
18
|
+
- a new field → `ADD COLUMN`; a wider text or number → `ALTER COLUMN … TYPE`
|
|
19
|
+
- narrowing, or a different kind of value → **refused**; tell the person why
|
|
20
|
+
- a **removed field drops its column and all its data** — Publish asks the person to confirm first, showing how many values would go. Never confirm a drop on their behalf.
|
|
21
|
+
- a column not created by a screen, or still used by another company's screen, is never dropped
|
|
22
|
+
|
|
23
|
+
Publish a table that others point at first: `departments` before `employees`. To preview the SQL without applying it: `npx xeplr-factory migration employee-edit.screen.json`.
|
|
24
|
+
|
|
25
|
+
**Never rename a field that is already a column** (the designer locks those names) — it would drop the old column and its data and start an empty one.
|
|
26
|
+
4. **Validate** — `screens` and `generate` already refuse a bad spec, but check any document you edited by hand:
|
|
14
27
|
```sh
|
|
15
28
|
npx xeplr-factory validate screen.json
|
|
16
29
|
```
|
|
17
30
|
Every problem names a path (`nodes[3].props.data.table`) and what belongs there. Fix and re-run until it prints `ok`.
|
|
18
|
-
|
|
31
|
+
5. **Save and publish.** Save the screens as drafts, then publish — edit screens of referenced tables first. Publishing changes the table; if it would drop columns it returns them for the person to confirm.
|
|
32
|
+
6. **Hooks, if the request needs them.** Anything beyond storing what the form shows — a value worked out on save, a check against other records, an email afterwards, a list narrowed to the user — goes in `employee.hooks.js`, never in the screen: `save`, `get` (list and one record) and `delete`, each with `before`, `after`, `error` and `override`. Register it with `factory.init({ hooks: { employee_edit: require('./employee.hooks') } })`. `override` replaces the whole operation — rules and the other hooks included — so use it only when the generic save / get / delete does not apply at all. The `screens` command never overwrites an existing hooks file, even with `--force`. Details: the [`@xeplr/factory` README](https://www.npmjs.com/package/@xeplr/factory#hooks).
|
|
33
|
+
7. **Hand over.** The person opens the screens in the designer to move, resize and restyle controls.
|
|
34
|
+
|
|
35
|
+
There is **no submit button** to add: a screen saves itself as it is filled in. If the request mentions seeing or editing what was entered ("…and show the employees below"), add a `list`.
|
|
19
36
|
|
|
20
37
|
Changing an existing screen: edit the document's `props` directly (labels, validation, options), validate, and hand it back. Regenerate from a spec only when the layout should start over.
|
|
21
38
|
|
|
22
|
-
## The spec
|
|
39
|
+
## The entity spec
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"entity": "employee",
|
|
44
|
+
"fields": [
|
|
45
|
+
{ "label": "First name", "required": true },
|
|
46
|
+
{ "label": "Last name", "required": true },
|
|
47
|
+
{ "label": "Work email", "required": true,
|
|
48
|
+
"validation": { "pattern": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$", "patternMessage": "Enter a valid email address" } },
|
|
49
|
+
{ "label": "Department", "type": "dropdown", "table": "departments", "required": true },
|
|
50
|
+
{ "label": "Start date", "type": "date", "required": true }
|
|
51
|
+
],
|
|
52
|
+
"listColumns": ["firstName", "lastName", "departmentId", "startDate"]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| key | meaning |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `entity` | **required** — singular, e.g. `"employee"`, `"leave request"` |
|
|
59
|
+
| `plural` | when the plural is irregular, e.g. `"people"` (default: `entity` + s / es / ies) |
|
|
60
|
+
| `source` | the table records are saved in (default: the plural, snake_cased — `employees`) |
|
|
61
|
+
| `fields` | **required** — the edit form's fields, exactly as in the screen spec below |
|
|
62
|
+
| `listColumns` | the list's columns: field names, or `{ "field", "label" }` (default: the first five fields). Pick the few that identify a record |
|
|
63
|
+
|
|
64
|
+
A dropdown reading another table stores that row's id, so its field (and column) is named for it: "Department" becomes `departmentId`, a foreign key to `departments.id`. The other table needs an `id` and a `name` column — create that entity first.
|
|
65
|
+
| `columns` | `1` or `2` — the edit form's layout (default `2`) |
|
|
66
|
+
| `pageSize`, `actions`, `width`, `style` | as below |
|
|
67
|
+
|
|
68
|
+
Names are derived once and agree everywhere: ids `employee_list` / `employee_edit`, files `employee-*.screen.json`, components `EmployeeList` / `EditEmployee`, table `employees`.
|
|
69
|
+
|
|
70
|
+
## The screen spec
|
|
23
71
|
|
|
24
72
|
```json
|
|
25
73
|
{
|
|
26
74
|
"name": "New employee",
|
|
75
|
+
"source": "employees",
|
|
27
76
|
"columns": 2,
|
|
28
77
|
"fields": [
|
|
29
78
|
{ "label": "First name", "required": true, "validation": { "maxLength": 80 } },
|
|
@@ -38,20 +87,21 @@ Changing an existing screen: edit the document's `props` directly (labels, valid
|
|
|
38
87
|
{ "label": "Remote", "type": "checkbox" },
|
|
39
88
|
{ "label": "Notes", "type": "textarea", "validation": { "maxLength": 1000 } }
|
|
40
89
|
],
|
|
41
|
-
"
|
|
42
|
-
"reset": "Clear"
|
|
90
|
+
"list": { "title": "Employees" }
|
|
43
91
|
}
|
|
44
92
|
```
|
|
45
93
|
|
|
46
94
|
| key | meaning |
|
|
47
95
|
|---|---|
|
|
48
96
|
| `name` | **required** — the screen's title |
|
|
97
|
+
| `source` | the table records are saved to, e.g. `"employees"` — use the app's real table name |
|
|
49
98
|
| `id` | document id; defaults to the name in snake_case |
|
|
50
99
|
| `columns` | `1` or `2` (default `2`) |
|
|
51
100
|
| `heading` | `true` (default) puts the name at the top; a string uses that text; `false` for none |
|
|
52
101
|
| `fields` | **required** — in reading order |
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
102
|
+
| `list` | `true`, or `{ title, source, columns: [{ field, label }], pageSize, actions: ["new","edit","delete"] }` — saved records below the fields |
|
|
103
|
+
| `width` | design width in px (default `800`) |
|
|
104
|
+
| `style` | screen-wide defaults: `{ fontFamily, fontSize, color, background }` |
|
|
55
105
|
|
|
56
106
|
Each field:
|
|
57
107
|
|
|
@@ -68,6 +118,7 @@ Each field:
|
|
|
68
118
|
| `options` | dropdown: `["Full time", "Part time"]` (ids are made from the names: `full_time`) or `[{ "id": "ft", "name": "Full time" }]` |
|
|
69
119
|
| `table` | dropdown: read options from this table in the app's database |
|
|
70
120
|
| `text`, `variant` | `label` only — `variant` is `heading`, `subheading` (default for a section) or `text` |
|
|
121
|
+
| `style` | how it looks — see Styles |
|
|
71
122
|
|
|
72
123
|
## Validation rules
|
|
73
124
|
|
|
@@ -81,6 +132,35 @@ Each field:
|
|
|
81
132
|
|
|
82
133
|
Anything else is rejected, with the list of what is allowed.
|
|
83
134
|
|
|
135
|
+
## Styles
|
|
136
|
+
|
|
137
|
+
Every control takes `style`. Sizes are **pixels at the screen's `width`**.
|
|
138
|
+
|
|
139
|
+
| key | controls | value |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `fontFamily` | all | a CSS font family, e.g. `"Georgia, serif"` |
|
|
142
|
+
| `fontSize` | all | 8–96 |
|
|
143
|
+
| `fontWeight` | all but list | 300, 400, 500, 600, 700, 800 |
|
|
144
|
+
| `fontStyle` | all but list | `"normal"` or `"italic"` |
|
|
145
|
+
| `textAlign` | text, textarea, number, date, label | `"left"`, `"center"`, `"right"` |
|
|
146
|
+
| `color` | all | `#rgb` or `#rrggbb` |
|
|
147
|
+
| `background`, `borderColor` | inputs (not checkbox), label, list | `#rgb`, `#rrggbb` or `"transparent"` |
|
|
148
|
+
| `borderWidth` | inputs (not checkbox), label, list | 0–10 |
|
|
149
|
+
| `borderRadius` | inputs (not checkbox), label, list | 0–40 |
|
|
150
|
+
| `labelFontSize`, `labelFontWeight`, `labelColor` | inputs (not checkbox) | the label above the field |
|
|
151
|
+
|
|
152
|
+
Only style when the request asks for a look ("make the heading blue", "bigger labels"); otherwise leave the defaults.
|
|
153
|
+
|
|
154
|
+
## Lists
|
|
155
|
+
|
|
156
|
+
For an entity, `screens` makes the list for you. By hand: a `list` shows the saved records of the screen's `source` (or its own `source`) with **New**, **Edit** and **Delete**; `editScreen` names the screen those open in a popup. Columns default to every field; name the few that identify a record instead:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{ "title": "Employees", "columns": [{ "field": "firstName", "label": "First name" }, { "field": "departmentId", "label": "Department" }] }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
A list needs a table: set the screen's `source` or the list's own.
|
|
163
|
+
|
|
84
164
|
## Dropdowns
|
|
85
165
|
|
|
86
166
|
A dropdown **always saves an `id` and shows a `name`.**
|
|
@@ -109,7 +189,7 @@ Keep labels short and in sentence case ("Start date", not "START DATE:"). Mark o
|
|
|
109
189
|
{
|
|
110
190
|
"kind": "xeplr-screen", "version": 1,
|
|
111
191
|
"id": "new_employee", "name": "New employee",
|
|
112
|
-
"units": "fraction", "aspect": 1,
|
|
192
|
+
"units": "fraction", "aspect": 1, "width": 800,
|
|
113
193
|
"nodes": [
|
|
114
194
|
{ "id": "firstName", "type": "text", "x": 0.04, "y": 0.15, "w": 0.4475, "h": 0.08, "z": 2,
|
|
115
195
|
"props": { "label": "First name", "name": "firstName", "required": true } }
|
|
@@ -121,9 +201,18 @@ Keep labels short and in sentence case ("Start date", not "START DATE:"). Mark o
|
|
|
121
201
|
- Node `id`s are unique; input `props.name`s are unique and match `^[A-Za-z_][A-Za-z0-9_]*$`.
|
|
122
202
|
- `props` allowed per control: `npx xeplr-factory controls`.
|
|
123
203
|
|
|
204
|
+
## Field names are column names
|
|
205
|
+
|
|
206
|
+
- camelCase, as generated from the label: `firstName`, `startDate`, `departmentId`.
|
|
207
|
+
- Not a standard column: `id`, `isActive`, `mtId1`–`mtId4`, `recordCreatedDate`, `recordModifiedDate`, `recordCreatedBy`, `recordModifiedBy`.
|
|
208
|
+
- At most 63 characters.
|
|
209
|
+
- Choose them as if naming a database column — because you are.
|
|
210
|
+
|
|
124
211
|
## Commands
|
|
125
212
|
|
|
126
213
|
```sh
|
|
214
|
+
npx xeplr-factory screens <entity.json|-> [-o <dir>] [--force] # entity → list + edit screens and pages
|
|
215
|
+
npx xeplr-factory migration <edit.screen.json> [--from <previous.screen.json>] # preview a table's SQL (Publish applies it)
|
|
127
216
|
npx xeplr-factory generate <spec.json|-> [-o <screen.json>] # spec → document
|
|
128
217
|
npx xeplr-factory validate <screen.json|-> # exit 1 with every problem listed
|
|
129
218
|
npx xeplr-factory controls # controls, props, validation rules
|
package/README.md
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
# @xeplr/ui-factory
|
|
2
2
|
|
|
3
|
-
**Design data-entry screens, save them as JSON, run them as working forms.** Drag controls onto a canvas, set their labels, validation
|
|
3
|
+
**Design data-entry screens, save them as JSON, run them as working forms.** Drag controls onto a canvas, set their labels, validation, options, fonts and colours. The same document renders as a live form in your app that **saves itself** as it is filled in, with a list of the saved records to open, edit or delete.
|
|
4
4
|
|
|
5
|
-
It is built to work with Claude: ask for *"a form for
|
|
5
|
+
It is built to work with Claude: ask for *"a form for employees with name, email, department and start date"*, and Claude creates **two screens** — an employee **list** and an **add / edit** form that the list's Edit and New open in a popup — plus the two pages that show them. A person then refines either one in the designer.
|
|
6
|
+
|
|
7
|
+
## One entity, two screens, four pages
|
|
8
|
+
|
|
9
|
+
| page | what it is |
|
|
10
|
+
|---|---|
|
|
11
|
+
| **Screen · List** | `EmployeeList.jsx` — the saved employees; **New** and **Edit** open the edit screen in a popup, **Delete** removes a row |
|
|
12
|
+
| **Screen · Edit** | `EditEmployee.jsx` — the add / edit form, on its own page (the same screen the popup shows) |
|
|
13
|
+
| **Designer · List** | `<FactoryBuilder document={employeeList} />` — its title, columns, actions, look |
|
|
14
|
+
| **Designer · Edit** | `<FactoryBuilder document={employeeEdit} />` — its fields, validation, options, look |
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx xeplr-factory screens employee.entity.json -o src/screens/employee
|
|
18
|
+
# employee-list.screen.json EmployeeList.jsx
|
|
19
|
+
# employee-edit.screen.json EditEmployee.jsx
|
|
20
|
+
# employee.hooks.js server hooks for its records: save / get / delete × before / after / error / override
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
See [examples/](./examples) for exactly what it writes.
|
|
6
24
|
|
|
7
25
|
(The package name on npm is `@xeplr/ui-factory` — the GitHub repo and folder are named `xeplr-ui-factory`.)
|
|
8
26
|
|
|
@@ -16,13 +34,15 @@ Most internal apps are a long tail of forms, and every one of them is hand-built
|
|
|
16
34
|
|
|
17
35
|
**The factory never has a database.** Your app stores screen documents and supplies the rows dropdowns read. The factory draws, edits and validates.
|
|
18
36
|
|
|
37
|
+
**Records live in real tables** — `employees`, one column per field — never in JSON, so `select * from employees` and ordinary reporting just work. **Publishing** a screen creates or changes its table directly: new fields add columns, and a removed field drops its column and data only after you confirm. [`@xeplr/factory`](https://www.npmjs.com/package/@xeplr/factory) is the server package that does it, saves screens (draft / publish) and reads and writes those tables with the screen's own validation. `createFactoryApi({ fetch })` connects the components to it.
|
|
38
|
+
|
|
19
39
|
## Install
|
|
20
40
|
|
|
21
41
|
```sh
|
|
22
|
-
npm i @xeplr/ui-factory @xeplr/ui-canvas
|
|
42
|
+
npm i @xeplr/ui-factory @xeplr/ui-canvas @xeplr/ui-table @tanstack/react-table
|
|
23
43
|
```
|
|
24
44
|
|
|
25
|
-
Peer dependencies: `react ^18 || ^19
|
|
45
|
+
Peer dependencies: `react ^18 || ^19`, [`@xeplr/ui-canvas`](https://www.npmjs.com/package/@xeplr/ui-canvas) (the canvas the builder places controls on) and [`@xeplr/ui-table`](https://www.npmjs.com/package/@xeplr/ui-table) (lists of saved records). Your bundler must compile JSX and import CSS from the package (Vite does both).
|
|
26
46
|
|
|
27
47
|
## The builder
|
|
28
48
|
|
|
@@ -32,9 +52,10 @@ import { FactoryBuilder } from '@xeplr/ui-factory'
|
|
|
32
52
|
<div style={{ height: '100vh' }}>
|
|
33
53
|
<FactoryBuilder
|
|
34
54
|
document={screen} // omit to start a new one
|
|
35
|
-
onSave={async (doc) => api.saveScreen(doc)} // your storage
|
|
36
|
-
listTables={async () => api.listTables()} //
|
|
37
|
-
fetchOptions={async ({ table }) => api.options(table)} //
|
|
55
|
+
onSave={async (doc) => api.saveScreen(doc)} // your storage — called automatically
|
|
56
|
+
listTables={async () => api.listTables()} // for "Saves to", lists and table dropdowns
|
|
57
|
+
fetchOptions={async ({ table }) => api.options(table)} // Preview with real options
|
|
58
|
+
fetchRecords={async ({ source }) => api.records(source)} // Preview with real records
|
|
38
59
|
/>
|
|
39
60
|
</div>
|
|
40
61
|
```
|
|
@@ -42,7 +63,11 @@ import { FactoryBuilder } from '@xeplr/ui-factory'
|
|
|
42
63
|
- Drag controls from the palette onto the screen, or click one to add it at the bottom.
|
|
43
64
|
- Move, resize, align with guides; drag a box to select several; <kbd>Delete</kbd> removes, <kbd>Ctrl/⌘ D</kbd> duplicates.
|
|
44
65
|
- Select a control to set its label, field name, placeholder, required, default, validation, and — for a dropdown — where its options come from.
|
|
45
|
-
- **
|
|
66
|
+
- **Every look is a property:** font, size, weight, italic, alignment, text colour, background, border colour/width, corner radius — and for an input, its label's size, weight and colour.
|
|
67
|
+
- With nothing selected, the panel shows the **screen**: the table it saves to, its width, and the font and colours every control inherits.
|
|
68
|
+
- **No Save button.** Edits are saved (as a draft) a moment after you stop, whenever the screen is valid; problems are marked on the controls that have them until they are fixed.
|
|
69
|
+
- **Publish** (when `onPublish` is given) makes the draft the version everyone sees and changes its table to match. If removed fields would drop columns, it asks first — "Publishing removes `phone` — 1,240 saved values" — with **Remove and publish** / **Cancel**. Unsafe changes (narrowing a field) are refused with the reason.
|
|
70
|
+
- **`lockedNames`**: fields that are already columns of the table. Their names are read-only, because renaming one would leave its data behind.
|
|
46
71
|
|
|
47
72
|
## The screen
|
|
48
73
|
|
|
@@ -51,14 +76,30 @@ import { FactoryScreen } from '@xeplr/ui-factory'
|
|
|
51
76
|
|
|
52
77
|
<FactoryScreen
|
|
53
78
|
document={screen}
|
|
54
|
-
|
|
79
|
+
record={employee} // optional: open an existing record
|
|
80
|
+
onSave={async (values, { id, source }) => api.save(source, id, values)} // returns the saved record, with its id
|
|
81
|
+
fetchRecords={async ({ source }) => api.records(source)}
|
|
82
|
+
fetchRecord={async ({ screen, id }) => api.record(screen, id)} // optional: Edit loads the record fresh
|
|
83
|
+
onDelete={async ({ id, source }) => api.remove(source, id)}
|
|
55
84
|
fetchOptions={async ({ table }) => api.options(table)}
|
|
56
|
-
|
|
85
|
+
screens={{ employee_edit: editScreen }} // what a list's Edit / New open in a popup
|
|
57
86
|
/>
|
|
58
87
|
```
|
|
59
88
|
|
|
89
|
+
**There is no submit.** The screen saves itself in the background (one AJAX call to your `onSave`) a moment after a change, once the entered values are acceptable. Until then it says what is missing — "Fill in the required fields to save" — and a field's message appears once the person has been in it. The first save of a new record creates it: return the saved record and its `id` makes every later save an update.
|
|
90
|
+
|
|
91
|
+
If `onSave` throws an error with `fields: [{ field, message }]` (what `createFactoryApi` does with the server's 422 — a rule, or a hook's `reject`), each message shows on its field until that field is changed.
|
|
92
|
+
|
|
60
93
|
Values arrive typed: numbers as numbers, checkboxes as booleans, dates as `"YYYY-MM-DD"`, and a dropdown as the option's own `id` (a table's numeric id stays a number).
|
|
61
94
|
|
|
95
|
+
### Lists of saved records
|
|
96
|
+
|
|
97
|
+
A **List** control shows the records of the table the screen saves to (or another), in `@xeplr/ui-table`, with the columns you tick. Its **Edit in** names the edit screen: **Edit** and **New** open that screen in a popup, where it saves itself and the list refreshes behind it. **Delete** removes a row after a confirmation. Dropdown columns show names, not ids — read from the edit screen's fields.
|
|
98
|
+
|
|
99
|
+
The edit screen comes from `screens` (`{ id → document }`) or your `loadScreen(id)`. A list on a form with no **Edit in** opens rows in that form's own fields instead.
|
|
100
|
+
|
|
101
|
+
`@xeplr/ui-table` reads the xeplr theme tokens (`--xeplr-bg-*`, `--xeplr-text-*`, `--xeplr-border-*`) and defaults to a dark look; define them for a light app.
|
|
102
|
+
|
|
62
103
|
On your server, check the submission against the same rules with [`@xeplr/schema-handler`](https://www.npmjs.com/package/@xeplr/schema-handler):
|
|
63
104
|
|
|
64
105
|
```js
|
|
@@ -78,8 +119,10 @@ const clean = schemaHandler.applySchema(formSchema(screen), req.body) // throw
|
|
|
78
119
|
| `date` | `"YYYY-MM-DD"` | `min`, `max` |
|
|
79
120
|
| `checkbox` | boolean | `required` = must be ticked |
|
|
80
121
|
| `dropdown` | option id | — |
|
|
81
|
-
| `label` | — | heading, subheading or text |
|
|
82
|
-
| `
|
|
122
|
+
| `label` | — | text with a heading, subheading or text preset |
|
|
123
|
+
| `list` | — | saved records, with New / Edit / Delete |
|
|
124
|
+
|
|
125
|
+
Every control also takes `style` — see [AUTHORING.md](./AUTHORING.md#styles) for the keys. Sizes are pixels at the screen's design `width` (default 800): a screen is shown at that size, never stretched, and scaled down only on a narrower display.
|
|
83
126
|
|
|
84
127
|
### Dropdown options
|
|
85
128
|
|
|
@@ -107,14 +150,18 @@ const screen = screenFromSpec({
|
|
|
107
150
|
{ label: 'Department', type: 'dropdown', table: 'departments', required: true },
|
|
108
151
|
{ label: 'Employment type', type: 'dropdown', options: ['Full time', 'Part time'] },
|
|
109
152
|
{ label: 'Start date', type: 'date' }
|
|
110
|
-
]
|
|
153
|
+
],
|
|
154
|
+
source: 'employees',
|
|
155
|
+
list: true
|
|
111
156
|
})
|
|
112
157
|
```
|
|
113
158
|
|
|
114
159
|
It lays the fields out as a tidy one- or two-column form and returns a checked document. The same is available on the command line:
|
|
115
160
|
|
|
116
161
|
```sh
|
|
117
|
-
npx xeplr-factory
|
|
162
|
+
npx xeplr-factory screens entity.json -o dir # an entity → list + edit screens and pages
|
|
163
|
+
npx xeplr-factory migration edit.screen.json # preview the SQL Publish would run
|
|
164
|
+
npx xeplr-factory generate spec.json -o screen.json # one screen from a spec
|
|
118
165
|
npx xeplr-factory validate screen.json # every problem, with its path
|
|
119
166
|
npx xeplr-factory controls # controls, props and rules
|
|
120
167
|
```
|
|
@@ -127,7 +174,8 @@ npx xeplr-factory controls # controls, props and rules
|
|
|
127
174
|
{
|
|
128
175
|
"kind": "xeplr-screen", "version": 1,
|
|
129
176
|
"id": "new_employee", "name": "New employee",
|
|
130
|
-
"
|
|
177
|
+
"source": "employees",
|
|
178
|
+
"units": "fraction", "aspect": 1, "width": 800,
|
|
131
179
|
"nodes": [
|
|
132
180
|
{ "id": "firstName", "type": "text", "x": 0.04, "y": 0.15, "w": 0.4475, "h": 0.08, "z": 2,
|
|
133
181
|
"props": { "label": "First name", "name": "firstName", "required": true, "validation": { "maxLength": 80 } } }
|
|
@@ -135,7 +183,7 @@ npx xeplr-factory controls # controls, props and rules
|
|
|
135
183
|
}
|
|
136
184
|
```
|
|
137
185
|
|
|
138
|
-
Layout is **proportional**, like `@xeplr/ui-canvas` dashboards: `x` and `w` are fractions of the width, `y` and `h` fractions of a page (`width × aspect`).
|
|
186
|
+
Layout is **proportional**, like `@xeplr/ui-canvas` dashboards: `x` and `w` are fractions of the width, `y` and `h` fractions of a page (`width × aspect`). The screen is shown at its design `width`; on a narrower display everything — positions and text — scales down together.
|
|
139
187
|
|
|
140
188
|
## Three ways to use it
|
|
141
189
|
|
|
@@ -151,7 +199,7 @@ Styles are namespaced `.xeplr-factory-*` and read the xeplr theme variables with
|
|
|
151
199
|
|
|
152
200
|
```sh
|
|
153
201
|
npm install
|
|
154
|
-
npm run dev # http://localhost:19006 —
|
|
202
|
+
npm run dev # http://localhost:19006 — Screen · List / Edit and Designer · List / Edit for the example employee, records kept in localStorage
|
|
155
203
|
```
|
|
156
204
|
|
|
157
205
|
## Files
|
|
@@ -161,15 +209,18 @@ src/
|
|
|
161
209
|
controls.js ─ the control registry: palette, property panel contract, rules
|
|
162
210
|
document.js ─ create and edit screen documents
|
|
163
211
|
validateDocument.js ─ the document checker
|
|
164
|
-
values.js ─
|
|
165
|
-
generate.js ─ screenFromSpec
|
|
212
|
+
values.js ─ values, validation, autosave readiness, records and list columns, formSchema
|
|
213
|
+
generate.js ─ screenFromSpec, screensFromSpec (list + edit), entity names
|
|
214
|
+
scaffold.js ─ an entity → its screen JSON, .jsx pages and hooks stub
|
|
215
|
+
tableSchema.js ─ a form → its table; the publish plan (create / add / widen / confirmed drops)
|
|
216
|
+
remote.js ─ createFactoryApi: the calls to @xeplr/factory's routes
|
|
166
217
|
model.js ─ everything above, React-free
|
|
167
218
|
useFactoryBuilder.js ─ builder controller
|
|
168
219
|
useFactoryScreen.js ─ screen controller
|
|
169
|
-
designs/ ─ BuilderSample, ScreenSample, ControlView, Palette, PropertyPanel, factory.css
|
|
220
|
+
designs/ ─ BuilderSample, ScreenSample, ControlView, ListView, Palette, PropertyPanel, styles.js, factory.css
|
|
170
221
|
pages.jsx ─ FactoryBuilder, FactoryScreen
|
|
171
|
-
bin/xeplr-factory.js ─ generate / validate / controls / schema
|
|
172
|
-
examples/ ─
|
|
222
|
+
bin/xeplr-factory.js ─ screens / generate / validate / controls / schema
|
|
223
|
+
examples/ ─ employee.entity.json and what `screens` makes of it
|
|
173
224
|
dev/ ─ the stand-in app for `npm run dev` (not published)
|
|
174
225
|
```
|
|
175
226
|
|
package/bin/xeplr-factory.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
// turn a short spec into a laid-out document, and check a document before
|
|
6
6
|
// handing it to an app, with errors precise enough to fix without guessing.
|
|
7
7
|
//
|
|
8
|
+
// xeplr-factory screens entity.json [-o dir] an entity → list + edit screens, their .jsx pages, a hooks stub
|
|
9
|
+
// xeplr-factory migration edit.screen.json [--from previous.screen.json] [-o migrations/]
|
|
10
|
+
// the SQL that makes the table match the form
|
|
8
11
|
// xeplr-factory generate spec.json [-o screen.json] spec → laid-out document
|
|
9
12
|
// xeplr-factory validate screen.json check a document (exit 1 on problems)
|
|
10
13
|
// xeplr-factory controls the controls and their props
|
|
@@ -12,13 +15,18 @@
|
|
|
12
15
|
//
|
|
13
16
|
// "-" as the file reads stdin.
|
|
14
17
|
|
|
15
|
-
import { readFileSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs'
|
|
19
|
+
import path from 'node:path'
|
|
16
20
|
import { CONTROLS } from '../src/controls.js'
|
|
17
21
|
import { screenFromSpec } from '../src/generate.js'
|
|
22
|
+
import { scaffoldEntity } from '../src/scaffold.js'
|
|
23
|
+
import { migrationFor, nextMigrationName } from '../src/tableSchema.js'
|
|
18
24
|
import { validateDocument } from '../src/validateDocument.js'
|
|
19
25
|
import { formSchema } from '../src/values.js'
|
|
20
26
|
|
|
21
27
|
const USAGE = `usage:
|
|
28
|
+
xeplr-factory screens <entity.json|-> [-o <dir>] [--force]
|
|
29
|
+
xeplr-factory migration <edit.screen.json> [--from <previous.screen.json>] [-o <migrations dir>]
|
|
22
30
|
xeplr-factory generate <spec.json|-> [-o <out.json>]
|
|
23
31
|
xeplr-factory validate <screen.json|->
|
|
24
32
|
xeplr-factory controls
|
|
@@ -62,6 +70,68 @@ switch (cmd) {
|
|
|
62
70
|
break
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
case 'screens': {
|
|
74
|
+
if (!args[0]) fail(USAGE, 2)
|
|
75
|
+
const o = args.indexOf('-o')
|
|
76
|
+
const dir = o !== -1 ? args[o + 1] : '.'
|
|
77
|
+
const force = args.includes('--force')
|
|
78
|
+
let result
|
|
79
|
+
try {
|
|
80
|
+
result = scaffoldEntity(readJson(args[0]))
|
|
81
|
+
} catch (err) {
|
|
82
|
+
fail(err.message)
|
|
83
|
+
}
|
|
84
|
+
// The hooks file is the app's own code — kept as it is, even with --force.
|
|
85
|
+
const hooksFile = Object.keys(result.files).find((f) => f.endsWith('.hooks.js'))
|
|
86
|
+
const keptHooks = hooksFile && existsSync(path.join(dir, hooksFile)) ? hooksFile : null
|
|
87
|
+
const writing = Object.keys(result.files).filter((f) => f !== keptHooks)
|
|
88
|
+
const targets = writing.map((f) => path.join(dir, f))
|
|
89
|
+
// Never overwrite a screen someone has since refined in the designer.
|
|
90
|
+
const existing = targets.filter((t) => existsSync(t))
|
|
91
|
+
if (existing.length && !force) fail(`would overwrite ${existing.join(', ')} — pass --force to replace them`)
|
|
92
|
+
mkdirSync(dir, { recursive: true })
|
|
93
|
+
writing.forEach((f) => writeFileSync(path.join(dir, f), result.files[f]))
|
|
94
|
+
process.stderr.write(`wrote ${targets.join(', ')}\n`)
|
|
95
|
+
if (keptHooks) process.stderr.write(`kept ${path.join(dir, keptHooks)} — it is your code\n`)
|
|
96
|
+
process.stderr.write(`list screen "${result.list.id}" opens "${result.edit.id}" for Edit / New; both use table "${result.edit.source}"\n`)
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case 'migration': {
|
|
101
|
+
if (!args[0]) fail(USAGE, 2)
|
|
102
|
+
const f = args.indexOf('--from')
|
|
103
|
+
const o = args.indexOf('-o')
|
|
104
|
+
const next = readJson(args[0])
|
|
105
|
+
const previous = f !== -1 ? readJson(args[f + 1]) : null
|
|
106
|
+
for (const [label, doc] of [['screen', next], ['--from screen', previous]]) {
|
|
107
|
+
if (!doc) continue
|
|
108
|
+
const { ok, errors } = validateDocument(doc)
|
|
109
|
+
if (!ok) fail(`the ${label} is not valid — run validate first (${errors.length} problems)`)
|
|
110
|
+
}
|
|
111
|
+
let result
|
|
112
|
+
try {
|
|
113
|
+
result = migrationFor(previous, next)
|
|
114
|
+
} catch (err) {
|
|
115
|
+
fail(err.message)
|
|
116
|
+
}
|
|
117
|
+
if (result.empty) {
|
|
118
|
+
process.stderr.write(`"${result.table}" already matches the form — no migration needed\n`)
|
|
119
|
+
if (result.diff.unused.length) process.stderr.write(`(columns no longer on the screen, kept: ${result.diff.unused.map((c) => c.name).join(', ')})\n`)
|
|
120
|
+
break
|
|
121
|
+
}
|
|
122
|
+
if (o !== -1) {
|
|
123
|
+
const dir = args[o + 1]
|
|
124
|
+
const existing = existsSync(dir) ? readdirSync(dir).filter((x) => x.endsWith('.sql')) : []
|
|
125
|
+
const file = path.join(dir, nextMigrationName(existing, result.table, result.diff.create))
|
|
126
|
+
mkdirSync(dir, { recursive: true })
|
|
127
|
+
writeFileSync(file, result.sql)
|
|
128
|
+
process.stderr.write(`wrote ${file}\n`)
|
|
129
|
+
} else {
|
|
130
|
+
process.stdout.write(result.sql)
|
|
131
|
+
}
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
|
|
65
135
|
case 'validate': {
|
|
66
136
|
if (!args[0]) fail(USAGE, 2)
|
|
67
137
|
const { ok, errors } = validateDocument(readJson(args[0]))
|
|
@@ -80,6 +150,7 @@ switch (cmd) {
|
|
|
80
150
|
process.stdout.write(`${c.type.padEnd(9)} ${c.label}${c.input ? ` — value: ${c.valueType || 'option id'}` : ''}\n`)
|
|
81
151
|
process.stdout.write(` props: ${c.props.join(', ')}\n`)
|
|
82
152
|
if (c.validation.length) process.stdout.write(` validation: ${c.validation.join(', ')}\n`)
|
|
153
|
+
if (c.styles && c.styles.length) process.stdout.write(` style: ${c.styles.join(', ')}\n`)
|
|
83
154
|
})
|
|
84
155
|
break
|
|
85
156
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xeplr/ui-factory",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Design data-entry screens on @xeplr/ui-canvas, save them as JSON,
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Design data-entry screens on @xeplr/ui-canvas, save them as JSON, run them as auto-saving forms with lists of saved records — built for Claude to draft and people to refine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -45,13 +45,17 @@
|
|
|
45
45
|
"access": "public"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
+
"@tanstack/react-table": "^8.0.0",
|
|
48
49
|
"@xeplr/ui-canvas": "^1.0.2",
|
|
50
|
+
"@xeplr/ui-table": "^1.0.6",
|
|
49
51
|
"react": "^18.0.0 || ^19.0.0"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
54
|
+
"@tanstack/react-table": "^8.21.3",
|
|
52
55
|
"@vitejs/plugin-react": "^5.2.0",
|
|
53
56
|
"@xeplr/schema-handler": "^1.0.5",
|
|
54
57
|
"@xeplr/ui-canvas": "^1.0.1",
|
|
58
|
+
"@xeplr/ui-table": "^1.0.6",
|
|
55
59
|
"react": "^19.2.4",
|
|
56
60
|
"react-dom": "^19.2.4",
|
|
57
61
|
"vite": "^7.3.1"
|