@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
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
3
|
+
import { IconSquareRounded } from '@tabler/icons-react'
|
|
4
|
+
import { Avatar } from './Avatar'
|
|
5
|
+
import { Button } from './Button'
|
|
6
|
+
import {
|
|
7
|
+
CommandPalette,
|
|
8
|
+
CommandPaletteAnswer,
|
|
9
|
+
CommandPaletteForm,
|
|
10
|
+
CommandPaletteQuote,
|
|
11
|
+
CommandPaletteSearch,
|
|
12
|
+
CommandPaletteWorking,
|
|
13
|
+
type CommandPaletteGroup,
|
|
14
|
+
type CommandPaletteRow,
|
|
15
|
+
} from './CommandPalette'
|
|
16
|
+
import { Field } from './Field'
|
|
17
|
+
import { Select } from './Select'
|
|
18
|
+
import { TextInput } from './TextInput'
|
|
19
|
+
import { Textarea } from './Textarea'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A window that searches and runs things. It shows one level at a time — a
|
|
23
|
+
* level of rows, or a form — and the caller keeps the levels. Every story
|
|
24
|
+
* below is the same small caller, started at a different place, so every key
|
|
25
|
+
* works in all of them.
|
|
26
|
+
*
|
|
27
|
+
* Nothing here is fetched: what "arrives later" is a timer.
|
|
28
|
+
*/
|
|
29
|
+
const meta = {
|
|
30
|
+
title: 'Overlays/CommandPalette',
|
|
31
|
+
component: CommandPalette,
|
|
32
|
+
parameters: {
|
|
33
|
+
layout: 'fullscreen',
|
|
34
|
+
// Portals a fixed overlay to document.body — an iframe on the Docs page,
|
|
35
|
+
// so it does not escape over the docs content.
|
|
36
|
+
docs: { story: { inline: false, height: '640px' } },
|
|
37
|
+
// axe color-contrast is off here until PLAN.md stage 0.10 is ruled: the
|
|
38
|
+
// footer and the "Searching…" and empty lines are muted text, 3.49:1 on
|
|
39
|
+
// --bg-elevated in signal (AA 4.5:1), measured by CI on PR #43; ship passes.
|
|
40
|
+
a11y: { config: { rules: [{ id: 'color-contrast', enabled: false }] } },
|
|
41
|
+
},
|
|
42
|
+
args: { open: true, onOpenChange: () => {}, label: 'Command palette', where: 'In Item one', modKey: 'Ctrl', children: null },
|
|
43
|
+
argTypes: { children: { control: false }, onOpenChange: { control: false }, open: { control: false } },
|
|
44
|
+
} satisfies Meta<typeof CommandPalette>
|
|
45
|
+
|
|
46
|
+
export default meta
|
|
47
|
+
type Story = StoryObj<typeof meta>
|
|
48
|
+
|
|
49
|
+
const icon = <IconSquareRounded size={16} stroke={1.5} />
|
|
50
|
+
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
51
|
+
|
|
52
|
+
type Level =
|
|
53
|
+
| { kind: 'first' }
|
|
54
|
+
| { kind: 'place'; name: string }
|
|
55
|
+
| { kind: 'ask' }
|
|
56
|
+
| { kind: 'draft' }
|
|
57
|
+
| { kind: 'form' }
|
|
58
|
+
| { kind: 'one-list' }
|
|
59
|
+
|
|
60
|
+
interface Frame {
|
|
61
|
+
level: Level
|
|
62
|
+
query: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type Start = 'first' | 'arriving' | 'nothing' | 'place' | 'working' | 'answer' | 'draft' | 'form' | 'refused' | 'one-list'
|
|
66
|
+
|
|
67
|
+
const PLACES = ['Place one', 'Place two']
|
|
68
|
+
const ACTIONS = [
|
|
69
|
+
{ id: 'action-one', label: 'Action one', description: 'Opens a form' },
|
|
70
|
+
{ id: 'action-two', label: 'Action two', description: 'Opens a form' },
|
|
71
|
+
]
|
|
72
|
+
const LATER = ['Item one, as it was on Monday', 'Item three, where item one is named']
|
|
73
|
+
const KINDS = [
|
|
74
|
+
{ value: 'one', label: 'Kind one' },
|
|
75
|
+
{ value: 'two', label: 'Kind two' },
|
|
76
|
+
{ value: 'three', label: 'Kind three' },
|
|
77
|
+
]
|
|
78
|
+
const GROUPS = [
|
|
79
|
+
{ value: 'first', label: 'Group one' },
|
|
80
|
+
{ value: 'second', label: 'Group two' },
|
|
81
|
+
]
|
|
82
|
+
const ANSWER = 'Item one moved on Monday [1], and item two is waiting on a reply [2].\n\nNothing else changed this week.'
|
|
83
|
+
const DRAFT = 'Thanks, both. I will look at item one today\nand reply on item two tomorrow.'
|
|
84
|
+
|
|
85
|
+
function startStack(start: Start): Frame[] {
|
|
86
|
+
const first: Frame = { level: { kind: 'first' }, query: '' }
|
|
87
|
+
switch (start) {
|
|
88
|
+
case 'arriving':
|
|
89
|
+
return [{ ...first, query: 'item' }]
|
|
90
|
+
case 'nothing':
|
|
91
|
+
return [{ ...first, query: 'nothing matches this' }]
|
|
92
|
+
case 'place':
|
|
93
|
+
return [first, { level: { kind: 'place', name: 'Place one' }, query: '' }]
|
|
94
|
+
case 'working':
|
|
95
|
+
case 'answer':
|
|
96
|
+
return [first, { level: { kind: 'ask' }, query: 'What changed this week?' }]
|
|
97
|
+
case 'draft':
|
|
98
|
+
return [first, { level: { kind: 'draft' }, query: 'Say I will look today' }]
|
|
99
|
+
case 'form':
|
|
100
|
+
case 'refused':
|
|
101
|
+
return [first, { level: { kind: 'place', name: 'Place one' }, query: '' }, { level: { kind: 'form' }, query: '' }]
|
|
102
|
+
case 'one-list':
|
|
103
|
+
return [first, { level: { kind: 'one-list' }, query: '' }]
|
|
104
|
+
default:
|
|
105
|
+
return [first]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The caller every story uses: the levels, the rows, and what each one does. */
|
|
110
|
+
function Demo({ start, where, modKey, label }: { start: Start; where?: string; modKey?: string; label: string }) {
|
|
111
|
+
const [open, setOpen] = useState(true)
|
|
112
|
+
const [stack, setStack] = useState<Frame[]>(() => startStack(start))
|
|
113
|
+
const [last, setLast] = useState<string>('Nothing chosen yet.')
|
|
114
|
+
const frame = stack[stack.length - 1]
|
|
115
|
+
const level = frame.level
|
|
116
|
+
|
|
117
|
+
const setQuery = (query: string) => setStack((s) => [...s.slice(0, -1), { ...s[s.length - 1], query }])
|
|
118
|
+
const push = (next: Level, query = '') => setStack((s) => [...s, { level: next, query }])
|
|
119
|
+
const back = () => setStack((s) => (s.length > 1 ? s.slice(0, -1) : s))
|
|
120
|
+
const close = (what: string) => {
|
|
121
|
+
setLast(what)
|
|
122
|
+
setOpen(false)
|
|
123
|
+
}
|
|
124
|
+
const reopen = () => {
|
|
125
|
+
setStack([{ level: { kind: 'first' }, query: '' }])
|
|
126
|
+
setOpen(true)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── A late group: arrives 600ms after the typing stops (slower in "arriving").
|
|
130
|
+
const term = level.kind === 'first' ? frame.query.trim().toLowerCase() : ''
|
|
131
|
+
const [searched, setSearched] = useState('')
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!term) return
|
|
134
|
+
const timer = setTimeout(() => setSearched(term), start === 'arriving' ? 2500 : 600)
|
|
135
|
+
return () => clearTimeout(timer)
|
|
136
|
+
}, [term, start])
|
|
137
|
+
const searching = !!term && searched !== term
|
|
138
|
+
|
|
139
|
+
// ── An answer that takes a while ("working" never finishes).
|
|
140
|
+
const [asked, setAsked] = useState<{ question: string; done: boolean } | null>(null)
|
|
141
|
+
const ask = async (question: string) => {
|
|
142
|
+
setAsked({ question, done: false })
|
|
143
|
+
if (start === 'working') return
|
|
144
|
+
await wait(1200)
|
|
145
|
+
setAsked((a) => (a?.question === question ? { question, done: true } : a))
|
|
146
|
+
}
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (level.kind === 'ask' || level.kind === 'draft') void ask(frame.query)
|
|
149
|
+
// Once per level: the question it was opened with.
|
|
150
|
+
// (The stack's length changes exactly when a level opens.)
|
|
151
|
+
}, [stack.length])
|
|
152
|
+
|
|
153
|
+
// ── Recent rows are the person's own, and can be forgotten.
|
|
154
|
+
const [recent, setRecent] = useState(['Item one', 'Item two', 'Person one'])
|
|
155
|
+
|
|
156
|
+
// ── The form keeps what was typed while the palette is open.
|
|
157
|
+
const [draft, setDraft] = useState({ title: '', notes: '', kind: 'one', group: '' })
|
|
158
|
+
const [missing, setMissing] = useState<{ title?: string; group?: string }>({})
|
|
159
|
+
const [working, setWorking] = useState(false)
|
|
160
|
+
const [refused, setRefused] = useState(false)
|
|
161
|
+
const [kind, setKind] = useState('one')
|
|
162
|
+
|
|
163
|
+
const groups = useMemo<CommandPaletteGroup[]>(() => {
|
|
164
|
+
const q = frame.query.trim().toLowerCase()
|
|
165
|
+
if (level.kind === 'place') {
|
|
166
|
+
return [
|
|
167
|
+
{
|
|
168
|
+
label: `${level.name} actions`,
|
|
169
|
+
rows: ACTIONS.filter((a) => a.label.toLowerCase().includes(q)).map((a) => ({
|
|
170
|
+
id: a.id,
|
|
171
|
+
label: a.label,
|
|
172
|
+
description: a.description,
|
|
173
|
+
icon,
|
|
174
|
+
onSelect: () => push({ kind: 'form' }),
|
|
175
|
+
})),
|
|
176
|
+
},
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
if (level.kind === 'ask' || level.kind === 'draft') {
|
|
180
|
+
if (!asked?.done) return []
|
|
181
|
+
const changed = frame.query.trim() !== asked.question.trim()
|
|
182
|
+
const again: CommandPaletteRow = { id: 'again', label: `Again: “${frame.query.trim()}”`, icon, onSelect: () => void ask(frame.query) }
|
|
183
|
+
if (level.kind === 'ask') {
|
|
184
|
+
return [
|
|
185
|
+
{ label: 'Ask again', rows: changed ? [again] : [] },
|
|
186
|
+
{
|
|
187
|
+
label: 'Sources',
|
|
188
|
+
rows: ['Note one', 'Note two'].map((note, i) => ({
|
|
189
|
+
id: note,
|
|
190
|
+
label: note,
|
|
191
|
+
description: `[${i + 1}] Person ${i === 0 ? 'one' : 'two'} · Monday`,
|
|
192
|
+
leading: <Avatar name={`Person ${i === 0 ? 'one' : 'two'}`} size={20} />,
|
|
193
|
+
onSelect: () => close(note),
|
|
194
|
+
})),
|
|
195
|
+
},
|
|
196
|
+
{ label: 'Next', rows: [{ id: 'use-answer', label: 'Use the answer', icon, onSelect: () => close('Use the answer') }] },
|
|
197
|
+
]
|
|
198
|
+
}
|
|
199
|
+
return [
|
|
200
|
+
{ label: 'Write again', rows: changed ? [again] : [] },
|
|
201
|
+
{
|
|
202
|
+
label: 'Next',
|
|
203
|
+
rows: [
|
|
204
|
+
{ id: 'use-draft', label: 'Use it', description: 'Nothing is sent. Edit it first.', icon, onSelect: () => close('Use it') },
|
|
205
|
+
{ id: 'draft-again', label: 'Write it again', icon, onSelect: () => void ask(asked.question) },
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const place = (name: string): CommandPaletteRow => ({
|
|
212
|
+
id: `place-${name}`,
|
|
213
|
+
label: name,
|
|
214
|
+
description: 'Has actions',
|
|
215
|
+
icon,
|
|
216
|
+
onSelect: () => push({ kind: 'place', name }),
|
|
217
|
+
onGoIn: () => push({ kind: 'place', name }),
|
|
218
|
+
})
|
|
219
|
+
const recentRow = (name: string): CommandPaletteRow => ({
|
|
220
|
+
id: `recent-${name}`,
|
|
221
|
+
label: name,
|
|
222
|
+
description: name.startsWith('Person') ? 'Role' : 'Opened today',
|
|
223
|
+
...(name.startsWith('Person') ? { leading: <Avatar name={name} size={24} /> } : { icon }),
|
|
224
|
+
onSelect: () => close(name),
|
|
225
|
+
onForget: () => setRecent((r) => r.filter((x) => x !== name)),
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
if (!q) {
|
|
229
|
+
return [
|
|
230
|
+
{
|
|
231
|
+
label: 'This page',
|
|
232
|
+
rows: [
|
|
233
|
+
{ id: 'ask', label: 'Ask about this page', icon, onSelect: () => push({ kind: 'ask' }) },
|
|
234
|
+
{ id: 'draft', label: 'Write a draft', icon, onSelect: () => push({ kind: 'draft' }) },
|
|
235
|
+
{ id: 'one-list', label: 'Change kind', description: 'Item one', icon, onSelect: () => push({ kind: 'one-list' }) },
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
label: 'Recent',
|
|
240
|
+
rows: recent.length ? [...recent.map(recentRow), { id: 'clear', label: 'Clear recent', icon, onSelect: () => setRecent([]) }] : [],
|
|
241
|
+
},
|
|
242
|
+
{ label: 'Places', rows: PLACES.map(place) },
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
const matches = (s: string) => s.toLowerCase().includes(q)
|
|
246
|
+
return [
|
|
247
|
+
{ label: 'Places', rows: PLACES.filter(matches).map(place) },
|
|
248
|
+
{ label: 'Recent', rows: recent.filter(matches).map(recentRow) },
|
|
249
|
+
{
|
|
250
|
+
label: 'Actions',
|
|
251
|
+
rows: ACTIONS.filter((a) => matches(a.label)).map((a) => ({
|
|
252
|
+
id: a.id,
|
|
253
|
+
label: a.label,
|
|
254
|
+
description: 'Place one',
|
|
255
|
+
icon,
|
|
256
|
+
onSelect: () => setStack((s) => [...s, { level: { kind: 'place', name: 'Place one' }, query: '' }, { level: { kind: 'form' }, query: '' }]),
|
|
257
|
+
})),
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
label: 'Later',
|
|
261
|
+
rows: searching ? [] : LATER.filter(matches).map((text) => ({ id: text, label: text, description: 'Person two · Monday', icon, onSelect: () => close(text) })),
|
|
262
|
+
},
|
|
263
|
+
]
|
|
264
|
+
}, [frame, level, asked, recent, searching])
|
|
265
|
+
|
|
266
|
+
const submitForm = async () => {
|
|
267
|
+
const need = { title: draft.title.trim() ? undefined : 'Give it a title.', group: draft.group ? undefined : 'Choose a group.' }
|
|
268
|
+
setMissing(need)
|
|
269
|
+
setRefused(false)
|
|
270
|
+
if (need.title || need.group) return
|
|
271
|
+
setWorking(true)
|
|
272
|
+
await wait(1200)
|
|
273
|
+
setWorking(false)
|
|
274
|
+
if (start === 'refused') {
|
|
275
|
+
setRefused(true)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
close(`Created “${draft.title.trim()}”`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const submitKind = async () => {
|
|
282
|
+
setWorking(true)
|
|
283
|
+
await wait(900)
|
|
284
|
+
setWorking(false)
|
|
285
|
+
close(`Kind set to ${KINDS.find((k) => k.value === kind)?.label}`)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const body = (() => {
|
|
289
|
+
if (level.kind !== 'ask' && level.kind !== 'draft') return undefined
|
|
290
|
+
if (!asked || !asked.done) return <CommandPaletteWorking>Reading 12 notes…</CommandPaletteWorking>
|
|
291
|
+
const changed = frame.query.trim() !== asked.question.trim()
|
|
292
|
+
return level.kind === 'ask' ? (
|
|
293
|
+
<CommandPaletteAnswer note={changed ? 'You changed the question. Press Enter to ask again.' : undefined}>{ANSWER}</CommandPaletteAnswer>
|
|
294
|
+
) : (
|
|
295
|
+
<CommandPaletteQuote>{DRAFT}</CommandPaletteQuote>
|
|
296
|
+
)
|
|
297
|
+
})()
|
|
298
|
+
|
|
299
|
+
const q = frame.query.trim()
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<div className="flex h-screen flex-col items-start gap-3 bg-bg-base p-6">
|
|
303
|
+
<Button variant="outlined" onClick={reopen}>
|
|
304
|
+
Open the palette
|
|
305
|
+
</Button>
|
|
306
|
+
<p className="text-body-2 text-text-secondary">{last}</p>
|
|
307
|
+
|
|
308
|
+
<CommandPalette open={open} onOpenChange={setOpen} label={label} where={where} modKey={modKey}>
|
|
309
|
+
{level.kind === 'form' ? (
|
|
310
|
+
<CommandPaletteForm
|
|
311
|
+
chip={{ label: 'Place one · Action one', onBack: back }}
|
|
312
|
+
icon={icon}
|
|
313
|
+
submitLabel="Create item"
|
|
314
|
+
onSubmit={() => void submitForm()}
|
|
315
|
+
working={working ? { button: 'Creating…', line: 'Creating the item…' } : undefined}
|
|
316
|
+
error={refused ? 'Group two is archived. Choose another one.' : undefined}
|
|
317
|
+
>
|
|
318
|
+
<Field label="Title" required error={missing.title}>
|
|
319
|
+
<TextInput value={draft.title} placeholder="A few words" onChange={(e) => setDraft({ ...draft, title: e.target.value })} />
|
|
320
|
+
</Field>
|
|
321
|
+
<Field label="Notes">
|
|
322
|
+
<Textarea rows={3} value={draft.notes} placeholder="Anything else" onChange={(e) => setDraft({ ...draft, notes: e.target.value })} />
|
|
323
|
+
</Field>
|
|
324
|
+
<Field label="Kind">
|
|
325
|
+
<Select value={draft.kind} onChange={(value) => setDraft({ ...draft, kind: value })} options={KINDS} />
|
|
326
|
+
</Field>
|
|
327
|
+
<Field label="Group" required error={missing.group}>
|
|
328
|
+
<Select value={draft.group} onChange={(value) => setDraft({ ...draft, group: value })} options={GROUPS} placeholder="Choose a group…" />
|
|
329
|
+
</Field>
|
|
330
|
+
</CommandPaletteForm>
|
|
331
|
+
) : level.kind === 'one-list' ? (
|
|
332
|
+
<CommandPaletteForm
|
|
333
|
+
chip={{ label: 'Item one · Change kind', onBack: back }}
|
|
334
|
+
icon={icon}
|
|
335
|
+
submitLabel="Change kind"
|
|
336
|
+
onSubmit={() => void submitKind()}
|
|
337
|
+
submitWaits={kind === 'one' ? 'Choose a different kind first' : undefined}
|
|
338
|
+
working={working ? { button: 'Saving…', line: 'Saving the kind…' } : undefined}
|
|
339
|
+
>
|
|
340
|
+
<Field label="Kind">
|
|
341
|
+
<Select value={kind} onChange={setKind} options={KINDS} />
|
|
342
|
+
</Field>
|
|
343
|
+
</CommandPaletteForm>
|
|
344
|
+
) : (
|
|
345
|
+
<CommandPaletteSearch
|
|
346
|
+
query={frame.query}
|
|
347
|
+
onQueryChange={setQuery}
|
|
348
|
+
placeholder={
|
|
349
|
+
level.kind === 'place'
|
|
350
|
+
? `Search ${level.name}'s actions`
|
|
351
|
+
: level.kind === 'ask'
|
|
352
|
+
? 'Ask anything about this page'
|
|
353
|
+
: level.kind === 'draft'
|
|
354
|
+
? 'What should it say?'
|
|
355
|
+
: 'Search, or choose a place'
|
|
356
|
+
}
|
|
357
|
+
groups={groups}
|
|
358
|
+
chip={
|
|
359
|
+
level.kind === 'place'
|
|
360
|
+
? { label: level.name, leading: icon, onBack: back }
|
|
361
|
+
: level.kind === 'ask'
|
|
362
|
+
? { label: 'Ask', onBack: back }
|
|
363
|
+
: level.kind === 'draft'
|
|
364
|
+
? { label: 'Draft', onBack: back }
|
|
365
|
+
: undefined
|
|
366
|
+
}
|
|
367
|
+
pending={level.kind === 'first' && searching ? 'Searching…' : undefined}
|
|
368
|
+
empty={
|
|
369
|
+
level.kind === 'first' && q && !searching
|
|
370
|
+
? `Nothing is called “${q}”.`
|
|
371
|
+
: level.kind === 'place'
|
|
372
|
+
? `${level.name} has no action called “${q}”.`
|
|
373
|
+
: undefined
|
|
374
|
+
}
|
|
375
|
+
>
|
|
376
|
+
{body}
|
|
377
|
+
</CommandPaletteSearch>
|
|
378
|
+
)}
|
|
379
|
+
</CommandPalette>
|
|
380
|
+
</div>
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const story = (start: Start): Story => ({
|
|
385
|
+
render: (args) => <Demo start={start} label={args.label} where={args.where} modKey={args.modKey} />,
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
/** The first level: rows in groups, the first one lit. ↑ ↓ stop at the ends, Tab goes into a place, Ctrl+Backspace forgets a recent row. */
|
|
389
|
+
export const FirstLevel: Story = story('first')
|
|
390
|
+
|
|
391
|
+
/** Typing: the rows that match now, a line while a later group is on its way, and that group arriving without moving the lit row. */
|
|
392
|
+
export const WhileRowsArrive: Story = story('arriving')
|
|
393
|
+
|
|
394
|
+
/** Nothing matches: the empty line, where the first row would be. */
|
|
395
|
+
export const NothingFound: Story = story('nothing')
|
|
396
|
+
|
|
397
|
+
/** Inside a level: the chip names it. Backspace at the start of the field, or the chip's ✕, goes back with the text you had. */
|
|
398
|
+
export const InsideALevel: Story = story('place')
|
|
399
|
+
|
|
400
|
+
/** Something is being worked on: the line says what, the grey bars hold the place. */
|
|
401
|
+
export const Working: Story = story('working')
|
|
402
|
+
|
|
403
|
+
/** An answer, its marks, and the rows they point at. Change the question and an "Again" row comes first. */
|
|
404
|
+
export const Answer: Story = story('answer')
|
|
405
|
+
|
|
406
|
+
/** Text written to be used somewhere else, above what you can do with it. */
|
|
407
|
+
export const Quote: Story = story('draft')
|
|
408
|
+
|
|
409
|
+
/** A form. Ctrl+Enter creates; a missing field says so and takes focus; Backspace in an empty field goes back and keeps what was typed. */
|
|
410
|
+
export const Form: Story = story('form')
|
|
411
|
+
|
|
412
|
+
/** Creating is refused: the fields unlock, the reason sits beside the button, and focus is back where it was. */
|
|
413
|
+
export const FormRefused: Story = story('refused')
|
|
414
|
+
|
|
415
|
+
/** A form that is one list. It waits, and says why, until the value changes; Backspace goes back from the list. */
|
|
416
|
+
export const FormThatWaits: Story = story('one-list')
|