@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.
@@ -0,0 +1,228 @@
1
+ import { getAtPath } from '../propertyPath.js'
2
+ import { slugify } from '../document.js'
3
+
4
+ // The selected control's properties, GENERATED from its entry in controls.js:
5
+ // each field there names a path and an editor type, and this file owns only
6
+ // what each editor type looks like. A new property is a line in controls.js.
7
+
8
+ export default function PropertyPanel({ node, control, errors, tables, onChange, onRemove, selectionCount }) {
9
+ if (selectionCount > 1) {
10
+ return (
11
+ <aside className="xeplr-factory-panel">
12
+ <p className="xeplr-factory-panel-empty">{selectionCount} controls selected</p>
13
+ <button type="button" className="xeplr-factory-danger" onClick={onRemove}>Delete them</button>
14
+ </aside>
15
+ )
16
+ }
17
+ if (!node || !control) {
18
+ return (
19
+ <aside className="xeplr-factory-panel">
20
+ <p className="xeplr-factory-panel-empty">
21
+ Select a control to set its label, validation and options — or drag one in from the left.
22
+ </p>
23
+ </aside>
24
+ )
25
+ }
26
+
27
+ const nodeErrors = errors || []
28
+ const errorsFor = (path) => nodeErrors.filter((e) => e.field === path || e.field.startsWith(path + '.') || e.field.startsWith(path + '['))
29
+ const shown = new Set()
30
+
31
+ return (
32
+ <aside className="xeplr-factory-panel" aria-label={`${control.label} properties`}>
33
+ <header className="xeplr-factory-panel-head">
34
+ <span className="xeplr-factory-panel-type">{control.label}</span>
35
+ <button type="button" className="xeplr-factory-link-danger" onClick={onRemove} title="Delete (Del)">Delete</button>
36
+ </header>
37
+
38
+ {control.properties.map((group) => (
39
+ <fieldset key={group.key} className="xeplr-factory-group">
40
+ <legend>{group.title}</legend>
41
+ {group.fields.map((field) => {
42
+ const errs = errorsFor(field.path)
43
+ errs.forEach((e) => shown.add(e))
44
+ const Editor = EDITORS[field.type] || TextEditor
45
+ const id = `xf-prop-${node.id}-${field.path.replace(/\W+/g, '-')}`
46
+ return (
47
+ <div key={field.path} className={`xeplr-factory-prop${errs.length ? ' has-error' : ''}`}>
48
+ {field.type !== 'toggle' && <label className="xeplr-factory-prop-label" htmlFor={id}>{field.label}</label>}
49
+ <Editor
50
+ id={id}
51
+ field={field}
52
+ value={getAtPath(node, field.path)}
53
+ onChange={(v) => onChange(field.path, v)}
54
+ tables={tables}
55
+ node={node}
56
+ errors={errs}
57
+ />
58
+ {field.help && <div className="xeplr-factory-prop-help">{field.help}</div>}
59
+ {errs.filter((e) => e.field === field.path).map((e, i) => (
60
+ <div key={i} className="xeplr-factory-prop-error">{e.message}</div>
61
+ ))}
62
+ </div>
63
+ )
64
+ })}
65
+ </fieldset>
66
+ ))}
67
+
68
+ {/* Anything the checker found that no editor above owns — geometry, a
69
+ property the panel does not offer. Shown rather than dropped: a save
70
+ that fails for a reason nowhere on screen is the worst kind. */}
71
+ {nodeErrors.filter((e) => !shown.has(e)).length > 0 && (
72
+ <div className="xeplr-factory-panel-errors" role="alert">
73
+ {nodeErrors.filter((e) => !shown.has(e)).map((e, i) => (
74
+ <div key={i}><code>{e.field || e.path}</code> {e.message}</div>
75
+ ))}
76
+ </div>
77
+ )}
78
+ </aside>
79
+ )
80
+ }
81
+
82
+ function TextEditor({ id, value, onChange }) {
83
+ return <input id={id} className="xeplr-factory-prop-input" type="text" value={value ?? ''} onChange={(e) => onChange(e.target.value)} />
84
+ }
85
+
86
+ function TextareaEditor({ id, value, onChange }) {
87
+ return <textarea id={id} className="xeplr-factory-prop-input" rows={3} value={value ?? ''} onChange={(e) => onChange(e.target.value)} />
88
+ }
89
+
90
+ function NumberEditor({ id, value, onChange }) {
91
+ return (
92
+ <input
93
+ id={id}
94
+ className="xeplr-factory-prop-input"
95
+ type="number"
96
+ value={value ?? ''}
97
+ onChange={(e) => {
98
+ const raw = e.target.value
99
+ onChange(raw === '' ? undefined : Number(raw))
100
+ }}
101
+ />
102
+ )
103
+ }
104
+
105
+ function DateEditor({ id, value, onChange }) {
106
+ return <input id={id} className="xeplr-factory-prop-input" type="date" value={value ?? ''} onChange={(e) => onChange(e.target.value)} />
107
+ }
108
+
109
+ function ToggleEditor({ id, field, value, onChange }) {
110
+ return (
111
+ <label className="xeplr-factory-toggle" htmlFor={id}>
112
+ <input id={id} type="checkbox" checked={Boolean(value)} onChange={(e) => onChange(e.target.checked ? true : undefined)} />
113
+ <span>{field.label}</span>
114
+ </label>
115
+ )
116
+ }
117
+
118
+ function SelectEditor({ id, field, value, onChange }) {
119
+ return (
120
+ <select id={id} className="xeplr-factory-prop-input" value={value ?? ''} onChange={(e) => onChange(e.target.value)}>
121
+ {field.options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
122
+ </select>
123
+ )
124
+ }
125
+
126
+ /**
127
+ * A dropdown's options: typed in here, or read from a table. Either way each
128
+ * option is { id, name } — the id is saved, the name is shown.
129
+ */
130
+ function DataSourceEditor({ id, value, onChange, tables, errors }) {
131
+ const data = value || { source: 'static', options: [] }
132
+ const setSource = (source) => {
133
+ if (source === data.source) return
134
+ onChange(source === 'table' ? { source: 'table', table: '' } : { source: 'static', options: [] })
135
+ }
136
+ const errorAt = (suffix) => errors.filter((e) => e.field.endsWith(suffix)).map((e) => e.message)[0]
137
+
138
+ return (
139
+ <div className="xeplr-factory-datasource" id={id}>
140
+ <div className="xeplr-factory-segment" role="radiogroup" aria-label="Options come from">
141
+ <button type="button" role="radio" aria-checked={data.source === 'static'} className={data.source === 'static' ? 'is-on' : ''} onClick={() => setSource('static')}>Fixed list</button>
142
+ <button type="button" role="radio" aria-checked={data.source === 'table'} className={data.source === 'table' ? 'is-on' : ''} onClick={() => setSource('table')}>From a table</button>
143
+ </div>
144
+
145
+ {data.source === 'table' ? (
146
+ <div className="xeplr-factory-table-pick">
147
+ {tables && tables.items.length > 0 ? (
148
+ <select
149
+ className="xeplr-factory-prop-input"
150
+ value={data.table || ''}
151
+ onChange={(e) => onChange({ source: 'table', table: e.target.value })}
152
+ aria-label="Table"
153
+ >
154
+ <option value="">{tables.loading ? 'Loading tables…' : 'Choose a table…'}</option>
155
+ {tables.items.map((t) => <option key={String(t.id)} value={String(t.id)}>{t.name}</option>)}
156
+ </select>
157
+ ) : (
158
+ <input
159
+ className="xeplr-factory-prop-input"
160
+ type="text"
161
+ placeholder={tables?.loading ? 'Loading tables…' : 'Table name'}
162
+ value={data.table || ''}
163
+ onChange={(e) => onChange({ source: 'table', table: e.target.value })}
164
+ aria-label="Table"
165
+ />
166
+ )}
167
+ {tables?.error && <div className="xeplr-factory-prop-error">{tables.error}</div>}
168
+ {errorAt('.table') && <div className="xeplr-factory-prop-error">{errorAt('.table')}</div>}
169
+ <div className="xeplr-factory-prop-help">Rows are read as <code>id</code> (saved) and <code>name</code> (shown).</div>
170
+ </div>
171
+ ) : (
172
+ <StaticOptions data={data} onChange={onChange} errors={errors} />
173
+ )}
174
+ </div>
175
+ )
176
+ }
177
+
178
+ function StaticOptions({ data, onChange, errors }) {
179
+ const options = data.options || []
180
+ const set = (next) => onChange({ source: 'static', options: next })
181
+ const update = (i, patch) => set(options.map((o, j) => (j === i ? { ...o, ...patch } : o)))
182
+ const rowError = (i) => errors.filter((e) => e.field.includes(`options[${i}]`)).map((e) => e.message)[0]
183
+
184
+ return (
185
+ <div className="xeplr-factory-options">
186
+ {options.length > 0 && (
187
+ <div className="xeplr-factory-options-head"><span>Shown as</span><span>Saved as</span><span /></div>
188
+ )}
189
+ {options.map((o, i) => (
190
+ <div key={i} className="xeplr-factory-option-row">
191
+ <input
192
+ className="xeplr-factory-prop-input"
193
+ type="text"
194
+ placeholder="Full time"
195
+ value={o.name ?? ''}
196
+ aria-label={`Option ${i + 1} name`}
197
+ onChange={(e) => update(i, { name: e.target.value })}
198
+ // An id nobody typed follows the name, so a list can be made by
199
+ // typing names alone. Once an id is set it is left alone — it is
200
+ // what saved records point at.
201
+ onBlur={(e) => { if (o.id === '' || o.id === undefined) update(i, { id: slugify(e.target.value, '_') }) }}
202
+ />
203
+ <input
204
+ className="xeplr-factory-prop-input xeplr-factory-mono"
205
+ type="text"
206
+ placeholder="full_time"
207
+ value={o.id ?? ''}
208
+ aria-label={`Option ${i + 1} id`}
209
+ onChange={(e) => update(i, { id: e.target.value })}
210
+ />
211
+ <button type="button" className="xeplr-factory-icon-button" aria-label={`Remove option ${i + 1}`} onClick={() => set(options.filter((_, j) => j !== i))}>×</button>
212
+ {rowError(i) && <div className="xeplr-factory-prop-error xeplr-factory-option-error">{rowError(i)}</div>}
213
+ </div>
214
+ ))}
215
+ <button type="button" className="xeplr-factory-add" onClick={() => set([...options, { id: '', name: '' }])}>+ Add option</button>
216
+ </div>
217
+ )
218
+ }
219
+
220
+ export const EDITORS = {
221
+ text: TextEditor,
222
+ textarea: TextareaEditor,
223
+ number: NumberEditor,
224
+ date: DateEditor,
225
+ toggle: ToggleEditor,
226
+ select: SelectEditor,
227
+ dataSource: DataSourceEditor
228
+ }
@@ -0,0 +1,65 @@
1
+ import ControlView from './ControlView.jsx'
2
+ import { contentBottom, MARGIN } from '../document.js'
3
+
4
+ // A saved screen, drawn. Presentation only — everything comes from
5
+ // useFactoryScreen.
6
+ //
7
+ // NO MEASURING. Positions are CSS: x and w as percentages of the width, y and h
8
+ // in container-query units (cqw = 1% of the screen's width) times the aspect.
9
+ // So the screen keeps exactly its proportions at any width, renders on the
10
+ // server, and never flashes at the wrong size while a ResizeObserver catches up.
11
+
12
+ export default function ScreenSample({ ctrl, className, style }) {
13
+ const { document: doc } = ctrl
14
+
15
+ if (ctrl.documentErrors.length) {
16
+ return (
17
+ <div className="xeplr-factory-invalid" role="alert">
18
+ <strong>This screen cannot be shown — its definition has problems:</strong>
19
+ <ul>
20
+ {ctrl.documentErrors.map((e, i) => <li key={i}><code>{e.path || '(screen)'}</code> {e.message}</li>)}
21
+ </ul>
22
+ </div>
23
+ )
24
+ }
25
+
26
+ const aspect = doc.aspect
27
+ const pageHeight = (contentBottom(doc) + MARGIN) * aspect * 100
28
+
29
+ return (
30
+ <div className={'xeplr-factory-screen' + (className ? ' ' + className : '')} style={style}>
31
+ <form
32
+ className="xeplr-factory-page"
33
+ style={{ height: `${pageHeight}cqw` }}
34
+ onSubmit={ctrl.submit}
35
+ noValidate
36
+ aria-label={doc.name}
37
+ >
38
+ {[...doc.nodes].sort((a, b) => (a.z || 0) - (b.z || 0)).map((node) => (
39
+ <div
40
+ key={node.id}
41
+ className={`xeplr-factory-node xeplr-factory-node--${node.type}`}
42
+ style={{
43
+ left: `${node.x * 100}%`,
44
+ top: `${node.y * aspect * 100}cqw`,
45
+ width: `${node.w * 100}%`,
46
+ height: `${node.h * aspect * 100}cqw`
47
+ }}
48
+ >
49
+ <ControlView
50
+ node={node}
51
+ mode="live"
52
+ value={node.props?.name ? ctrl.values[node.props.name] : undefined}
53
+ error={node.props?.name ? ctrl.errors[node.props.name] : undefined}
54
+ options={node.type === 'dropdown' ? ctrl.optionsFor(node) : undefined}
55
+ onChange={(raw) => ctrl.setValue(node, raw)}
56
+ onAction={(action) => { if (action === 'reset') ctrl.reset() }}
57
+ disabled={ctrl.submitting}
58
+ />
59
+ </div>
60
+ ))}
61
+ </form>
62
+ {ctrl.submitError && <div className="xeplr-factory-submit-error" role="alert">{ctrl.submitError}</div>}
63
+ </div>
64
+ )
65
+ }
@@ -0,0 +1,177 @@
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.
4
+
5
+ ONE SIZE UNIT, --xf-u: one design pixel on an 800px-wide screen. It is in
6
+ container-query units, so text and controls scale with the screen exactly as
7
+ their positions do — the whole screen keeps its proportions at any width. */
8
+
9
+ .xeplr-factory-screen,
10
+ .xeplr-factory-canvas {
11
+ container-type: inline-size;
12
+ --xf-u: calc(100cqw / 800);
13
+ --xf-accent: var(--xeplr-accent, #2563eb);
14
+ --xf-ink: var(--xeplr-text, #1a1a19);
15
+ --xf-muted: var(--xeplr-muted, #6b6a63);
16
+ --xf-border: var(--xeplr-border, #d9d8d0);
17
+ --xf-surface: var(--xeplr-surface, #ffffff);
18
+ --xf-danger: var(--xeplr-danger, #c2372e);
19
+ }
20
+
21
+ /* ── the screen ─────────────────────────────────────────────────────────── */
22
+ .xeplr-factory-screen { position: relative; width: 100%; color: var(--xf-ink); }
23
+ .xeplr-factory-page { position: relative; width: 100%; margin: 0; }
24
+ .xeplr-factory-node { position: absolute; box-sizing: border-box; }
25
+
26
+ .xeplr-factory-invalid {
27
+ border: 1px solid var(--xeplr-danger, #c2372e); border-radius: 8px; padding: 12px 16px;
28
+ color: var(--xeplr-danger, #c2372e); background: color-mix(in srgb, var(--xeplr-danger, #c2372e) 6%, transparent);
29
+ font-size: 13px;
30
+ }
31
+ .xeplr-factory-invalid ul { margin: 8px 0 0; padding-left: 18px; }
32
+ .xeplr-factory-submit-error { margin-top: 8px; color: var(--xf-danger); font-size: 13px; }
33
+
34
+ /* ── controls (shared by the screen and the canvas) ─────────────────────── */
35
+ .xeplr-factory-field {
36
+ position: relative;
37
+ display: flex; flex-direction: column; gap: calc(4 * var(--xf-u));
38
+ width: 100%; height: 100%; box-sizing: border-box;
39
+ font-size: max(11px, calc(14 * var(--xf-u)));
40
+ }
41
+ .xeplr-factory-field--inline { justify-content: center; }
42
+ .xeplr-factory-label { font-weight: 600; font-size: max(11px, calc(13 * var(--xf-u))); color: var(--xf-ink); line-height: 1.2; }
43
+ .xeplr-factory-required { color: var(--xf-danger); }
44
+ .xeplr-factory-input {
45
+ flex: 1; min-height: 0; width: 100%; box-sizing: border-box;
46
+ font: inherit; color: var(--xf-ink); background: var(--xeplr-input-bg, var(--xf-surface));
47
+ border: 1px solid var(--xeplr-input-border, var(--xf-border)); border-radius: calc(6 * var(--xf-u));
48
+ padding: 0 calc(10 * var(--xf-u));
49
+ }
50
+ .xeplr-factory-textarea { padding-block: calc(8 * var(--xf-u)); resize: none; }
51
+ .xeplr-factory-input:focus { outline: 2px solid color-mix(in srgb, var(--xf-accent) 40%, transparent); border-color: var(--xf-accent); }
52
+ .xeplr-factory-field.has-error .xeplr-factory-input { border-color: var(--xf-danger); }
53
+ .xeplr-factory-error {
54
+ /* In the gap BELOW the field, out of flow: a message appearing must not
55
+ shrink the input it is about, or the whole form jumps as you type. */
56
+ position: absolute; top: calc(100% + 2 * var(--xf-u)); left: 0; right: 0;
57
+ color: var(--xf-danger); font-size: max(10px, calc(12 * var(--xf-u))); line-height: 1.2;
58
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
59
+ }
60
+ .xeplr-factory-hint { color: var(--xf-muted); font-size: max(10px, calc(11 * var(--xf-u))); }
61
+ .xeplr-factory-check { display: flex; align-items: center; gap: calc(8 * var(--xf-u)); cursor: pointer; font-weight: 500; }
62
+ .xeplr-factory-check input { width: calc(18 * var(--xf-u)); height: calc(18 * var(--xf-u)); min-width: 12px; min-height: 12px; margin: 0; accent-color: var(--xf-accent); }
63
+
64
+ .xeplr-factory-text { margin: 0; color: var(--xf-ink); height: 100%; overflow: hidden; line-height: 1.25; }
65
+ .xeplr-factory-text--heading { font-size: max(16px, calc(26 * var(--xf-u))); font-weight: 700; }
66
+ .xeplr-factory-text--subheading { font-size: max(13px, calc(18 * var(--xf-u))); font-weight: 600; }
67
+ .xeplr-factory-text--text { font-size: max(11px, calc(14 * var(--xf-u))); color: var(--xf-muted); }
68
+
69
+ .xeplr-factory-button {
70
+ width: 100%; height: 100%; box-sizing: border-box; cursor: pointer;
71
+ padding: 0 calc(8 * var(--xf-u)); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
72
+ font: inherit; font-weight: 600; font-size: max(12px, calc(14 * var(--xf-u)));
73
+ border-radius: calc(6 * var(--xf-u)); border: 1px solid var(--xf-accent);
74
+ }
75
+ .xeplr-factory-button--submit { background: var(--xf-accent); color: #fff; }
76
+ .xeplr-factory-button--reset { background: var(--xf-surface); color: var(--xf-accent); }
77
+ .xeplr-factory-button:disabled { opacity: .6; cursor: default; }
78
+
79
+ /* ── the builder ────────────────────────────────────────────────────────── */
80
+ .xeplr-factory-builder {
81
+ display: flex; flex-direction: column; height: 100%; min-height: 560px;
82
+ color: var(--xeplr-text, #1a1a19); font-size: 13px;
83
+ --xf-accent: var(--xeplr-accent, #2563eb);
84
+ --xf-border: var(--xeplr-border, #d9d8d0);
85
+ --xf-muted: var(--xeplr-muted, #6b6a63);
86
+ --xf-danger: var(--xeplr-danger, #c2372e);
87
+ }
88
+ .xeplr-factory-bar {
89
+ display: flex; align-items: center; gap: 10px; padding: 8px 12px;
90
+ border-bottom: 1px solid var(--xf-border); background: var(--xeplr-surface, #fff);
91
+ }
92
+ .xeplr-factory-name {
93
+ flex: 1; min-width: 0; font: inherit; font-size: 16px; font-weight: 600;
94
+ border: 1px solid transparent; border-radius: 6px; padding: 5px 8px; background: transparent; color: inherit;
95
+ }
96
+ .xeplr-factory-name:hover, .xeplr-factory-name:focus { border-color: var(--xf-border); outline: none; }
97
+ .xeplr-factory-bar-status { color: var(--xf-muted); font-size: 12px; white-space: nowrap; }
98
+ .xeplr-factory-problems { font-size: 12px; color: var(--xf-muted); white-space: nowrap; cursor: help; }
99
+ .xeplr-factory-problems.is-loud { color: var(--xf-danger); font-weight: 600; }
100
+ .xeplr-factory-primary, .xeplr-factory-secondary, .xeplr-factory-danger {
101
+ font: inherit; font-weight: 600; padding: 6px 14px; border-radius: 6px; cursor: pointer; border: 1px solid var(--xf-accent);
102
+ }
103
+ .xeplr-factory-primary { background: var(--xf-accent); color: #fff; }
104
+ .xeplr-factory-primary:disabled { opacity: .6; }
105
+ .xeplr-factory-secondary { background: transparent; color: var(--xf-accent); }
106
+ .xeplr-factory-danger { background: transparent; color: var(--xf-danger); border-color: var(--xf-danger); }
107
+ .xeplr-factory-save-error { padding: 6px 12px; color: var(--xf-danger); background: color-mix(in srgb, var(--xf-danger) 7%, transparent); font-size: 12px; }
108
+
109
+ .xeplr-factory-body { flex: 1; min-height: 0; display: flex; }
110
+ .xeplr-factory-preview { flex: 1; min-height: 0; overflow: auto; padding: 16px; background: var(--xeplr-plane, #f7f7f5); }
111
+ .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; }
112
+
113
+ /* palette */
114
+ .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); }
115
+ .xeplr-factory-palette-group + .xeplr-factory-palette-group { margin-top: 12px; }
116
+ .xeplr-factory-palette-title { margin: 0 0 6px; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--xf-muted); }
117
+ .xeplr-factory-palette-item {
118
+ display: flex; align-items: center; gap: 8px; width: 100%; margin-bottom: 4px; padding: 7px 8px;
119
+ font: inherit; text-align: left; color: inherit; background: var(--xeplr-surface, #fff);
120
+ border: 1px solid var(--xf-border); border-radius: 6px; cursor: grab;
121
+ }
122
+ .xeplr-factory-palette-item:hover { border-color: var(--xf-accent); }
123
+ .xeplr-factory-palette-icon { width: 14px; height: 10px; border: 1.5px solid var(--xf-muted); border-radius: 2px; flex: 0 0 auto; }
124
+ .xeplr-factory-palette-icon--checkbox { width: 10px; }
125
+ .xeplr-factory-palette-icon--label { border: none; border-top: 2px solid var(--xf-muted); border-bottom: 2px solid var(--xf-muted); height: 6px; }
126
+ .xeplr-factory-palette-icon--button { background: var(--xf-muted); }
127
+ .xeplr-factory-palette-icon--textarea { height: 14px; }
128
+
129
+ /* canvas */
130
+ .xeplr-factory-stage { flex: 1; min-width: 0; min-height: 0; display: flex; background: var(--xeplr-plane, #f7f7f5); }
131
+ .xeplr-factory-canvas {
132
+ flex: 1; height: 100%;
133
+ background:
134
+ radial-gradient(circle, var(--xeplr-border-secondary, #e4e3db) 1px, transparent 1px) 0 0 / 20px 20px,
135
+ var(--xeplr-surface, #ffffff);
136
+ }
137
+ .xeplr-factory-design-node { width: 100%; height: 100%; box-sizing: border-box; border-radius: 4px; outline: 1px dashed transparent; cursor: move; }
138
+ .xeplr-factory-design-node:hover { outline-color: color-mix(in srgb, var(--xf-accent) 45%, transparent); }
139
+ .xeplr-factory-design-node.is-selected { outline: 1.5px solid var(--xf-accent); }
140
+ .xeplr-factory-design-node.has-error { outline: 1.5px solid var(--xf-danger); }
141
+ /* Inert on the canvas: a press drags the control, it does not type into it. */
142
+ .xeplr-factory-design-node * { pointer-events: none; }
143
+ .xeplr-factory-empty {
144
+ position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
145
+ color: var(--xf-muted); font-size: 14px; pointer-events: none;
146
+ }
147
+
148
+ /* property panel */
149
+ .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); }
150
+ .xeplr-factory-panel-empty { color: var(--xf-muted); line-height: 1.5; }
151
+ .xeplr-factory-panel-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
152
+ .xeplr-factory-panel-type { font-weight: 700; font-size: 14px; }
153
+ .xeplr-factory-link-danger { background: none; border: none; color: var(--xf-danger); cursor: pointer; font: inherit; padding: 2px 4px; }
154
+ .xeplr-factory-group { border: none; border-top: 1px solid var(--xf-border); margin: 0; padding: 10px 0 4px; }
155
+ .xeplr-factory-group legend { padding: 0 4px 0 0; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--xf-muted); }
156
+ .xeplr-factory-prop { margin-bottom: 10px; }
157
+ .xeplr-factory-prop-label { display: block; margin-bottom: 3px; font-weight: 600; font-size: 12px; }
158
+ .xeplr-factory-prop-input {
159
+ width: 100%; box-sizing: border-box; font: inherit; padding: 5px 7px; color: inherit;
160
+ border: 1px solid var(--xf-border); border-radius: 5px; background: var(--xeplr-input-bg, #fff);
161
+ }
162
+ .xeplr-factory-prop.has-error .xeplr-factory-prop-input { border-color: var(--xf-danger); }
163
+ .xeplr-factory-prop-help { margin-top: 3px; color: var(--xf-muted); font-size: 11px; line-height: 1.4; }
164
+ .xeplr-factory-prop-error { margin-top: 3px; color: var(--xf-danger); font-size: 11px; }
165
+ .xeplr-factory-panel-errors { margin-top: 8px; padding: 8px; border-radius: 6px; color: var(--xf-danger); background: color-mix(in srgb, var(--xf-danger) 7%, transparent); font-size: 11px; }
166
+ .xeplr-factory-toggle { display: flex; align-items: center; gap: 6px; font-weight: 600; font-size: 12px; cursor: pointer; }
167
+ .xeplr-factory-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
168
+
169
+ .xeplr-factory-segment { display: flex; margin-bottom: 8px; border: 1px solid var(--xf-border); border-radius: 6px; overflow: hidden; }
170
+ .xeplr-factory-segment button { flex: 1; font: inherit; font-size: 12px; padding: 5px; border: none; background: transparent; cursor: pointer; color: inherit; }
171
+ .xeplr-factory-segment button.is-on { background: var(--xf-accent); color: #fff; }
172
+ .xeplr-factory-options-head, .xeplr-factory-option-row { display: grid; grid-template-columns: 1fr 1fr 24px; gap: 4px; align-items: center; }
173
+ .xeplr-factory-options-head { font-size: 11px; color: var(--xf-muted); margin-bottom: 3px; }
174
+ .xeplr-factory-option-row { margin-bottom: 4px; }
175
+ .xeplr-factory-option-error { grid-column: 1 / -1; margin-top: 0; }
176
+ .xeplr-factory-icon-button { border: none; background: none; cursor: pointer; font-size: 16px; color: var(--xf-muted); padding: 0; }
177
+ .xeplr-factory-add { font: inherit; font-size: 12px; background: none; border: 1px dashed var(--xf-border); border-radius: 5px; padding: 5px; width: 100%; cursor: pointer; color: var(--xf-accent); }
@@ -0,0 +1,7 @@
1
+ import './factory.css'
2
+
3
+ export { default as BuilderSample } from './BuilderSample.jsx'
4
+ export { default as ScreenSample } from './ScreenSample.jsx'
5
+ export { default as ControlView, VIEWS } from './ControlView.jsx'
6
+ export { default as Palette, DRAG_TYPE } from './Palette.jsx'
7
+ export { default as PropertyPanel, EDITORS } from './PropertyPanel.jsx'