@xeplr/ui-factory 1.0.1 → 1.0.2

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,74 @@ 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` and `EditEmployee.jsx`. 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. **Write the table's migration.** Records live in a **real table** — `employees`, one column per field — never in JSON. Draft the migration from the edit screen into the app's migrations folder:
18
+ ```sh
19
+ npx xeplr-factory migration src/screens/employee/employee-edit.screen.json -o migrations
20
+ ```
21
+ It writes the next numbered file (`0067_factory_employees_create.sql`): a `CREATE TABLE` with a column per field, real foreign keys for table dropdowns, and the standard `id` / `isActive` / `mtId1–4` / audit columns. Read it, and apply it with the app's `migrate:up`.
22
+
23
+ **Changing an existing form** is a migration too. Pass the version the table was built from: `--from employee-edit.published.json` (from git, or `GET /factory/screens/employee_edit`). New fields become `ADD COLUMN`; a wider text or number becomes `ALTER COLUMN … TYPE`. Narrowing, or changing a field to a different kind of value, is **refused** — explain why to the person and write that migration by hand only if they confirm. A removed field's column is kept.
24
+
25
+ **Never rename a field that is already a column** (the designer locks those names). Renaming would start a new empty column and strand the data in the old 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 and publish them. Publishing refuses a screen whose table is missing a column, and returns the SQL that would add it — run the migration first.
32
+ 6. **Hand over.** The person opens the screens in the designer to move, resize and restyle controls.
33
+
34
+ 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
35
 
20
36
  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
37
 
22
- ## The spec
38
+ ## The entity spec
39
+
40
+ ```json
41
+ {
42
+ "entity": "employee",
43
+ "fields": [
44
+ { "label": "First name", "required": true },
45
+ { "label": "Last name", "required": true },
46
+ { "label": "Work email", "required": true,
47
+ "validation": { "pattern": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$", "patternMessage": "Enter a valid email address" } },
48
+ { "label": "Department", "type": "dropdown", "table": "departments", "required": true },
49
+ { "label": "Start date", "type": "date", "required": true }
50
+ ],
51
+ "listColumns": ["firstName", "lastName", "departmentId", "startDate"]
52
+ }
53
+ ```
54
+
55
+ | key | meaning |
56
+ |---|---|
57
+ | `entity` | **required** — singular, e.g. `"employee"`, `"leave request"` |
58
+ | `plural` | when the plural is irregular, e.g. `"people"` (default: `entity` + s / es / ies) |
59
+ | `source` | the table records are saved in (default: the plural, snake_cased — `employees`) |
60
+ | `fields` | **required** — the edit form's fields, exactly as in the screen spec below |
61
+ | `listColumns` | the list's columns: field names, or `{ "field", "label" }` (default: the first five fields). Pick the few that identify a record |
62
+
63
+ 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.
64
+ | `columns` | `1` or `2` — the edit form's layout (default `2`) |
65
+ | `pageSize`, `actions`, `width`, `style` | as below |
66
+
67
+ Names are derived once and agree everywhere: ids `employee_list` / `employee_edit`, files `employee-*.screen.json`, components `EmployeeList` / `EditEmployee`, table `employees`.
68
+
69
+ ## The screen spec
23
70
 
24
71
  ```json
25
72
  {
26
73
  "name": "New employee",
74
+ "source": "employees",
27
75
  "columns": 2,
28
76
  "fields": [
29
77
  { "label": "First name", "required": true, "validation": { "maxLength": 80 } },
@@ -38,20 +86,21 @@ Changing an existing screen: edit the document's `props` directly (labels, valid
38
86
  { "label": "Remote", "type": "checkbox" },
39
87
  { "label": "Notes", "type": "textarea", "validation": { "maxLength": 1000 } }
40
88
  ],
41
- "submit": "Create employee",
42
- "reset": "Clear"
89
+ "list": { "title": "Employees" }
43
90
  }
44
91
  ```
45
92
 
46
93
  | key | meaning |
47
94
  |---|---|
48
95
  | `name` | **required** — the screen's title |
96
+ | `source` | the table records are saved to, e.g. `"employees"` — use the app's real table name |
49
97
  | `id` | document id; defaults to the name in snake_case |
50
98
  | `columns` | `1` or `2` (default `2`) |
51
99
  | `heading` | `true` (default) puts the name at the top; a string uses that text; `false` for none |
52
100
  | `fields` | **required** — in reading order |
53
- | `submit` | submit button label (default `"Save"`); `false` for none |
54
- | `reset` | reset button label; omit for none |
101
+ | `list` | `true`, or `{ title, source, columns: [{ field, label }], pageSize, actions: ["new","edit","delete"] }` — saved records below the fields |
102
+ | `width` | design width in px (default `800`) |
103
+ | `style` | screen-wide defaults: `{ fontFamily, fontSize, color, background }` |
55
104
 
56
105
  Each field:
57
106
 
@@ -68,6 +117,7 @@ Each field:
68
117
  | `options` | dropdown: `["Full time", "Part time"]` (ids are made from the names: `full_time`) or `[{ "id": "ft", "name": "Full time" }]` |
69
118
  | `table` | dropdown: read options from this table in the app's database |
70
119
  | `text`, `variant` | `label` only — `variant` is `heading`, `subheading` (default for a section) or `text` |
120
+ | `style` | how it looks — see Styles |
71
121
 
72
122
  ## Validation rules
73
123
 
@@ -81,6 +131,35 @@ Each field:
81
131
 
82
132
  Anything else is rejected, with the list of what is allowed.
83
133
 
134
+ ## Styles
135
+
136
+ Every control takes `style`. Sizes are **pixels at the screen's `width`**.
137
+
138
+ | key | controls | value |
139
+ |---|---|---|
140
+ | `fontFamily` | all | a CSS font family, e.g. `"Georgia, serif"` |
141
+ | `fontSize` | all | 8–96 |
142
+ | `fontWeight` | all but list | 300, 400, 500, 600, 700, 800 |
143
+ | `fontStyle` | all but list | `"normal"` or `"italic"` |
144
+ | `textAlign` | text, textarea, number, date, label | `"left"`, `"center"`, `"right"` |
145
+ | `color` | all | `#rgb` or `#rrggbb` |
146
+ | `background`, `borderColor` | inputs (not checkbox), label, list | `#rgb`, `#rrggbb` or `"transparent"` |
147
+ | `borderWidth` | inputs (not checkbox), label, list | 0–10 |
148
+ | `borderRadius` | inputs (not checkbox), label, list | 0–40 |
149
+ | `labelFontSize`, `labelFontWeight`, `labelColor` | inputs (not checkbox) | the label above the field |
150
+
151
+ Only style when the request asks for a look ("make the heading blue", "bigger labels"); otherwise leave the defaults.
152
+
153
+ ## Lists
154
+
155
+ 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:
156
+
157
+ ```json
158
+ { "title": "Employees", "columns": [{ "field": "firstName", "label": "First name" }, { "field": "departmentId", "label": "Department" }] }
159
+ ```
160
+
161
+ A list needs a table: set the screen's `source` or the list's own.
162
+
84
163
  ## Dropdowns
85
164
 
86
165
  A dropdown **always saves an `id` and shows a `name`.**
@@ -109,7 +188,7 @@ Keep labels short and in sentence case ("Start date", not "START DATE:"). Mark o
109
188
  {
110
189
  "kind": "xeplr-screen", "version": 1,
111
190
  "id": "new_employee", "name": "New employee",
112
- "units": "fraction", "aspect": 1,
191
+ "units": "fraction", "aspect": 1, "width": 800,
113
192
  "nodes": [
114
193
  { "id": "firstName", "type": "text", "x": 0.04, "y": 0.15, "w": 0.4475, "h": 0.08, "z": 2,
115
194
  "props": { "label": "First name", "name": "firstName", "required": true } }
@@ -121,9 +200,18 @@ Keep labels short and in sentence case ("Start date", not "START DATE:"). Mark o
121
200
  - Node `id`s are unique; input `props.name`s are unique and match `^[A-Za-z_][A-Za-z0-9_]*$`.
122
201
  - `props` allowed per control: `npx xeplr-factory controls`.
123
202
 
203
+ ## Field names are column names
204
+
205
+ - camelCase, as generated from the label: `firstName`, `startDate`, `departmentId`.
206
+ - Not a standard column: `id`, `isActive`, `mtId1`–`mtId4`, `recordCreatedDate`, `recordModifiedDate`, `recordCreatedBy`, `recordModifiedBy`.
207
+ - At most 63 characters.
208
+ - Choose them as if naming a database column — because you are.
209
+
124
210
  ## Commands
125
211
 
126
212
  ```sh
213
+ npx xeplr-factory screens <entity.json|-> [-o <dir>] [--force] # entity → list + edit screens and pages
214
+ npx xeplr-factory migration <edit.screen.json> [--from <previous.screen.json>] [-o <migrations dir>] # the table's SQL
127
215
  npx xeplr-factory generate <spec.json|-> [-o <screen.json>] # spec → document
128
216
  npx xeplr-factory validate <screen.json|-> # exit 1 with every problem listed
129
217
  npx xeplr-factory controls # controls, props, validation rules
package/README.md CHANGED
@@ -1,8 +1,25 @@
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
+ ```
21
+
22
+ See [examples/](./examples) for exactly what it writes.
6
23
 
7
24
  (The package name on npm is `@xeplr/ui-factory` — the GitHub repo and folder are named `xeplr-ui-factory`.)
8
25
 
@@ -16,13 +33,15 @@ Most internal apps are a long tail of forms, and every one of them is hand-built
16
33
 
17
34
  **The factory never has a database.** Your app stores screen documents and supplies the rows dropdowns read. The factory draws, edits and validates.
18
35
 
36
+ **Records live in real tables** — `employees`, one column per field — never in JSON, so `select * from employees` and ordinary reporting just work. `xeplr-factory migration` drafts each table's migration from its form, and [`@xeplr/factory`](https://www.npmjs.com/package/@xeplr/factory) is the server package that saves screens (draft / publish) and reads and writes those tables with the screen's own validation. `createFactoryApi({ fetch })` connects the components to it.
37
+
19
38
  ## Install
20
39
 
21
40
  ```sh
22
- npm i @xeplr/ui-factory @xeplr/ui-canvas
41
+ npm i @xeplr/ui-factory @xeplr/ui-canvas @xeplr/ui-table @tanstack/react-table
23
42
  ```
24
43
 
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).
44
+ 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
45
 
27
46
  ## The builder
28
47
 
@@ -32,9 +51,10 @@ import { FactoryBuilder } from '@xeplr/ui-factory'
32
51
  <div style={{ height: '100vh' }}>
33
52
  <FactoryBuilder
34
53
  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
54
+ onSave={async (doc) => api.saveScreen(doc)} // your storage — called automatically
55
+ listTables={async () => api.listTables()} // for "Saves to", lists and table dropdowns
56
+ fetchOptions={async ({ table }) => api.options(table)} // Preview with real options
57
+ fetchRecords={async ({ source }) => api.records(source)} // Preview with real records
38
58
  />
39
59
  </div>
40
60
  ```
@@ -42,7 +62,11 @@ import { FactoryBuilder } from '@xeplr/ui-factory'
42
62
  - Drag controls from the palette onto the screen, or click one to add it at the bottom.
43
63
  - Move, resize, align with guides; drag a box to select several; <kbd>Delete</kbd> removes, <kbd>Ctrl/⌘ D</kbd> duplicates.
44
64
  - 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.
65
+ - **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.
66
+ - With nothing selected, the panel shows the **screen**: the table it saves to, its width, and the font and colours every control inherits.
67
+ - **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.
68
+ - **Publish** (when `onPublish` is given) makes the draft the version everyone sees. It may be refused — e.g. the table has no column for a new field — and the reason, with the migration to run, is shown.
69
+ - **`lockedNames`**: fields that are already columns of the table. Their names are read-only, because renaming one would leave its data behind.
46
70
 
47
71
  ## The screen
48
72
 
@@ -51,14 +75,27 @@ import { FactoryScreen } from '@xeplr/ui-factory'
51
75
 
52
76
  <FactoryScreen
53
77
  document={screen}
54
- values={employee} // optional: the record being edited
78
+ record={employee} // optional: open an existing record
79
+ onSave={async (values, { id, source }) => api.save(source, id, values)} // returns the saved record, with its id
80
+ fetchRecords={async ({ source }) => api.records(source)}
81
+ onDelete={async ({ id, source }) => api.remove(source, id)}
55
82
  fetchOptions={async ({ table }) => api.options(table)}
56
- onSubmit={async (values) => api.saveEmployee(values)}
83
+ screens={{ employee_edit: editScreen }} // what a list's Edit / New open in a popup
57
84
  />
58
85
  ```
59
86
 
87
+ **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.
88
+
60
89
  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
90
 
91
+ ### Lists of saved records
92
+
93
+ 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.
94
+
95
+ 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.
96
+
97
+ `@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.
98
+
62
99
  On your server, check the submission against the same rules with [`@xeplr/schema-handler`](https://www.npmjs.com/package/@xeplr/schema-handler):
63
100
 
64
101
  ```js
@@ -78,8 +115,10 @@ const clean = schemaHandler.applySchema(formSchema(screen), req.body) // throw
78
115
  | `date` | `"YYYY-MM-DD"` | `min`, `max` |
79
116
  | `checkbox` | boolean | `required` = must be ticked |
80
117
  | `dropdown` | option id | — |
81
- | `label` | — | heading, subheading or text |
82
- | `button` | — | submit or reset |
118
+ | `label` | — | text with a heading, subheading or text preset |
119
+ | `list` | — | saved records, with New / Edit / Delete |
120
+
121
+ 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
122
 
84
123
  ### Dropdown options
85
124
 
@@ -107,14 +146,18 @@ const screen = screenFromSpec({
107
146
  { label: 'Department', type: 'dropdown', table: 'departments', required: true },
108
147
  { label: 'Employment type', type: 'dropdown', options: ['Full time', 'Part time'] },
109
148
  { label: 'Start date', type: 'date' }
110
- ]
149
+ ],
150
+ source: 'employees',
151
+ list: true
111
152
  })
112
153
  ```
113
154
 
114
155
  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
156
 
116
157
  ```sh
117
- npx xeplr-factory generate spec.json -o screen.json
158
+ npx xeplr-factory screens entity.json -o dir # an entity → list + edit screens and pages
159
+ npx xeplr-factory migration edit.screen.json -o migrations # the entity's table, as a migration
160
+ npx xeplr-factory generate spec.json -o screen.json # one screen from a spec
118
161
  npx xeplr-factory validate screen.json # every problem, with its path
119
162
  npx xeplr-factory controls # controls, props and rules
120
163
  ```
@@ -127,7 +170,8 @@ npx xeplr-factory controls # controls, props and rules
127
170
  {
128
171
  "kind": "xeplr-screen", "version": 1,
129
172
  "id": "new_employee", "name": "New employee",
130
- "units": "fraction", "aspect": 1,
173
+ "source": "employees",
174
+ "units": "fraction", "aspect": 1, "width": 800,
131
175
  "nodes": [
132
176
  { "id": "firstName", "type": "text", "x": 0.04, "y": 0.15, "w": 0.4475, "h": 0.08, "z": 2,
133
177
  "props": { "label": "First name", "name": "firstName", "required": true, "validation": { "maxLength": 80 } } }
@@ -135,7 +179,7 @@ npx xeplr-factory controls # controls, props and rules
135
179
  }
136
180
  ```
137
181
 
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.
182
+ 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
183
 
140
184
  ## Three ways to use it
141
185
 
@@ -151,7 +195,7 @@ Styles are namespaced `.xeplr-factory-*` and read the xeplr theme variables with
151
195
 
152
196
  ```sh
153
197
  npm install
154
- npm run dev # http://localhost:19006 — builder + saved screen, with an in-memory stand-in app
198
+ npm run dev # http://localhost:19006 — Screen · List / Edit and Designer · List / Edit for the example employee, records kept in localStorage
155
199
  ```
156
200
 
157
201
  ## Files
@@ -161,15 +205,18 @@ src/
161
205
  controls.js ─ the control registry: palette, property panel contract, rules
162
206
  document.js ─ create and edit screen documents
163
207
  validateDocument.js ─ the document checker
164
- values.js ─ initial values, parsing, field validation, formSchema
165
- generate.js ─ screenFromSpec
208
+ values.js ─ values, validation, autosave readiness, records and list columns, formSchema
209
+ generate.js ─ screenFromSpec, screensFromSpec (list + edit), entity names
210
+ scaffold.js ─ an entity → its screen JSON and .jsx pages
211
+ tableSchema.js ─ a form → its table; CREATE / ALTER TABLE migrations, safe changes only
212
+ remote.js ─ createFactoryApi: the calls to @xeplr/factory's routes
166
213
  model.js ─ everything above, React-free
167
214
  useFactoryBuilder.js ─ builder controller
168
215
  useFactoryScreen.js ─ screen controller
169
- designs/ ─ BuilderSample, ScreenSample, ControlView, Palette, PropertyPanel, factory.css
216
+ designs/ ─ BuilderSample, ScreenSample, ControlView, ListView, Palette, PropertyPanel, styles.js, factory.css
170
217
  pages.jsx ─ FactoryBuilder, FactoryScreen
171
- bin/xeplr-factory.js ─ generate / validate / controls / schema
172
- examples/ ─ new-employee spec and screen
218
+ bin/xeplr-factory.js ─ screens / generate / validate / controls / schema
219
+ examples/ ─ employee.entity.json and what `screens` makes of it
173
220
  dev/ ─ the stand-in app for `npm run dev` (not published)
174
221
  ```
175
222
 
@@ -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 and their .jsx pages
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,63 @@ 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
+ const targets = Object.keys(result.files).map((f) => path.join(dir, f))
85
+ // Never overwrite a screen someone has since refined in the designer.
86
+ const existing = targets.filter((t) => existsSync(t))
87
+ if (existing.length && !force) fail(`would overwrite ${existing.join(', ')} — pass --force to replace them`)
88
+ mkdirSync(dir, { recursive: true })
89
+ Object.entries(result.files).forEach(([f, text]) => writeFileSync(path.join(dir, f), text))
90
+ process.stderr.write(`wrote ${targets.join(', ')}\n`)
91
+ process.stderr.write(`list screen "${result.list.id}" opens "${result.edit.id}" for Edit / New; both use table "${result.edit.source}"\n`)
92
+ break
93
+ }
94
+
95
+ case 'migration': {
96
+ if (!args[0]) fail(USAGE, 2)
97
+ const f = args.indexOf('--from')
98
+ const o = args.indexOf('-o')
99
+ const next = readJson(args[0])
100
+ const previous = f !== -1 ? readJson(args[f + 1]) : null
101
+ for (const [label, doc] of [['screen', next], ['--from screen', previous]]) {
102
+ if (!doc) continue
103
+ const { ok, errors } = validateDocument(doc)
104
+ if (!ok) fail(`the ${label} is not valid — run validate first (${errors.length} problems)`)
105
+ }
106
+ let result
107
+ try {
108
+ result = migrationFor(previous, next)
109
+ } catch (err) {
110
+ fail(err.message)
111
+ }
112
+ if (result.empty) {
113
+ process.stderr.write(`"${result.table}" already matches the form — no migration needed\n`)
114
+ if (result.diff.unused.length) process.stderr.write(`(columns no longer on the screen, kept: ${result.diff.unused.map((c) => c.name).join(', ')})\n`)
115
+ break
116
+ }
117
+ if (o !== -1) {
118
+ const dir = args[o + 1]
119
+ const existing = existsSync(dir) ? readdirSync(dir).filter((x) => x.endsWith('.sql')) : []
120
+ const file = path.join(dir, nextMigrationName(existing, result.table, result.diff.create))
121
+ mkdirSync(dir, { recursive: true })
122
+ writeFileSync(file, result.sql)
123
+ process.stderr.write(`wrote ${file}\n`)
124
+ } else {
125
+ process.stdout.write(result.sql)
126
+ }
127
+ break
128
+ }
129
+
65
130
  case 'validate': {
66
131
  if (!args[0]) fail(USAGE, 2)
67
132
  const { ok, errors } = validateDocument(readJson(args[0]))
@@ -80,6 +145,7 @@ switch (cmd) {
80
145
  process.stdout.write(`${c.type.padEnd(9)} ${c.label}${c.input ? ` — value: ${c.valueType || 'option id'}` : ''}\n`)
81
146
  process.stdout.write(` props: ${c.props.join(', ')}\n`)
82
147
  if (c.validation.length) process.stdout.write(` validation: ${c.validation.join(', ')}\n`)
148
+ if (c.styles && c.styles.length) process.stdout.write(` style: ${c.styles.join(', ')}\n`)
83
149
  })
84
150
  break
85
151
  }
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.2",
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"