@xeplr/ui-factory 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +133 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/bin/xeplr-factory.js +98 -0
- package/package.json +59 -0
- package/src/controls.js +238 -0
- package/src/designs/BuilderSample.jsx +117 -0
- package/src/designs/ControlView.jsx +192 -0
- package/src/designs/Palette.jsx +34 -0
- package/src/designs/PropertyPanel.jsx +228 -0
- package/src/designs/ScreenSample.jsx +65 -0
- package/src/designs/factory.css +177 -0
- package/src/designs/index.js +7 -0
- package/src/document.js +215 -0
- package/src/generate.js +167 -0
- package/src/index.js +19 -0
- package/src/model.js +15 -0
- package/src/pages.jsx +42 -0
- package/src/propertyPath.js +35 -0
- package/src/useFactoryBuilder.js +209 -0
- package/src/useFactoryScreen.js +155 -0
- package/src/validateDocument.js +202 -0
- package/src/values.js +148 -0
|
@@ -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'
|