@estiva-app/ui 0.72.0-widgets2.1 → 0.72.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 (49) hide show
  1. package/dist/CommentHighlights.d.ts +100 -0
  2. package/dist/CommentHighlights.d.ts.map +1 -0
  3. package/dist/ComposeBox.d.ts +55 -0
  4. package/dist/ComposeBox.d.ts.map +1 -0
  5. package/dist/ComposerTriggers.d.ts.map +1 -1
  6. package/dist/SelectionToolbar.d.ts +17 -1
  7. package/dist/SelectionToolbar.d.ts.map +1 -1
  8. package/dist/editor.d.ts +3 -1
  9. package/dist/editor.d.ts.map +1 -1
  10. package/dist/editor.js +618 -217
  11. package/dist/editor.js.map +4 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/looks.d.ts +6 -0
  14. package/dist/looks.d.ts.map +1 -1
  15. package/package.json +1 -5
  16. package/registry.json +309 -3
  17. package/src/CommentHighlights.mdx +61 -0
  18. package/src/CommentHighlights.stories.tsx +72 -0
  19. package/src/CommentHighlights.test.ts +100 -0
  20. package/src/CommentHighlights.ts +413 -0
  21. package/src/ComposeBox.mdx +58 -0
  22. package/src/ComposeBox.stories.tsx +81 -0
  23. package/src/ComposeBox.tsx +183 -0
  24. package/src/ComposerTriggers.tsx +9 -3
  25. package/src/SelectionToolbar.actions.test.tsx +44 -0
  26. package/src/SelectionToolbar.mdx +1 -0
  27. package/src/SelectionToolbar.tsx +36 -2
  28. package/src/editor.ts +15 -1
  29. package/src/looks.ts +8 -0
  30. package/src/registry/app.test.ts +7 -1
  31. package/src/registry/registry.test.ts +6 -6
  32. package/stories/Choosing.mdx +2 -0
  33. package/dist/widgets-2/fixtures.d.ts +0 -102
  34. package/dist/widgets-2/fixtures.d.ts.map +0 -1
  35. package/dist/widgets-2/places.d.ts +0 -99
  36. package/dist/widgets-2/places.d.ts.map +0 -1
  37. package/dist/widgets-2/proposed.d.ts +0 -130
  38. package/dist/widgets-2/proposed.d.ts.map +0 -1
  39. package/dist/widgets-2.d.ts +0 -12
  40. package/dist/widgets-2.d.ts.map +0 -1
  41. package/dist/widgets-2.js +0 -584
  42. package/dist/widgets-2.js.map +0 -7
  43. package/src/widgets-2/fixtures.ts +0 -161
  44. package/src/widgets-2/places.tsx +0 -309
  45. package/src/widgets-2/proposed.tsx +0 -596
  46. package/src/widgets-2.ts +0 -11
  47. package/stories/widgets-2/ReadMe.mdx +0 -60
  48. package/stories/widgets-2/Widgets.stories.tsx +0 -504
  49. package/stories/widgets-2/today.tsx +0 -352
@@ -0,0 +1,183 @@
1
+ import { useCallback, useEffect, useState, type ReactNode } from 'react'
2
+ import { Extension } from '@tiptap/core'
3
+ import { EditorContent, type Editor } from '@tiptap/react'
4
+ import { IconArrowUp } from '@tabler/icons-react'
5
+ import { IconButton } from './IconButton'
6
+ import { Toolbar } from './Toolbar'
7
+ import { cn } from './cn'
8
+ import { richTextClassName } from './RichText'
9
+ import { WRITING_BOX_CLASSES } from './looks'
10
+ import { SelectionToolbar, type SelectionToolbarProps } from './SelectionToolbar'
11
+ import { isSuggestionActive } from './suggestionPopup'
12
+
13
+ /**
14
+ * The box a message, a reply or a comment is written in, from Katerina's Leaf
15
+ * prototype (9 October). Peek's `ComposeBox`, its look and its keys, with
16
+ * what only Peek knows left in Peek: drafts, uploads, highlights, its text
17
+ * format. The app makes the editor (its extensions are its own) and adds
18
+ * `ComposeKeys`; this draws the box around it and sends.
19
+ */
20
+
21
+ /** The editor's own classes inside the box: Peek's, rich text with no outline. Put them in `editorProps.attributes.class`. */
22
+ export const COMPOSE_EDITOR_CLASSES = richTextClassName('default', 'outline-none w-full bg-transparent min-h-[20px] [&>p+p]:-mt-1')
23
+
24
+ type ComposeKeysStorage = { send: (() => boolean) | null }
25
+
26
+ /**
27
+ * Enter sends; Shift+Enter is a new line — in a list a new item (or out of the
28
+ * list, on an empty one), in a quote a new line (or out of it, on an empty one).
29
+ * Peek's keys, as its ComposeBox has them. Enter while a `/`, `@` or `[` list is
30
+ * open picks a row instead. `ComposeBox` says what sending does.
31
+ */
32
+ export const ComposeKeys = Extension.create<Record<string, never>, ComposeKeysStorage>({
33
+ name: 'composeKeys',
34
+ addStorage() {
35
+ return { send: null }
36
+ },
37
+ addKeyboardShortcuts() {
38
+ return {
39
+ Enter: () => {
40
+ if (isSuggestionActive()) return false
41
+ this.storage.send?.()
42
+ return true
43
+ },
44
+ 'Shift-Enter': ({ editor }) => {
45
+ const { $from } = editor.state.selection
46
+ for (let d = $from.depth; d > 0; d--) {
47
+ const node = $from.node(d)
48
+ if (node.type.name === 'listItem') {
49
+ return node.textContent.length === 0 ? editor.commands.liftListItem('listItem') : editor.commands.splitListItem('listItem')
50
+ }
51
+ if (node.type.name === 'blockquote') {
52
+ if ($from.parent.textContent.length === 0) return editor.commands.lift('paragraph')
53
+ break
54
+ }
55
+ }
56
+ return editor.commands.splitBlock()
57
+ },
58
+ }
59
+ },
60
+ })
61
+
62
+ /** Nothing written: no text, no chip and no picture — empty lines, lists or quotes only. */
63
+ function isEmptyDoc(editor: Editor): boolean {
64
+ const doc = editor.state.doc
65
+ if (doc.textContent.length > 0) return false
66
+ let empty = true
67
+ doc.descendants((node) => {
68
+ if (node.isAtom && node.type.name !== 'hardBreak') empty = false
69
+ })
70
+ return empty
71
+ }
72
+
73
+ export interface ComposeBoxProps {
74
+ /** The app's editor, with `ComposeKeys` in its extensions and `COMPOSE_EDITOR_CLASSES` on it. `null` while it loads. */
75
+ editor: Editor | null
76
+ /**
77
+ * Send what is written. The box empties afterwards — unless this returns
78
+ * `false`: nothing was sent, and the words stay.
79
+ */
80
+ onSend: (editor: Editor) => void | boolean
81
+ /** Said in the empty box: words, and a `Kbd` where a key helps ("type / for commands"). */
82
+ placeholder: ReactNode
83
+ /** The tools under the text, left: `ToolbarButton`s (attach, highlight). One Tab stop. */
84
+ tools?: ReactNode
85
+ /** What is attached, under the text: `AttachmentCard pending`s. */
86
+ attachments?: ReactNode
87
+ /** Something to send even with no words — a file ready. */
88
+ ready?: boolean
89
+ /** Sending waits — a file still uploading. */
90
+ holding?: boolean
91
+ /** The bar down the text's left: an urgent mention or a highlight, as Peek draws it. */
92
+ marked?: boolean
93
+ /** The selection strip's buttons: its marks, Link, and the app's own (`actions`). */
94
+ selection?: Pick<SelectionToolbarProps, 'marks' | 'link' | 'actions'>
95
+ className?: string
96
+ }
97
+
98
+ /**
99
+ * The box for writing: the text, the selection strip over it, what is
100
+ * attached, the tools and Send. Enter sends (`ComposeKeys`), and so does the
101
+ * button.
102
+ */
103
+ export function ComposeBox({ editor, onSend, placeholder, tools, attachments, ready = false, holding = false, marked = false, selection, className }: ComposeBoxProps) {
104
+ const [empty, setEmpty] = useState(true)
105
+ const canSend = (!empty || ready) && !holding
106
+
107
+ const send = useCallback((): boolean => {
108
+ if (!editor || holding || (isEmptyDoc(editor) && !ready)) return true
109
+ if (onSend(editor) === false) return true
110
+ editor.commands.clearContent(true)
111
+ editor.commands.focus()
112
+ return true
113
+ }, [editor, holding, ready, onSend])
114
+
115
+ // Enter, through `ComposeKeys`, sends the way the button does.
116
+ useEffect(() => {
117
+ if (!editor) return
118
+ const storage = (editor.storage as unknown as { composeKeys?: ComposeKeysStorage }).composeKeys
119
+ if (!storage) return
120
+ storage.send = send
121
+ return () => {
122
+ storage.send = null
123
+ }
124
+ }, [editor, send])
125
+
126
+ useEffect(() => {
127
+ if (!editor) return
128
+ const read = () => setEmpty(isEmptyDoc(editor))
129
+ read()
130
+ editor.on('update', read)
131
+ return () => {
132
+ editor.off('update', read)
133
+ }
134
+ }, [editor])
135
+
136
+ return (
137
+ <div className={cn('relative', className)}>
138
+ <div className={cn(WRITING_BOX_CLASSES, 'focus-within:border-border-strong transition-colors')}>
139
+ <div data-composer-editor className={cn('relative min-h-[20px] transition-all', marked && 'border-l-4 border-border-strong pl-2')}>
140
+ <EditorContent editor={editor} />
141
+ <SelectionToolbar editor={editor} {...selection} />
142
+ {empty && (
143
+ <div className="pointer-events-none absolute inset-0 flex flex-wrap items-center gap-1 text-body-2 text-text-muted">{placeholder}</div>
144
+ )}
145
+ </div>
146
+
147
+ {attachments && <div className="flex flex-wrap gap-2">{attachments}</div>}
148
+
149
+ {/* The tools are one strip — one Tab stop, arrows along it; Send, the answer rather than a tool, keeps its own. */}
150
+ <div className="flex items-center justify-between">
151
+ {tools ? (
152
+ <Toolbar aria-label="Compose tools" surface={false} className="flex items-center gap-1">
153
+ {tools}
154
+ </Toolbar>
155
+ ) : (
156
+ <span />
157
+ )}
158
+ <div className="flex items-center gap-2.5">
159
+ <span className="hidden select-none font-mono text-menu font-normal tracking-wider text-text-muted signal:inline">Enter to send · Shift+Enter new line</span>
160
+ {/* Focus stays in the text: the press sends on its way down. A keyboard press is the click (`detail` 0), so a click never sends twice. */}
161
+ <IconButton
162
+ variant="primary"
163
+ onMouseDown={(e) => {
164
+ e.preventDefault()
165
+ send()
166
+ }}
167
+ onClick={(e) => {
168
+ if (e.detail === 0) send()
169
+ }}
170
+ disabled={!canSend}
171
+ aria-label="Send"
172
+ tooltip="Send"
173
+ tooltipShortcut="Enter"
174
+ glow={canSend}
175
+ >
176
+ <IconArrowUp size={16} stroke={1.5} />
177
+ </IconButton>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ )
183
+ }
@@ -7,6 +7,7 @@ import { NodeViewWrapper, ReactNodeViewRenderer, type ReactNodeViewProps } from
7
7
  import { Avatar } from './Avatar'
8
8
  import { EnterHint } from './Menu'
9
9
  import { inlineChipClassName } from './InlineChip'
10
+ import { WithTooltip } from './Tooltip'
10
11
  import type { SuggestionMenuRow } from './SuggestionMenu'
11
12
  import { suggestionPopup, type SuggestionPopupOptions } from './suggestionPopup'
12
13
 
@@ -308,9 +309,14 @@ export const CaptionedReference = Mention.extend<MentionOptions & CaptionedRefer
308
309
  const { icon, iconFor } = this.options
309
310
  return ReactNodeViewRenderer(
310
311
  ({ node }: ReactNodeViewProps) => (
311
- <NodeViewWrapper as="span" className={inlineChipClassName('neutral', 'max-w-[24ch] cursor-default')}>
312
- <span className="flex items-center justify-center w-4 h-4 shrink-0 text-text-secondary">{iconFor?.(node.attrs.uri ?? null) ?? icon}</span>
313
- <span className="truncate">{captionOf(node.attrs.label, node.attrs.snippet)}</span>
312
+ // The caption is cut at 24ch, so the whole of it is on hover, in the package's tooltip (UIG-14) — as Peek's sent chip has it.
313
+ <NodeViewWrapper as="span">
314
+ <WithTooltip inline label={captionOf(node.attrs.label, node.attrs.snippet)}>
315
+ <span className={inlineChipClassName('neutral', 'max-w-[24ch] cursor-default')}>
316
+ <span className="flex items-center justify-center w-4 h-4 shrink-0 text-text-secondary">{iconFor?.(node.attrs.uri ?? null) ?? icon}</span>
317
+ <span className="truncate">{captionOf(node.attrs.label, node.attrs.snippet)}</span>
318
+ </span>
319
+ </WithTooltip>
314
320
  </NodeViewWrapper>
315
321
  ),
316
322
  { as: 'span' },
@@ -0,0 +1,44 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * The app's own buttons on the selection strip (`actions`, LEAF-4): pressing
4
+ * one runs it with the selection in place, and the strip closes. jsdom has no
5
+ * layout, so the view's focus and coordinates are stood in for.
6
+ */
7
+ import { afterEach, expect, it, vi } from 'vitest'
8
+ import { act, cleanup, render, screen } from '@testing-library/react'
9
+ import userEvent from '@testing-library/user-event'
10
+ import { Editor } from '@tiptap/core'
11
+ import StarterKit from '@tiptap/starter-kit'
12
+ import { KeptSelection, SelectionToolbar } from './SelectionToolbar'
13
+
14
+ let editor: Editor | undefined
15
+
16
+ afterEach(() => {
17
+ cleanup()
18
+ editor?.destroy()
19
+ editor = undefined
20
+ vi.useRealTimers()
21
+ })
22
+
23
+ it('runs an action with the selection in place, and closes the strip', async () => {
24
+ vi.useFakeTimers({ shouldAdvanceTime: true })
25
+ editor = new Editor({ extensions: [StarterKit, KeptSelection], content: '<p>some words to comment on</p>' })
26
+ vi.spyOn(editor.view, 'hasFocus').mockReturnValue(true)
27
+ vi.spyOn(editor.view, 'coordsAtPos').mockReturnValue({ top: 10, bottom: 20, left: 10, right: 20 })
28
+ const seen: { from: number; to: number }[] = []
29
+ const run = vi.fn((e: Editor) => seen.push({ from: e.state.selection.from, to: e.state.selection.to }))
30
+ render(<SelectionToolbar editor={editor} actions={[{ label: 'Comment', icon: <span aria-hidden />, run }]} />)
31
+
32
+ act(() => {
33
+ editor!.commands.setTextSelection({ from: 6, to: 11 })
34
+ })
35
+ await act(async () => {
36
+ await vi.advanceTimersByTimeAsync(400)
37
+ })
38
+ const button = await screen.findByRole('button', { name: 'Comment' })
39
+ await userEvent.setup({ advanceTimers: vi.advanceTimersByTime }).click(button)
40
+
41
+ expect(run).toHaveBeenCalledTimes(1)
42
+ expect(seen).toEqual([{ from: 6, to: 11 }])
43
+ expect(screen.queryByRole('button', { name: 'Comment' })).toBeNull()
44
+ })
@@ -16,6 +16,7 @@ over text you have already selected in an editor.
16
16
  itself when to appear: once a selection has stood still, centred over it.
17
17
  - `marks` chooses the buttons, in order: `bold`, `italic`, `underline`, `code`.
18
18
  The default is the first three.
19
+ - `actions` adds the app's own buttons at the end, past a separator — Comment, in a document. Each `run`s with the selection in place, and the strip closes after it.
19
20
  - `link` adds the Link button, which turns the strip into one field for the
20
21
  address. Select words that are already a link and the field opens by itself,
21
22
  their address in it.
@@ -7,7 +7,7 @@ import { IconBold, IconCode, IconItalic, IconLink, IconUnderline } from '@tabler
7
7
  import { Form } from './Form'
8
8
  import { Popover } from './Popover'
9
9
  import { TextInput } from './TextInput'
10
- import { Toolbar, ToolbarButton } from './Toolbar'
10
+ import { Toolbar, ToolbarButton, ToolbarSeparator } from './Toolbar'
11
11
  import { shortcutLabel } from './shortcuts'
12
12
 
13
13
  /**
@@ -228,9 +228,26 @@ export interface SelectionToolbarProps {
228
228
  * nothing.
229
229
  */
230
230
  children?: ReactNode
231
+ /**
232
+ * The app's own buttons after the marks, past a separator: "Comment". Each
233
+ * runs with the selection still in place, and the strip closes after it.
234
+ */
235
+ actions?: readonly SelectionAction[]
236
+ }
237
+
238
+ /** One of the app's buttons in the strip. */
239
+ export interface SelectionAction {
240
+ /** Says what it does: the tooltip and the button's name. */
241
+ label: string
242
+ /** A 16px icon. */
243
+ icon: ReactNode
244
+ /** The keys that do the same, shown in the tooltip ("Mod-Shift-M"). */
245
+ keys?: string
246
+ /** What it does, with the selection as it is. */
247
+ run: (editor: Editor) => void
231
248
  }
232
249
 
233
- export function SelectionToolbar({ editor, marks = ['bold', 'italic', 'underline'], link = false, children }: SelectionToolbarProps) {
250
+ export function SelectionToolbar({ editor, marks = ['bold', 'italic', 'underline'], link = false, children, actions = [] }: SelectionToolbarProps) {
234
251
  /** The selection's top edge and horizontal centre; the face decides the rest. */
235
252
  const [anchor, setAnchor] = useState<{ top: number; centre: number } | null>(null)
236
253
  const [mode, setMode] = useState<Mode>('marks')
@@ -488,6 +505,23 @@ export function SelectionToolbar({ editor, marks = ['bold', 'italic', 'underline
488
505
  </ToolbarButton>
489
506
  )}
490
507
  {children}
508
+ {actions.length > 0 && <ToolbarSeparator />}
509
+ {actions.map((action) => (
510
+ <ToolbarButton
511
+ key={action.label}
512
+ aria-label={action.label}
513
+ tooltip={action.label}
514
+ tooltipShortcut={action.keys ? shortcutLabel(action.keys) : undefined}
515
+ onMouseDown={(e) => e.preventDefault()}
516
+ onClick={() => {
517
+ // Closing first: it lets go of a kept selection, which the action may want to keep.
518
+ hide()
519
+ action.run(editor)
520
+ }}
521
+ >
522
+ {action.icon}
523
+ </ToolbarButton>
524
+ ))}
491
525
  </Toolbar>
492
526
  )}
493
527
  </Popover>
package/src/editor.ts CHANGED
@@ -7,7 +7,21 @@
7
7
  * as the eslint ones are. The rest of the package it draws with — `Popover`,
8
8
  * `Toolbar`, `MenuItem`, `SuggestionMenu` — is the main entry's own, not a copy.
9
9
  */
10
- export { KeptSelection, SelectionToolbar, normalizeHref, type MarkId, type SelectionToolbarProps } from './SelectionToolbar'
10
+ export { KeptSelection, SelectionToolbar, keepSelection, releaseSelection, normalizeHref, type MarkId, type SelectionAction, type SelectionToolbarProps } from './SelectionToolbar'
11
+ export { ComposeBox, ComposeKeys, COMPOSE_EDITOR_CLASSES, type ComposeBoxProps } from './ComposeBox'
12
+ export {
13
+ CommentHighlights,
14
+ setCommentAnchors,
15
+ commentPlace,
16
+ commentElement,
17
+ anchorOfSelection,
18
+ quoteText,
19
+ type CommentAnchor,
20
+ type CommentHighlightsOptions,
21
+ type CommentPlace,
22
+ type CommentQuote,
23
+ type SelectionAnchor,
24
+ } from './CommentHighlights'
11
25
  export { PastedImages, pastedImages, pastedImageName, type PastedImagesOptions, type ClipboardSource } from './PastedImages'
12
26
  export { PastedMarkdown, type PastedMarkdownOptions } from './PastedMarkdown'
13
27
  export { CodeBlockColours } from './CodeBlockColours'
package/src/looks.ts CHANGED
@@ -67,3 +67,11 @@ export const MENU_TRIGGER_CLASSES = 'cursor-pointer rounded-md transition-colors
67
67
  * prefers-reduced-motion. CollapsibleSection's rows, and the rows under a NavItem.
68
68
  */
69
69
  export const COLLAPSIBLE_PANEL_CLASSES = 'h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] duration-150 ease-out motion-reduce:transition-none data-[starting-style]:h-0 data-[ending-style]:h-0'
70
+
71
+ /**
72
+ * The box a message or a comment is written in: Peek's writing box (its
73
+ * `looks.ts`, Katerina 24 September), moved with ComposeBox. Inset, a hairline,
74
+ * 12px in, 16px between the text and the row under it; the focus ring in Signal.
75
+ */
76
+ export const WRITING_BOX_CLASSES =
77
+ 'relative bg-bg-inset border border-border-default rounded-lg p-3 flex flex-col gap-4 signal:transition-shadow signal:focus-within:border-border-focus signal:focus-within:shadow-focus-ring'
@@ -596,7 +596,13 @@ describe('one search over the package and the apps', () => {
596
596
  root: app({ 'src/Composer.tsx': '/** Where a comment is written and sent. */\nexport function Composer() {\n return <form />\n}\n' }),
597
597
  repo: 'fixture',
598
598
  })
599
- expect(findInRegistries([packageRegistry, composer], 'comment composer')[0].entry).toMatchObject({ repo: 'fixture', name: 'Composer' })
599
+ // Since the Leaf prototype (9 October) the package has a box for writing comments, ComposeBox, and it answers first by its
600
+ // own purpose. What this guards still holds: the app's composer is found above the parts the words reach only through notes.
601
+ const found = findInRegistries([packageRegistry, composer], 'comment composer', { limit: 10 }).map((f) => `${f.entry.repo}:${f.entry.name}`)
602
+ expect(found[0]).toBe('estiva-ui:ComposeBox')
603
+ expect(found.indexOf('fixture:Composer')).toBeGreaterThan(0)
604
+ const triggers = found.indexOf('estiva-ui:ComposerTriggers')
605
+ if (triggers >= 0) expect(found.indexOf('fixture:Composer')).toBeLessThan(triggers)
600
606
  })
601
607
 
602
608
  it('says which apps hand a package part on, and how a default is imported', () => {
@@ -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', 'PastedMarkdown', 'CodeBlockColours', 'Checklist', 'Callout', 'ReferenceCards', 'LinkChips', 'ReferenceMenu']
29
+ const EDITOR_EXTENSIONS = ['KeptSelection', 'BlockId', 'UnknownBlock', 'ReferenceNode', 'AttachmentNode', 'PersonMention', 'UrgentPersonMention', 'CaptionedReference', 'ReferenceTrigger', 'SlashCommands', 'PastedImages', 'PastedMarkdown', 'CodeBlockColours', 'Checklist', 'Callout', 'ReferenceCards', 'LinkChips', 'ReferenceMenu', 'CommentHighlights', 'ComposeKeys']
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 166 names over 81 files: 115 components, 49 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; PastedMarkdown, markdown pasted as the blocks it spells (LEAF-10), on 15 October; turnIntoRows, turnListInto and turnQuoteInto, the block menu’s Turn into for a list and a quote (LEAF-11), on 15 October)', () => {
51
+ it('counts 177 names over 83 files: 118 components, 57 helpers and 2 hooks (ComposeBox with ComposeKeys and its classes, CommentHighlights with its five helpers, and keepSelection and releaseSelection, from the Leaf prototype (LEAF-4), on 16 October;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; PastedMarkdown, markdown pasted as the blocks it spells (LEAF-10), on 15 October; turnIntoRows, turnListInto and turnQuoteInto, the block menu’s Turn into for a list and a quote (LEAF-11), on 15 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: 115, helper: 49, hook: 2 })
58
- expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(81)
57
+ expect(kinds).toEqual({ component: 118, helper: 57, hook: 2 })
58
+ expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(83)
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(77)
64
- expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(89)
63
+ expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(79)
64
+ expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(98)
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', () => {
@@ -72,6 +72,8 @@ fork freely, owe nothing back, mention it on the package's ticket.
72
72
  | Bold, italic and the like over selected text in an editor | **SelectionToolbar**, from `@estiva-app/ui/editor` — it follows the selection by itself |
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
+ | The box a message, a reply or a comment is written in and sent | **ComposeBox**, from `@estiva-app/ui/editor` — the app brings its editor and adds `ComposeKeys` |
76
+ | Comments on words, a passage or whole blocks of a document, drawn where they are | **CommentHighlights**, from `@estiva-app/ui/editor` — a run of blocks and the words quoted; says which was clicked and which moved or are gone |
75
77
  | 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
78
  | Add or delete a table's rows and columns in a document being edited | **EditorTableControls**, from `@estiva-app/ui/editor`, inside the EditorBlockHandle |
77
79
  | Offer a link or a comment on one block of a document being read | **BlockHandle** — your rows only, and no Tiptap loaded |
@@ -1,102 +0,0 @@
1
- /**
2
- * Widgets 2 prototype: the placeholder things every widget on these pages
3
- * draws. Words are placeholders (stories-and-pages.md); the kinds are named,
4
- * because telling them apart is what the prototype is for.
5
- */
6
- import type { ThingKind } from '../KindIcon';
7
- /** The colours a status draws in, as the apps' manifests name them. */
8
- export type StatusColour = 'neutral' | 'blue' | 'green' | 'muted';
9
- export interface Status {
10
- label: string;
11
- colour: StatusColour;
12
- }
13
- /** One thing a link can stand for, with every field any app draws today. */
14
- export interface Thing {
15
- /** The kinds the prototype covers, one more than KindIcon has: a page. */
16
- kind: ThingKind | 'page';
17
- /** The noun a card's corner says. */
18
- noun: string;
19
- /** The app it opens in. */
20
- app: string;
21
- title: string;
22
- /** Its key, the way people say it: an issue's "WID-2". */
23
- key?: string;
24
- /** The line under the title: the project an issue is in, a page's Folder. */
25
- subtitle?: string;
26
- /** The opening of its description, a page's text, a message's words: what the apps draw today. */
27
- body?: string;
28
- /**
29
- * A short summary of the file, one or two sentences (Miky, 15 Oct: detailed
30
- * shows the summary, not the opening of the long description). No file
31
- * carries one yet; it is its own project.
32
- */
33
- summary?: string;
34
- status?: Status;
35
- assignee?: string;
36
- author?: string;
37
- /** Everyone who wrote a page. */
38
- authors?: string[];
39
- comments?: number;
40
- /**
41
- * The reader has something unread in its conversation: they are a member
42
- * and it moved since they last read it (SPEC §11.8, membership-replaces-following).
43
- */
44
- unread?: boolean;
45
- /** A project's issues, drawn as rows inside its detailed card. */
46
- children?: {
47
- title: string;
48
- key: string;
49
- status: Status;
50
- assignee?: string;
51
- }[];
52
- /** A project's progress line. */
53
- progress?: string;
54
- /** A project's progress as numbers, for the ring. */
55
- done?: {
56
- done: number;
57
- total: number;
58
- };
59
- /** A topic: how many conversations, who is in it, and its latest one. */
60
- conversations?: number;
61
- members?: string[];
62
- latest?: {
63
- author: string;
64
- body: string;
65
- at: string;
66
- };
67
- updated?: string;
68
- /** Where it is placed (WID-3): still loading, or not available to this reader. Ready when absent. */
69
- state?: 'loading' | 'unavailable';
70
- }
71
- export declare const ISSUE: Thing;
72
- export declare const PROJECT: Thing;
73
- export declare const TOPIC: Thing;
74
- export declare const MESSAGE: Thing;
75
- export declare const PAGE: Thing;
76
- /** A block of a description: drawn live, as the part it is. */
77
- export declare const PART: {
78
- heading: string;
79
- noun: string;
80
- app: string;
81
- blocks: string[];
82
- };
83
- export declare const ATTACHMENT: {
84
- name: string;
85
- size: number;
86
- contentType: string;
87
- };
88
- export declare const ATTACHMENT_FILE: {
89
- name: string;
90
- size: number;
91
- contentType: string;
92
- };
93
- /** A file the reader cannot read: only its kind and app are known, from its address. */
94
- export declare const UNREADABLE: {
95
- kind: "issue";
96
- noun: string;
97
- app: string;
98
- address: string;
99
- };
100
- /** A small picture for the attachment, drawn rather than fetched. */
101
- export declare const PICTURE: string;
102
- //# sourceMappingURL=fixtures.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../../src/widgets-2/fixtures.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C,uEAAuE;AACvE,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAA;AAEjE,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,YAAY,CAAA;CACrB;AAED,4EAA4E;AAC5E,MAAM,WAAW,KAAK;IACpB,0EAA0E;IAC1E,IAAI,EAAE,SAAS,GAAG,MAAM,CAAA;IACxB,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAA;IACZ,2BAA2B;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kGAAkG;IAClG,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,iCAAiC;IACjC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,kEAAkE;IAClE,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC9E,iCAAiC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,qDAAqD;IACrD,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACtC,yEAAyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,MAAM,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;IACrD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qGAAqG;IACrG,KAAK,CAAC,EAAE,SAAS,GAAG,aAAa,CAAA;CAClC;AAWD,eAAO,MAAM,KAAK,EAAE,KAanB,CAAA;AAED,eAAO,MAAM,OAAO,EAAE,KAiBrB,CAAA;AAED,eAAO,MAAM,KAAK,EAAE,KAUnB,CAAA;AAED,eAAO,MAAM,OAAO,EAAE,KAQrB,CAAA;AAED,eAAO,MAAM,IAAI,EAAE,KAYlB,CAAA;AAED,+DAA+D;AAC/D,eAAO,MAAM,IAAI;;;;;CAKhB,CAAA;AAED,eAAO,MAAM,UAAU;;;;CAAuE,CAAA;AAC9F,eAAO,MAAM,eAAe;;;;CAAgF,CAAA;AAE5G,wFAAwF;AACxF,eAAO,MAAM,UAAU;;;;;CAAiH,CAAA;AAExI,qEAAqE;AACrE,eAAO,MAAM,OAAO,QAIjB,CAAA"}
@@ -1,99 +0,0 @@
1
- import { type ReactNode } from 'react';
2
- import type { Thing } from './fixtures';
3
- /** A piece of a sentence: words, or a link to a file. */
4
- export type Piece = string | Thing;
5
- /** One line of text: a link alone on its line, or a sentence of words and links. */
6
- export type Line = Thing | Piece[];
7
- /** Every file the lines link to, each once. */
8
- export declare function linksIn(lines: Line[]): Thing[];
9
- /** The file whose card goes under a message: the one it links. None when it links two or more. */
10
- export declare function cardFileOf(lines: Line[]): Thing | undefined;
11
- /** A message that is nothing but one link: it shows the card, not the row. */
12
- export declare function isOnlyOneLink(lines: Line[]): boolean;
13
- /** A file's card: a linked message draws as a message, every other file as its detailed card; loading and not available as the proposed kit draws them. */
14
- export declare function CardOf({ thing }: {
15
- thing: Thing;
16
- }): import("react").JSX.Element;
17
- export interface PlacedLinesProps {
18
- lines: Line[];
19
- /** In a document: the lone-line links switched to their card, by line index. */
20
- cards?: number[];
21
- /** In a document: a click on a lone-line link, which opens its chip menu. */
22
- onLink?: (index: number) => void;
23
- /** Drawn under a line, by line index: the chip menu, open. */
24
- under?: (index: number) => ReactNode;
25
- className?: string;
26
- }
27
- /** Text whose links draw by where they sit: a chip in a sentence, a row alone on its line, or (in a document) the card chosen for that line. */
28
- export declare function PlacedLines({ lines, cards, onLink, under, className }: PlacedLinesProps): import("react").JSX.Element;
29
- export interface CardUnderProps {
30
- thing: Thing;
31
- /** The ✕ that removes it: under the message box before sending, or the author's own on a sent message. */
32
- onRemove?: () => void;
33
- /** On a sent message the ✕ shows on hover; under the box it always shows. */
34
- removeOnHover?: boolean;
35
- className?: string;
36
- }
37
- /** The one file's card under a message, and the ✕ that removes it. */
38
- export declare function CardUnder({ thing, onRemove, removeOnHover, className }: CardUnderProps): import("react").JSX.Element;
39
- export interface PlacedMessageProps {
40
- lines: Line[];
41
- /** The card was removed with ✕: before sending, or by the author afterwards. */
42
- removed?: boolean;
43
- /** Draws the ✕; without it the card shows with none, as to anyone but the author. */
44
- onRemove?: () => void;
45
- removeOnHover?: boolean;
46
- /**
47
- * An old message, sent before Widgets 2 with a `compact` tag for each of
48
- * these: no card was added to it, and each still draws as a row under it.
49
- */
50
- oldCompact?: Thing[];
51
- className?: string;
52
- }
53
- /**
54
- * A message's text, and under it the one file's card. A file with its card
55
- * under the message draws no row in it (a chip in a sentence still draws), so
56
- * a message that is only one link shows its card alone.
57
- */
58
- export declare function PlacedMessage({ lines, removed, onRemove, removeOnHover, oldCompact, className }: PlacedMessageProps): import("react").JSX.Element;
59
- /** A sent message whose author can remove its card with ✕, live: try it. */
60
- export declare function SentMessage({ lines, author, className }: {
61
- lines: Line[];
62
- author?: boolean;
63
- className?: string;
64
- }): import("react").JSX.Element;
65
- export interface ChipMenuProps {
66
- thing: Thing;
67
- /**
68
- * A lone-line link offers its card; a chip in a sentence offers Open only,
69
- * and so does a link to what has no card worth choosing (a Folder, a file
70
- * that is not a picture).
71
- */
72
- alone?: boolean;
73
- shownAs?: 'row' | 'card';
74
- onShowAs?: (as: 'row' | 'card') => void;
75
- onOpen?: () => void;
76
- }
77
- /** What a link in a document offers its writer: Open, and on a lone-line link of a kind that has one, its card in that line's place. */
78
- export declare function ChipMenu({ thing, alone, shownAs, onShowAs, onOpen }: ChipMenuProps): import("react").JSX.Element;
79
- /** A document's text with its chip menu, live: click a lone-line link, then choose Row or Card. */
80
- export declare function PlacedDocument({ lines, initialCards, className }: {
81
- lines: Line[];
82
- initialCards?: number[];
83
- className?: string;
84
- }): import("react").JSX.Element;
85
- /**
86
- * A description's and a page's `/` menu with the widget entry (Miky's QA note,
87
- * 15 Oct: "`/` section where you can use `[` to insert a widget"). Format rows
88
- * shortened to three; Insert as the app has it, plus the link.
89
- */
90
- export declare function SlashMenuWithLink({ file, table }: {
91
- file?: boolean;
92
- table?: boolean;
93
- }): import("react").JSX.Element;
94
- /** The `[` list the widget entry opens: the files to link, each led by its kind's icon. */
95
- export declare function LinkPicker({ files, query }: {
96
- files: Thing[];
97
- query?: string;
98
- }): import("react").JSX.Element;
99
- //# sourceMappingURL=places.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"places.d.ts","sourceRoot":"","sources":["../../src/widgets-2/places.tsx"],"names":[],"mappings":"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"}