@estiva-app/ui 0.66.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.
Files changed (54) hide show
  1. package/dist/BlockNodes.d.ts +12 -1
  2. package/dist/BlockNodes.d.ts.map +1 -1
  3. package/dist/EditorTableControls.d.ts +38 -0
  4. package/dist/EditorTableControls.d.ts.map +1 -0
  5. package/dist/LinkChips.d.ts +82 -0
  6. package/dist/LinkChips.d.ts.map +1 -0
  7. package/dist/NavItem.d.ts +11 -1
  8. package/dist/NavItem.d.ts.map +1 -1
  9. package/dist/ReferenceCards.d.ts +77 -0
  10. package/dist/ReferenceCards.d.ts.map +1 -0
  11. package/dist/ReferenceMenu.d.ts +18 -0
  12. package/dist/ReferenceMenu.d.ts.map +1 -0
  13. package/dist/RichText.d.ts +9 -2
  14. package/dist/RichText.d.ts.map +1 -1
  15. package/dist/SelectionToolbar.d.ts +9 -1
  16. package/dist/SelectionToolbar.d.ts.map +1 -1
  17. package/dist/editor.d.ts +4 -0
  18. package/dist/editor.d.ts.map +1 -1
  19. package/dist/editor.js +750 -27
  20. package/dist/editor.js.map +4 -4
  21. package/dist/index.js +62 -33
  22. package/dist/index.js.map +4 -4
  23. package/package.json +2 -1
  24. package/registry.json +414 -4
  25. package/src/BlockNodes.test.tsx +2 -1
  26. package/src/BlockNodes.tsx +51 -9
  27. package/src/EditorTableControls.mdx +78 -0
  28. package/src/EditorTableControls.stories.tsx +63 -0
  29. package/src/EditorTableControls.test.tsx +288 -0
  30. package/src/EditorTableControls.tsx +255 -0
  31. package/src/LinkChips.mdx +57 -0
  32. package/src/LinkChips.stories.tsx +67 -0
  33. package/src/LinkChips.test.tsx +178 -0
  34. package/src/LinkChips.ts +243 -0
  35. package/src/NavItem.mdx +10 -0
  36. package/src/NavItem.stories.tsx +23 -1
  37. package/src/NavItem.test.tsx +36 -0
  38. package/src/NavItem.tsx +40 -16
  39. package/src/ReferenceCards.mdx +79 -0
  40. package/src/ReferenceCards.stories.tsx +64 -0
  41. package/src/ReferenceCards.test.tsx +143 -0
  42. package/src/ReferenceCards.ts +225 -0
  43. package/src/ReferenceMenu.mdx +75 -0
  44. package/src/ReferenceMenu.stories.tsx +59 -0
  45. package/src/ReferenceMenu.test.tsx +289 -0
  46. package/src/ReferenceMenu.tsx +346 -0
  47. package/src/RichText.test.tsx +17 -0
  48. package/src/RichText.tsx +36 -4
  49. package/src/SelectionToolbar.tsx +10 -2
  50. package/src/editor.ts +14 -0
  51. package/src/pages.test.ts +3 -0
  52. package/src/registry/registry.test.ts +7 -7
  53. package/src/registry/siblings.test.ts +2 -2
  54. package/stories/Choosing.mdx +4 -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
@@ -125,7 +132,8 @@ const RICH_TEXT_CLASSES = {
125
132
  syntax: '[&_:is(.hljs-keyword,.hljs-doctag,.hljs-type,.hljs-template-tag,.hljs-template-variable)]:text-syntax-keyword [&_.hljs-title]:text-syntax-function [&_:is(.hljs-number,.hljs-literal,.hljs-attr,.hljs-attribute,.hljs-meta,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id)]:text-syntax-constant [&_:is(.hljs-string,.hljs-regexp)]:text-syntax-string [&_:is(.hljs-built_in,.hljs-symbol)]:text-syntax-variable [&_.hljs-comment]:text-syntax-comment [&_:is(.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo)]:text-syntax-tag',
126
133
  /* A divider splits the text in two: 16px either side (Katerina, 9 October; 4 before). */
127
134
  divider: '[&_hr]:my-2 [&_hr]:border-border-default',
128
- table: '[&_:is(th,td)>p]:m-0 [&_:is(th,td)]:border [&_:is(th,td)]:border-border-default [&_:is(th,td)]:px-2 [&_:is(th,td)]:py-1 [&_:is(th,td)]:text-left [&_:is(th,td)]:align-top [&_table]:w-full [&_table]:border-collapse [&_th]:bg-bg-code [&_th]:font-medium [&_th]:text-text-primary',
135
+ /* A table 16px above and below, as a divider (Katerina, 14 October; 8 before): the editor's row ➕ sits in that room, not over the next line. */
136
+ table: '[&_:is(th,td)>p]:m-0 [&_:is(th,td)]:border [&_:is(th,td)]:border-border-default [&_:is(th,td)]:px-2 [&_:is(th,td)]:py-1 [&_:is(th,td)]:text-left [&_:is(th,td)]:align-top [&_table]:my-2 [&_table]:w-full [&_table]:border-collapse [&_th]:bg-bg-code [&_th]:font-medium [&_th]:text-text-primary',
129
137
  /* Bold in the heading colour, not only heavier. An underlined word draws a faint line, so it does not read as a link (Katerina, 9 October). */
130
138
  marks: '[&_em]:italic [&_strong]:font-semibold [&_strong]:text-text-primary [&_u]:underline [&_u]:decoration-text-muted [&_u]:underline-offset-2',
131
139
  /* Lines 1.5 apart (Katerina, 9 October; 1.4 before). */
@@ -185,6 +193,7 @@ interface Draw {
185
193
  renderText: (text: string) => ReactNode
186
194
  renderReference: (reference: string, run: RichTextRun) => ReactNode
187
195
  renderBlock?: (block: RichTextBlock) => ReactNode | undefined
196
+ renderAfter?: (block: RichTextBlock) => ReactNode
188
197
  onTick?: (blockId: string, checked: boolean) => void
189
198
  }
190
199
 
@@ -244,8 +253,14 @@ function Table({ block, draw }: { block: RichTextBlock; draw: Draw }) {
244
253
  colSpan={colspan && colspan > 1 ? colspan : undefined}
245
254
  rowSpan={rowspan && rowspan > 1 ? rowspan : undefined}
246
255
  >
247
- {(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
+ ))}
248
262
  <Runs runs={cell.inline} draw={draw} />
263
+ {draw.renderAfter?.(cell)}
249
264
  </Cell>
250
265
  )
251
266
  })
@@ -280,6 +295,7 @@ function ListItem({ item, draw, below }: { item: RichTextBlock; draw: Draw; belo
280
295
  return (
281
296
  <li data-block-id={item.id}>
282
297
  <Runs runs={item.inline} draw={draw} />
298
+ {draw.renderAfter?.(item)}
283
299
  {below}
284
300
  </li>
285
301
  )
@@ -298,6 +314,7 @@ function ListItem({ item, draw, below }: { item: RichTextBlock; draw: Draw; belo
298
314
  <p>
299
315
  <Runs runs={item.inline} draw={draw} />
300
316
  </p>
317
+ {draw.renderAfter?.(item)}
301
318
  {below}
302
319
  </div>
303
320
  </li>
@@ -332,6 +349,20 @@ function nestItems(items: readonly RichTextBlock[]): NestedItem[] {
332
349
  function Block({ block, draw }: { block: RichTextBlock; draw: Draw }): ReactNode {
333
350
  const own = draw.renderBlock?.(block)
334
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 {
335
366
  const runs = <Runs runs={block.inline} draw={draw} />
336
367
  switch (block.type) {
337
368
  case 'heading': {
@@ -400,10 +431,11 @@ export function RichText({
400
431
  renderText = (text) => text,
401
432
  renderReference = (_reference, run) => run.text,
402
433
  renderBlock,
434
+ renderAfter,
403
435
  onTick,
404
436
  }: RichTextProps) {
405
437
  if (blocks.length === 0) return null
406
- const draw: Draw = { renderText, renderReference, renderBlock, onTick }
438
+ const draw: Draw = { renderText, renderReference, renderBlock, renderAfter, onTick }
407
439
  return (
408
440
  <div data-rich-text className={richTextClassName(size)}>
409
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
@@ -12,6 +12,7 @@ export { PastedImages, pastedImages, pastedImageName, type PastedImagesOptions,
12
12
  export { CodeBlockColours } from './CodeBlockColours'
13
13
  export { suggestionPopup, isSuggestionActive, isSuggestionOpen, type SuggestionPopupOptions } from './suggestionPopup'
14
14
  export { EditorBlockHandle, blockDropCursor, deleteBlock, moveBlock, duplicateBlock, startBlockDrag, type EditorBlockHandleProps, type BlockTurnInto } from './EditorBlockHandle'
15
+ export { EditorTableControls, appendTableRow, appendTableColumn, deleteTableRow, deleteTableColumn, type EditorTableControlsProps } from './EditorTableControls'
15
16
  export {
16
17
  PersonMention,
17
18
  UrgentPersonMention,
@@ -54,3 +55,16 @@ export {
54
55
  type AttachmentNodeOptions,
55
56
  type AttachmentViewProps,
56
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 144 names over 76 files: 109 components, 34 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)', () => {
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: 109, helper: 34, hook: 1 })
58
- expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(76)
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(72)
64
- expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(72)
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', () => {
@@ -299,7 +299,7 @@ describe('the ids point at something', () => {
299
299
  describe('a second parser agrees', async () => {
300
300
  const docgen = await import('react-docgen').catch(() => null)
301
301
 
302
- it.skipIf(!docgen)('finds the same props on all 79 components', async () => {
302
+ it.skipIf(!docgen)('finds the same props on all 80 components', async () => {
303
303
  if (!docgen) return
304
304
  const resolver = new docgen.builtinResolvers.FindExportedDefinitionsResolver({ limit: 0 })
305
305
  const byName = new Map(registry.entries.map((one) => [one.name, one]))
@@ -26,7 +26,7 @@ function appAt(folder: string, name: string, deps: Record<string, string> = UI,
26
26
  put(`${folder}/src/${part}.tsx`, `/** The ${part.toLowerCase()} of ${name}. */\nexport function ${part}() {\n return <i />\n}\n`)
27
27
  }
28
28
  // A repository is a folder with `.git`: a directory, or a worktree's file.
29
- appAt('alpha', 'alpha-app', UI, 'Tabletop')
29
+ appAt('alpha', 'alpha-app', UI, 'Sundial')
30
30
  put('alpha/.git/HEAD', 'ref: refs/heads/main\n')
31
31
  put('beta/package.json', JSON.stringify({ name: 'beta' }))
32
32
  put('beta/.git/HEAD', 'ref: refs/heads/main\n')
@@ -75,7 +75,7 @@ describe.skipIf(!existsSync(cli))('estiva-ui find, with nothing else typed', ()
75
75
  })
76
76
 
77
77
  it('answers the same from the folder that holds them', () => {
78
- expect(find(workspace, 'tabletop')).toMatch(/^Tabletop {2}· .* · {2}alpha/m)
78
+ expect(find(workspace, 'sundial')).toMatch(/^Sundial {2}· .* · {2}alpha/m)
79
79
  expect(find(workspace, 'zzqqx')).toContain('Nothing in estiva-ui, alpha, beta matches')
80
80
  })
81
81
 
@@ -73,6 +73,7 @@ fork freely, owe nothing back, mention it on the package's ticket.
73
73
  | A composer's `@`, `!@`, `[` and `/` — mention a person, reference something, format or insert | **ComposerTriggers** — `PersonMention`, `UrgentPersonMention`, `CaptionedReference`, `ReferenceTrigger` and `SlashCommands` from `@estiva-app/ui/editor`; the app passes its people, its items and its own `/` rows |
74
74
  | An editor that saves a block document and must keep its block ids | **BlockNodes** — `BlockId`, `UnknownBlock`, `ReferenceNode` and `AttachmentNode` from `@estiva-app/ui/editor`, beside StarterKit and TableKit |
75
75
  | Move, turn, duplicate or delete one block of a document being edited | **EditorBlockHandle**, from `@estiva-app/ui/editor`, with your rows after its own |
76
+ | Add or delete a table's rows and columns in a document being edited | **EditorTableControls**, from `@estiva-app/ui/editor`, inside the EditorBlockHandle |
76
77
  | Offer a link or a comment on one block of a document being read | **BlockHandle** — your rows only, and no Tiptap loaded |
77
78
  | Anything with a title, body and footer | **DialogShell** |
78
79
  | "Are you sure?" | **ConfirmDialog** |
@@ -97,6 +98,9 @@ fork freely, owe nothing back, mention it on the package's ticket.
97
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 |
98
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 |
99
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 |
100
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 |
101
105
  | A person or a thing named inside a sentence | **InlineChip** — one line high, level with the words; `href` makes it a link |
102
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 |