@estiva-app/ui 0.14.0 → 0.16.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.
- package/README.md +28 -0
- package/dist/Breadcrumb.d.ts.map +1 -1
- package/dist/ChipInput.d.ts +14 -1
- package/dist/ChipInput.d.ts.map +1 -1
- package/dist/CommandPalette.d.ts +114 -0
- package/dist/CommandPalette.d.ts.map +1 -0
- package/dist/Menu.d.ts +4 -0
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Toast.d.ts.map +1 -1
- package/dist/eslint/escape.d.ts +61 -0
- package/dist/eslint/escape.d.ts.map +1 -0
- package/dist/eslint/has-a-page-and-a-story.d.ts +4 -0
- package/dist/eslint/has-a-page-and-a-story.d.ts.map +1 -0
- package/dist/eslint/index.d.ts +59 -0
- package/dist/eslint/index.d.ts.map +1 -0
- package/dist/eslint/index.js +286 -0
- package/dist/eslint/index.js.map +7 -0
- package/dist/eslint/no-hand-rolled-behaviour.d.ts +3 -0
- package/dist/eslint/no-hand-rolled-behaviour.d.ts.map +1 -0
- package/dist/eslint/no-raw-button.d.ts +11 -0
- package/dist/eslint/no-raw-button.d.ts.map +1 -0
- package/dist/eslint/raw-element-outside-a-wrapper.d.ts +29 -0
- package/dist/eslint/raw-element-outside-a-wrapper.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +614 -292
- package/dist/index.js.map +4 -4
- package/package.json +9 -1
- package/src/Avatar.tsx +1 -1
- package/src/AvatarGroup.tsx +1 -1
- package/src/Breadcrumb.tsx +6 -2
- package/src/ChipInput.mdx +9 -0
- package/src/ChipInput.stories.tsx +18 -0
- package/src/ChipInput.test.tsx +18 -0
- package/src/ChipInput.tsx +18 -4
- package/src/CommandPalette.mdx +133 -0
- package/src/CommandPalette.stories.tsx +416 -0
- package/src/CommandPalette.test.tsx +392 -0
- package/src/CommandPalette.tsx +643 -0
- package/src/Menu.tsx +4 -2
- package/src/Select.test.tsx +20 -3
- package/src/Toast.tsx +12 -24
- package/src/eslint/escape.ts +112 -0
- package/src/eslint/has-a-page-and-a-story.test.ts +57 -0
- package/src/eslint/has-a-page-and-a-story.ts +97 -0
- package/src/eslint/index.test.ts +114 -0
- package/src/eslint/index.ts +145 -0
- package/src/eslint/no-hand-rolled-behaviour.test.ts +70 -0
- package/src/eslint/no-hand-rolled-behaviour.ts +116 -0
- package/src/eslint/no-raw-button.test.ts +118 -0
- package/src/eslint/no-raw-button.ts +39 -0
- package/src/eslint/raw-element-outside-a-wrapper.test.ts +82 -0
- package/src/eslint/raw-element-outside-a-wrapper.ts +85 -0
- package/src/index.ts +17 -0
- package/stories/Choosing.mdx +1 -0
- package/stories/TokensPage.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estiva-app/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Estiva's design tokens (the contract) and a small set of primitives (a convenience) for every Estiva app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"types": "./dist/index.d.ts",
|
|
20
20
|
"default": "./dist/index.js"
|
|
21
21
|
},
|
|
22
|
+
"./eslint": {
|
|
23
|
+
"types": "./dist/eslint/index.d.ts",
|
|
24
|
+
"default": "./dist/eslint/index.js"
|
|
25
|
+
},
|
|
22
26
|
"./tailwind-preset": "./tailwind-preset.js",
|
|
23
27
|
"./tokens.css": "./tokens.css",
|
|
24
28
|
"./package.json": "./package.json",
|
|
@@ -32,7 +36,11 @@
|
|
|
32
36
|
"test:watch": "vitest --project unit",
|
|
33
37
|
"test:a11y": "vitest run --project storybook-signal --project storybook-ship",
|
|
34
38
|
"typecheck": "tsc --noEmit",
|
|
39
|
+
"prelint": "node build.mjs",
|
|
35
40
|
"lint": "eslint .",
|
|
41
|
+
"prelint:rules": "node build.mjs",
|
|
42
|
+
"lint:rules": "eslint --config eslint.gates.config.js .",
|
|
43
|
+
"postlint:rules": "node scripts/gates-count.mjs",
|
|
36
44
|
"gates:status": "node scripts/gates-status.mjs",
|
|
37
45
|
"prepublishOnly": "npm run build"
|
|
38
46
|
},
|
package/src/Avatar.tsx
CHANGED
|
@@ -118,7 +118,7 @@ export function Avatar({ src, name, alt = '', size = 36, label: spoken, classNam
|
|
|
118
118
|
*/
|
|
119
119
|
className="w-full h-full flex items-center justify-center font-semibold leading-none"
|
|
120
120
|
style={{
|
|
121
|
-
/* eslint-disable no-restricted-syntax -- the per-person palette (the note at
|
|
121
|
+
/* eslint-disable no-restricted-syntax -- @estiva-escape: the per-person palette (the note at
|
|
122
122
|
the top): eight hues picked from the name, the one ink that reads on all
|
|
123
123
|
of them, and a size that follows `size`. None of it can be a token. */
|
|
124
124
|
// The one ink colour that reads on all eight hues, which are a
|
package/src/AvatarGroup.tsx
CHANGED
|
@@ -55,7 +55,7 @@ export function AvatarGroup({ members, size = 24 }: AvatarGroupProps) {
|
|
|
55
55
|
<span
|
|
56
56
|
key={i}
|
|
57
57
|
className="relative flex rounded-sm"
|
|
58
|
-
/* eslint-disable-next-line no-restricted-syntax -- the ring's width is the `ring` prop, so it cannot be a class; its colour is the surface token */
|
|
58
|
+
/* eslint-disable-next-line no-restricted-syntax -- @estiva-escape: the ring's width is the `ring` prop, so it cannot be a class; its colour is the surface token */
|
|
59
59
|
style={{ marginRight: -overlap, boxShadow: `0 0 0 ${ring}px var(--bg-surface)` }}
|
|
60
60
|
>
|
|
61
61
|
{/* Each face says whose it is: a stack stands on its own, with no
|
package/src/Breadcrumb.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Fragment, useCallback, useLayoutEffect, useRef, useState, type MouseEventHandler, type ReactNode } from 'react'
|
|
2
2
|
import { cn } from './cn'
|
|
3
|
+
import { Link } from './Link'
|
|
3
4
|
import { WithTooltip } from './Tooltip'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -88,9 +89,12 @@ export function Breadcrumb({ items, className }: BreadcrumbProps) {
|
|
|
88
89
|
labelRefs.current[index] = el
|
|
89
90
|
}
|
|
90
91
|
const crumb = item.href ? (
|
|
91
|
-
|
|
92
|
+
// `plain` adds no look of its own: a crumb keeps the trail's size and
|
|
93
|
+
// tone, and brightens on hover. The package's own Link, so a trail
|
|
94
|
+
// follows whatever a link learns to do (UIG-5).
|
|
95
|
+
<Link ref={setLabelRef} variant="plain" href={item.href} onClick={item.onClick} className={cn(text, 'hover:text-text-primary')}>
|
|
92
96
|
{item.label}
|
|
93
|
-
</
|
|
97
|
+
</Link>
|
|
94
98
|
) : (
|
|
95
99
|
<span ref={setLabelRef} className={text} aria-current={last ? 'page' : undefined}>
|
|
96
100
|
{item.label}
|
package/src/ChipInput.mdx
CHANGED
|
@@ -65,6 +65,15 @@ import { ChipInput } from '@estiva-app/ui'
|
|
|
65
65
|
<ChipInput aria-labelledby="to-label" value={chosen} onChange={setChosen} options={directory} />
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
- **`InputChip` on its own** names its ✕ `Remove <label>`. Where the ✕ does
|
|
69
|
+
something else, say what with `removeLabel` (a scope chip: "Leave Ship").
|
|
70
|
+
- To cap a long label, give the chip a `max-w-*` and `truncate`: the label is
|
|
71
|
+
cut and the ✕ keeps its size. `truncate` is off unless asked, because
|
|
72
|
+
cutting clips a letter's soft edge by up to 4px at 1x even when the label
|
|
73
|
+
fits.
|
|
74
|
+
|
|
75
|
+
<Canvas of={ChipInputStories.ALongLabelCut} />
|
|
76
|
+
|
|
68
77
|
## Keys
|
|
69
78
|
|
|
70
79
|
| Input | What happens |
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
2
|
import { useState } from 'react'
|
|
3
|
+
import { IconSquareRounded } from '@tabler/icons-react'
|
|
3
4
|
import { Avatar } from './Avatar'
|
|
4
5
|
import { ChipInput, InputChip, type ChipInputOption } from './ChipInput'
|
|
5
6
|
|
|
@@ -97,3 +98,20 @@ export const TheChipItself: Story = {
|
|
|
97
98
|
</div>
|
|
98
99
|
),
|
|
99
100
|
}
|
|
101
|
+
|
|
102
|
+
/** A chip capped in width, its long label cut, and a ✕ named for what it does. */
|
|
103
|
+
export const ALongLabelCut: Story = {
|
|
104
|
+
parameters: { controls: { disable: true } },
|
|
105
|
+
render: () => (
|
|
106
|
+
<div className="flex items-center gap-2">
|
|
107
|
+
<InputChip
|
|
108
|
+
label="A label much longer than the chip may be"
|
|
109
|
+
leading={<IconSquareRounded size={16} stroke={1.5} className="text-text-secondary" />}
|
|
110
|
+
onRemove={() => {}}
|
|
111
|
+
removeLabel="Leave the label"
|
|
112
|
+
truncate
|
|
113
|
+
className="max-w-44"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
),
|
|
117
|
+
}
|
package/src/ChipInput.test.tsx
CHANGED
|
@@ -215,4 +215,22 @@ describe('InputChip', () => {
|
|
|
215
215
|
render(<InputChip label="Label" />)
|
|
216
216
|
expect(screen.queryByRole('button')).toBeNull()
|
|
217
217
|
})
|
|
218
|
+
|
|
219
|
+
it('names its ✕ with removeLabel when given', async () => {
|
|
220
|
+
const user = userEvent.setup()
|
|
221
|
+
const onRemove = vi.fn()
|
|
222
|
+
render(<InputChip label="Label" onRemove={onRemove} removeLabel="Leave Label" />)
|
|
223
|
+
await user.click(screen.getByRole('button', { name: 'Leave Label' }))
|
|
224
|
+
expect(onRemove).toHaveBeenCalledTimes(1)
|
|
225
|
+
expect(screen.queryByRole('button', { name: 'Remove Label' })).toBeNull()
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('cuts its label only when asked, and then the ✕ never gives way', () => {
|
|
229
|
+
const { rerender } = render(<InputChip label="Label" onRemove={() => {}} />)
|
|
230
|
+
expect(screen.getByText('Label').className).not.toMatch(/\btruncate\b/)
|
|
231
|
+
expect(screen.getByRole('button').className).not.toMatch(/\bshrink-0\b/)
|
|
232
|
+
rerender(<InputChip label="Label" onRemove={() => {}} truncate className="max-w-[160px]" />)
|
|
233
|
+
expect(screen.getByText('Label').className).toMatch(/\bmin-w-0\b.*\btruncate\b/)
|
|
234
|
+
expect(screen.getByRole('button').className).toMatch(/\bshrink-0\b/)
|
|
235
|
+
})
|
|
218
236
|
})
|
package/src/ChipInput.tsx
CHANGED
|
@@ -38,14 +38,27 @@ export interface InputChipProps {
|
|
|
38
38
|
leading?: ReactNode
|
|
39
39
|
/** Draws the ✕; absent, the chip is display-only. */
|
|
40
40
|
onRemove?: () => void
|
|
41
|
+
/**
|
|
42
|
+
* The ✕'s name for a screen reader. `Remove <label>` when not given. A chip
|
|
43
|
+
* whose ✕ does something other than remove it names what it does — a
|
|
44
|
+
* launcher's scope chip leaves the scope ("Leave Ship").
|
|
45
|
+
*/
|
|
46
|
+
removeLabel?: string
|
|
47
|
+
/**
|
|
48
|
+
* Cut a long label with an ellipsis once the chip is capped, by a
|
|
49
|
+
* `max-w-*` on `className`. Off unless asked: cutting clips up to 4px of a
|
|
50
|
+
* letter's soft edge at 1x even when the label fits (measured 2026-09-15),
|
|
51
|
+
* so a chip that is never capped keeps every pixel.
|
|
52
|
+
*/
|
|
53
|
+
truncate?: boolean
|
|
41
54
|
className?: string
|
|
42
55
|
}
|
|
43
56
|
|
|
44
|
-
export function InputChip({ label, leading, onRemove, className }: InputChipProps) {
|
|
57
|
+
export function InputChip({ label, leading, onRemove, removeLabel, truncate, className }: InputChipProps) {
|
|
45
58
|
return (
|
|
46
59
|
<div className={cn(CHIP_BOX, chipPadding(!!leading, !!onRemove), className)}>
|
|
47
60
|
{leading && <span className="flex shrink-0 items-center">{leading}</span>}
|
|
48
|
-
<span className={CHIP_LABEL}>{label}</span>
|
|
61
|
+
<span className={cn(CHIP_LABEL, truncate && 'min-w-0 truncate')}>{label}</span>
|
|
49
62
|
{/* Base UI's `Button`, as every button in the package is (D6). Base UI
|
|
50
63
|
has no chip of its own — its only chips are `Combobox.Chip` and
|
|
51
64
|
`ChipRemove`, which throw outside a combobox — so the ✕ is the one
|
|
@@ -57,8 +70,9 @@ export function InputChip({ label, leading, onRemove, className }: InputChipProp
|
|
|
57
70
|
e.stopPropagation()
|
|
58
71
|
onRemove()
|
|
59
72
|
}}
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
// In a capped chip the label gives way, never the ✕.
|
|
74
|
+
className={cn(CHIP_REMOVE, truncate && 'shrink-0')}
|
|
75
|
+
aria-label={removeLabel ?? `Remove ${label}`}
|
|
62
76
|
>
|
|
63
77
|
<IconX size={10} stroke={1.5} />
|
|
64
78
|
</BaseButton>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Meta, Canvas, Controls } from '@storybook/addon-docs/blocks'
|
|
2
|
+
import * as CommandPaletteStories from './CommandPalette.stories'
|
|
3
|
+
|
|
4
|
+
<Meta of={CommandPaletteStories} />
|
|
5
|
+
|
|
6
|
+
# CommandPalette
|
|
7
|
+
|
|
8
|
+
A window that searches and runs things: a field, rows in groups, and a footer
|
|
9
|
+
naming the keys that work right now. It shows one level at a time — rows, or a
|
|
10
|
+
form — and the app keeps the levels.
|
|
11
|
+
|
|
12
|
+
<Canvas of={CommandPaletteStories.FirstLevel} />
|
|
13
|
+
|
|
14
|
+
## When
|
|
15
|
+
|
|
16
|
+
- **One place to find something and act on it**, from anywhere in an app,
|
|
17
|
+
opened by a shortcut the app binds.
|
|
18
|
+
- **`CommandPaletteSearch`** — a level of rows. Without a `chip` it is the
|
|
19
|
+
first level; with one, you are inside something, and the chip names it.
|
|
20
|
+
- **`CommandPaletteForm`** — a level that is a form: what a row creates or
|
|
21
|
+
changes, without leaving the palette.
|
|
22
|
+
- **`CommandPaletteWorking`, `CommandPaletteAnswer`, `CommandPaletteQuote`** —
|
|
23
|
+
above the rows, for a result that is written rather than listed: a line while
|
|
24
|
+
it is on its way, an answer with marks pointing at the rows it came from, and
|
|
25
|
+
text written to be used somewhere else.
|
|
26
|
+
|
|
27
|
+
<Canvas of={CommandPaletteStories.Answer} />
|
|
28
|
+
|
|
29
|
+
<Canvas of={CommandPaletteStories.Form} />
|
|
30
|
+
|
|
31
|
+
## When not
|
|
32
|
+
|
|
33
|
+
- A short list of verbs from a trigger → **Menu**.
|
|
34
|
+
- One value for a field → **Select**; several → **ChipInput**.
|
|
35
|
+
- A form with a title and buttons of its own, not reached from a palette →
|
|
36
|
+
**DialogShell**. The palette is not built on it: a palette has no title bar,
|
|
37
|
+
no close button and no button row, and on the shell it would be the shell with
|
|
38
|
+
every slot switched off.
|
|
39
|
+
- Filtering a list that stays on the page → **SearchInput**.
|
|
40
|
+
|
|
41
|
+
## How
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { CommandPalette, CommandPaletteSearch } from '@estiva-app/ui'
|
|
45
|
+
|
|
46
|
+
<CommandPalette open={open} onOpenChange={setOpen} label="Command palette" where="In Item one" modKey="Ctrl">
|
|
47
|
+
<CommandPaletteSearch
|
|
48
|
+
query={query}
|
|
49
|
+
onQueryChange={setQuery}
|
|
50
|
+
placeholder="Search, or choose a place"
|
|
51
|
+
groups={[
|
|
52
|
+
{ label: 'Recent', rows: [{ id: 'item-one', label: 'Item one', icon: <IconSquareRounded size={16} stroke={1.5} />, onSelect: open, onForget: forget }] },
|
|
53
|
+
{ label: 'Places', rows: [{ id: 'place-one', label: 'Place one', icon, onSelect: goIn, onGoIn: goIn }] },
|
|
54
|
+
]}
|
|
55
|
+
/>
|
|
56
|
+
</CommandPalette>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- **The app opens it.** The palette listens to nothing outside its own window:
|
|
60
|
+
bind the shortcut in the app, pass `open`, and give `modKey` the platform's
|
|
61
|
+
spelling (`Cmd` on a Mac). The keys answer to Ctrl and Cmd either way.
|
|
62
|
+
- **One level at a time, and the app keeps the stack.** Render a
|
|
63
|
+
`CommandPaletteSearch` or a `CommandPaletteForm` as the only child, and keep
|
|
64
|
+
each level's query yourself, so going back puts back the text that was there.
|
|
65
|
+
A level inside another passes `chip` — its label, and `onBack`.
|
|
66
|
+
- **A row says what it can do, and the keys and the footer follow.** `onSelect`
|
|
67
|
+
is Enter and a click. `onGoIn` makes Tab, and → at the end of the text, go in,
|
|
68
|
+
and draws the chevron. `onForget` makes Ctrl+Backspace forget it — for rows
|
|
69
|
+
that are the person's own history. The footer names only what the lit row
|
|
70
|
+
answers to.
|
|
71
|
+
- **`icon` sits on the row's 32px tile; `leading` takes the same 32px with no
|
|
72
|
+
tile**, for something with a look of its own — a face, a status mark.
|
|
73
|
+
- **`id` is unique in the level.** The lit row is followed by it, so a caller
|
|
74
|
+
that builds its rows again on every render still runs the handler of the row
|
|
75
|
+
on screen.
|
|
76
|
+
- **`pending` while rows are still on their way; `empty` only once none are.**
|
|
77
|
+
Pass `empty` while a search is running and the page says "nothing" before it
|
|
78
|
+
knows.
|
|
79
|
+
- **Rows that arrive late do not take the highlight.** Base UI keeps the
|
|
80
|
+
highlight's position rather than its row, so a group arriving above the lit
|
|
81
|
+
row would hand the highlight — and Enter — to something else. The palette
|
|
82
|
+
walks it back to the row you had. Typing starts again from the first row.
|
|
83
|
+
- **Home and End move the text cursor, and nothing else.** Base UI would also
|
|
84
|
+
send the highlight to the first or last row; the palette stops that.
|
|
85
|
+
- **The field is named by its placeholder**, so say what it asks for.
|
|
86
|
+
- **A form's fields go in `Field`s.** Mark the missing ones with `error` inside
|
|
87
|
+
`onSubmit`, and the first of them takes focus. `working` locks the fields,
|
|
88
|
+
puts its words on the button and beside it, and holds focus on the form so
|
|
89
|
+
Esc still closes it; when it clears, focus goes to the first field that needs
|
|
90
|
+
something, or back where it was. `error` is what went wrong, beside the
|
|
91
|
+
button. `submitWaits` says why there is nothing to submit yet, on the button,
|
|
92
|
+
and Ctrl+Enter does nothing until it clears.
|
|
93
|
+
- **Focus never falls out of a level.** When the element that had it goes — a
|
|
94
|
+
row picked, a level swapped — focus goes to the level's first control.
|
|
95
|
+
|
|
96
|
+
## Keys
|
|
97
|
+
|
|
98
|
+
| Where | Input | What happens |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| a level of rows | ↑ / ↓ | move between rows, and stop at the ends |
|
|
101
|
+
| | Enter, or a click | what the lit row does |
|
|
102
|
+
| | Tab | goes in, on a row that leads somewhere; anywhere else nothing, and focus stays in the field |
|
|
103
|
+
| | → at the end of the text | goes in, on a row that leads somewhere; anywhere else it moves the cursor |
|
|
104
|
+
| | Ctrl+Backspace | forgets the lit row, if it can be forgotten; otherwise deletes a word |
|
|
105
|
+
| | Home / End | move the text cursor; the lit row stays |
|
|
106
|
+
| inside a level | Backspace at the start of the field, or the chip's ✕ | back, with focus in the field |
|
|
107
|
+
| a form | Tab / Shift+Tab | between the fields, never out of the window |
|
|
108
|
+
| | Ctrl+Enter | submits; if a field needs something, focus goes to the first |
|
|
109
|
+
| | Backspace in an empty text field | back. In a form that is one list, Backspace anywhere; in a list inside a form with text fields, nothing |
|
|
110
|
+
| | while working | nothing but Esc |
|
|
111
|
+
| | Esc in an open list | closes the list first |
|
|
112
|
+
| anywhere | Esc, or a click outside | closes; focus returns to whatever opened it |
|
|
113
|
+
|
|
114
|
+
## Props
|
|
115
|
+
|
|
116
|
+
<Controls of={CommandPaletteStories.FirstLevel} />
|
|
117
|
+
|
|
118
|
+
**`CommandPaletteSearch`** — `query` and `onQueryChange` (the field, controlled),
|
|
119
|
+
`placeholder` (also the field's name), `groups` (`{ label, rows }`, a group
|
|
120
|
+
with no rows is not drawn), `chip?` (`{ label, leading?, onBack }`), `pending?`,
|
|
121
|
+
`empty?`, and `children` for what sits above the rows.
|
|
122
|
+
|
|
123
|
+
**A row** — `id`, `label`, `description?`, `icon?` or `leading?`, `onSelect`,
|
|
124
|
+
`onGoIn?`, `onForget?`.
|
|
125
|
+
|
|
126
|
+
**`CommandPaletteForm`** — `chip`, `icon?`, `submitLabel`, `onSubmit`,
|
|
127
|
+
`submitWaits?`, `working?` (`{ button, line }`), `error?`, and the fields as
|
|
128
|
+
`children`.
|
|
129
|
+
|
|
130
|
+
**`CommandPaletteWorking`** — the words as `children`, `bars?` (0–3, default 2).
|
|
131
|
+
**`CommandPaletteAnswer`** — the text as `children` (a blank line starts a
|
|
132
|
+
paragraph; `[1]` is a mark), `note?`. **`CommandPaletteQuote`** — the text as
|
|
133
|
+
`children`, line breaks kept.
|