@estiva-app/ui 0.67.0 → 0.68.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/dist/BlockNodes.d.ts +12 -1
- package/dist/BlockNodes.d.ts.map +1 -1
- package/dist/LinkChips.d.ts +82 -0
- package/dist/LinkChips.d.ts.map +1 -0
- package/dist/ReferenceCards.d.ts +77 -0
- package/dist/ReferenceCards.d.ts.map +1 -0
- package/dist/ReferenceMenu.d.ts +18 -0
- package/dist/ReferenceMenu.d.ts.map +1 -0
- package/dist/RichText.d.ts +9 -2
- package/dist/RichText.d.ts.map +1 -1
- package/dist/SelectionToolbar.d.ts +9 -1
- package/dist/SelectionToolbar.d.ts.map +1 -1
- package/dist/editor.d.ts +3 -0
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +589 -20
- package/dist/editor.js.map +4 -4
- package/dist/index.js +39 -19
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/registry.json +290 -3
- package/src/BlockNodes.test.tsx +2 -1
- package/src/BlockNodes.tsx +51 -9
- package/src/LinkChips.mdx +57 -0
- package/src/LinkChips.stories.tsx +67 -0
- package/src/LinkChips.test.tsx +178 -0
- package/src/LinkChips.ts +243 -0
- package/src/ReferenceCards.mdx +79 -0
- package/src/ReferenceCards.stories.tsx +64 -0
- package/src/ReferenceCards.test.tsx +143 -0
- package/src/ReferenceCards.ts +225 -0
- package/src/ReferenceMenu.mdx +75 -0
- package/src/ReferenceMenu.stories.tsx +59 -0
- package/src/ReferenceMenu.test.tsx +289 -0
- package/src/ReferenceMenu.tsx +346 -0
- package/src/RichText.test.tsx +17 -0
- package/src/RichText.tsx +34 -3
- package/src/SelectionToolbar.tsx +10 -2
- package/src/editor.ts +13 -0
- package/src/pages.test.ts +3 -0
- package/src/registry/registry.test.ts +6 -6
- package/stories/Choosing.mdx +3 -0
package/src/RichText.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Fragment, type MouseEvent, type ReactNode } from 'react'
|
|
2
2
|
import { Checkbox } from './Checkbox'
|
|
3
3
|
import { cn } from './cn'
|
|
4
4
|
import { Link } from './Link'
|
|
@@ -84,6 +84,13 @@ export interface RichTextProps {
|
|
|
84
84
|
* `undefined` to leave it to the part, which draws its words.
|
|
85
85
|
*/
|
|
86
86
|
renderBlock?: (block: RichTextBlock) => ReactNode | undefined
|
|
87
|
+
/**
|
|
88
|
+
* Draws something after a line of text: after a paragraph, a heading or a
|
|
89
|
+
* quote, and inside a list item or a table cell after its words — where a
|
|
90
|
+
* document's chip asks for its card (SPEC §13.3). Asked of every such line;
|
|
91
|
+
* return nothing for most.
|
|
92
|
+
*/
|
|
93
|
+
renderAfter?: (block: RichTextBlock) => ReactNode
|
|
87
94
|
/**
|
|
88
95
|
* Ticks or unticks a checklist item, by its block id: its box is then a
|
|
89
96
|
* control, one click where the text is read. Without it the box is a picture
|
|
@@ -186,6 +193,7 @@ interface Draw {
|
|
|
186
193
|
renderText: (text: string) => ReactNode
|
|
187
194
|
renderReference: (reference: string, run: RichTextRun) => ReactNode
|
|
188
195
|
renderBlock?: (block: RichTextBlock) => ReactNode | undefined
|
|
196
|
+
renderAfter?: (block: RichTextBlock) => ReactNode
|
|
189
197
|
onTick?: (blockId: string, checked: boolean) => void
|
|
190
198
|
}
|
|
191
199
|
|
|
@@ -245,8 +253,14 @@ function Table({ block, draw }: { block: RichTextBlock; draw: Draw }) {
|
|
|
245
253
|
colSpan={colspan && colspan > 1 ? colspan : undefined}
|
|
246
254
|
rowSpan={rowspan && rowspan > 1 ? rowspan : undefined}
|
|
247
255
|
>
|
|
248
|
-
{(cell.children ?? []).map((child, j) =>
|
|
256
|
+
{(cell.children ?? []).map((child, j) => (
|
|
257
|
+
<Fragment key={j}>
|
|
258
|
+
<Runs runs={child.inline} draw={draw} />
|
|
259
|
+
{draw.renderAfter?.(child)}
|
|
260
|
+
</Fragment>
|
|
261
|
+
))}
|
|
249
262
|
<Runs runs={cell.inline} draw={draw} />
|
|
263
|
+
{draw.renderAfter?.(cell)}
|
|
250
264
|
</Cell>
|
|
251
265
|
)
|
|
252
266
|
})
|
|
@@ -281,6 +295,7 @@ function ListItem({ item, draw, below }: { item: RichTextBlock; draw: Draw; belo
|
|
|
281
295
|
return (
|
|
282
296
|
<li data-block-id={item.id}>
|
|
283
297
|
<Runs runs={item.inline} draw={draw} />
|
|
298
|
+
{draw.renderAfter?.(item)}
|
|
284
299
|
{below}
|
|
285
300
|
</li>
|
|
286
301
|
)
|
|
@@ -299,6 +314,7 @@ function ListItem({ item, draw, below }: { item: RichTextBlock; draw: Draw; belo
|
|
|
299
314
|
<p>
|
|
300
315
|
<Runs runs={item.inline} draw={draw} />
|
|
301
316
|
</p>
|
|
317
|
+
{draw.renderAfter?.(item)}
|
|
302
318
|
{below}
|
|
303
319
|
</div>
|
|
304
320
|
</li>
|
|
@@ -333,6 +349,20 @@ function nestItems(items: readonly RichTextBlock[]): NestedItem[] {
|
|
|
333
349
|
function Block({ block, draw }: { block: RichTextBlock; draw: Draw }): ReactNode {
|
|
334
350
|
const own = draw.renderBlock?.(block)
|
|
335
351
|
if (own !== undefined) return own
|
|
352
|
+
const drawn = Drawn({ block, draw })
|
|
353
|
+
// After a line of its own words; a list and a table ask each item and cell.
|
|
354
|
+
const lineOfWords = block.type === 'paragraph' || block.type === 'heading' || block.type === 'blockquote'
|
|
355
|
+
const after = lineOfWords ? draw.renderAfter?.(block) : undefined
|
|
356
|
+
if (after === undefined || after === null || after === false) return drawn
|
|
357
|
+
return (
|
|
358
|
+
<>
|
|
359
|
+
{drawn}
|
|
360
|
+
{after}
|
|
361
|
+
</>
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function Drawn({ block, draw }: { block: RichTextBlock; draw: Draw }): ReactNode {
|
|
336
366
|
const runs = <Runs runs={block.inline} draw={draw} />
|
|
337
367
|
switch (block.type) {
|
|
338
368
|
case 'heading': {
|
|
@@ -401,10 +431,11 @@ export function RichText({
|
|
|
401
431
|
renderText = (text) => text,
|
|
402
432
|
renderReference = (_reference, run) => run.text,
|
|
403
433
|
renderBlock,
|
|
434
|
+
renderAfter,
|
|
404
435
|
onTick,
|
|
405
436
|
}: RichTextProps) {
|
|
406
437
|
if (blocks.length === 0) return null
|
|
407
|
-
const draw: Draw = { renderText, renderReference, renderBlock, onTick }
|
|
438
|
+
const draw: Draw = { renderText, renderReference, renderBlock, renderAfter, onTick }
|
|
408
439
|
return (
|
|
409
440
|
<div data-rich-text className={richTextClassName(size)}>
|
|
410
441
|
{blocks.map((block, i) => <Block key={block.id ?? i} block={block} draw={draw} />)}
|
package/src/SelectionToolbar.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
2
2
|
import type { Editor } from '@tiptap/react'
|
|
3
3
|
import { Extension } from '@tiptap/core'
|
|
4
4
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
|
@@ -221,9 +221,16 @@ export interface SelectionToolbarProps {
|
|
|
221
221
|
marks?: readonly MarkId[]
|
|
222
222
|
/** The Link button and its address field. Needs a link mark in the editor and `KeptSelection`. Default off. */
|
|
223
223
|
link?: boolean
|
|
224
|
+
/**
|
|
225
|
+
* More of the strip, after the marks: the app's own `ToolbarButton`s for what
|
|
226
|
+
* is selected — a chip's card, say. Each keeps the selection by preventing
|
|
227
|
+
* its press's default, as the marks do. Nothing is added when it renders
|
|
228
|
+
* nothing.
|
|
229
|
+
*/
|
|
230
|
+
children?: ReactNode
|
|
224
231
|
}
|
|
225
232
|
|
|
226
|
-
export function SelectionToolbar({ editor, marks = ['bold', 'italic', 'underline'], link = false }: SelectionToolbarProps) {
|
|
233
|
+
export function SelectionToolbar({ editor, marks = ['bold', 'italic', 'underline'], link = false, children }: SelectionToolbarProps) {
|
|
227
234
|
/** The selection's top edge and horizontal centre; the face decides the rest. */
|
|
228
235
|
const [anchor, setAnchor] = useState<{ top: number; centre: number } | null>(null)
|
|
229
236
|
const [mode, setMode] = useState<Mode>('marks')
|
|
@@ -480,6 +487,7 @@ export function SelectionToolbar({ editor, marks = ['bold', 'italic', 'underline
|
|
|
480
487
|
<IconLink size={16} stroke={1.5} />
|
|
481
488
|
</ToolbarButton>
|
|
482
489
|
)}
|
|
490
|
+
{children}
|
|
483
491
|
</Toolbar>
|
|
484
492
|
)}
|
|
485
493
|
</Popover>
|
package/src/editor.ts
CHANGED
|
@@ -55,3 +55,16 @@ export {
|
|
|
55
55
|
type AttachmentNodeOptions,
|
|
56
56
|
type AttachmentViewProps,
|
|
57
57
|
} from './BlockNodes'
|
|
58
|
+
export {
|
|
59
|
+
ReferenceCards,
|
|
60
|
+
useReferenceCards,
|
|
61
|
+
referenceAtSelection,
|
|
62
|
+
referenceCardPlacements,
|
|
63
|
+
setReferenceCard,
|
|
64
|
+
setReferenceCardAt,
|
|
65
|
+
type ReferenceCard,
|
|
66
|
+
type ReferenceCardLayout,
|
|
67
|
+
type ReferenceCardSlot,
|
|
68
|
+
} from './ReferenceCards'
|
|
69
|
+
export { LinkChips, findLinkChips, refreshLinkChips, type LinkChipsOptions, type PastedRange } from './LinkChips'
|
|
70
|
+
export { ReferenceMenu, ReferenceMenuView, isReferenceMenuOpen, referenceMenuClick, type ReferenceMenuViewProps } from './ReferenceMenu'
|
package/src/pages.test.ts
CHANGED
|
@@ -42,6 +42,9 @@ const HOLDS_ONLY: Record<string, string> = {
|
|
|
42
42
|
AppProvider: 'Nothing. It draws nothing; it holds two answers for the parts below it.',
|
|
43
43
|
PastedImages: 'Nothing. It draws nothing; it hands a pasted picture to the app, named.',
|
|
44
44
|
CodeBlockColours: 'Nothing. It names each word of a code block; the RichText class list colours them.',
|
|
45
|
+
LinkChips: 'Nothing. It draws nothing; it turns the URLs the app recognises into chips.',
|
|
46
|
+
// Its box is the Popover's, which owns what a box does; it holds which chip the menu is on.
|
|
47
|
+
ReferenceMenu: 'Nothing. Its box is the Popover; it holds which chip the menu is open on, and its keys.',
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
const dir = new URL('.', import.meta.url)
|
|
@@ -26,7 +26,7 @@ import { SCHEMA_VERSION, validateRegistry, type Registry, type RegistryEntry } f
|
|
|
26
26
|
const root = process.cwd()
|
|
27
27
|
const registry = buildRegistry({ root })
|
|
28
28
|
/** Editor extensions: named like components by the catalogue, and not React components. */
|
|
29
|
-
const EDITOR_EXTENSIONS = ['KeptSelection', 'BlockId', 'UnknownBlock', 'ReferenceNode', 'AttachmentNode', 'PersonMention', 'UrgentPersonMention', 'CaptionedReference', 'ReferenceTrigger', 'SlashCommands', 'PastedImages', 'CodeBlockColours', 'Checklist', 'Callout']
|
|
29
|
+
const EDITOR_EXTENSIONS = ['KeptSelection', 'BlockId', 'UnknownBlock', 'ReferenceNode', 'AttachmentNode', 'PersonMention', 'UrgentPersonMention', 'CaptionedReference', 'ReferenceTrigger', 'SlashCommands', 'PastedImages', 'CodeBlockColours', 'Checklist', 'Callout', 'ReferenceCards', 'LinkChips', 'ReferenceMenu']
|
|
30
30
|
const entry = (name: string): RegistryEntry => {
|
|
31
31
|
const found = registry.entries.find((candidate) => candidate.name === name)
|
|
32
32
|
if (!found) throw new Error(`no entry for ${name}`)
|
|
@@ -48,20 +48,20 @@ describe('the registry builds', () => {
|
|
|
48
48
|
expect(registry.entries.map((one) => one.name).sort()).toEqual(values.map((one) => one.name).sort())
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
it('counts
|
|
51
|
+
it('counts 162 names over 80 files: 114 components, 46 helpers and 2 hooks (NavTree, NavTreeSection, the composer triggers and UnreadDot joined on 2 October; BlockHandle and EditorBlockHandle (RIC-18) on 2 October, moveBlock and blockDropCursor, the drop in the gutter, on 5 October, MembersPill and MembersDialog, moved from Peek (SHI-31), on 6 October, ShowMore on 7 October, AppProvider on 8 October, and KindIcon, thingKindOf and PastedImages with its two helpers, moved from Peek, on 8 October; CodeBlockColours, the coloured code block of the editor, on 9 October; Checklist, Callout, isChecklistTick and calloutLook (RIC-19) on 13 October; EditorTableControls and its four actions (RIC-21) on 13 October; ReferenceCards, useReferenceCards and its three helpers, the card a chip in a document asks for, on 14 October; LinkChips and its two helpers, a URL in a document read as its chip, on 14 October; ReferenceMenu, ReferenceMenuView and three helpers, the menu of a chip while writing, on 14 October)', () => {
|
|
52
52
|
// The reconciliation GATES.md §24 explains. If this changes, that changes.
|
|
53
53
|
// The `EDITOR_EXTENSIONS` count as components by their names; they are
|
|
54
54
|
// editor extensions, documented on SelectionToolbar's, BlockNodes' and
|
|
55
55
|
// ComposerTriggers' pages.
|
|
56
56
|
const kinds = registry.entries.reduce<Record<string, number>>((all, one) => ({ ...all, [one.kind]: (all[one.kind] ?? 0) + 1 }), {})
|
|
57
|
-
expect(kinds).toEqual({ component:
|
|
58
|
-
expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(
|
|
57
|
+
expect(kinds).toEqual({ component: 114, helper: 46, hook: 2 })
|
|
58
|
+
expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(80)
|
|
59
59
|
})
|
|
60
60
|
|
|
61
61
|
it('gives every entry a purpose, from its own page or from the comment above it', () => {
|
|
62
62
|
expect(registry.entries.filter((one) => one.purpose.trim() === '')).toEqual([])
|
|
63
|
-
expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(
|
|
64
|
-
expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(
|
|
63
|
+
expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(76)
|
|
64
|
+
expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(86)
|
|
65
65
|
})
|
|
66
66
|
|
|
67
67
|
it('takes a name with a page of its own from the page, and a name without one from the code', () => {
|
package/stories/Choosing.mdx
CHANGED
|
@@ -98,6 +98,9 @@ fork freely, owe nothing back, mention it on the package's ticket.
|
|
|
98
98
|
| A link — in text, on a title, beside a note, or a whole box | **Link** — `text`, `quiet`, `underlined`, `plain`; `external` for a new tab |
|
|
99
99
|
| A document or an image attached to something, posted or waiting to be sent | **AttachmentCard** — the file decides the shape; `pending` in a composer |
|
|
100
100
|
| A picture pasted into an editor, attached rather than pasted | **PastedImages** — from `@estiva-app/ui/editor`; the app hands it its upload |
|
|
101
|
+
| A card a chip in a document asks for, drawn after its line | **ReferenceCards** — from `@estiva-app/ui/editor`, beside `ReferenceNode`; the app draws its card into each slot |
|
|
102
|
+
| A URL in a document that reads as the chip of what it points at | **LinkChips** — from `@estiva-app/ui/editor`, beside `ReferenceNode`; the app says what each URL names |
|
|
103
|
+
| Open a chip in a document, or choose its card, while writing | **ReferenceMenu** — from `@estiva-app/ui/editor`, beside `ReferenceCards`; a click on the chip, or putting one in, opens it |
|
|
101
104
|
| A code block in an editor, coloured by its language as you type | **CodeBlockColours** — from `@estiva-app/ui/editor`, in place of the starter kit's code block |
|
|
102
105
|
| A person or a thing named inside a sentence | **InlineChip** — one line high, level with the words; `href` makes it a link |
|
|
103
106
|
| What kind of thing a name stands for — a topic, a project, an issue, a file, a message | **KindIcon** — before the name in a row, or inside an **InlineChip**; `thingKindOf` turns an event kind into one |
|