@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 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
- 1. **Write a spec** — a short list of what is on the screen, in reading order. Do not write coordinates.
8
- 2. **Generate** the screen document from it:
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 generate spec.json -o screen.json
12
+ npx xeplr-factory screens employee.entity.json -o src/screens/employee
11
13
  ```
12
- or in code: `screenFromSpec(spec)` from `@xeplr/ui-factory/model`.
13
- 3. **Validate** — `generate` already refuses a bad spec, but check any document you edited by hand:
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
- 4. **Hand the document to the app.** The app stores it and opens it in `<FactoryBuilder>`, where the person moves, resizes and adjusts controls.
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
- "submit": "Create employee",
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
- | `submit` | submit button label (default `"Save"`); `false` for none |
54
- | `reset` | reset button label; omit for none |
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 and options, and save. The same document renders as a live form in your app.
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 a new employee with name, email, department and start date"*, Claude drafts the screen, and a person refines it in the builder.
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` and [`@xeplr/ui-canvas`](https://www.npmjs.com/package/@xeplr/ui-canvas) (the canvas the builder places controls on). Your bundler must compile JSX and import CSS from the package (Vite does both).
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()} // offered for "dropdown from a table"
37
- fetchOptions={async ({ table }) => api.options(table)} // enables Preview with real options
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
- - **Save** checks the whole screen first; problems are shown on the controls that have them, and `onSave` is only called with a valid document.
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
- values={employee} // optional: the record being edited
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
- onSubmit={async (values) => api.saveEmployee(values)}
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
- | `button` | — | submit or reset |
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 generate spec.json -o screen.json
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
- "units": "fraction", "aspect": 1,
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`). Text and controls scale with it, so a screen looks the same at any width.
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 — builder + saved screen, with an in-memory stand-in app
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 ─ initial values, parsing, field validation, formSchema
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/ ─ new-employee spec and screen
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
 
@@ -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.1",
4
- "description": "Design data-entry screens on @xeplr/ui-canvas, save them as JSON, render them as working forms — built for Claude to draft and people to refine",
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"