@estiva-app/ui 0.67.0 → 0.69.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.
Files changed (41) hide show
  1. package/dist/BlockNodes.d.ts +12 -1
  2. package/dist/BlockNodes.d.ts.map +1 -1
  3. package/dist/LinkChips.d.ts +82 -0
  4. package/dist/LinkChips.d.ts.map +1 -0
  5. package/dist/ReferenceCards.d.ts +77 -0
  6. package/dist/ReferenceCards.d.ts.map +1 -0
  7. package/dist/ReferenceMenu.d.ts +18 -0
  8. package/dist/ReferenceMenu.d.ts.map +1 -0
  9. package/dist/RichText.d.ts +9 -2
  10. package/dist/RichText.d.ts.map +1 -1
  11. package/dist/SelectionToolbar.d.ts +9 -1
  12. package/dist/SelectionToolbar.d.ts.map +1 -1
  13. package/dist/editor.d.ts +3 -0
  14. package/dist/editor.d.ts.map +1 -1
  15. package/dist/editor.js +595 -20
  16. package/dist/editor.js.map +4 -4
  17. package/dist/index.js +39 -19
  18. package/dist/index.js.map +4 -4
  19. package/package.json +1 -1
  20. package/registry.json +290 -3
  21. package/src/BlockNodes.test.tsx +2 -1
  22. package/src/BlockNodes.tsx +51 -9
  23. package/src/LinkChips.mdx +59 -0
  24. package/src/LinkChips.stories.tsx +67 -0
  25. package/src/LinkChips.test.tsx +221 -0
  26. package/src/LinkChips.ts +252 -0
  27. package/src/ReferenceCards.mdx +79 -0
  28. package/src/ReferenceCards.stories.tsx +64 -0
  29. package/src/ReferenceCards.test.tsx +143 -0
  30. package/src/ReferenceCards.ts +225 -0
  31. package/src/ReferenceMenu.mdx +75 -0
  32. package/src/ReferenceMenu.stories.tsx +59 -0
  33. package/src/ReferenceMenu.test.tsx +289 -0
  34. package/src/ReferenceMenu.tsx +346 -0
  35. package/src/RichText.test.tsx +17 -0
  36. package/src/RichText.tsx +34 -3
  37. package/src/SelectionToolbar.tsx +10 -2
  38. package/src/editor.ts +13 -0
  39. package/src/pages.test.ts +3 -0
  40. package/src/registry/registry.test.ts +6 -6
  41. package/stories/Choosing.mdx +3 -0
package/src/RichText.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import type { MouseEvent, ReactNode } from 'react'
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) => <Runs key={j} runs={child.inline} draw={draw} />)}
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} />)}
@@ -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 149 names over 77 files: 110 components, 38 helpers and 1 hook (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)', () => {
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: 110, helper: 38, hook: 1 })
58
- expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(77)
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(73)
64
- expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(76)
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', () => {
@@ -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 |