@xeplr/ui-factory 1.0.14 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,16 @@
1
1
  /* @xeplr/ui-factory — builder and screen.
2
- Colours come from the xeplr theme variables, each with a light fallback, so
3
- a host that defines none still gets a finished look.
2
+ Colours come from the app's theme — @xeplr/ui-account's --xeplr-* variables
3
+ (theme.css) — each with a light fallback, so a host that defines none still
4
+ gets a finished look.
5
+
6
+ THE REAL NAMES, NOT LOOK-ALIKES. This file used to read --xeplr-text,
7
+ --xeplr-surface, --xeplr-border and --xeplr-muted, which no theme defines —
8
+ so every fallback won, and in a dark app a screen came out as a white sheet
9
+ while the @xeplr/ui-table inside it (which reads the real names) came out
10
+ dark. Everything below resolves through the --xf-* tokens, and those read
11
+ the variables theme.css actually sets. "Elevated" is --xeplr-bg-tertiary in
12
+ every built-in theme (white in light, the lighter grey in dark), which is
13
+ what a sheet, a panel and a popup are.
4
14
 
5
15
  ONE SIZE UNIT, --xf-u: one pixel at the screen's design width (--xf-width,
6
16
  set from the document). At that width or wider it IS a pixel — text is
@@ -12,11 +22,24 @@
12
22
  .xeplr-factory-flow-builder {
13
23
  container-type: inline-size;
14
24
  --xf-u: min(1px, calc(100cqw / var(--xf-width, 800)));
25
+ }
26
+ .xeplr-factory-screen,
27
+ .xeplr-factory-canvas,
28
+ .xeplr-factory-flow-builder,
29
+ .xeplr-factory-builder,
30
+ .xeplr-factory-modal-backdrop {
15
31
  --xf-accent: var(--xeplr-accent, #2563eb);
16
- --xf-ink: var(--xeplr-text, #1a1a19);
17
- --xf-muted: var(--xeplr-muted, #6b6a63);
18
- --xf-border: var(--xeplr-border, #d9d8d0);
19
- --xf-surface: var(--xeplr-surface, #ffffff);
32
+ --xf-accent-ink: var(--xeplr-accent-text, #ffffff);
33
+ --xf-ink: var(--xeplr-text-primary, #1a1a19);
34
+ --xf-muted: var(--xeplr-text-secondary, #6b6a63);
35
+ --xf-border: var(--xeplr-border-primary, #d9d8d0);
36
+ --xf-surface: var(--xeplr-bg-tertiary, #ffffff);
37
+ --xf-plane: var(--xeplr-bg-primary, #f7f7f5);
38
+ --xf-hover: var(--xeplr-bg-hover, #f0f0ee);
39
+ --xf-field-bg: var(--xeplr-input-bg, var(--xf-surface));
40
+ --xf-field-border: var(--xeplr-input-border, var(--xf-border));
41
+ --xf-good: var(--xeplr-success, #1d7a4c);
42
+ --xf-warn: var(--xeplr-warning, #a86a00);
20
43
  --xf-danger: var(--xeplr-danger, #c2372e);
21
44
  }
22
45
 
@@ -28,21 +51,19 @@
28
51
  host with --xeplr-factory-paper. */
29
52
  .xeplr-factory-screen {
30
53
  position: relative; width: 100%;
54
+ /* Centred in whatever it is given: shown at its design width, a screen that
55
+ hugs the left edge of a wide page looks abandoned there. */
56
+ margin-inline: auto;
57
+ box-sizing: border-box;
31
58
  color: var(--xf-ink);
32
59
  background: var(--xeplr-factory-paper, var(--xf-surface));
33
- border-radius: calc(8 * var(--xf-u));
34
- }
35
- /* The status line and the step buttons live on the sheet, so they keep a
36
- margin of their own rather than touching its edge. */
37
- .xeplr-factory-screen > .xeplr-factory-status,
38
- .xeplr-factory-screen > .xeplr-factory-stepnav,
39
- .xeplr-factory-screen > .xeplr-factory-done { padding-inline: calc(12 * var(--xf-u)); }
40
- .xeplr-factory-status {
41
- display: flex; justify-content: space-between; gap: 12px; min-height: 18px;
42
- padding: 4px 2px 8px; font-size: 12px; color: var(--xf-muted);
43
- }
44
- .xeplr-factory-status--saved { color: var(--xeplr-status-good, #1d7a4c); }
45
- .xeplr-factory-status--invalid, .xeplr-factory-status--error { color: var(--xf-danger); }
60
+ border: 1px solid var(--xf-border);
61
+ border-radius: 10px;
62
+ }
63
+ /* A screen of lists only (nothing to fill in) paints no sheet: the list is
64
+ its own card, and a sheet around it is a second frame with a block of empty
65
+ paper under the rows. */
66
+ .xeplr-factory-screen--bare { background: none; border: none; }
46
67
  .xeplr-factory-page { position: relative; width: 100%; margin: 0; }
47
68
  .xeplr-factory-node { position: absolute; box-sizing: border-box; }
48
69
 
@@ -66,15 +87,17 @@
66
87
  .xeplr-factory-required { color: var(--xf-danger); }
67
88
  .xeplr-factory-input {
68
89
  flex: 1; min-height: 0; width: 100%; box-sizing: border-box;
69
- /* The screen's own colours, never the host theme's input tokens: a dark theme
70
- would give the box a dark background while the text stays the screen's
71
- dark ink — unreadable. A screen's look is its style properties. */
72
- font: inherit; color: var(--xf-ink); background: var(--xf-surface);
73
- border: 1px solid var(--xf-border); border-radius: calc(6 * var(--xf-u));
90
+ /* The app's input colours — unless the SCREEN sets its own colours, when
91
+ screenStyle points --xf-ink / --xf-field-bg at those, so a screen designed
92
+ in dark ink on paper never gets a dark theme's box under that ink. */
93
+ font: inherit; color: var(--xf-ink); background: var(--xf-field-bg);
94
+ border: 1px solid var(--xf-field-border); border-radius: calc(6 * var(--xf-u));
74
95
  padding: 0 calc(10 * var(--xf-u));
96
+ transition: border-color .12s, box-shadow .12s;
75
97
  }
98
+ .xeplr-factory-input:hover { border-color: color-mix(in srgb, var(--xf-accent) 45%, var(--xf-field-border)); }
76
99
  .xeplr-factory-textarea { padding-block: calc(8 * var(--xf-u)); resize: none; }
77
- .xeplr-factory-input:focus { outline: 2px solid color-mix(in srgb, var(--xf-accent) 40%, transparent); border-color: var(--xf-accent); }
100
+ .xeplr-factory-input:focus { outline: none; border-color: var(--xf-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--xf-accent) 25%, transparent); }
78
101
  .xeplr-factory-field.has-error .xeplr-factory-input { border-color: var(--xf-danger); }
79
102
  .xeplr-factory-error {
80
103
  /* In the gap BELOW the field, out of flow: a message appearing must not
@@ -92,7 +115,10 @@
92
115
  list of options cannot cover the next field. */
93
116
  .xeplr-factory-choices { display: flex; gap: calc(6 * var(--xf-u)) calc(16 * var(--xf-u)); overflow: auto; min-height: 0; }
94
117
  .xeplr-factory-choices--vertical { flex-direction: column; }
95
- .xeplr-factory-choices--horizontal { flex-direction: row; flex-wrap: wrap; align-items: center; }
118
+ /* Side by side, the options sit in the band an input box would fill — so a
119
+ row of radios beside a dropdown lines up with the dropdown's box, not with
120
+ its label. */
121
+ .xeplr-factory-choices--horizontal { flex: 1; flex-direction: row; flex-wrap: wrap; align-items: center; align-content: center; }
96
122
  .xeplr-factory-choice { display: flex; align-items: center; gap: calc(8 * var(--xf-u)); cursor: pointer; color: var(--xf-ink); }
97
123
  .xeplr-factory-choice input { width: calc(16 * var(--xf-u)); height: calc(16 * var(--xf-u)); min-width: 11px; min-height: 11px; margin: 0; accent-color: var(--xf-accent); }
98
124
 
@@ -100,48 +126,81 @@
100
126
  .xeplr-factory-file { display: flex; align-items: center; gap: calc(8 * var(--xf-u)); min-width: 0; }
101
127
  .xeplr-factory-file-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--xf-ink); }
102
128
  .xeplr-factory-file-clear {
103
- flex: none; border: 1px solid var(--xf-border); background: var(--xf-surface); color: var(--xf-muted);
129
+ flex: none; border: 1px solid var(--xf-field-border); background: var(--xf-field-bg); color: var(--xf-muted);
104
130
  border-radius: calc(6 * var(--xf-u)); padding: calc(3 * var(--xf-u)) calc(8 * var(--xf-u)); cursor: pointer; font: inherit;
105
131
  }
106
132
  .xeplr-factory-file-clear:hover { color: var(--xf-ink); border-color: var(--xf-accent); }
107
133
  .xeplr-factory-file-input { padding: calc(5 * var(--xf-u)); }
108
134
 
109
135
  /* ── stepper ──────────────────────────────────────────────────────────────
110
- The journey across the top: a circle per step, joined by a line. The line is
111
- drawn behind the circles, from the middle of one to the middle of the next. */
112
- .xeplr-factory-stepper { display: flex; align-items: center; width: 100%; height: 100%; overflow: hidden; }
136
+ The journey across the top: a circle per step, joined by a line. The line
137
+ runs from the EDGE of one circle to the edge of the next, with a small gap —
138
+ never through them. (It used to run centre to centre behind the circles,
139
+ and each step's line was painted after the previous step's circle, so it
140
+ struck straight through the numbers.) --xf-mark is the circle's size. */
141
+ .xeplr-factory-stepper {
142
+ --xf-mark: max(22px, calc(28 * var(--xf-u)));
143
+ display: flex; align-items: center; width: 100%; height: 100%; overflow: hidden;
144
+ }
113
145
  .xeplr-factory-steps { display: flex; align-items: flex-start; width: 100%; margin: 0; padding: 0; list-style: none; }
114
146
  .xeplr-factory-step { flex: 1; position: relative; min-width: 0; text-align: center; }
115
147
  .xeplr-factory-step + .xeplr-factory-step::before {
116
- content: ''; position: absolute; top: calc(13 * var(--xf-u)); right: 50%; left: -50%; height: 2px;
148
+ content: ''; position: absolute;
149
+ top: calc(var(--xf-mark) / 2 - 1px); height: 2px; border-radius: 1px;
150
+ left: calc(-50% + var(--xf-mark) / 2 + 8px);
151
+ right: calc(50% + var(--xf-mark) / 2 + 8px);
117
152
  background: var(--xf-border);
153
+ transition: background .2s;
118
154
  }
119
155
  .xeplr-factory-step.is-active::before, .xeplr-factory-step.is-done::before { background: var(--xf-accent); }
120
156
  .xeplr-factory-step-button {
121
- position: relative; display: flex; flex-direction: column; align-items: center; gap: calc(4 * var(--xf-u));
157
+ position: relative; display: flex; flex-direction: column; align-items: center; gap: calc(6 * var(--xf-u));
122
158
  width: 100%; padding: 0; border: none; background: none; color: var(--xf-muted); font: inherit; cursor: pointer;
123
159
  }
124
160
  .xeplr-factory-step-mark {
125
- display: flex; align-items: center; justify-content: center;
126
- width: calc(26 * var(--xf-u)); height: calc(26 * var(--xf-u)); min-width: 18px; min-height: 18px;
127
- border-radius: 50%; background: var(--xf-border); color: var(--xf-ink); font-weight: 600;
161
+ display: flex; align-items: center; justify-content: center; box-sizing: border-box;
162
+ width: var(--xf-mark); height: var(--xf-mark);
163
+ border-radius: 50%; border: 2px solid var(--xf-border); background: var(--xf-surface);
164
+ color: var(--xf-muted); font-weight: 600; font-size: .9em; line-height: 1;
165
+ transition: background .2s, border-color .2s, box-shadow .2s, transform .08s;
128
166
  }
167
+ .xeplr-factory-step-button:hover .xeplr-factory-step-mark { border-color: var(--xf-accent); }
168
+ .xeplr-factory-step-button:active .xeplr-factory-step-mark { transform: scale(.92); }
129
169
  .xeplr-factory-step-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
130
- .xeplr-factory-step.is-active .xeplr-factory-step-mark,
131
- .xeplr-factory-step.is-done .xeplr-factory-step-mark { background: var(--xf-accent); color: #fff; }
170
+ .xeplr-factory-step.is-done .xeplr-factory-step-mark { background: var(--xf-accent); border-color: var(--xf-accent); color: var(--xf-accent-ink); }
171
+ /* Where you are: filled, with a soft halo. */
172
+ .xeplr-factory-step.is-active .xeplr-factory-step-mark {
173
+ background: var(--xf-accent); border-color: var(--xf-accent); color: var(--xf-accent-ink);
174
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--xf-accent) 22%, transparent);
175
+ }
176
+ .xeplr-factory-step.is-done .xeplr-factory-step-button { color: var(--xf-ink); }
132
177
  .xeplr-factory-step.is-active .xeplr-factory-step-button { color: var(--xf-accent); font-weight: 600; }
178
+ .xeplr-factory-step-button:focus { outline: none; }
179
+ .xeplr-factory-step-button:focus-visible .xeplr-factory-step-mark { outline: 2px solid var(--xf-accent); outline-offset: 3px; }
133
180
  /* A step the app has ruled out: still shown, so the journey reads the same,
134
181
  but plainly not one of the stops. */
135
182
  .xeplr-factory-step.is-off .xeplr-factory-step-button { opacity: 0.4; cursor: default; }
136
183
  .xeplr-factory-step.is-off .xeplr-factory-step-label { text-decoration: line-through; }
137
184
 
138
- /* Back / Next, under the page. */
139
- .xeplr-factory-stepnav { display: flex; align-items: center; gap: 12px; padding: 10px 0; }
140
- .xeplr-factory-stepnav-where { flex: 1; text-align: center; color: var(--xf-muted); font-size: 12px; }
141
- .xeplr-factory-stepnav button[disabled], .xeplr-factory-done button[disabled] { opacity: 0.45; cursor: default; }
142
-
143
- /* Done — a page's way back, when the list opens records on one. */
144
- .xeplr-factory-done { display: flex; justify-content: flex-end; padding: 10px 0; }
185
+ /* ── the footer ─────────────────────────────────────────────────────────────
186
+ ONE bar under the fields: Back on the left, where you are and what the save
187
+ is doing in the middle, the ways forward on the right. Inset by the screen's
188
+ own margin (--xf-margin, set by ScreenSample) so the buttons line up with
189
+ the fields' edges instead of poking out past them. */
190
+ .xeplr-factory-footer {
191
+ display: flex; align-items: center; gap: 10px;
192
+ margin-top: calc(8 * var(--xf-u));
193
+ padding: 14px var(--xf-margin, 16px);
194
+ border-top: 1px solid var(--xf-border);
195
+ }
196
+ .xeplr-factory-footer-mid {
197
+ flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 2px;
198
+ color: var(--xf-muted); font-size: 12px; text-align: center;
199
+ }
200
+ .xeplr-factory-footer-mid:first-child { align-items: flex-start; text-align: left; }
201
+ .xeplr-factory-footer-status--saved { color: var(--xf-good); }
202
+ .xeplr-factory-footer-status--invalid, .xeplr-factory-footer-status--error { color: var(--xf-danger); }
203
+ .xeplr-factory-footer button[disabled] { opacity: 0.4; cursor: default; }
145
204
 
146
205
  /* Which step the builder is on. */
147
206
  .xeplr-factory-building { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 0 auto 8px; padding: 6px 0; }
@@ -159,37 +218,42 @@
159
218
 
160
219
  /* ── list ───────────────────────────────────────────────────────────────── */
161
220
  .xeplr-factory-list {
162
- display: flex; flex-direction: column; width: 100%; height: 100%; box-sizing: border-box; overflow: auto;
221
+ display: flex; flex-direction: column; width: 100%; box-sizing: border-box; overflow: auto;
222
+ /* As tall as its rows, up to the box it was given — never a frame around a
223
+ block of empty space under two records. */
224
+ height: auto; max-height: 100%;
163
225
  font-size: max(10px, calc(var(--xf-font, 14) * var(--xf-u)));
164
- border: 1px solid var(--xf-border); border-radius: calc(8 * var(--xf-u)); background: var(--xf-surface);
226
+ border: 1px solid var(--xf-border); border-radius: 10px; background: var(--xf-surface);
165
227
  }
166
- .xeplr-factory-list-head { display: flex; align-items: center; gap: 10px; padding: calc(8 * var(--xf-u)) calc(10 * var(--xf-u)); border-bottom: 1px solid var(--xf-border); }
167
- .xeplr-factory-list-title { font-weight: 600; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
228
+ .xeplr-factory-list-head { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-bottom: 1px solid var(--xf-border); }
229
+ .xeplr-factory-list-title { font-weight: 600; font-size: 1.05em; color: var(--xf-ink); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
168
230
  .xeplr-factory-list-new {
169
- font: inherit; font-weight: 600; font-size: .9em; padding: 3px 10px; border-radius: 5px; cursor: pointer;
170
- color: var(--xf-accent); background: transparent; border: 1px solid var(--xf-accent);
231
+ font: inherit; font-weight: 600; font-size: .9em; padding: 5px 12px; border-radius: 6px; cursor: pointer;
232
+ color: var(--xf-accent-ink); background: var(--xf-accent); border: 1px solid var(--xf-accent);
171
233
  }
234
+ /* The table IS the card here: the list's frame is enough, so the table's own
235
+ border and corners go, and its toolbar becomes the list's header. */
236
+ .xeplr-factory-list--table { overflow: hidden; }
237
+ .xeplr-factory-list--table .xeplr-table-container { border: none; border-radius: 0; background: transparent; }
238
+ .xeplr-factory-list--table .xeplr-table-toolbar { padding: 12px 14px; background: transparent; gap: 10px; }
239
+ .xeplr-factory-list--design { height: 100%; }
172
240
  .xeplr-factory-list-empty { padding: 16px; color: var(--xf-muted); text-align: center; }
173
241
  .xeplr-factory-error-inline { color: var(--xf-danger); font-size: .9em; }
174
242
  .xeplr-factory-list-mock { width: 100%; border-collapse: collapse; }
175
243
  .xeplr-factory-list-mock th { text-align: left; font-weight: 600; font-size: .9em; padding: calc(6 * var(--xf-u)) calc(10 * var(--xf-u)); border-bottom: 1px solid var(--xf-border); color: var(--xf-muted); }
176
244
  .xeplr-factory-list-mock td { padding: calc(8 * var(--xf-u)) calc(10 * var(--xf-u)); border-bottom: 1px solid var(--xf-border); }
177
- .xeplr-factory-list-bar { display: block; height: calc(8 * var(--xf-u)); width: 70%; border-radius: 4px; background: #ecebe4; }
245
+ .xeplr-factory-list-bar { display: block; height: calc(8 * var(--xf-u)); width: 70%; border-radius: 4px; background: var(--xf-border); }
178
246
  .xeplr-factory-list .xeplr-table-row.xeplr-factory-row-open td,
179
247
  .xeplr-factory-list tr.xeplr-factory-row-open td { background: color-mix(in srgb, var(--xf-accent) 10%, transparent); }
180
248
 
181
249
  /* ── the builder ────────────────────────────────────────────────────────── */
182
250
  .xeplr-factory-builder {
183
251
  display: flex; flex-direction: column; height: 100%; min-height: 560px;
184
- color: var(--xeplr-text, #1a1a19); font-size: 13px;
185
- --xf-accent: var(--xeplr-accent, #2563eb);
186
- --xf-border: var(--xeplr-border, #d9d8d0);
187
- --xf-muted: var(--xeplr-muted, #6b6a63);
188
- --xf-danger: var(--xeplr-danger, #c2372e);
252
+ color: var(--xf-ink); font-size: 13px;
189
253
  }
190
254
  .xeplr-factory-bar {
191
255
  display: flex; align-items: center; gap: 10px; padding: 8px 12px;
192
- border-bottom: 1px solid var(--xf-border); background: var(--xeplr-surface, #fff);
256
+ border-bottom: 1px solid var(--xf-border); background: var(--xf-surface);
193
257
  }
194
258
  .xeplr-factory-name {
195
259
  flex: 1; min-width: 0; font: inherit; font-size: 16px; font-weight: 600;
@@ -200,27 +264,41 @@
200
264
  .xeplr-factory-problems { font-size: 12px; color: var(--xf-muted); white-space: nowrap; cursor: help; }
201
265
  .xeplr-factory-problems.is-loud { color: var(--xf-danger); font-weight: 600; }
202
266
  .xeplr-factory-primary, .xeplr-factory-secondary, .xeplr-factory-danger {
203
- font: inherit; font-weight: 600; padding: 6px 14px; border-radius: 6px; cursor: pointer; border: 1px solid var(--xf-accent);
267
+ font: inherit; font-weight: 600; padding: 7px 16px; border-radius: 6px; cursor: pointer; border: 1px solid var(--xf-accent);
268
+ }
269
+ .xeplr-factory-primary { background: var(--xf-accent); color: var(--xf-accent-ink); }
270
+ .xeplr-factory-primary, .xeplr-factory-secondary, .xeplr-factory-danger, .xeplr-factory-list-new {
271
+ transition: background .12s, filter .12s, transform .08s, box-shadow .12s;
272
+ }
273
+ .xeplr-factory-primary:hover:not(:disabled) { filter: brightness(1.08); }
274
+ .xeplr-factory-secondary:hover:not(:disabled) { background: color-mix(in srgb, var(--xf-accent) 12%, transparent); }
275
+ .xeplr-factory-list-new:hover { filter: brightness(1.08); }
276
+ .xeplr-factory-danger:hover:not(:disabled) { background: color-mix(in srgb, var(--xf-danger) 12%, transparent); }
277
+ /* A press you can see — the button gives a little. */
278
+ .xeplr-factory-primary:active:not(:disabled), .xeplr-factory-secondary:active:not(:disabled),
279
+ .xeplr-factory-danger:active:not(:disabled), .xeplr-factory-list-new:active { transform: scale(.96); }
280
+ .xeplr-factory-primary:focus-visible, .xeplr-factory-secondary:focus-visible,
281
+ .xeplr-factory-danger:focus-visible, .xeplr-factory-list-new:focus-visible {
282
+ outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--xf-accent) 35%, transparent);
204
283
  }
205
- .xeplr-factory-primary { background: var(--xf-accent); color: #fff; }
206
284
  .xeplr-factory-primary:disabled { opacity: .6; }
207
285
  .xeplr-factory-secondary { background: transparent; color: var(--xf-accent); }
208
286
  .xeplr-factory-danger { background: transparent; color: var(--xf-danger); border-color: var(--xf-danger); }
209
287
  .xeplr-factory-save-error { padding: 6px 12px; color: var(--xf-danger); background: color-mix(in srgb, var(--xf-danger) 7%, transparent); font-size: 12px; }
210
288
 
211
289
  .xeplr-factory-body { flex: 1; min-height: 0; display: flex; }
212
- .xeplr-factory-bar-status--saved { color: var(--xeplr-status-good, #1d7a4c); }
290
+ .xeplr-factory-bar-status--saved { color: var(--xf-good); }
213
291
  .xeplr-factory-bar-status--invalid, .xeplr-factory-bar-status--error { color: var(--xf-danger); }
214
- .xeplr-factory-preview { flex: 1; min-height: 0; overflow: auto; padding: 16px; background: var(--xeplr-plane, #f7f7f5); }
215
- .xeplr-factory-preview > .xeplr-factory-screen { max-width: 960px; margin: 0 auto; background: var(--xeplr-surface, #fff); border: 1px solid var(--xf-border); border-radius: 8px; }
292
+ .xeplr-factory-preview { flex: 1; min-height: 0; overflow: auto; padding: 16px; background: var(--xf-plane); }
293
+ .xeplr-factory-preview > .xeplr-factory-screen { max-width: 960px; margin: 0 auto; background: var(--xf-surface); border: 1px solid var(--xf-border); border-radius: 8px; }
216
294
 
217
295
  /* palette */
218
- .xeplr-factory-palette { width: 168px; flex: 0 0 auto; overflow-y: auto; padding: 10px; border-right: 1px solid var(--xf-border); background: var(--xeplr-surface, #fff); }
296
+ .xeplr-factory-palette { width: 168px; flex: 0 0 auto; overflow-y: auto; padding: 10px; border-right: 1px solid var(--xf-border); background: var(--xf-surface); }
219
297
  .xeplr-factory-palette-group + .xeplr-factory-palette-group { margin-top: 12px; }
220
298
  .xeplr-factory-palette-title { margin: 0 0 6px; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--xf-muted); }
221
299
  .xeplr-factory-palette-item {
222
300
  display: flex; align-items: center; gap: 8px; width: 100%; margin-bottom: 4px; padding: 7px 8px;
223
- font: inherit; text-align: left; color: inherit; background: var(--xeplr-surface, #fff);
301
+ font: inherit; text-align: left; color: inherit; background: var(--xf-surface);
224
302
  border: 1px solid var(--xf-border); border-radius: 6px; cursor: grab;
225
303
  }
226
304
  .xeplr-factory-palette-item:hover { border-color: var(--xf-accent); }
@@ -236,14 +314,14 @@
236
314
  .xeplr-factory-palette-icon--textarea { height: 14px; }
237
315
 
238
316
  /* canvas */
239
- .xeplr-factory-stage { flex: 1; min-width: 0; min-height: 0; display: flex; justify-content: center; padding: 12px; box-sizing: border-box; background: var(--xeplr-plane, #f7f7f5); }
317
+ .xeplr-factory-stage { flex: 1; min-width: 0; min-height: 0; display: flex; justify-content: center; padding: 12px; box-sizing: border-box; background: var(--xf-plane); }
240
318
  /* Exactly the screen's design width, never wider: what you design is the size it is shown at. */
241
319
  .xeplr-factory-frame { flex: 1; min-width: 0; height: 100%; display: flex; box-shadow: 0 1px 4px rgba(0,0,0,.08); }
242
320
  .xeplr-factory-canvas {
243
321
  flex: 1; height: 100%;
244
322
  background:
245
323
  radial-gradient(circle, var(--xeplr-border-secondary, #e4e3db) 1px, transparent 1px) 0 0 / 20px 20px,
246
- var(--xeplr-surface, #ffffff);
324
+ var(--xf-surface);
247
325
  }
248
326
  .xeplr-factory-design-node { width: 100%; height: 100%; box-sizing: border-box; border-radius: 4px; outline: 1px dashed transparent; cursor: move; }
249
327
  .xeplr-factory-design-node:hover { outline-color: color-mix(in srgb, var(--xf-accent) 45%, transparent); }
@@ -257,7 +335,21 @@
257
335
  }
258
336
 
259
337
  /* property panel */
260
- .xeplr-factory-panel { width: 280px; flex: 0 0 auto; overflow-y: auto; padding: 10px 12px; border-left: 1px solid var(--xf-border); background: var(--xeplr-surface, #fff); }
338
+ .xeplr-factory-panel { width: 280px; flex: 0 0 auto; overflow-y: auto; padding: 10px 12px; border-left: 1px solid var(--xf-border); background: var(--xf-surface); }
339
+ .xeplr-factory-fieldtype { padding: 2px 0 12px; margin-bottom: 4px; border-bottom: 1px solid var(--xf-border); }
340
+ .xeplr-factory-suggest {
341
+ margin-top: 8px; padding: 8px 10px; border-radius: 6px; font-size: 12px; line-height: 1.45;
342
+ color: var(--xf-ink); background: color-mix(in srgb, var(--xf-accent) 10%, transparent);
343
+ border: 1px solid color-mix(in srgb, var(--xf-accent) 35%, transparent);
344
+ }
345
+ .xeplr-factory-suggest-actions { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
346
+ .xeplr-factory-suggest-actions button { padding: 4px 10px; font-size: 12px; }
347
+ .xeplr-factory-hints {
348
+ font: inherit; font-size: 12px; white-space: nowrap; cursor: pointer; padding: 3px 10px; border-radius: 999px;
349
+ color: var(--xf-accent); background: color-mix(in srgb, var(--xf-accent) 10%, transparent);
350
+ border: 1px solid color-mix(in srgb, var(--xf-accent) 35%, transparent);
351
+ }
352
+ .xeplr-factory-hints:hover { background: color-mix(in srgb, var(--xf-accent) 18%, transparent); }
261
353
  .xeplr-factory-panel-empty { color: var(--xf-muted); line-height: 1.5; }
262
354
  .xeplr-factory-panel-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
263
355
  .xeplr-factory-panel-type { font-weight: 700; font-size: 14px; }
@@ -268,7 +360,7 @@
268
360
  .xeplr-factory-prop-label { display: block; margin-bottom: 3px; font-weight: 600; font-size: 12px; }
269
361
  .xeplr-factory-prop-input {
270
362
  width: 100%; box-sizing: border-box; font: inherit; padding: 5px 7px;
271
- border: 1px solid var(--xf-border); border-radius: 5px; background: #fff; color: var(--xf-ink, #1a1a19);
363
+ border: 1px solid var(--xf-field-border); border-radius: 5px; background: var(--xf-field-bg); color: var(--xf-ink);
272
364
  }
273
365
  .xeplr-factory-prop.has-error .xeplr-factory-prop-input { border-color: var(--xf-danger); }
274
366
  /* An editor made of two inputs — a set to pick from, and the list it fills in. */
@@ -281,7 +373,7 @@
281
373
 
282
374
  .xeplr-factory-segment { display: flex; margin-bottom: 8px; border: 1px solid var(--xf-border); border-radius: 6px; overflow: hidden; }
283
375
  .xeplr-factory-segment button { flex: 1; font: inherit; font-size: 12px; padding: 5px; border: none; background: transparent; cursor: pointer; color: inherit; }
284
- .xeplr-factory-segment button.is-on { background: var(--xf-accent); color: #fff; }
376
+ .xeplr-factory-segment button.is-on { background: var(--xf-accent); color: var(--xf-accent-ink); }
285
377
  .xeplr-factory-options-head, .xeplr-factory-option-row { display: grid; grid-template-columns: 1fr 1fr 24px; gap: 4px; align-items: center; }
286
378
  .xeplr-factory-options-head { font-size: 11px; color: var(--xf-muted); margin-bottom: 3px; }
287
379
  .xeplr-factory-option-row { margin-bottom: 4px; }
@@ -298,24 +390,26 @@
298
390
  .xeplr-factory-modal-backdrop {
299
391
  position: fixed; inset: 0; z-index: 10000; display: flex; align-items: flex-start; justify-content: center;
300
392
  padding: 4vh 2vw; box-sizing: border-box; overflow: auto;
301
- background: rgba(20, 20, 18, .45);
393
+ background: var(--xeplr-bg-overlay, rgba(20, 20, 18, .45));
302
394
  }
303
395
  .xeplr-factory-modal {
304
- max-width: 96vw; background: var(--xeplr-surface, #fff); color: var(--xeplr-text, #1a1a19);
396
+ max-width: 96vw; background: var(--xf-surface); color: var(--xf-ink);
305
397
  border-radius: 10px; box-shadow: 0 12px 40px rgba(0, 0, 0, .25); outline: none;
306
398
  }
307
- .xeplr-factory-modal-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 16px; border-bottom: 1px solid var(--xeplr-border, #e2e1d9); }
399
+ .xeplr-factory-modal-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 16px; border-bottom: 1px solid var(--xf-border); }
308
400
  .xeplr-factory-modal-title { font-weight: 700; font-size: 15px; }
309
- .xeplr-factory-modal-close { border: none; background: none; font-size: 22px; line-height: 1; cursor: pointer; color: var(--xeplr-muted, #6b6a63); padding: 2px 6px; }
401
+ .xeplr-factory-modal-close { border: none; background: none; font-size: 22px; line-height: 1; cursor: pointer; color: var(--xf-muted); padding: 2px 6px; }
310
402
  .xeplr-factory-modal-body { padding: 8px 24px 16px; }
403
+ /* The popup is the frame; the screen inside it does not draw a second one. */
404
+ .xeplr-factory-modal-body > .xeplr-factory-screen { border: none; border-radius: 0; }
311
405
 
312
406
  /* ── publish result ─────────────────────────────────────────────────────── */
313
407
  .xeplr-factory-publish { display: flex; align-items: flex-start; gap: 10px; padding: 8px 12px; font-size: 12.5px; border-bottom: 1px solid var(--xf-border); }
314
408
  .xeplr-factory-publish > span { flex: 1; }
315
- .xeplr-factory-publish.is-ok { color: var(--xeplr-status-good, #1d7a4c); background: color-mix(in srgb, var(--xeplr-status-good, #1d7a4c) 7%, transparent); }
409
+ .xeplr-factory-publish.is-ok { color: var(--xf-good); background: color-mix(in srgb, var(--xf-good) 7%, transparent); }
316
410
  .xeplr-factory-publish.is-error { color: var(--xf-danger); background: color-mix(in srgb, var(--xf-danger) 7%, transparent); flex-wrap: wrap; }
317
- .xeplr-factory-publish-detail { flex-basis: 100%; order: 3; margin: 4px 0 0; padding: 8px; max-height: 220px; overflow: auto; font-size: 11.5px; color: var(--xeplr-text, #1a1a19); background: var(--xeplr-surface, #fff); border: 1px solid var(--xf-border); border-radius: 6px; white-space: pre; }
318
- .xeplr-factory-publish.is-confirm { align-items: center; color: var(--xeplr-text, #1a1a19); background: color-mix(in srgb, var(--xeplr-status-warn, #a86a00) 10%, transparent); border-bottom-color: color-mix(in srgb, var(--xeplr-status-warn, #a86a00) 40%, transparent); }
411
+ .xeplr-factory-publish-detail { flex-basis: 100%; order: 3; margin: 4px 0 0; padding: 8px; max-height: 220px; overflow: auto; font-size: 11.5px; color: var(--xf-ink); background: var(--xf-surface); border: 1px solid var(--xf-border); border-radius: 6px; white-space: pre; }
412
+ .xeplr-factory-publish.is-confirm { align-items: center; color: var(--xf-ink); background: color-mix(in srgb, var(--xf-warn) 10%, transparent); border-bottom-color: color-mix(in srgb, var(--xf-warn) 40%, transparent); }
319
413
  .xeplr-factory-confirm-text { flex: 1; line-height: 1.5; }
320
414
  .xeplr-factory-confirm-text ul { margin: 4px 0; padding-left: 18px; }
321
415
  .xeplr-factory-confirm-actions { display: flex; gap: 8px; flex-shrink: 0; }
@@ -9,3 +9,7 @@ export { default as FlowBuilderSample } from './FlowBuilderSample.jsx'
9
9
  export { default as ControlView, VIEWS } from './ControlView.jsx'
10
10
  export { default as Palette, DRAG_TYPE } from './Palette.jsx'
11
11
  export { default as PropertyPanel, EDITORS } from './PropertyPanel.jsx'
12
+
13
+ // The body of another package's "design this screen" window — see
14
+ // ../useScreenSource.js, which is how it is normally reached.
15
+ export { ScreenEditorSample } from './ScreenEditorSample.jsx'
@@ -40,12 +40,22 @@ export function boxStyle(style) {
40
40
  })
41
41
  }
42
42
 
43
- /** The screen's defaults, on its container: what every control inherits. */
43
+ /**
44
+ * The screen's defaults, on its container: what every control inherits.
45
+ *
46
+ * A screen with no colours of its own follows the app's theme (factory.css).
47
+ * One that sets them keeps them EVERYWHERE — its ink and paper become the
48
+ * tokens the controls read, input boxes included — so a screen designed dark
49
+ * ink on white is never shown as that ink on a dark theme's input box.
50
+ */
44
51
  export function screenStyle(doc) {
45
52
  const s = doc.style || {}
46
53
  return clean({
47
54
  '--xf-width': doc.width,
48
55
  '--xf-font': s.fontSize,
56
+ '--xf-ink': s.color,
57
+ '--xf-surface': s.background,
58
+ '--xf-field-bg': s.background,
49
59
  fontFamily: s.fontFamily,
50
60
  color: s.color,
51
61
  background: s.background
package/src/generate.js CHANGED
@@ -26,6 +26,7 @@
26
26
  import { CONTROLS, CHOICE_TYPES } from './controls.js'
27
27
  import { createScreen, addControl, MARGIN, GAP, slugify, camelName, stepsOf } from './document.js'
28
28
  import { assertValidDocument } from './validateDocument.js'
29
+ import { presetFor, presetProps, suggestPreset } from './presets.js'
29
30
 
30
31
  /** Heights by control, as page fractions (a page is as tall as it is wide). */
31
32
  export const SPEC_HEIGHTS = {
@@ -45,7 +46,7 @@ export const SPEC_HEIGHTS = {
45
46
  */
46
47
  export const ROW_GAP = 0.04
47
48
 
48
- const FIELD_KEYS = ['type', 'label', 'name', 'required', 'placeholder', 'default', 'validation', 'data', 'options', 'table', 'layout', 'accept', 'maxSize', 'step', 'steps', 'showNumbers', 'width', 'text', 'variant', 'style']
49
+ const FIELD_KEYS = ['type', 'preset', 'label', 'name', 'required', 'placeholder', 'default', 'validation', 'data', 'options', 'table', 'layout', 'accept', 'maxSize', 'step', 'steps', 'showNumbers', 'width', 'text', 'variant', 'style']
49
50
  const SPEC_KEYS = ['name', 'id', 'source', 'columns', 'heading', 'fields', 'list', 'aspect', 'width', 'style']
50
51
  const LIST_KEYS = ['title', 'source', 'columns', 'pageSize', 'actions', 'style']
51
52
 
@@ -58,6 +59,12 @@ const LIST_KEYS = ['title', 'source', 'columns', 'pageSize', 'actions', 'style']
58
59
  * fields in reading order. Each: { label, type?='text', name?, required?, placeholder?,
59
60
  * default?, validation?, width?: 'half'|'full' }
60
61
  * dropdown: options: ['Full time', …] or [{ id, name }] —or— table: 'departments'
62
+ * READY-MADE: `type` may be a preset key — 'email', 'phone', 'url', 'linkedin',
63
+ * 'age', 'dateOfBirth', 'gender', 'country', 'amount', 'percentage', 'yesNo',
64
+ * 'postalCode' (presets.js) — or `preset: '<key>'`. A field with NO type and
65
+ * no options is matched by its label: { label: 'Age' } is a whole number 0–130,
66
+ * { label: 'LinkedIn URL' } a LinkedIn address. `type: 'text'` opts out.
67
+ * Anything the field also gives (label, required, validation…) wins.
61
68
  * section heading: { type: 'label', text, variant?: 'subheading' }
62
69
  * source? the table records are saved to, e.g. 'employees'
63
70
  * width? design width in px (default 800) — style sizes are px at this width
@@ -65,7 +72,8 @@ const LIST_KEYS = ['title', 'source', 'columns', 'pageSize', 'actions', 'style']
65
72
  * list? true, or { title, source, columns: [{ field, label }], pageSize, actions } —
66
73
  * a list of the saved records below the fields, with New / Edit / Delete
67
74
  *
68
- * There is no submit button: a screen saves itself as it is filled in.
75
+ * Do not add a button control for saving: every screen already has Save (and
76
+ * Cancel where it can go back) in its footer — an AJAX call, never a form submit.
69
77
  */
70
78
  export function screenFromSpec(spec, controls = CONTROLS) {
71
79
  if (!spec || typeof spec !== 'object') throw new Error('screenFromSpec: spec must be an object')
@@ -134,14 +142,19 @@ export function screenFromSpec(spec, controls = CONTROLS) {
134
142
  const at = `spec.fields[${i}]`
135
143
  if (!field || typeof field !== 'object') throw new Error(`screenFromSpec: ${at} must be an object`)
136
144
  unknownKeys(field, FIELD_KEYS, at)
137
- const type = field.type || 'text'
138
- if (!controls[type]) throw new Error(`screenFromSpec: ${at}.type "${type}" is not a control — one of: ${Object.keys(controls).join(', ')}`)
145
+ // A ready-made field: named (type or preset), or recognised from a label
146
+ // that says nothing else about itself.
147
+ const bare = !field.type && !field.preset && !field.options && !field.table && !field.data
148
+ const preset = presetFor(field.preset) || presetFor(field.type) || (bare ? suggestPreset(field.label) : null)
149
+ if (field.preset && !presetFor(field.preset)) throw new Error(`screenFromSpec: ${at}.preset "${field.preset}" is not a ready-made field`)
150
+ const type = preset ? preset.control : (field.type || 'text')
151
+ if (!controls[type]) throw new Error(`screenFromSpec: ${at}.type "${type}" is not a control or a ready-made field — controls: ${Object.keys(controls).join(', ')}`)
139
152
  if (field.width !== undefined && field.width !== 'half' && field.width !== 'full') throw new Error(`screenFromSpec: ${at}.width must be "half" or "full"`)
140
153
  if (field.step !== undefined) {
141
154
  if (!stepper) throw new Error(`screenFromSpec: ${at}.step needs a stepper before it — { "type": "stepper", "steps": [...] }`)
142
155
  openStep(field.step)
143
156
  }
144
- place(type, propsFromField(type, field, at), field.width)
157
+ place(type, preset ? withPreset(preset, propsFromField(type, { ...field, type }, at, true), field) : propsFromField(type, field, at), field.width)
145
158
  })
146
159
 
147
160
  if (col > 0) { y += rowH + ROW_GAP; col = 0; rowH = 0 }
@@ -157,7 +170,17 @@ export function screenFromSpec(spec, controls = CONTROLS) {
157
170
  return assertValidDocument(doc, controls)
158
171
  }
159
172
 
160
- function propsFromField(type, field, at) {
173
+ /** A preset's props under what the field itself gave. */
174
+ function withPreset(preset, own, field) {
175
+ const base = presetProps(preset)
176
+ const out = { ...base, ...own }
177
+ // Rules merge key by key: { label: 'Age', validation: { max: 120 } } keeps min and integer.
178
+ if (base.validation || own.validation) out.validation = { ...(base.validation || {}), ...(own.validation || {}) }
179
+ if (!field.label) out.label = base.label
180
+ return out
181
+ }
182
+
183
+ function propsFromField(type, field, at, fromPreset) {
161
184
  if (type === 'stepper') {
162
185
  const steps = field.steps
163
186
  if (!Array.isArray(steps) || steps.length < 2) throw new Error(`screenFromSpec: ${at} is a stepper — give it steps: ["Connect", "Transform", …]`)
@@ -175,13 +198,15 @@ function propsFromField(type, field, at) {
175
198
  if (field.style) props.style = field.style
176
199
  return props
177
200
  }
178
- if (!field.label) throw new Error(`screenFromSpec: ${at}.label is required`)
179
- const props = { label: field.label }
201
+ if (!field.label && !fromPreset) throw new Error(`screenFromSpec: ${at}.label is required`)
202
+ const props = field.label ? { label: field.label } : {}
180
203
  ;['name', 'required', 'placeholder', 'default', 'validation', 'style'].forEach((k) => {
181
204
  if (field[k] !== undefined) props[k] = field[k]
182
205
  })
183
- if (type === 'dropdown' && props.placeholder === undefined) props.placeholder = 'Select…'
184
- if (CHOICE_TYPES.includes(type)) {
206
+ if (type === 'dropdown' && props.placeholder === undefined && !fromPreset) props.placeholder = 'Select…'
207
+ if (CHOICE_TYPES.includes(type) && fromPreset && !field.options && !field.table && !field.data) {
208
+ if (field.layout !== undefined && type !== 'dropdown') props.layout = field.layout
209
+ } else if (CHOICE_TYPES.includes(type)) {
185
210
  props.data = dataFromField(field, at, type)
186
211
  if (field.layout !== undefined && type !== 'dropdown') props.layout = field.layout
187
212
  // A field reading another table stores that row's id: name the column
package/src/hooks.js CHANGED
@@ -40,7 +40,7 @@ export class FactoryHooks {
40
40
  }
41
41
 
42
42
  /**
43
- * Save — called on every autosave, so keep it quick and safe to repeat.
43
+ * Save — called when the person presses Save (or a flow's Next with unsaved changes).
44
44
  * @param values what the form holds
45
45
  * @param ctx.steps switch steps off or on as the record decides them —
46
46
  * ctx.steps.disable('approval'), ctx.steps.enable()
package/src/index.js CHANGED
@@ -10,12 +10,16 @@ export * from './model.js'
10
10
 
11
11
  // Controllers
12
12
  export { useFactoryBuilder } from './useFactoryBuilder.js'
13
- export { useFactoryScreen, normaliseOptions, AUTOSAVE_DELAY } from './useFactoryScreen.js'
13
+ export { useFactoryScreen, normaliseOptions } from './useFactoryScreen.js'
14
+ // This app's screens, shaped for another package's designer — see
15
+ // useScreenSource.js. Two props instead of a page of glue per application.
16
+ export { useScreenSource } from './useScreenSource.js'
17
+ export { describeScreen, screenStub, loadScreens, keyFromName, editScreenOf } from './screenSource.js'
14
18
  export { useFlowRun } from './useFlowRun.js'
15
19
  export { useFlowBuilder, FLOW_AUTOSAVE_DELAY } from './useFlowBuilder.js'
16
20
 
17
21
  // Designs
18
- export { BuilderSample, ScreenSample, ListView, ScreenModal, FlowRunnerSample, FlowBuilderSample, ControlView, VIEWS, Palette, DRAG_TYPE, PropertyPanel, EDITORS } from './designs/index.js'
22
+ export { BuilderSample, ScreenSample, ListView, ScreenModal, FlowRunnerSample, FlowBuilderSample, ScreenEditorSample, ControlView, VIEWS, Palette, DRAG_TYPE, PropertyPanel, EDITORS } from './designs/index.js'
19
23
 
20
24
  // Front-end hooks — extend FactoryHooks, override what you need, call super
21
25
  export { FactoryHooks } from './hooks.js'
package/src/model.js CHANGED
@@ -19,8 +19,9 @@ export { createFactoryApi } from './remote.js'
19
19
  export {
20
20
  STANDARD_COLUMNS, RESERVED_COLUMNS, MAX_IDENTIFIER,
21
21
  columnForField, tableForScreen, columnSql, widening, diffTables, migrationFor, nextMigrationName,
22
- createStatements, addStatements, alterStatement, dropStatement, columnFromDatabase, planTableChange
22
+ createStatements, addStatements, alterStatement, dropStatement, columnFromDatabase, planTableChange, conversionFor
23
23
  } from './tableSchema.js'
24
+ export { FIELD_PRESETS, PATTERNS, INPUT_TYPES, presetFor, presetProps, suggestPreset, presetGroup, presetHints, convertField } from './presets.js'
24
25
  export { createFlowsApi } from './flows.js'
25
26
  export {
26
27
  FLOW_KIND, FLOW_VERSION, FLOW_END, FLOW_OPERATORS, VALUELESS_OPERATORS,