@xeplr/ui-factory 1.0.8 → 1.0.10
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 +1 -1
- package/README.md +29 -1
- package/package.json +1 -1
- package/src/controls.js +11 -5
- package/src/designs/ScreenSample.jsx +12 -5
- package/src/designs/factory.css +20 -2
- package/src/generate.js +4 -2
- package/src/hooks.js +21 -0
- package/src/useFactoryScreen.js +40 -2
- package/src/validateDocument.js +3 -0
package/AUTHORING.md
CHANGED
|
@@ -159,7 +159,7 @@ Names are derived once and agree everywhere: ids `employee_list` / `employee_edi
|
|
|
159
159
|
| `columns` | `1` or `2` (default `2`) |
|
|
160
160
|
| `heading` | `true` (default) puts the name at the top; a string uses that text; `false` for none |
|
|
161
161
|
| `fields` | **required** — in reading order |
|
|
162
|
-
| `list` | `true`, or `{ title, source, columns: [{ field, label }], pageSize, actions: ["new","edit","delete"] }` — saved records below the fields |
|
|
162
|
+
| `list` | `true`, or `{ title, source, columns: [{ field, label }], pageSize, actions: ["new","edit","delete"], openIn: "popup" \| "page" }` — saved records below the fields |
|
|
163
163
|
| `width` | design width in px (default `800`) |
|
|
164
164
|
| `style` | screen-wide defaults: `{ fontFamily, fontSize, color, background }` |
|
|
165
165
|
|
package/README.md
CHANGED
|
@@ -153,7 +153,17 @@ Rules that must hold belong in the server's hooks; the browser only shapes what
|
|
|
153
153
|
|
|
154
154
|
### Lists of saved records
|
|
155
155
|
|
|
156
|
-
A **List** control shows the records of the table the screen saves to (or another), in `@xeplr/ui-table`, with the columns you tick. Its **Edit in** names the edit screen: **Edit** and **New** open that screen in a popup, where it saves itself and the list refreshes behind it.
|
|
156
|
+
A **List** control shows the records of the table the screen saves to (or another), in `@xeplr/ui-table`, with the columns you tick. Its **Edit in** names the edit screen: **Edit** and **New** open that screen in a popup, where it saves itself and the list refreshes behind it.
|
|
157
|
+
|
|
158
|
+
**Opens in: a page of its own** sends them to a page in your app instead. The factory has no router, so it asks — `onOpenRecord` on the list's screen, and `onDone` on the page, which draws a **Done** button that saves what is pending and calls it:
|
|
159
|
+
|
|
160
|
+
```jsx
|
|
161
|
+
<FactoryScreen document={taskList} {...factory.screenProps}
|
|
162
|
+
onOpenRecord={({ id }) => navigate(id ? `/tasks/${id}` : '/tasks/new')} />
|
|
163
|
+
|
|
164
|
+
<FactoryScreen document={taskEdit} record={task} {...factory.screenProps}
|
|
165
|
+
onDone={() => navigate('/tasks')} />
|
|
166
|
+
``` **Delete** removes a row after a confirmation. Dropdown columns show names, not ids — read from the edit screen's fields.
|
|
157
167
|
|
|
158
168
|
The edit screen comes from `screens` (`{ id → document }`) or your `loadScreen(id)`. A list on a form with no **Edit in** opens rows in that form's own fields instead.
|
|
159
169
|
|
|
@@ -186,6 +196,11 @@ const clean = schemaHandler.applySchema(formSchema(screen), req.body) // throw
|
|
|
186
196
|
| `label` | — | text with a heading, subheading or text preset |
|
|
187
197
|
| `list` | — | saved records, with New / Edit / Delete |
|
|
188
198
|
|
|
199
|
+
A screen is a **sheet**: it paints its own surface, so a screen designed in
|
|
200
|
+
dark ink stays readable inside a dark app. Give the screen a `background` in
|
|
201
|
+
its own style to change it, or set `--xeplr-factory-paper` on the host for all
|
|
202
|
+
of them.
|
|
203
|
+
|
|
189
204
|
Every control also takes `style` — see [AUTHORING.md](./AUTHORING.md#styles) for the keys. Sizes are pixels at the screen's design `width` (default 800): a screen is shown at that size, never stretched, and scaled down only on a narrower display.
|
|
190
205
|
|
|
191
206
|
### Steps
|
|
@@ -207,6 +222,19 @@ document stays flat, so a control is still moved, styled, checked and saved
|
|
|
207
222
|
exactly as before; the step only decides when it is on screen. Renaming a step
|
|
208
223
|
keeps its `key`, so the controls on it stay where they are.
|
|
209
224
|
|
|
225
|
+
A stepper also gives the front-end hooks a **`step`** method, run before every
|
|
226
|
+
move — Next, Back and a click on the bar alike, so a rule cannot be walked
|
|
227
|
+
around by clicking ahead:
|
|
228
|
+
|
|
229
|
+
```js
|
|
230
|
+
class TaskHooks extends FactoryHooks {
|
|
231
|
+
step(ctx) { // { from, to, direction, values, screen }
|
|
232
|
+
if (ctx.direction === 'next' && ctx.from === 0 && !ctx.values.title) return false
|
|
233
|
+
return super.step(ctx) // false stays, a number goes there instead
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
210
238
|
**A step is not a separate form.** Every field is a column, is checked, and is
|
|
211
239
|
saved, whichever step is showing — a screen still saves itself as it is filled
|
|
212
240
|
in. **Next** holds at a step whose own fields are not filled in, and says which,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xeplr/ui-factory",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
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",
|
package/src/controls.js
CHANGED
|
@@ -477,10 +477,12 @@ CONTROLS.list = {
|
|
|
477
477
|
input: false,
|
|
478
478
|
defaultSize: { w: 0.92, h: 0.42 },
|
|
479
479
|
// No columns → the screen's own fields, in reading order.
|
|
480
|
-
defaults: { title: 'Saved records', pageSize: 10, actions: ['new', 'edit', 'delete'] },
|
|
481
|
-
// editScreen: the id of the screen Edit and New open in a POPUP
|
|
482
|
-
//
|
|
483
|
-
|
|
480
|
+
defaults: { title: 'Saved records', pageSize: 10, actions: ['new', 'edit', 'delete'], openIn: 'popup' },
|
|
481
|
+
// editScreen: the id of the screen Edit and New open — in a POPUP, or as a
|
|
482
|
+
// PAGE of the app's own (openIn: 'page', which calls onOpenRecord and lets
|
|
483
|
+
// the app navigate). Without one, Edit opens the row in this screen's own
|
|
484
|
+
// fields (a list on a form).
|
|
485
|
+
props: ['title', 'source', 'editScreen', 'openIn', 'columns', 'pageSize', 'actions', 'style'],
|
|
484
486
|
validation: [],
|
|
485
487
|
styles: ['fontFamily', 'fontSize', 'color', 'background', 'borderColor', 'borderWidth', 'borderRadius'],
|
|
486
488
|
properties: [
|
|
@@ -490,7 +492,11 @@ CONTROLS.list = {
|
|
|
490
492
|
fields: [
|
|
491
493
|
{ path: 'props.title', label: 'Title', type: 'text' },
|
|
492
494
|
{ path: 'props.source', label: 'Records from', type: 'table', help: 'Blank → the table this screen saves to' },
|
|
493
|
-
{ path: 'props.editScreen', label: 'Edit in', type: 'screen', help: 'The screen Edit and New open
|
|
495
|
+
{ path: 'props.editScreen', label: 'Edit in', type: 'screen', help: 'The screen Edit and New open' },
|
|
496
|
+
{ path: 'props.openIn', label: 'Opens in', type: 'select', options: [
|
|
497
|
+
{ value: 'popup', label: 'A popup' },
|
|
498
|
+
{ value: 'page', label: 'A page of its own' }
|
|
499
|
+
], help: 'A page asks the app to go there — see onOpenRecord' },
|
|
494
500
|
{ path: 'props.pageSize', label: 'Rows per page', type: 'number' }
|
|
495
501
|
]
|
|
496
502
|
},
|
|
@@ -31,15 +31,15 @@ function StepNav({ ctrl, node }) {
|
|
|
31
31
|
const at = ctrl.steps.active(node.id)
|
|
32
32
|
const last = ctrl.steps.count(node.id) - 1
|
|
33
33
|
const unfinished = ctrl.steps.fieldsOn(node.id, at).filter((n) => ctrl.allErrors[n.props.name])
|
|
34
|
-
const go = (to) => {
|
|
34
|
+
const go = (to, direction) => {
|
|
35
35
|
if (to > at && unfinished.length) { unfinished.forEach((n) => ctrl.touch(n)); return }
|
|
36
|
-
ctrl.steps.go(node.id, Math.min(Math.max(to, 0), last))
|
|
36
|
+
ctrl.steps.go(node.id, Math.min(Math.max(to, 0), last), direction)
|
|
37
37
|
}
|
|
38
38
|
return (
|
|
39
39
|
<div className="xeplr-factory-stepnav">
|
|
40
|
-
<button type="button" className="xeplr-factory-secondary" onClick={() => go(at - 1)} disabled={at === 0}>Back</button>
|
|
40
|
+
<button type="button" className="xeplr-factory-secondary" onClick={() => go(at - 1, 'back')} disabled={at === 0}>Back</button>
|
|
41
41
|
<span className="xeplr-factory-stepnav-where">Step {at + 1} of {last + 1}</span>
|
|
42
|
-
<button type="button" className="xeplr-factory-primary" onClick={() => go(at + 1)} disabled={at === last}>Next</button>
|
|
42
|
+
<button type="button" className="xeplr-factory-primary" onClick={() => go(at + 1, 'next')} disabled={at === last}>Next</button>
|
|
43
43
|
</div>
|
|
44
44
|
)
|
|
45
45
|
}
|
|
@@ -105,7 +105,7 @@ export default function ScreenSample({ ctrl, className, style, renderPopup }) {
|
|
|
105
105
|
error={node.props?.name ? ctrl.errors[node.props.name] : undefined}
|
|
106
106
|
options={CHOICE_TYPES.includes(node.type) ? ctrl.optionsFor(node) : undefined}
|
|
107
107
|
upload={node.type === 'file' ? ctrl.upload : undefined}
|
|
108
|
-
stepper={node.type === 'stepper' ? { active: ctrl.steps.active(node.id), onStep: (i) => ctrl.steps.go(node.id, i) } : undefined}
|
|
108
|
+
stepper={node.type === 'stepper' ? { active: ctrl.steps.active(node.id), onStep: (i) => ctrl.steps.go(node.id, i, 'jump') } : undefined}
|
|
109
109
|
onChange={(raw) => ctrl.setValue(node, raw)}
|
|
110
110
|
onBlur={() => ctrl.touch(node)}
|
|
111
111
|
list={node.type === 'list' ? {
|
|
@@ -131,6 +131,13 @@ export default function ScreenSample({ ctrl, className, style, renderPopup }) {
|
|
|
131
131
|
{ctrl.steps.nodes.map((node) => (
|
|
132
132
|
<StepNav key={node.id} ctrl={ctrl} node={node} />
|
|
133
133
|
))}
|
|
134
|
+
{ctrl.done && (
|
|
135
|
+
<div className="xeplr-factory-done">
|
|
136
|
+
<button type="button" className="xeplr-factory-primary" onClick={() => ctrl.flush().then(ctrl.done, ctrl.done)}>
|
|
137
|
+
{ctrl.status === 'pending' || ctrl.status === 'saving' ? 'Saving…' : 'Done'}
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
134
141
|
{ctrl.popup && renderPopup && renderPopup(ctrl.popup, ctrl)}
|
|
135
142
|
</div>
|
|
136
143
|
)
|
package/src/designs/factory.css
CHANGED
|
@@ -19,8 +19,23 @@
|
|
|
19
19
|
--xf-danger: var(--xeplr-danger, #c2372e);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/* ── the screen ───────────────────────────────────────────────────────────
|
|
23
|
-
|
|
22
|
+
/* ── the screen ───────────────────────────────────────────────────────────
|
|
23
|
+
A screen is a SHEET: it paints its own surface, the way the builder's canvas
|
|
24
|
+
does. Without that it is transparent, and a screen designed in dark ink
|
|
25
|
+
disappears into a dark app — what you designed is not what you get. The
|
|
26
|
+
screen's own `background` style overrides this (screenStyle), as does the
|
|
27
|
+
host with --xeplr-factory-paper. */
|
|
28
|
+
.xeplr-factory-screen {
|
|
29
|
+
position: relative; width: 100%;
|
|
30
|
+
color: var(--xf-ink);
|
|
31
|
+
background: var(--xeplr-factory-paper, var(--xf-surface));
|
|
32
|
+
border-radius: calc(8 * var(--xf-u));
|
|
33
|
+
}
|
|
34
|
+
/* The status line and the step buttons live on the sheet, so they keep a
|
|
35
|
+
margin of their own rather than touching its edge. */
|
|
36
|
+
.xeplr-factory-screen > .xeplr-factory-status,
|
|
37
|
+
.xeplr-factory-screen > .xeplr-factory-stepnav,
|
|
38
|
+
.xeplr-factory-screen > .xeplr-factory-done { padding-inline: calc(12 * var(--xf-u)); }
|
|
24
39
|
.xeplr-factory-status {
|
|
25
40
|
display: flex; justify-content: space-between; gap: 12px; min-height: 18px;
|
|
26
41
|
padding: 4px 2px 8px; font-size: 12px; color: var(--xf-muted);
|
|
@@ -119,6 +134,9 @@
|
|
|
119
134
|
.xeplr-factory-stepnav { display: flex; align-items: center; gap: 12px; padding: 10px 0; }
|
|
120
135
|
.xeplr-factory-stepnav-where { flex: 1; text-align: center; color: var(--xf-muted); font-size: 12px; }
|
|
121
136
|
|
|
137
|
+
/* Done — a page's way back, when the list opens records on one. */
|
|
138
|
+
.xeplr-factory-done { display: flex; justify-content: flex-end; padding: 10px 0; }
|
|
139
|
+
|
|
122
140
|
/* Which step the builder is on. */
|
|
123
141
|
.xeplr-factory-building { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 0 auto 8px; padding: 6px 0; }
|
|
124
142
|
.xeplr-factory-building-what { color: var(--xf-muted); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; }
|
package/src/generate.js
CHANGED
|
@@ -247,7 +247,7 @@ function round(v) {
|
|
|
247
247
|
// The list names the edit screen (`editScreen`), which is the only link
|
|
248
248
|
// between them; both save to and read from the same table.
|
|
249
249
|
|
|
250
|
-
const ENTITY_KEYS = ['entity', 'plural', 'source', 'fields', 'columns', 'listColumns', 'pageSize', 'actions', 'width', 'style', 'aspect']
|
|
250
|
+
const ENTITY_KEYS = ['entity', 'plural', 'source', 'fields', 'columns', 'listColumns', 'pageSize', 'actions', 'width', 'style', 'aspect', 'openIn']
|
|
251
251
|
|
|
252
252
|
/**
|
|
253
253
|
* @param spec
|
|
@@ -301,7 +301,9 @@ export function screensFromSpec(spec, controls = CONTROLS) {
|
|
|
301
301
|
editScreen: edit.id,
|
|
302
302
|
columns: listColumns,
|
|
303
303
|
pageSize: spec.pageSize || 10,
|
|
304
|
-
actions: spec.actions || ['new', 'edit', 'delete']
|
|
304
|
+
actions: spec.actions || ['new', 'edit', 'delete'],
|
|
305
|
+
// 'page' sends Edit / New to a page of the app's own — see onOpenRecord.
|
|
306
|
+
openIn: spec.openIn || 'popup'
|
|
305
307
|
}
|
|
306
308
|
}, controls))
|
|
307
309
|
|
package/src/hooks.js
CHANGED
|
@@ -53,6 +53,27 @@ export class FactoryHooks {
|
|
|
53
53
|
return ctx.defaults.delete(record)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Before a stepper moves — Next, Back, or a click on the bar itself.
|
|
58
|
+
*
|
|
59
|
+
* step(ctx) {
|
|
60
|
+
* if (ctx.direction === 'next' && ctx.from === 0 && !ctx.values.title) return false
|
|
61
|
+
* return super.step(ctx)
|
|
62
|
+
* }
|
|
63
|
+
*
|
|
64
|
+
* @param ctx.from the step being left, counting from 0
|
|
65
|
+
* @param ctx.to the step asked for
|
|
66
|
+
* @param ctx.direction 'next' | 'back' | 'jump' (a click on the bar)
|
|
67
|
+
* @param ctx.values what the form holds
|
|
68
|
+
* @param ctx.stepper the stepper's node id
|
|
69
|
+
* @returns false to stay where you are, a step number to go somewhere else,
|
|
70
|
+
* anything else to move as asked. The step's own required fields are
|
|
71
|
+
* checked before this runs, so this is for your rules, not theirs.
|
|
72
|
+
*/
|
|
73
|
+
step(ctx) {
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
/**
|
|
57
78
|
* Extra buttons on each row, beside Edit and Delete:
|
|
58
79
|
* [{ label: 'Mark done', onClick: (record, ctx) => … }]
|
package/src/useFactoryScreen.js
CHANGED
|
@@ -35,6 +35,8 @@ export const AUTOSAVE_DELAY = 700
|
|
|
35
35
|
* @param recordKey the id field on a record (default 'id')
|
|
36
36
|
* @param onSave async (values, { id, source, document }) → saved record | void
|
|
37
37
|
* @param fetchOptions async ({ table, node }) → [{ id, name }]
|
|
38
|
+
* @param onOpenRecord ({ screen, id, record, source }) → void — where a list whose "Opens in" is a page sends Edit / New
|
|
39
|
+
* @param onDone () → void — a page's "Done": back to wherever the person came from
|
|
38
40
|
* @param uploadFile async (file, { screen, field, node }) → { path } — what a file field saves
|
|
39
41
|
* @param fileUrl (path) → the address an attached file is read back from
|
|
40
42
|
* @param fetchRecords async ({ source, node }) → records, for lists
|
|
@@ -49,6 +51,7 @@ export const AUTOSAVE_DELAY = 700
|
|
|
49
51
|
*/
|
|
50
52
|
export function useFactoryScreen({
|
|
51
53
|
document: doc, record, recordKey = 'id', onSave, fetchOptions, fetchRecords, fetchRecord, onDelete, onChange, screens, loadScreen, hooks, uploadFile, fileUrl,
|
|
54
|
+
onOpenRecord, onDone,
|
|
52
55
|
autosaveDelay = AUTOSAVE_DELAY, controls = CONTROLS
|
|
53
56
|
} = {}) {
|
|
54
57
|
const check = useMemo(() => validateDocument(doc, controls), [doc, controls])
|
|
@@ -174,7 +177,31 @@ export function useFactoryScreen({
|
|
|
174
177
|
nodes: stepperNodes,
|
|
175
178
|
active: at,
|
|
176
179
|
count: (id) => stepsOf(stepperNodes.find((n) => n.id === id)).length,
|
|
177
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Move, if the app's `step` hook lets it: false keeps the person where
|
|
182
|
+
* they are, a number sends them somewhere else. The hook runs for Next,
|
|
183
|
+
* Back and a click on the bar alike, so a rule cannot be walked around.
|
|
184
|
+
*/
|
|
185
|
+
go: async (id, index, direction) => {
|
|
186
|
+
const from = at(id)
|
|
187
|
+
const last = stepsOf(stepperNodes.find((n) => n.id === id)).length - 1
|
|
188
|
+
const to = Math.min(Math.max(index, 0), Math.max(last, 0))
|
|
189
|
+
if (to === from) return from
|
|
190
|
+
const decide = hookMethod(hooksRef.current, 'step')
|
|
191
|
+
const answer = await decide({
|
|
192
|
+
from,
|
|
193
|
+
to,
|
|
194
|
+
direction: direction || (to > from ? 'next' : 'back'),
|
|
195
|
+
values: live.current.values,
|
|
196
|
+
stepper: id,
|
|
197
|
+
screen: doc.id,
|
|
198
|
+
document: doc
|
|
199
|
+
})
|
|
200
|
+
if (answer === false) return from
|
|
201
|
+
const where = Number.isInteger(answer) ? Math.min(Math.max(answer, 0), Math.max(last, 0)) : to
|
|
202
|
+
setOpenSteps((o) => ({ ...o, [id]: where }))
|
|
203
|
+
return where
|
|
204
|
+
},
|
|
178
205
|
/** The fields on this step, so Next can hold at a step that is not filled in. */
|
|
179
206
|
fieldsOn: (id, index) => inputNodes(doc, controls).filter((n) => {
|
|
180
207
|
const step = stepOf(n)
|
|
@@ -406,9 +433,19 @@ export function useFactoryScreen({
|
|
|
406
433
|
const fetchRecordRef = useRef(fetchRecord); fetchRecordRef.current = fetchRecord
|
|
407
434
|
const loadScreenRef = useRef(loadScreen); loadScreenRef.current = loadScreen
|
|
408
435
|
|
|
409
|
-
/**
|
|
436
|
+
/**
|
|
437
|
+
* Edit (a record) or New (null) in the list's edit screen — in a popup, or
|
|
438
|
+
* on a page of the app's own when the list says so. A page is the app's to
|
|
439
|
+
* open: the factory has no router, so it asks.
|
|
440
|
+
*/
|
|
441
|
+
const openRecordRef = useRef(onOpenRecord); openRecordRef.current = onOpenRecord
|
|
410
442
|
const openEditor = useCallback(async (node, rec) => {
|
|
411
443
|
const screenId = node.props.editScreen
|
|
444
|
+
if (node.props.openIn === 'page') {
|
|
445
|
+
if (!openRecordRef.current) throw new Error('This list opens records on a page, but no onOpenRecord was provided to go there')
|
|
446
|
+
const id = rec ? rec[live.current.recordKey] : null
|
|
447
|
+
return openRecordRef.current({ screen: screenId, id, record: rec || null, source: listSource(doc, node), node })
|
|
448
|
+
}
|
|
412
449
|
const base = { node, screenId, record: rec || null, document: null, loading: true, error: null }
|
|
413
450
|
setPopup(base)
|
|
414
451
|
try {
|
|
@@ -464,6 +501,7 @@ export function useFactoryScreen({
|
|
|
464
501
|
setValue,
|
|
465
502
|
touch,
|
|
466
503
|
optionsFor,
|
|
504
|
+
done: onDone || null,
|
|
467
505
|
steps: stepControl,
|
|
468
506
|
visibleNodes,
|
|
469
507
|
upload,
|
package/src/validateDocument.js
CHANGED
|
@@ -319,6 +319,9 @@ function checkStyle(style, allowed, at, err, owner) {
|
|
|
319
319
|
* must be set.
|
|
320
320
|
*/
|
|
321
321
|
function checkList(props, doc, at, err) {
|
|
322
|
+
if (props.openIn !== undefined && props.openIn !== 'popup' && props.openIn !== 'page') {
|
|
323
|
+
err(`${at}.props.openIn`, 'must be "popup" or "page"')
|
|
324
|
+
}
|
|
322
325
|
if (props.title !== undefined && typeof props.title !== 'string') err(`${at}.props.title`, 'must be a string')
|
|
323
326
|
if (props.source !== undefined && !(nonEmptyString(props.source) && TABLE_NAME.test(props.source))) err(`${at}.props.source`, 'must name the table to list records from')
|
|
324
327
|
if (props.source === undefined && doc.source === undefined) err(`${at}.props.source`, 'is required when the screen has no "source" — which table should the list read?')
|