@xeplr/ui-factory 1.0.3 → 1.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xeplr/ui-factory",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Design data-entry screens on @xeplr/ui-canvas, save them as JSON, run them as auto-saving forms with lists of saved records — built for Claude to draft and people to refine",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -50,8 +50,11 @@
50
50
  .xeplr-factory-required { color: var(--xf-danger); }
51
51
  .xeplr-factory-input {
52
52
  flex: 1; min-height: 0; width: 100%; box-sizing: border-box;
53
- font: inherit; color: var(--xf-ink); background: var(--xeplr-input-bg, var(--xf-surface));
54
- border: 1px solid var(--xeplr-input-border, var(--xf-border)); border-radius: calc(6 * var(--xf-u));
53
+ /* The screen's own colours, never the host theme's input tokens: a dark theme
54
+ would give the box a dark background while the text stays the screen's
55
+ dark ink — unreadable. A screen's look is its style properties. */
56
+ font: inherit; color: var(--xf-ink); background: var(--xf-surface);
57
+ border: 1px solid var(--xf-border); border-radius: calc(6 * var(--xf-u));
55
58
  padding: 0 calc(10 * var(--xf-u));
56
59
  }
57
60
  .xeplr-factory-textarea { padding-block: calc(8 * var(--xf-u)); resize: none; }
@@ -89,7 +92,7 @@
89
92
  .xeplr-factory-list-mock { width: 100%; border-collapse: collapse; }
90
93
  .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); }
91
94
  .xeplr-factory-list-mock td { padding: calc(8 * var(--xf-u)) calc(10 * var(--xf-u)); border-bottom: 1px solid var(--xf-border); }
92
- .xeplr-factory-list-bar { display: block; height: calc(8 * var(--xf-u)); width: 70%; border-radius: 4px; background: var(--xeplr-border-secondary, #ecebe4); }
95
+ .xeplr-factory-list-bar { display: block; height: calc(8 * var(--xf-u)); width: 70%; border-radius: 4px; background: #ecebe4; }
93
96
  .xeplr-factory-list .xeplr-table-row.xeplr-factory-row-open td,
94
97
  .xeplr-factory-list tr.xeplr-factory-row-open td { background: color-mix(in srgb, var(--xf-accent) 10%, transparent); }
95
98
 
@@ -177,8 +180,8 @@
177
180
  .xeplr-factory-prop { margin-bottom: 10px; }
178
181
  .xeplr-factory-prop-label { display: block; margin-bottom: 3px; font-weight: 600; font-size: 12px; }
179
182
  .xeplr-factory-prop-input {
180
- width: 100%; box-sizing: border-box; font: inherit; padding: 5px 7px; color: inherit;
181
- border: 1px solid var(--xf-border); border-radius: 5px; background: var(--xeplr-input-bg, #fff);
183
+ width: 100%; box-sizing: border-box; font: inherit; padding: 5px 7px;
184
+ border: 1px solid var(--xf-border); border-radius: 5px; background: #fff; color: var(--xf-ink, #1a1a19);
182
185
  }
183
186
  .xeplr-factory-prop.has-error .xeplr-factory-prop-input { border-color: var(--xf-danger); }
184
187
  .xeplr-factory-prop-help { margin-top: 3px; color: var(--xf-muted); font-size: 11px; line-height: 1.4; }
package/src/remote.js CHANGED
@@ -16,27 +16,44 @@
16
16
  export function createFactoryApi({ fetch: doFetch, base = '' } = {}) {
17
17
  if (typeof doFetch !== 'function') throw new Error('createFactoryApi: pass { fetch } — usually authFetch from @xeplr/ui-account')
18
18
 
19
+ // Two kinds of fetch are accepted, because both are what apps have:
20
+ // window.fetch-like → a Response: read it, and throw on !res.ok
21
+ // authFetch-like → the PARSED body, and on failure it throws an Error
22
+ // carrying status and the body (err.status, err.body)
23
+ // Either way a failure becomes the same Error, with the server's fields,
24
+ // confirm and refused lists on it.
19
25
  async function call(method, path, body) {
20
- const res = await doFetch(`${base}/factory${path}`, {
21
- method,
22
- headers: body === undefined ? undefined : { 'Content-Type': 'application/json' },
23
- body: body === undefined ? undefined : JSON.stringify(body)
24
- })
25
- let json = null
26
- try { json = await res.json() } catch (_) { /* not JSON — reported below */ }
27
- if (!res.ok) {
28
- const err = new Error((json && json.message) || `${method} ${path} failed (${res.status})`)
29
- err.status = res.status
30
- err.fields = json && json.error && json.error.fields
31
- const extra = (json && json.dataArray && json.dataArray[0]) || {}
32
- // Publish: columns it would drop, waiting for a yes — and what it keeps.
33
- err.confirm = extra.confirm
34
- err.keep = extra.keep
35
- err.refused = extra.refused
36
- if (extra.statements) err.detail = extra.statements.join('\n')
26
+ let res
27
+ try {
28
+ res = await doFetch(`${base}/factory${path}`, {
29
+ method,
30
+ headers: body === undefined ? undefined : { 'Content-Type': 'application/json' },
31
+ body: body === undefined ? undefined : JSON.stringify(body)
32
+ })
33
+ } catch (err) {
34
+ if (err && err.body !== undefined && err.status) throw failure(method, path, err.status, err.body, err.message)
37
35
  throw err
38
36
  }
39
- return json ? json.dataArray : []
37
+ if (res && typeof res.json === 'function' && typeof res.ok === 'boolean') {
38
+ let json = null
39
+ try { json = await res.json() } catch (_) { /* not JSON — reported below */ }
40
+ if (!res.ok) throw failure(method, path, res.status, json)
41
+ return json ? json.dataArray : []
42
+ }
43
+ return res && Array.isArray(res.dataArray) ? res.dataArray : []
44
+ }
45
+
46
+ function failure(method, path, status, json, fallback) {
47
+ const err = new Error((json && json.message) || fallback || `${method} ${path} failed (${status})`)
48
+ err.status = status
49
+ err.fields = json && json.error && json.error.fields
50
+ const extra = (json && json.dataArray && json.dataArray[0]) || {}
51
+ // Publish: columns it would drop, waiting for a yes — and what it keeps.
52
+ err.confirm = extra.confirm
53
+ err.keep = extra.keep
54
+ err.refused = extra.refused
55
+ if (extra.statements) err.detail = extra.statements.join('\n')
56
+ return err
40
57
  }
41
58
  const one = (rows) => (Array.isArray(rows) ? rows[0] : rows)
42
59
  const enc = encodeURIComponent
@@ -53,11 +53,32 @@ export function useFactoryScreen({
53
53
  // `department` is a dropdown whose 2 reads "Finance" — lives on the screen it
54
54
  // edits in. Those fields are borrowed for display, and their table options
55
55
  // loaded alongside this screen's own.
56
+ //
57
+ // An edit screen not passed in `screens` is fetched with loadScreen as soon
58
+ // as the list shows — not when Edit is first pressed — or the list's columns
59
+ // would show stored values ("in_progress") until then.
60
+ const [loadedScreens, setLoadedScreens] = useState({})
61
+ const knownScreens = useMemo(() => ({ ...loadedScreens, ...(screens || {}) }), [loadedScreens, screens])
62
+ const editScreenIds = check.ok && !inputs.length
63
+ ? [...new Set(doc.nodes.filter((n) => n.type === 'list' && n.props.editScreen).map((n) => n.props.editScreen))].join('|')
64
+ : ''
65
+ useEffect(() => {
66
+ if (!editScreenIds || !loadScreen) return undefined
67
+ let cancelled = false
68
+ editScreenIds.split('|').filter((id) => !(screens && screens[id])).forEach((id) => {
69
+ loadScreen(id).then(
70
+ (found) => { if (!cancelled && found) setLoadedScreens((m) => (m[id] ? m : { ...m, [id]: found })) },
71
+ () => { /* shown when Edit is pressed, where the person is looking */ }
72
+ )
73
+ })
74
+ return () => { cancelled = true }
75
+ }, [editScreenIds]) // eslint-disable-line react-hooks/exhaustive-deps
76
+
56
77
  const fieldsFor = useCallback((node) => {
57
78
  if (inputs.length || !node || !node.props.editScreen) return inputs
58
- const editDoc = screens && screens[node.props.editScreen]
79
+ const editDoc = knownScreens[node.props.editScreen]
59
80
  return editDoc && validateDocument(editDoc, controls).ok ? inputNodes(editDoc, controls) : []
60
- }, [inputs, screens, controls])
81
+ }, [inputs, knownScreens, controls])
61
82
  const optionNodes = useMemo(() => {
62
83
  if (!check.ok) return []
63
84
  const borrowed = doc.nodes.filter((n) => n.type === 'list').flatMap((n) => (inputs.length ? [] : fieldsFor(n)))
@@ -290,7 +311,7 @@ export function useFactoryScreen({
290
311
 
291
312
  // ── popup: a list's Edit / New in its edit screen ─────────────────────
292
313
  const [popup, setPopup] = useState(null) // { node, screenId, record, document, loading, error }
293
- const screensRef = useRef(screens); screensRef.current = screens
314
+ const screensRef = useRef(knownScreens); screensRef.current = knownScreens
294
315
  const fetchRecordRef = useRef(fetchRecord); fetchRecordRef.current = fetchRecord
295
316
  const loadScreenRef = useRef(loadScreen); loadScreenRef.current = loadScreen
296
317