@xeplr/ui-factory 1.0.14 → 1.2.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.
@@ -11,23 +11,39 @@ import { CHOICE_TYPES } from '../controls.js'
11
11
  // The screen is never wider than the width it was designed at, so what was
12
12
  // designed at 800px is shown at 800px, and only a narrower screen scales it.
13
13
  //
14
- // NO SUBMIT. The status line says what the background save is doing.
14
+ // SAVE IS A BUTTON, AND AN AJAX CALL — never a form submit. The <form> below
15
+ // exists for semantics only: its submit is swallowed, so Enter in a field
16
+ // posts nothing and saves nothing. Only the Save button saves.
17
+ //
18
+ // ONE FOOTER. Back, "Step 2 of 3" with the save status under it, then Next,
19
+ // Cancel and Save — a single bar under the fields, inset by the screen's
20
+ // margin so its edges line up with theirs.
15
21
 
16
22
  const STATUS = {
17
23
  idle: '',
18
- pending: 'Unsaved changes…',
24
+ pending: 'Unsaved changes',
19
25
  saving: 'Saving…',
20
- saved: 'All changes saved',
21
- incomplete: 'Fill in the required fields to save',
26
+ saved: 'Saved',
27
+ incomplete: 'Fill in the required fields',
22
28
  invalid: 'Fix the highlighted fields to save'
23
29
  }
24
30
 
31
+ /** What the background save is doing, in words. */
32
+ function SaveStatus({ ctrl }) {
33
+ const text = ctrl.status === 'error' ? `Not saved — ${ctrl.saveError}` : STATUS[ctrl.status]
34
+ return (
35
+ <span className={`xeplr-factory-footer-status xeplr-factory-footer-status--${ctrl.status}`} role="status" aria-live="polite">
36
+ {text}
37
+ </span>
38
+ )
39
+ }
40
+
25
41
  /**
26
42
  * Back and Next, under the page. Next holds at a step whose own fields are not
27
43
  * filled in — and shows why, rather than moving on and leaving a problem
28
44
  * behind on a step nobody is looking at.
29
45
  */
30
- function StepNav({ ctrl, node, children }) {
46
+ function StepNav({ ctrl, node, status, children }) {
31
47
  const at = ctrl.steps.active(node.id)
32
48
  const total = ctrl.steps.count(node.id)
33
49
  const off = ctrl.steps.disabled(node.id)
@@ -51,29 +67,60 @@ function StepNav({ ctrl, node, children }) {
51
67
  const handsOn = Boolean(children) && Boolean(ctrl.doneLabel)
52
68
  const lastStep = forward === null
53
69
  return (
54
- <div className="xeplr-factory-stepnav">
70
+ // Moving between steps (Back, Next) sits on the left as quiet buttons;
71
+ // Save — the one thing that writes — is the only filled button, on the
72
+ // right. In a flow, Next IS the way forward, so there it stays filled.
73
+ <div className="xeplr-factory-footer xeplr-factory-stepnav">
55
74
  <button type="button" className="xeplr-factory-secondary" onClick={() => go(back, 'back')} disabled={back === null}>Back</button>
56
- <span className="xeplr-factory-stepnav-where">Step {place} of {live}</span>
57
- {!(handsOn && lastStep) && (
58
- <button type="button" className="xeplr-factory-primary" onClick={() => go(forward, 'next')} disabled={lastStep}>Next</button>
75
+ {!handsOn && (
76
+ <button type="button" className="xeplr-factory-secondary" onClick={() => go(forward, 'next')} disabled={lastStep}>Next</button>
77
+ )}
78
+ <div className="xeplr-factory-footer-mid">
79
+ <span className="xeplr-factory-stepnav-where">Step {place} of {live}</span>
80
+ {status}
81
+ </div>
82
+ {handsOn && !lastStep && (
83
+ <button type="button" className="xeplr-factory-primary" onClick={() => go(forward, 'next')}>Next</button>
59
84
  )}
60
85
  {(!handsOn || lastStep) && children}
61
86
  </div>
62
87
  )
63
88
  }
64
89
 
65
- /** Leaves the page — after whatever is still being saved has gone out. */
66
- function DoneButton({ ctrl }) {
67
- const saving = ctrl.status === 'pending' || ctrl.status === 'saving'
90
+ /**
91
+ * The ways out, at the end of the footer.
92
+ *
93
+ * Save one AJAX call. On a page or in a popup (the host gave onDone) it
94
+ * then goes back; a form embedded in a page stays, showing "Saved".
95
+ * Cancel goes back without saving — asking first if anything is unsaved.
96
+ * Only where there is somewhere to go back to.
97
+ *
98
+ * Inside a flow the flow names the button (doneLabel, "Next"): it saves what
99
+ * is unsaved, then moves the journey on — one button, not two.
100
+ */
101
+ function SaveButtons({ ctrl }) {
102
+ const saving = ctrl.status === 'saving'
103
+ if (ctrl.doneLabel) {
104
+ return (
105
+ <button type="button" className="xeplr-factory-primary" disabled={saving}
106
+ onClick={() => ctrl.flush().then((r) => { if (r && r.ok) ctrl.done(r) })}>
107
+ {ctrl.doneLabel}
108
+ </button>
109
+ )
110
+ }
111
+ const save = () => ctrl.save().then((r) => { if (r && r.ok && ctrl.done) ctrl.done() })
68
112
  return (
69
- <button
70
- type="button"
71
- className={ctrl.doneLabel ? 'xeplr-factory-primary' : 'xeplr-factory-secondary'}
72
- disabled={!ctrl.canSave && ctrl.status === 'invalid'}
73
- onClick={() => ctrl.flush().then(ctrl.done, ctrl.done)}
74
- >
75
- {saving ? 'Saving…' : (ctrl.doneLabel || 'Done')}
76
- </button>
113
+ <>
114
+ {ctrl.done && (
115
+ <button type="button" className="xeplr-factory-secondary" disabled={saving}
116
+ onClick={() => { if (ctrl.confirmDiscard()) ctrl.done() }}>
117
+ Cancel
118
+ </button>
119
+ )}
120
+ <button type="button" className="xeplr-factory-primary" disabled={saving} onClick={save}>
121
+ {saving ? 'Saving…' : 'Save'}
122
+ </button>
123
+ </>
77
124
  )
78
125
  }
79
126
 
@@ -97,26 +144,22 @@ export default function ScreenSample({ ctrl, className, style, renderPopup }) {
97
144
  const shown = ctrl.visibleNodes && ctrl.visibleNodes.length ? ctrl.visibleNodes : doc.nodes
98
145
  const pageHeight = (contentBottom(doc, shown) + MARGIN) * aspect * 100
99
146
  const fieldNodes = inputNodes(doc)
100
- const statusText = ctrl.status === 'error' ? `Not saved — ${ctrl.saveError}` : STATUS[ctrl.status]
101
- // A screen with no fields of its own (a list screen) has nothing to save.
147
+ // A screen with no fields of its own (a list screen) has nothing to save —
148
+ // and no sheet: its list is the card (factory.css, --bare).
102
149
  const hasFields = fieldNodes.length > 0
150
+ const status = hasFields ? <SaveStatus ctrl={ctrl} /> : null
151
+ const steppers = ctrl.steps.nodes
103
152
 
104
153
  return (
105
154
  <div
106
- className={'xeplr-factory-screen' + (className ? ' ' + className : '')}
107
- style={{ maxWidth: doc.width, ...screenStyle(doc), ...style }}
155
+ className={'xeplr-factory-screen' + (hasFields ? '' : ' xeplr-factory-screen--bare') + (className ? ' ' + className : '')}
156
+ style={{ maxWidth: doc.width, '--xf-margin': `${MARGIN * 100}%`, ...screenStyle(doc), ...style }}
108
157
  >
109
- {hasFields && (
110
- <div className={`xeplr-factory-status xeplr-factory-status--${ctrl.status}`} role="status" aria-live="polite">
111
- <span>{ctrl.recordId !== null && ctrl.recordId !== undefined ? 'Editing record' : 'New record'}</span>
112
- <span>{statusText}</span>
113
- </div>
114
- )}
115
158
  <form
116
159
  className="xeplr-factory-page"
117
160
  style={{ height: `${pageHeight}cqw` }}
118
- // Enter in a field saves now rather than posting a page.
119
- onSubmit={(e) => { e.preventDefault(); ctrl.saveNow() }}
161
+ // Never a submit: Enter posts nothing and saves nothing. Save is a button.
162
+ onSubmit={(e) => e.preventDefault()}
120
163
  noValidate
121
164
  aria-label={doc.name}
122
165
  >
@@ -156,22 +199,30 @@ export default function ScreenSample({ ctrl, className, style, renderPopup }) {
156
199
  extraActions: ctrl.actionsFor ? ctrl.actionsFor(node) : [],
157
200
  // A list with an edit screen opens it in a popup; one on a form
158
201
  // opens the row in the form's own fields.
159
- onEdit: (rec) => (node.props.editScreen ? ctrl.openEditor(node, rec) : ctrl.openRecord(rec)),
202
+ // A list on the form itself swaps the record in the fields — so
203
+ // unsaved changes to the one showing are asked about first.
204
+ onEdit: (rec) => (node.props.editScreen ? ctrl.openEditor(node, rec) : (ctrl.confirmDiscard() && ctrl.openRecord(rec))),
160
205
  // eslint-disable-next-line no-alert
161
206
  onDelete: (rec) => ctrl.deleteRecord(node, rec).catch((err) => window.alert(err.message || 'Could not delete')),
162
- onNew: () => (node.props.editScreen ? ctrl.openEditor(node, null) : ctrl.newRecord())
207
+ onNew: () => (node.props.editScreen ? ctrl.openEditor(node, null) : (ctrl.confirmDiscard() && ctrl.newRecord()))
163
208
  } : undefined}
164
209
  />
165
210
  </div>
166
211
  ))}
167
212
  </form>
168
- {ctrl.steps.nodes.map((node, i) => (
169
- <StepNav key={node.id} ctrl={ctrl} node={node}>
170
- {ctrl.done && i === ctrl.steps.nodes.length - 1 && <DoneButton ctrl={ctrl} />}
171
- </StepNav>
172
- ))}
173
- {ctrl.done && !ctrl.steps.nodes.length && (
174
- <div className="xeplr-factory-done"><DoneButton ctrl={ctrl} /></div>
213
+ {steppers.map((node, i) => {
214
+ const last = i === steppers.length - 1
215
+ return (
216
+ <StepNav key={node.id} ctrl={ctrl} node={node} status={last ? status : null}>
217
+ {last && (hasFields || ctrl.doneLabel) && <SaveButtons ctrl={ctrl} />}
218
+ </StepNav>
219
+ )
220
+ })}
221
+ {!steppers.length && (hasFields || ctrl.doneLabel) && (
222
+ <div className="xeplr-factory-footer">
223
+ <div className="xeplr-factory-footer-mid">{status}</div>
224
+ <SaveButtons ctrl={ctrl} />
225
+ </div>
175
226
  )}
176
227
  {ctrl.popup && renderPopup && renderPopup(ctrl.popup, ctrl)}
177
228
  </div>
@@ -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'