@estiva-app/ui 0.73.0 → 0.74.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/dist/CommandPalette.d.ts +3 -1
- package/dist/CommandPalette.d.ts.map +1 -1
- package/dist/CommentHighlights.d.ts.map +1 -1
- package/dist/ComposerTriggers.d.ts +7 -0
- package/dist/ComposerTriggers.d.ts.map +1 -1
- package/dist/Menu.d.ts +8 -2
- package/dist/Menu.d.ts.map +1 -1
- package/dist/SuggestionMenu.d.ts +2 -0
- package/dist/SuggestionMenu.d.ts.map +1 -1
- package/dist/editor.js +49 -18
- package/dist/editor.js.map +3 -3
- package/dist/index.js +10 -4
- package/dist/index.js.map +2 -2
- package/package.json +1 -1
- package/registry.json +7 -1
- package/src/CommandPalette.test.tsx +6 -0
- package/src/CommandPalette.tsx +4 -1
- package/src/CommentHighlights.test.ts +56 -2
- package/src/CommentHighlights.ts +51 -12
- package/src/ComposerTriggers.test.tsx +21 -0
- package/src/ComposerTriggers.tsx +14 -1
- package/src/Menu.tsx +19 -5
- package/src/SuggestionMenu.tsx +4 -1
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"kind": "package",
|
|
5
5
|
"repo": "estiva-ui",
|
|
6
6
|
"package": "@estiva-app/ui",
|
|
7
|
-
"packageVersion": "0.
|
|
7
|
+
"packageVersion": "0.74.1",
|
|
8
8
|
"exports": 178,
|
|
9
9
|
"typeExports": 164,
|
|
10
10
|
"files": null
|
|
@@ -4395,6 +4395,12 @@
|
|
|
4395
4395
|
"required": false,
|
|
4396
4396
|
"note": "A second line under the label — a role, an address — 12px, secondary, truncating."
|
|
4397
4397
|
},
|
|
4398
|
+
{
|
|
4399
|
+
"name": "aside",
|
|
4400
|
+
"takes": "text",
|
|
4401
|
+
"required": false,
|
|
4402
|
+
"note": "After the label, on its line — 12px, muted, never cut: where a file sits (an issue's ref and project) when its second line is its summary (SUM-7)."
|
|
4403
|
+
},
|
|
4398
4404
|
{
|
|
4399
4405
|
"name": "leading",
|
|
4400
4406
|
"takes": "anything",
|
|
@@ -94,6 +94,12 @@ describe('CommandPaletteSearch — rows', () => {
|
|
|
94
94
|
expect(groups.map((g) => g.firstElementChild?.textContent)).toEqual(['First', 'Second'])
|
|
95
95
|
})
|
|
96
96
|
|
|
97
|
+
it('draws a row’s aside after its label, on the label’s line, above the second line (SUM-7)', () => {
|
|
98
|
+
render(<Search groups={[{ label: 'Recent', rows: [row('Ship launch', { aside: 'SHI-12 · Ship 1', description: 'What we show on Friday' })] }]} />)
|
|
99
|
+
const option = screen.getByRole('option')
|
|
100
|
+
expect(within(option).getByText('What we show on Friday').previousElementSibling?.textContent).toBe('Ship launchSHI-12 · Ship 1')
|
|
101
|
+
})
|
|
102
|
+
|
|
97
103
|
it('puts 2px between its rows and under each heading, as a menu does', () => {
|
|
98
104
|
render(<Search groups={[{ label: 'Group', rows: [row('One'), row('Two')] }]} />)
|
|
99
105
|
const [first] = screen.getAllByRole('option')
|
package/src/CommandPalette.tsx
CHANGED
|
@@ -196,8 +196,10 @@ export interface CommandPaletteRow {
|
|
|
196
196
|
/** Unique in the level. The footer follows the lit row by it. */
|
|
197
197
|
id: string
|
|
198
198
|
label: string
|
|
199
|
-
/** The second line — where it is, who said it, when. */
|
|
199
|
+
/** The second line — where it is, who said it, when; a file's summary. */
|
|
200
200
|
description?: string
|
|
201
|
+
/** After the label, muted: where a file sits (an issue's ref and project) when its second line is its summary. */
|
|
202
|
+
aside?: string
|
|
201
203
|
/** A 16px icon, drawn on the row's 32px tile. */
|
|
202
204
|
icon?: ReactNode
|
|
203
205
|
/** Instead of `icon`: something with a look of its own — a face, a status mark — in the same 32px space, with no tile. */
|
|
@@ -414,6 +416,7 @@ export function CommandPaletteSearch({ query, onQueryChange, placeholder, groups
|
|
|
414
416
|
size="tall"
|
|
415
417
|
label={row.label}
|
|
416
418
|
description={row.description}
|
|
419
|
+
aside={row.aside}
|
|
417
420
|
leading={<RowLeading row={row} />}
|
|
418
421
|
submenu={!!row.onGoIn}
|
|
419
422
|
hint={row.onGoIn ? <Kbd>Tab</Kbd> : <EnterHint />}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
8
8
|
import { Editor } from '@tiptap/core'
|
|
9
9
|
import StarterKit from '@tiptap/starter-kit'
|
|
10
|
-
import { BlockId } from './BlockNodes'
|
|
10
|
+
import { BlockId, ReferenceNode } from './BlockNodes'
|
|
11
11
|
import { CommentHighlights, anchorOfSelection, commentPlace } from './CommentHighlights'
|
|
12
12
|
|
|
13
13
|
let editor: Editor | undefined
|
|
@@ -62,7 +62,49 @@ describe('anchorOfSelection', () => {
|
|
|
62
62
|
])
|
|
63
63
|
// The quote opens at 0, its first line's text at 2–10, the second's at 12–20.
|
|
64
64
|
e.commands.setTextSelection({ from: 6, to: 15 })
|
|
65
|
-
|
|
65
|
+
// The quote's lines are joined with '\n', as the protocol reads the quote's text.
|
|
66
|
+
expect(anchorOfSelection(e)).toEqual({ blocks: ['q1'], quote: { start: 'line\ntwo' } })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('quotes a line break inside a paragraph as a new line', () => {
|
|
70
|
+
const e = open([{ type: 'paragraph', attrs: { blockId: 'p1' }, content: [{ type: 'text', text: 'A line' }, { type: 'hardBreak' }, { type: 'text', text: 'broken here' }] }])
|
|
71
|
+
// "A line" at 1–7, the break at 7, "broken here" at 8–19.
|
|
72
|
+
e.commands.setTextSelection({ from: 3, to: 14 })
|
|
73
|
+
expect(anchorOfSelection(e)).toEqual({ blocks: ['p1'], quote: { start: 'line\nbroken' } })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('anchors nothing when only the edge between two lines of a quote, or a line break, is selected', () => {
|
|
77
|
+
const e = open([
|
|
78
|
+
{ type: 'blockquote', attrs: { blockId: 'q1' }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'one line' }] }, { type: 'paragraph', content: [{ type: 'text', text: 'two line' }] }] },
|
|
79
|
+
{ type: 'paragraph', attrs: { blockId: 'p1' }, content: [{ type: 'text', text: 'A line' }, { type: 'hardBreak' }, { type: 'text', text: 'broken here' }] },
|
|
80
|
+
])
|
|
81
|
+
// The quote's first line ends at 10, its second starts at 12; the paragraph opens at 22, its break at 29.
|
|
82
|
+
e.commands.setTextSelection({ from: 10, to: 12 })
|
|
83
|
+
expect(anchorOfSelection(e)).toBeNull()
|
|
84
|
+
e.commands.setTextSelection({ from: 29, to: 30 })
|
|
85
|
+
expect(anchorOfSelection(e)).toBeNull()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('quotes a list item as its own words, its nested items as the items after it', () => {
|
|
89
|
+
const item = (blockId: string, text: string, nested?: object) => ({
|
|
90
|
+
type: 'listItem',
|
|
91
|
+
attrs: { blockId },
|
|
92
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text }] }, ...(nested ? [nested] : [])],
|
|
93
|
+
})
|
|
94
|
+
const e = open([{ type: 'bulletList', attrs: { blockId: 'l1' }, content: [item('a', 'alpha', { type: 'bulletList', content: [item('b', 'beta')] }), item('c', 'gamma')] }])
|
|
95
|
+
// "alpha" at 3–8; the nested list's item "beta" at 12–16.
|
|
96
|
+
e.commands.setTextSelection({ from: 3, to: 8 })
|
|
97
|
+
expect(anchorOfSelection(e)).toEqual({ blocks: ['a'] })
|
|
98
|
+
// The list's text on the wire is "alpha\nbeta\ngamma": the nested item sits between its parent and the next.
|
|
99
|
+
expect(commentPlace(e, { id: 'c', blocks: ['l1'], quote: { start: 'alpha\nbeta' } })).toEqual({ state: 'resolved', from: 3, to: 16, words: true })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('quotes a URL chip as its URL, as on the wire', () => {
|
|
103
|
+
const content = [
|
|
104
|
+
{ type: 'paragraph', attrs: { blockId: 'p1' }, content: [{ type: 'text', text: 'see ' }, { type: 'reference', attrs: { uri: '', href: 'https://ship.estiva.app/x' } }, { type: 'text', text: ' ok' }] },
|
|
105
|
+
]
|
|
106
|
+
const e = (editor = new Editor({ extensions: [StarterKit, BlockId, ReferenceNode, CommentHighlights], content: { type: 'doc', content } }))
|
|
107
|
+
expect(commentPlace(e, { id: 'c', blocks: ['p1'], quote: { start: 'see https://ship' } })).toMatchObject({ state: 'resolved', from: 1 })
|
|
66
108
|
})
|
|
67
109
|
|
|
68
110
|
it('keeps the words around a quote only when the quoted words repeat', () => {
|
|
@@ -88,6 +130,18 @@ describe('commentPlace', () => {
|
|
|
88
130
|
expect(commentPlace(e, { id: 'c', blocks: ['p1'], quote: { start: 'omega' } })).toEqual({ state: 'reworded', from: 0, to: 18 })
|
|
89
131
|
})
|
|
90
132
|
|
|
133
|
+
it('finds words across two lines of a quote, as the protocol writes them', () => {
|
|
134
|
+
const e = open([
|
|
135
|
+
{ type: 'blockquote', attrs: { blockId: 'q1' }, content: [{ type: 'paragraph', content: [{ type: 'text', text: 'one line' }] }, { type: 'paragraph', content: [{ type: 'text', text: 'two line' }] }] },
|
|
136
|
+
])
|
|
137
|
+
expect(commentPlace(e, { id: 'c', blocks: ['q1'], quote: { start: 'line\ntwo' } })).toEqual({ state: 'resolved', from: 6, to: 15, words: true })
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('finds words across a line break, as the protocol writes them', () => {
|
|
141
|
+
const e = open([{ type: 'paragraph', attrs: { blockId: 'p1' }, content: [{ type: 'text', text: 'A line' }, { type: 'hardBreak' }, { type: 'text', text: 'broken here' }] }])
|
|
142
|
+
expect(commentPlace(e, { id: 'c', blocks: ['p1'], quote: { start: 'line\nbroken' } })).toEqual({ state: 'resolved', from: 3, to: 14, words: true })
|
|
143
|
+
})
|
|
144
|
+
|
|
91
145
|
it('says detached when none of the blocks is left', () => {
|
|
92
146
|
const e = open()
|
|
93
147
|
expect(commentPlace(e, { id: 'c', blocks: ['gone'], quote: { start: 'alpha' } })).toEqual({ state: 'detached' })
|
package/src/CommentHighlights.ts
CHANGED
|
@@ -106,27 +106,64 @@ function placesIn(doc: ProseMirrorNode): Map<string, Place> {
|
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
108
|
* A block's text, and the document position where each of its characters
|
|
109
|
-
* starts and ends.
|
|
109
|
+
* starts and ends. It is the text the protocol's `inlineTextOf` reads from the
|
|
110
|
+
* same block on the wire, or a quote written here would not be found there: a
|
|
111
|
+
* chip counts as the text it is on the wire (a URL chip its URL, any other its
|
|
112
|
+
* `nostr:` reference), a line break as `\n`, and the blocks inside a block (a
|
|
113
|
+
* quote's lines, a list's items, a table's cells) are joined with `\n`. A list
|
|
114
|
+
* item is its own text: the list inside it is flat on the wire, its items
|
|
115
|
+
* following it in the list.
|
|
110
116
|
*/
|
|
111
117
|
function textOf(block: Place): { text: string; starts: number[]; ends: number[] } {
|
|
112
118
|
let text = ''
|
|
113
119
|
const starts: number[] = []
|
|
114
120
|
const ends: number[] = []
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
const add = (chars: string, start: number, end: number) => {
|
|
122
|
+
for (let i = 0; i < chars.length; i++) (starts.push(start), ends.push(end))
|
|
123
|
+
text += chars
|
|
124
|
+
}
|
|
125
|
+
const walk = (node: ProseMirrorNode, pos: number) => {
|
|
126
|
+
if (node.isTextblock) {
|
|
127
|
+
node.forEach((child, offset) => {
|
|
128
|
+
const at = pos + 1 + offset
|
|
129
|
+
if (child.isText && child.text) {
|
|
130
|
+
for (let i = 0; i < child.text.length; i++) (starts.push(at + i), ends.push(at + i + 1))
|
|
131
|
+
text += child.text
|
|
132
|
+
} else if (child.type.name === 'hardBreak') add('\n', at, at + child.nodeSize)
|
|
133
|
+
else if (child.isInline && child.isLeaf) add(String(child.attrs.href || child.attrs.uri || ''), at, at + child.nodeSize)
|
|
134
|
+
})
|
|
135
|
+
return
|
|
124
136
|
}
|
|
125
|
-
|
|
126
|
-
|
|
137
|
+
// A list item's paragraphs are one run on the wire, with nothing between them.
|
|
138
|
+
if (node.type.name === 'listItem') {
|
|
139
|
+
node.forEach((child, offset) => void (LISTS.has(child.type.name) || walk(child, pos + 1 + offset)))
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
const children = LISTS.has(node.type.name) ? itemsOf(node, pos) : childrenOf(node, pos)
|
|
143
|
+
children.forEach(({ node: child, pos: at }, index) => {
|
|
144
|
+
// Between two blocks: the boundary, where neither one's text is.
|
|
145
|
+
if (index > 0) add('\n', at, at)
|
|
146
|
+
walk(child, at)
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
walk(block.node, block.pos)
|
|
127
150
|
return { text, starts, ends }
|
|
128
151
|
}
|
|
129
152
|
|
|
153
|
+
/** The lists whose nested items the wire holds flat, after the item they are in. */
|
|
154
|
+
const LISTS = new Set(['bulletList', 'orderedList'])
|
|
155
|
+
|
|
156
|
+
function childrenOf(node: ProseMirrorNode, pos: number): { node: ProseMirrorNode; pos: number }[] {
|
|
157
|
+
const out: { node: ProseMirrorNode; pos: number }[] = []
|
|
158
|
+
node.forEach((child, offset) => void out.push({ node: child, pos: pos + 1 + offset }))
|
|
159
|
+
return out
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A list's items as the wire lists them: each one, then the items of a list inside it. */
|
|
163
|
+
function itemsOf(list: ProseMirrorNode, pos: number): { node: ProseMirrorNode; pos: number }[] {
|
|
164
|
+
return childrenOf(list, pos).flatMap((item) => [item, ...childrenOf(item.node, item.pos).flatMap((c) => (LISTS.has(c.node.type.name) ? itemsOf(c.node, c.pos) : []))])
|
|
165
|
+
}
|
|
166
|
+
|
|
130
167
|
/** Where `words` sits in `text` at or after `from`: the occurrence whose context matches, else the first. */
|
|
131
168
|
function find(text: string, words: string, from: number, context: (at: number) => boolean): number {
|
|
132
169
|
let first = -1
|
|
@@ -391,6 +428,8 @@ export function anchorOfSelection(editor: Editor): SelectionAnchor | null {
|
|
|
391
428
|
if (run.length === 1) lastText = firstText
|
|
392
429
|
// Between two blocks, with no character selected: nothing to comment on.
|
|
393
430
|
if (run.length === 1 && firstText.text && start >= end) return null
|
|
431
|
+
// Only a line break, or the edge between two lines of one block: no word to comment on either.
|
|
432
|
+
if (run.length === 1 && firstText.text && !firstText.text.slice(start, end).replace(/\n/g, '')) return null
|
|
394
433
|
const blocks = run.map((p) => p.node.attrs.blockId as string)
|
|
395
434
|
if (start === 0 && end >= lastText.text.length) return { blocks }
|
|
396
435
|
|
|
@@ -176,6 +176,27 @@ describe('[', () => {
|
|
|
176
176
|
expect(icons).toEqual(['kind-a', 'own', 'list'])
|
|
177
177
|
})
|
|
178
178
|
|
|
179
|
+
it('draws a summary as the second line, with where the thing sits beside the label; without one, the row is as before (SUM-7)', async () => {
|
|
180
|
+
const rows = [
|
|
181
|
+
{ id: 's', label: 'Ship launch', snippet: '', uri: 'nostr:s', description: 'SHI-12 · Ship 1', summary: 'What we show on Friday' },
|
|
182
|
+
{ id: 'n', label: 'Old plan', snippet: '', uri: 'nostr:n', description: 'SHI-3 · Ship 1' },
|
|
183
|
+
]
|
|
184
|
+
function Summarised() {
|
|
185
|
+
const made = useEditor({
|
|
186
|
+
extensions: [StarterKit, Chip, ReferenceTrigger.configure({ items: () => rows, nodeName: 'messageMention', ariaLabel: 'Things', sectionLabel: 'Things' })],
|
|
187
|
+
})
|
|
188
|
+
editor = made
|
|
189
|
+
return <EditorContent editor={made} />
|
|
190
|
+
}
|
|
191
|
+
render(<Summarised />)
|
|
192
|
+
await type('[')
|
|
193
|
+
const [withSummary, without] = within(screen.getByRole('listbox')).getAllByRole('option')
|
|
194
|
+
// The line above a row's second line, as a person reads it.
|
|
195
|
+
const lineAbove = (row: HTMLElement, second: string) => within(row).getByText(second).previousElementSibling?.textContent
|
|
196
|
+
expect(lineAbove(withSummary, 'What we show on Friday')).toBe('Ship launchSHI-12 · Ship 1')
|
|
197
|
+
expect(lineAbove(without, 'SHI-3 · Ship 1')).toBe('Old plan')
|
|
198
|
+
})
|
|
199
|
+
|
|
179
200
|
it('draws the app’s sections in order, skips an empty one, and redraws the open list when told', async () => {
|
|
180
201
|
const listeners = new Set<() => void>()
|
|
181
202
|
let late: CaptionedItem[] = []
|
package/src/ComposerTriggers.tsx
CHANGED
|
@@ -232,6 +232,13 @@ export interface CaptionedItem {
|
|
|
232
232
|
search?: string
|
|
233
233
|
/** The row's second line, e.g. where the thing sits. The chip does not wear it. */
|
|
234
234
|
description?: string
|
|
235
|
+
/**
|
|
236
|
+
* What the thing is about, in a line of plain text — a file's summary. When
|
|
237
|
+
* set, it is the row's second line and `description` moves up after the
|
|
238
|
+
* label, muted; absent, the row is as before. The chip does not wear it, and
|
|
239
|
+
* the query is not matched against it.
|
|
240
|
+
*/
|
|
241
|
+
summary?: string
|
|
235
242
|
/**
|
|
236
243
|
* This row's own 16px icon, in place of the list's `icon`: what the thing is,
|
|
237
244
|
* when the list holds several kinds — a topic, a project, a message
|
|
@@ -363,7 +370,13 @@ export function referenceMenu({ ariaLabel, sectionLabel, icon, iconFor }: Refere
|
|
|
363
370
|
return groups
|
|
364
371
|
},
|
|
365
372
|
itemKey: (item) => `reference-${item.id}`,
|
|
366
|
-
row: (item) => ({
|
|
373
|
+
row: (item) => ({
|
|
374
|
+
leading: item.icon ?? iconFor?.(item.uri) ?? icon,
|
|
375
|
+
label: captionOf(item.label, item.snippet),
|
|
376
|
+
// A summary is the second line, and where the thing sits moves up beside the label (SUM-7's story).
|
|
377
|
+
...(item.summary ? { description: item.summary, aside: item.description } : { description: item.description }),
|
|
378
|
+
hint: <EnterHint />,
|
|
379
|
+
}),
|
|
367
380
|
}
|
|
368
381
|
}
|
|
369
382
|
|
package/src/Menu.tsx
CHANGED
|
@@ -350,6 +350,12 @@ export interface MenuItemProps extends Omit<ComponentPropsWithRef<'button'>, 'ch
|
|
|
350
350
|
size?: 'default' | 'tall'
|
|
351
351
|
/** A second line under the label — a role, an address — 12px, secondary, truncating. */
|
|
352
352
|
description?: string
|
|
353
|
+
/**
|
|
354
|
+
* After the label, on its line — 12px, muted, never cut: where a file sits
|
|
355
|
+
* (an issue's ref and project) when its second line is its summary (SUM-7).
|
|
356
|
+
* The label truncates before it does.
|
|
357
|
+
*/
|
|
358
|
+
aside?: string
|
|
353
359
|
/** Before the label: a 16px icon (stroke 1.5, secondary), or an Avatar, for rows led by a face. */
|
|
354
360
|
leading?: ReactNode
|
|
355
361
|
/** At the right edge: a hint, a value — anything. Wins over `shortcut` and `submenu`. */
|
|
@@ -425,7 +431,7 @@ export function menuItemClassName({ size, selected, className }: { size: 'defaul
|
|
|
425
431
|
/** Everything inside the row — written once, for the same reason. Shared
|
|
426
432
|
* inside the package like `menuItemClassName`: `CommandPalette` draws it
|
|
427
433
|
* inside Base UI's `Autocomplete.Item`. Not exported from the index. */
|
|
428
|
-
export function MenuItemBody({ label, children, size = 'default', description, leading, trailing, hint, shortcut, submenu, destructive, selected }: Pick<MenuItemProps, 'label' | 'children' | 'size' | 'description' | 'leading' | 'trailing' | 'hint' | 'shortcut' | 'submenu' | 'destructive' | 'selected'>) {
|
|
434
|
+
export function MenuItemBody({ label, children, size = 'default', description, aside, leading, trailing, hint, shortcut, submenu, destructive, selected }: Pick<MenuItemProps, 'label' | 'children' | 'size' | 'description' | 'aside' | 'leading' | 'trailing' | 'hint' | 'shortcut' | 'submenu' | 'destructive' | 'selected'>) {
|
|
429
435
|
const edge =
|
|
430
436
|
trailing ??
|
|
431
437
|
(shortcut ? (
|
|
@@ -441,9 +447,16 @@ export function MenuItemBody({ label, children, size = 'default', description, l
|
|
|
441
447
|
colour. Ship's copy said `text-sm`, which was never the ramp. */}
|
|
442
448
|
{children ?? (
|
|
443
449
|
<span className={cn('flex min-w-0 flex-1 flex-col', size === 'tall' && 'gap-[2px]')}>
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
450
|
+
{aside ? (
|
|
451
|
+
<span className="flex min-w-0 items-baseline gap-2">
|
|
452
|
+
<span className={cn('truncate text-body-2', destructive ? 'text-error-default' : 'text-text-primary')}>{label}</span>
|
|
453
|
+
<span className="shrink-0 text-caption text-text-muted">{aside}</span>
|
|
454
|
+
</span>
|
|
455
|
+
) : (
|
|
456
|
+
<span className={cn('truncate text-body-2', destructive ? 'text-error-default' : 'text-text-primary')}>
|
|
457
|
+
{label}
|
|
458
|
+
</span>
|
|
459
|
+
)}
|
|
447
460
|
{description && <span className="truncate text-caption text-text-secondary">{description}</span>}
|
|
448
461
|
</span>
|
|
449
462
|
)}
|
|
@@ -469,12 +482,13 @@ export function MenuItemBody({ label, children, size = 'default', description, l
|
|
|
469
482
|
)
|
|
470
483
|
}
|
|
471
484
|
|
|
472
|
-
export function MenuItem({ label, children, size = 'default', description, leading, trailing, hint, shortcut, submenu, destructive, selected, className, ...props }: MenuItemProps) {
|
|
485
|
+
export function MenuItem({ label, children, size = 'default', description, aside, leading, trailing, hint, shortcut, submenu, destructive, selected, className, ...props }: MenuItemProps) {
|
|
473
486
|
const body = (
|
|
474
487
|
<MenuItemBody
|
|
475
488
|
label={label}
|
|
476
489
|
size={size}
|
|
477
490
|
description={description}
|
|
491
|
+
aside={aside}
|
|
478
492
|
leading={leading}
|
|
479
493
|
trailing={trailing}
|
|
480
494
|
hint={hint}
|
package/src/SuggestionMenu.tsx
CHANGED
|
@@ -34,6 +34,8 @@ import { Popover } from './Popover'
|
|
|
34
34
|
export interface SuggestionMenuRow {
|
|
35
35
|
label: string
|
|
36
36
|
description?: string
|
|
37
|
+
/** After the label, muted — `MenuItem`'s. */
|
|
38
|
+
aside?: string
|
|
37
39
|
leading?: ReactNode
|
|
38
40
|
/** What you would type — drawn as keys at the right, e.g. `#` for a heading. */
|
|
39
41
|
shortcut?: string
|
|
@@ -158,7 +160,7 @@ export function SuggestionMenu<T>({ rect, editorElement, ariaLabel, width, maxHe
|
|
|
158
160
|
const drawRow = (item: T) => {
|
|
159
161
|
const index = items.indexOf(item)
|
|
160
162
|
const selected = index === highlight
|
|
161
|
-
const { label, description, leading, shortcut, hint, size } = row(item)
|
|
163
|
+
const { label, description, aside, leading, shortcut, hint, size } = row(item)
|
|
162
164
|
return (
|
|
163
165
|
// A row of a list the editor drives from its caret: focus stays in the text, so the row is no Tab stop.
|
|
164
166
|
<MenuItem
|
|
@@ -171,6 +173,7 @@ export function SuggestionMenu<T>({ rect, editorElement, ariaLabel, width, maxHe
|
|
|
171
173
|
size={size}
|
|
172
174
|
label={label}
|
|
173
175
|
description={description}
|
|
176
|
+
aside={aside}
|
|
174
177
|
leading={leading}
|
|
175
178
|
shortcut={shortcut}
|
|
176
179
|
hint={hint}
|