@xeplr/ui-factory 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +133 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/bin/xeplr-factory.js +98 -0
- package/package.json +59 -0
- package/src/controls.js +238 -0
- package/src/designs/BuilderSample.jsx +117 -0
- package/src/designs/ControlView.jsx +192 -0
- package/src/designs/Palette.jsx +34 -0
- package/src/designs/PropertyPanel.jsx +228 -0
- package/src/designs/ScreenSample.jsx +65 -0
- package/src/designs/factory.css +177 -0
- package/src/designs/index.js +7 -0
- package/src/document.js +215 -0
- package/src/generate.js +167 -0
- package/src/index.js +19 -0
- package/src/model.js +15 -0
- package/src/pages.jsx +42 -0
- package/src/propertyPath.js +35 -0
- package/src/useFactoryBuilder.js +209 -0
- package/src/useFactoryScreen.js +155 -0
- package/src/validateDocument.js +202 -0
- package/src/values.js +148 -0
package/AUTHORING.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Authoring screens with @xeplr/ui-factory
|
|
2
|
+
|
|
3
|
+
This guide is for whoever **writes** a screen definition — usually Claude, from a request like *"make a form for a new employee with name, email, department and start date"*. A person then refines the result in the builder.
|
|
4
|
+
|
|
5
|
+
## The workflow
|
|
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:
|
|
9
|
+
```sh
|
|
10
|
+
npx xeplr-factory generate spec.json -o screen.json
|
|
11
|
+
```
|
|
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
|
+
```sh
|
|
15
|
+
npx xeplr-factory validate screen.json
|
|
16
|
+
```
|
|
17
|
+
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.
|
|
19
|
+
|
|
20
|
+
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
|
+
|
|
22
|
+
## The spec
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"name": "New employee",
|
|
27
|
+
"columns": 2,
|
|
28
|
+
"fields": [
|
|
29
|
+
{ "label": "First name", "required": true, "validation": { "maxLength": 80 } },
|
|
30
|
+
{ "label": "Last name", "required": true },
|
|
31
|
+
{ "label": "Work email", "required": true,
|
|
32
|
+
"validation": { "pattern": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$", "patternMessage": "Enter a valid email address" } },
|
|
33
|
+
{ "type": "label", "text": "Employment", "variant": "subheading" },
|
|
34
|
+
{ "label": "Department", "type": "dropdown", "table": "departments", "required": true },
|
|
35
|
+
{ "label": "Employment type", "type": "dropdown", "options": ["Full time", "Part time", "Contract"] },
|
|
36
|
+
{ "label": "Start date", "type": "date", "required": true },
|
|
37
|
+
{ "label": "Annual salary", "type": "number", "validation": { "min": 0 } },
|
|
38
|
+
{ "label": "Remote", "type": "checkbox" },
|
|
39
|
+
{ "label": "Notes", "type": "textarea", "validation": { "maxLength": 1000 } }
|
|
40
|
+
],
|
|
41
|
+
"submit": "Create employee",
|
|
42
|
+
"reset": "Clear"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
| key | meaning |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `name` | **required** — the screen's title |
|
|
49
|
+
| `id` | document id; defaults to the name in snake_case |
|
|
50
|
+
| `columns` | `1` or `2` (default `2`) |
|
|
51
|
+
| `heading` | `true` (default) puts the name at the top; a string uses that text; `false` for none |
|
|
52
|
+
| `fields` | **required** — in reading order |
|
|
53
|
+
| `submit` | submit button label (default `"Save"`); `false` for none |
|
|
54
|
+
| `reset` | reset button label; omit for none |
|
|
55
|
+
|
|
56
|
+
Each field:
|
|
57
|
+
|
|
58
|
+
| key | meaning |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `label` | **required** for inputs — shown above the field |
|
|
61
|
+
| `type` | `text` (default), `textarea`, `number`, `date`, `checkbox`, `dropdown`, or `label` for a section heading |
|
|
62
|
+
| `name` | the key the value is saved under; defaults to the label in camelCase (`"Date of birth"` → `dateOfBirth`) |
|
|
63
|
+
| `required` | `true` to require a value (a checkbox must be ticked) |
|
|
64
|
+
| `placeholder` | text, textarea, number, dropdown |
|
|
65
|
+
| `default` | starting value — a string, a number, `true`/`false`, or a date as `"YYYY-MM-DD"` |
|
|
66
|
+
| `validation` | see below |
|
|
67
|
+
| `width` | `"full"` to take the whole row in a two-column form (textareas and labels always do) |
|
|
68
|
+
| `options` | dropdown: `["Full time", "Part time"]` (ids are made from the names: `full_time`) or `[{ "id": "ft", "name": "Full time" }]` |
|
|
69
|
+
| `table` | dropdown: read options from this table in the app's database |
|
|
70
|
+
| `text`, `variant` | `label` only — `variant` is `heading`, `subheading` (default for a section) or `text` |
|
|
71
|
+
|
|
72
|
+
## Validation rules
|
|
73
|
+
|
|
74
|
+
| control | rules |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `text` | `minLength`, `maxLength`, `pattern` (a regex string), `patternMessage` (shown when it fails) |
|
|
77
|
+
| `textarea` | `minLength`, `maxLength` |
|
|
78
|
+
| `number` | `min`, `max`, `integer` |
|
|
79
|
+
| `date` | `min`, `max` — as `"YYYY-MM-DD"` |
|
|
80
|
+
| `checkbox`, `dropdown` | none (use `required`) |
|
|
81
|
+
|
|
82
|
+
Anything else is rejected, with the list of what is allowed.
|
|
83
|
+
|
|
84
|
+
## Dropdowns
|
|
85
|
+
|
|
86
|
+
A dropdown **always saves an `id` and shows a `name`.**
|
|
87
|
+
|
|
88
|
+
- **Fixed values** — `options`. Use when the choices belong to the form: employment type, yes/no/maybe, priority.
|
|
89
|
+
- **A table** — `table`. Use when the choices are records the app already keeps: departments, locations, managers. The factory never queries the database: the app returns `[{ id, name }]` rows for the table. Use the app's real table name; if you do not know it, ask, or leave a fixed list and say so.
|
|
90
|
+
|
|
91
|
+
## Choosing controls
|
|
92
|
+
|
|
93
|
+
| the request says | use |
|
|
94
|
+
|---|---|
|
|
95
|
+
| a name, title, email, phone, code | `text` (add a `pattern` for email/phone/code formats) |
|
|
96
|
+
| notes, description, address, comments | `textarea` |
|
|
97
|
+
| amount, salary, quantity, age | `number` (`integer: true` for counts) |
|
|
98
|
+
| a date — birth, start, due | `date` |
|
|
99
|
+
| yes/no, agree, active, remote | `checkbox` |
|
|
100
|
+
| one of a known set | `dropdown` with `options` |
|
|
101
|
+
| one of the app's records | `dropdown` with `table` |
|
|
102
|
+
| a section title between groups | `{ "type": "label", "text": "…", "variant": "subheading" }` |
|
|
103
|
+
|
|
104
|
+
Keep labels short and in sentence case ("Start date", not "START DATE:"). Mark only what the request needs as required.
|
|
105
|
+
|
|
106
|
+
## The document, if you edit it directly
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"kind": "xeplr-screen", "version": 1,
|
|
111
|
+
"id": "new_employee", "name": "New employee",
|
|
112
|
+
"units": "fraction", "aspect": 1,
|
|
113
|
+
"nodes": [
|
|
114
|
+
{ "id": "firstName", "type": "text", "x": 0.04, "y": 0.15, "w": 0.4475, "h": 0.08, "z": 2,
|
|
115
|
+
"props": { "label": "First name", "name": "firstName", "required": true } }
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- `x`, `w` are fractions of the screen width; `y`, `h` fractions of a page (with `aspect: 1`, also of the width). `x + w` must not exceed 1.
|
|
121
|
+
- Node `id`s are unique; input `props.name`s are unique and match `^[A-Za-z_][A-Za-z0-9_]*$`.
|
|
122
|
+
- `props` allowed per control: `npx xeplr-factory controls`.
|
|
123
|
+
|
|
124
|
+
## Commands
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
npx xeplr-factory generate <spec.json|-> [-o <screen.json>] # spec → document
|
|
128
|
+
npx xeplr-factory validate <screen.json|-> # exit 1 with every problem listed
|
|
129
|
+
npx xeplr-factory controls # controls, props, validation rules
|
|
130
|
+
npx xeplr-factory schema <screen.json|-> # the form's @xeplr/schema-handler schema
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The `schema` output is what the app's server checks a submission against: `applySchema(formSchema(document), body)`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Xeplr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# @xeplr/ui-factory
|
|
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.
|
|
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.
|
|
6
|
+
|
|
7
|
+
(The package name on npm is `@xeplr/ui-factory` — the GitHub repo and folder are named `xeplr-ui-factory`.)
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
Most internal apps are a long tail of forms, and every one of them is hand-built: markup, layout, validation in the browser, validation again on the server, and dropdowns wired to tables. This turns a form into **data**:
|
|
12
|
+
|
|
13
|
+
- a person — or Claude — describes the screen once;
|
|
14
|
+
- the builder lets anyone move, resize and adjust it without a developer;
|
|
15
|
+
- the renderer runs it, and the server checks submissions against the **same** rules.
|
|
16
|
+
|
|
17
|
+
**The factory never has a database.** Your app stores screen documents and supplies the rows dropdowns read. The factory draws, edits and validates.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm i @xeplr/ui-factory @xeplr/ui-canvas
|
|
23
|
+
```
|
|
24
|
+
|
|
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).
|
|
26
|
+
|
|
27
|
+
## The builder
|
|
28
|
+
|
|
29
|
+
```jsx
|
|
30
|
+
import { FactoryBuilder } from '@xeplr/ui-factory'
|
|
31
|
+
|
|
32
|
+
<div style={{ height: '100vh' }}>
|
|
33
|
+
<FactoryBuilder
|
|
34
|
+
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
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- Drag controls from the palette onto the screen, or click one to add it at the bottom.
|
|
43
|
+
- Move, resize, align with guides; drag a box to select several; <kbd>Delete</kbd> removes, <kbd>Ctrl/⌘ D</kbd> duplicates.
|
|
44
|
+
- 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.
|
|
46
|
+
|
|
47
|
+
## The screen
|
|
48
|
+
|
|
49
|
+
```jsx
|
|
50
|
+
import { FactoryScreen } from '@xeplr/ui-factory'
|
|
51
|
+
|
|
52
|
+
<FactoryScreen
|
|
53
|
+
document={screen}
|
|
54
|
+
values={employee} // optional: the record being edited
|
|
55
|
+
fetchOptions={async ({ table }) => api.options(table)}
|
|
56
|
+
onSubmit={async (values) => api.saveEmployee(values)}
|
|
57
|
+
/>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
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
|
+
|
|
62
|
+
On your server, check the submission against the same rules with [`@xeplr/schema-handler`](https://www.npmjs.com/package/@xeplr/schema-handler):
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { formSchema } from '@xeplr/ui-factory/model'
|
|
66
|
+
import schemaHandler from '@xeplr/schema-handler' // CommonJS: default import in ESM,
|
|
67
|
+
// or require('@xeplr/schema-handler')
|
|
68
|
+
const clean = schemaHandler.applySchema(formSchema(screen), req.body) // throws ValidationError listing each field
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Controls
|
|
72
|
+
|
|
73
|
+
| type | value | validation |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `text` | string | `minLength`, `maxLength`, `pattern`, `patternMessage` |
|
|
76
|
+
| `textarea` | string | `minLength`, `maxLength` |
|
|
77
|
+
| `number` | number | `min`, `max`, `integer` |
|
|
78
|
+
| `date` | `"YYYY-MM-DD"` | `min`, `max` |
|
|
79
|
+
| `checkbox` | boolean | `required` = must be ticked |
|
|
80
|
+
| `dropdown` | option id | — |
|
|
81
|
+
| `label` | — | heading, subheading or text |
|
|
82
|
+
| `button` | — | submit or reset |
|
|
83
|
+
|
|
84
|
+
### Dropdown options
|
|
85
|
+
|
|
86
|
+
A dropdown **always saves an `id` and shows a `name`**, from either source:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{ "source": "static", "options": [{ "id": "ft", "name": "Full time" }, { "id": "pt", "name": "Part time" }] }
|
|
90
|
+
{ "source": "table", "table": "departments" }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
For a table, your `fetchOptions({ table })` returns `[{ id, name }]` — in SQL terms, `select id, name from departments`. The factory never builds or runs a query.
|
|
94
|
+
|
|
95
|
+
## Drafting screens with Claude
|
|
96
|
+
|
|
97
|
+
A screen can start from a **spec** — what is on it, in order — instead of coordinates:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
import { screenFromSpec } from '@xeplr/ui-factory/model'
|
|
101
|
+
|
|
102
|
+
const screen = screenFromSpec({
|
|
103
|
+
name: 'New employee',
|
|
104
|
+
fields: [
|
|
105
|
+
{ label: 'First name', required: true },
|
|
106
|
+
{ label: 'Last name', required: true },
|
|
107
|
+
{ label: 'Department', type: 'dropdown', table: 'departments', required: true },
|
|
108
|
+
{ label: 'Employment type', type: 'dropdown', options: ['Full time', 'Part time'] },
|
|
109
|
+
{ label: 'Start date', type: 'date' }
|
|
110
|
+
]
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
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
|
+
|
|
116
|
+
```sh
|
|
117
|
+
npx xeplr-factory generate spec.json -o screen.json
|
|
118
|
+
npx xeplr-factory validate screen.json # every problem, with its path
|
|
119
|
+
npx xeplr-factory controls # controls, props and rules
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
[AUTHORING.md](./AUTHORING.md) is the full guide for writing screens: the spec, every control and rule, and how to choose between them.
|
|
123
|
+
|
|
124
|
+
## The document
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"kind": "xeplr-screen", "version": 1,
|
|
129
|
+
"id": "new_employee", "name": "New employee",
|
|
130
|
+
"units": "fraction", "aspect": 1,
|
|
131
|
+
"nodes": [
|
|
132
|
+
{ "id": "firstName", "type": "text", "x": 0.04, "y": 0.15, "w": 0.4475, "h": 0.08, "z": 2,
|
|
133
|
+
"props": { "label": "First name", "name": "firstName", "required": true, "validation": { "maxLength": 80 } } }
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
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.
|
|
139
|
+
|
|
140
|
+
## Three ways to use it
|
|
141
|
+
|
|
142
|
+
1. **Ready-made** — `<FactoryBuilder>` and `<FactoryScreen>`.
|
|
143
|
+
2. **Your own design** — `useFactoryBuilder` / `useFactoryScreen` hold all the state and actions; draw them however you like.
|
|
144
|
+
3. **Model only** — `@xeplr/ui-factory/model` has no React: `screenFromSpec`, `validateDocument`, `formSchema`, `validateValues`, and the document operations (`addControl`, `moveNode`, `setNodeProperty`, …).
|
|
145
|
+
|
|
146
|
+
## Theming
|
|
147
|
+
|
|
148
|
+
Styles are namespaced `.xeplr-factory-*` and read the xeplr theme variables with light fallbacks: `--xeplr-accent`, `--xeplr-text`, `--xeplr-muted`, `--xeplr-border`, `--xeplr-surface`, `--xeplr-danger`, `--xeplr-input-bg`, `--xeplr-input-border`.
|
|
149
|
+
|
|
150
|
+
## Try it
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
npm install
|
|
154
|
+
npm run dev # http://localhost:19006 — builder + saved screen, with an in-memory stand-in app
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Files
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
src/
|
|
161
|
+
controls.js ─ the control registry: palette, property panel contract, rules
|
|
162
|
+
document.js ─ create and edit screen documents
|
|
163
|
+
validateDocument.js ─ the document checker
|
|
164
|
+
values.js ─ initial values, parsing, field validation, formSchema
|
|
165
|
+
generate.js ─ screenFromSpec
|
|
166
|
+
model.js ─ everything above, React-free
|
|
167
|
+
useFactoryBuilder.js ─ builder controller
|
|
168
|
+
useFactoryScreen.js ─ screen controller
|
|
169
|
+
designs/ ─ BuilderSample, ScreenSample, ControlView, Palette, PropertyPanel, factory.css
|
|
170
|
+
pages.jsx ─ FactoryBuilder, FactoryScreen
|
|
171
|
+
bin/xeplr-factory.js ─ generate / validate / controls / schema
|
|
172
|
+
examples/ ─ new-employee spec and screen
|
|
173
|
+
dev/ ─ the stand-in app for `npm run dev` (not published)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Tests
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
npm test
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// xeplr-factory — the command-line side of @xeplr/ui-factory.
|
|
3
|
+
//
|
|
4
|
+
// Mostly for Claude, which drafts screens from plain-English requests: it can
|
|
5
|
+
// turn a short spec into a laid-out document, and check a document before
|
|
6
|
+
// handing it to an app, with errors precise enough to fix without guessing.
|
|
7
|
+
//
|
|
8
|
+
// xeplr-factory generate spec.json [-o screen.json] spec → laid-out document
|
|
9
|
+
// xeplr-factory validate screen.json check a document (exit 1 on problems)
|
|
10
|
+
// xeplr-factory controls the controls and their props
|
|
11
|
+
// xeplr-factory schema screen.json the form's @xeplr/schema-handler schema
|
|
12
|
+
//
|
|
13
|
+
// "-" as the file reads stdin.
|
|
14
|
+
|
|
15
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
16
|
+
import { CONTROLS } from '../src/controls.js'
|
|
17
|
+
import { screenFromSpec } from '../src/generate.js'
|
|
18
|
+
import { validateDocument } from '../src/validateDocument.js'
|
|
19
|
+
import { formSchema } from '../src/values.js'
|
|
20
|
+
|
|
21
|
+
const USAGE = `usage:
|
|
22
|
+
xeplr-factory generate <spec.json|-> [-o <out.json>]
|
|
23
|
+
xeplr-factory validate <screen.json|->
|
|
24
|
+
xeplr-factory controls
|
|
25
|
+
xeplr-factory schema <screen.json|->`
|
|
26
|
+
|
|
27
|
+
function readJson(file) {
|
|
28
|
+
const text = readFileSync(file === '-' ? 0 : file, 'utf8')
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(text)
|
|
31
|
+
} catch (err) {
|
|
32
|
+
fail(`${file === '-' ? 'stdin' : file} is not valid JSON: ${err.message}`)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fail(message, code = 1) {
|
|
37
|
+
process.stderr.write(message + '\n')
|
|
38
|
+
process.exit(code)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function out(json, outFile) {
|
|
42
|
+
const text = JSON.stringify(json, null, 2) + '\n'
|
|
43
|
+
if (outFile) writeFileSync(outFile, text)
|
|
44
|
+
else process.stdout.write(text)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [cmd, ...args] = process.argv.slice(2)
|
|
48
|
+
|
|
49
|
+
switch (cmd) {
|
|
50
|
+
case 'generate': {
|
|
51
|
+
if (!args[0]) fail(USAGE, 2)
|
|
52
|
+
const o = args.indexOf('-o')
|
|
53
|
+
const outFile = o !== -1 ? args[o + 1] : null
|
|
54
|
+
let doc
|
|
55
|
+
try {
|
|
56
|
+
doc = screenFromSpec(readJson(args[0]))
|
|
57
|
+
} catch (err) {
|
|
58
|
+
fail(err.message)
|
|
59
|
+
}
|
|
60
|
+
out(doc, outFile)
|
|
61
|
+
if (outFile) process.stderr.write(`wrote ${outFile} — ${doc.nodes.length} controls\n`)
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case 'validate': {
|
|
66
|
+
if (!args[0]) fail(USAGE, 2)
|
|
67
|
+
const { ok, errors } = validateDocument(readJson(args[0]))
|
|
68
|
+
if (ok) {
|
|
69
|
+
process.stdout.write('ok — valid screen document\n')
|
|
70
|
+
} else {
|
|
71
|
+
process.stdout.write(`${errors.length} problem${errors.length === 1 ? '' : 's'}:\n`)
|
|
72
|
+
errors.forEach((e) => process.stdout.write(` ${e.path || '(document)'}: ${e.message}\n`))
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case 'controls': {
|
|
79
|
+
Object.values(CONTROLS).forEach((c) => {
|
|
80
|
+
process.stdout.write(`${c.type.padEnd(9)} ${c.label}${c.input ? ` — value: ${c.valueType || 'option id'}` : ''}\n`)
|
|
81
|
+
process.stdout.write(` props: ${c.props.join(', ')}\n`)
|
|
82
|
+
if (c.validation.length) process.stdout.write(` validation: ${c.validation.join(', ')}\n`)
|
|
83
|
+
})
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case 'schema': {
|
|
88
|
+
if (!args[0]) fail(USAGE, 2)
|
|
89
|
+
const doc = readJson(args[0])
|
|
90
|
+
const { ok, errors } = validateDocument(doc)
|
|
91
|
+
if (!ok) fail(`invalid document — run validate first (${errors.length} problems)`)
|
|
92
|
+
out(formSchema(doc))
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
default:
|
|
97
|
+
fail(USAGE, cmd ? 2 : 0)
|
|
98
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
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",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./model": "./src/model.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"xeplr-factory": "bin/xeplr-factory.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src/",
|
|
17
|
+
"bin/",
|
|
18
|
+
"AUTHORING.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "vite",
|
|
22
|
+
"test": "for f in test/*.test.js; do node \"$f\" || exit 1; done"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"form-builder",
|
|
26
|
+
"ui-builder",
|
|
27
|
+
"screen-designer",
|
|
28
|
+
"canvas",
|
|
29
|
+
"react",
|
|
30
|
+
"low-code",
|
|
31
|
+
"claude",
|
|
32
|
+
"xeplr"
|
|
33
|
+
],
|
|
34
|
+
"author": "xeplr",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/Xeplr/xeplr-ui-factory"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/Xeplr/xeplr-ui-factory#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/Xeplr/xeplr-ui-factory/issues"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@xeplr/ui-canvas": "^1.0.2",
|
|
49
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@vitejs/plugin-react": "^5.2.0",
|
|
53
|
+
"@xeplr/schema-handler": "^1.0.5",
|
|
54
|
+
"@xeplr/ui-canvas": "^1.0.1",
|
|
55
|
+
"react": "^19.2.4",
|
|
56
|
+
"react-dom": "^19.2.4",
|
|
57
|
+
"vite": "^7.3.1"
|
|
58
|
+
}
|
|
59
|
+
}
|