@estiva-app/ui 0.72.0-widgets2.2 → 0.73.0-inline-text.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.
Files changed (55) 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/InlineText.d.ts +56 -0
  7. package/dist/InlineText.d.ts.map +1 -0
  8. package/dist/SelectionToolbar.d.ts +17 -1
  9. package/dist/SelectionToolbar.d.ts.map +1 -1
  10. package/dist/editor.d.ts +4 -1
  11. package/dist/editor.d.ts.map +1 -1
  12. package/dist/editor.js +748 -240
  13. package/dist/editor.js.map +4 -4
  14. package/dist/index.js.map +1 -1
  15. package/dist/looks.d.ts +6 -0
  16. package/dist/looks.d.ts.map +1 -1
  17. package/package.json +1 -5
  18. package/registry.json +403 -3
  19. package/src/CommentHighlights.mdx +61 -0
  20. package/src/CommentHighlights.stories.tsx +72 -0
  21. package/src/CommentHighlights.test.ts +100 -0
  22. package/src/CommentHighlights.ts +413 -0
  23. package/src/ComposeBox.mdx +58 -0
  24. package/src/ComposeBox.stories.tsx +81 -0
  25. package/src/ComposeBox.tsx +183 -0
  26. package/src/ComposerTriggers.tsx +9 -3
  27. package/src/InlineText.mdx +64 -0
  28. package/src/InlineText.stories.tsx +86 -0
  29. package/src/InlineText.test.tsx +187 -0
  30. package/src/InlineText.tsx +187 -0
  31. package/src/SelectionToolbar.actions.test.tsx +44 -0
  32. package/src/SelectionToolbar.mdx +1 -0
  33. package/src/SelectionToolbar.tsx +36 -2
  34. package/src/editor.ts +16 -1
  35. package/src/looks.ts +8 -0
  36. package/src/registry/app.test.ts +7 -1
  37. package/src/registry/registry.test.ts +6 -6
  38. package/stories/Choosing.mdx +3 -0
  39. package/dist/widgets-2/fixtures.d.ts +0 -102
  40. package/dist/widgets-2/fixtures.d.ts.map +0 -1
  41. package/dist/widgets-2/places.d.ts +0 -105
  42. package/dist/widgets-2/places.d.ts.map +0 -1
  43. package/dist/widgets-2/proposed.d.ts +0 -130
  44. package/dist/widgets-2/proposed.d.ts.map +0 -1
  45. package/dist/widgets-2.d.ts +0 -12
  46. package/dist/widgets-2.d.ts.map +0 -1
  47. package/dist/widgets-2.js +0 -585
  48. package/dist/widgets-2.js.map +0 -7
  49. package/src/widgets-2/fixtures.ts +0 -161
  50. package/src/widgets-2/places.tsx +0 -316
  51. package/src/widgets-2/proposed.tsx +0 -596
  52. package/src/widgets-2.ts +0 -11
  53. package/stories/widgets-2/ReadMe.mdx +0 -60
  54. package/stories/widgets-2/Widgets.stories.tsx +0 -504
  55. package/stories/widgets-2/today.tsx +0 -352
@@ -0,0 +1,187 @@
1
+ import { useCallback, useEffect, useImperativeHandle, useRef, useState, type Ref } from 'react'
2
+ import { Extension, Node } from '@tiptap/core'
3
+ import { history, redo, undo } from '@tiptap/pm/history'
4
+ import { EditorContent, useEditor } from '@tiptap/react'
5
+ import { cn } from './cn'
6
+
7
+ /**
8
+ * A line of plain text on a page that you edit where it stands, as in a
9
+ * document: a title, a summary. Ship's title editor (Katerina, 29 September,
10
+ * findings E1 and E2), moved here for the summary (Miky, 16 October: "it feels
11
+ * like a document, not a form") so every app's header edits the same way.
12
+ *
13
+ * - **No box**, on hover or on click, and nothing moves: the text is the
14
+ * editor, in its own size and colour. The text cursor is the only sign.
15
+ * - **The caret goes where you click**, and nothing is selected.
16
+ * - **Enter saves and moves on** — `onEnter` says where: the title to the
17
+ * summary, the summary to the description.
18
+ * - **Escape stops editing and keeps the text**, saving it, as leaving does.
19
+ * Nothing is thrown away; Ctrl+Z undoes.
20
+ * - **An unchanged value is not a write.** Leaving it empty brings the saved
21
+ * one back, unless `clearable`: then empty is saved, as nothing.
22
+ * - **A refused save keeps your text where it is**, and the app says why
23
+ * beside it; a change made elsewhere does not overwrite it until you have
24
+ * saved, changed or emptied it.
25
+ *
26
+ * One paragraph and no marks, so pasted line breaks become spaces and it wraps
27
+ * like text, which a one-line `<input>` cannot. A Tiptap editor rather than a
28
+ * raw `contenteditable`, so it is the same kind of surface as the description
29
+ * under it.
30
+ */
31
+ export interface InlineTextProps {
32
+ /** The saved text. A new value is loaded when you are not in the field and hold nothing unsaved. */
33
+ value: string
34
+ /** What it is for, muted, while it is empty: "Untitled", "Add a summary". */
35
+ placeholder: string
36
+ /** The accessible name: "Title", "Summary". */
37
+ label: string
38
+ /** Save it: resolve `true` when saved; `false` keeps your text in place. */
39
+ onCommit: (value: string) => Promise<boolean> | boolean
40
+ /** The text as it is being written, on focus and every change; `null` when you leave. For a count. */
41
+ onText?: (text: string | null) => void
42
+ /** Enter, after the save has started: where the caret goes next. */
43
+ onEnter?: () => void
44
+ /** `title`: the page's heading. `summary`: the line under it, quieter. */
45
+ variant?: 'title' | 'summary'
46
+ /** Empty is a value (a summary). Otherwise leaving it empty brings the saved one back (a title). */
47
+ clearable?: boolean
48
+ /** Text only, the same size and place: nobody can change it, or the editor is not ready. */
49
+ readOnly?: boolean
50
+ /** Placement only. */
51
+ className?: string
52
+ ref?: Ref<InlineTextHandle>
53
+ }
54
+
55
+ type CaretAt = 'start' | 'end'
56
+
57
+ export interface InlineTextHandle {
58
+ /** Put the caret in it, at the start or the end. */
59
+ focus: (at?: CaretAt) => void
60
+ }
61
+
62
+ /** Size and tone, per variant: the same for the text at rest and while it is written. */
63
+ const VARIANT = {
64
+ title: 'text-h2 text-text-primary',
65
+ summary: 'text-body-1 text-text-secondary',
66
+ } as const
67
+
68
+ /** The room around the text: the caret has a little space at the edges, and the text still lines up with what is above and below it. */
69
+ const ROOM = '-mx-2 px-2 py-1'
70
+
71
+ // The whole schema: one paragraph of text. Written with the core, so the
72
+ // package's peers stay what they were.
73
+ const OneLine = Node.create({ name: 'doc', topNode: true, content: 'paragraph' })
74
+ const Paragraph = Node.create({
75
+ name: 'paragraph',
76
+ content: 'text*',
77
+ parseHTML: () => [{ tag: 'p' }],
78
+ renderHTML: () => ['p', 0],
79
+ })
80
+ const Text = Node.create({ name: 'text' })
81
+ const Undo = Extension.create({
82
+ name: 'inlineTextUndo',
83
+ addProseMirrorPlugins: () => [history()],
84
+ addKeyboardShortcuts() {
85
+ const run = (command: typeof undo) => () => command(this.editor.state, this.editor.view.dispatch)
86
+ return { 'Mod-z': run(undo), 'Shift-Mod-z': run(redo), 'Mod-y': run(redo) }
87
+ },
88
+ })
89
+
90
+ const doc = (text: string) => ({ type: 'doc', content: [{ type: 'paragraph', content: text ? [{ type: 'text', text }] : [] }] })
91
+
92
+ /** One line: every run of white space, line breaks included, is one space. */
93
+ const oneLine = (text: string) => text.replace(/\s+/g, ' ').trim()
94
+
95
+ export function InlineText({ value, placeholder, label, onCommit, onText, onEnter, variant = 'title', clearable = false, readOnly = false, className, ref }: InlineTextProps) {
96
+ const saved = useRef(value)
97
+ // The placeholder follows the text as it is typed or deleted, not the saved value.
98
+ const [empty, setEmpty] = useState(!value)
99
+ // Read at key time: the editor's handlers are built once, and a prop they
100
+ // closed over would be the first render's.
101
+ const latest = useRef({ onEnter, onText, onCommit, clearable })
102
+ latest.current = { onEnter, onText, onCommit, clearable }
103
+
104
+ const save = useCallback(async (text: string) => {
105
+ const next = oneLine(text)
106
+ if (next === saved.current) return
107
+ if (!next && !latest.current.clearable) return
108
+ if (await latest.current.onCommit(next)) saved.current = next
109
+ }, [])
110
+
111
+ const editor = useEditor({
112
+ extensions: [OneLine, Paragraph, Text, Undo],
113
+ content: doc(value),
114
+ editable: !readOnly,
115
+ editorProps: {
116
+ attributes: {
117
+ 'aria-label': label,
118
+ role: 'textbox',
119
+ 'aria-multiline': 'false',
120
+ 'aria-placeholder': placeholder,
121
+ ...(readOnly ? { 'aria-readonly': 'true' } : {}),
122
+ class: 'outline-none',
123
+ },
124
+ handleKeyDown: (view, event) => {
125
+ if (event.key === 'Enter') {
126
+ event.preventDefault()
127
+ void save(view.state.doc.textContent)
128
+ latest.current.onEnter?.()
129
+ return true
130
+ }
131
+ if (event.key === 'Escape') {
132
+ view.dom.blur()
133
+ return true
134
+ }
135
+ return false
136
+ },
137
+ // A paste is its plain text on this one line, whatever it was copied from.
138
+ handlePaste: (view, event) => {
139
+ const text = event.clipboardData?.getData('text/plain')
140
+ if (text == null) return false
141
+ view.dispatch(view.state.tr.insertText(text.replace(/\s+/g, ' ')))
142
+ return true
143
+ },
144
+ },
145
+ onFocus: ({ editor: e }) => latest.current.onText?.(e.getText()),
146
+ onUpdate: ({ editor: e }) => {
147
+ setEmpty(e.isEmpty)
148
+ latest.current.onText?.(e.getText())
149
+ },
150
+ onBlur: ({ editor: e }) => {
151
+ latest.current.onText?.(null)
152
+ // Left empty where empty is not a value: the saved text comes back, so
153
+ // the page never shows the placeholder while everyone else sees the text.
154
+ if (e.isEmpty && saved.current && !latest.current.clearable) {
155
+ e.commands.setContent(doc(saved.current), { emitUpdate: false })
156
+ setEmpty(false)
157
+ return
158
+ }
159
+ void save(e.getText())
160
+ },
161
+ }, [readOnly])
162
+
163
+ // A change made elsewhere is loaded while you are not in the field and hold
164
+ // nothing unsaved; your own save coming back is left alone.
165
+ useEffect(() => {
166
+ if (!editor || editor.isDestroyed || value === saved.current || editor.isFocused) return
167
+ if (oneLine(editor.getText()) !== saved.current) return
168
+ editor.commands.setContent(doc(value), { emitUpdate: false })
169
+ saved.current = value
170
+ setEmpty(!value)
171
+ }, [editor, value])
172
+
173
+ useImperativeHandle(ref, () => ({
174
+ focus: (at = 'start') => editor?.commands.focus(at),
175
+ }), [editor])
176
+
177
+ return (
178
+ <div className={cn('relative', VARIANT[variant], className)}>
179
+ <EditorContent editor={editor} className={cn(ROOM, !readOnly && 'cursor-text')} />
180
+ {/* Empty — never saved, or just deleted — shows what it is for, muted,
181
+ and is not part of the text. Drawn over the editor, where the text starts. */}
182
+ {empty && !readOnly && (
183
+ <span aria-hidden className="pointer-events-none absolute left-0 top-1 text-text-muted">{placeholder}</span>
184
+ )}
185
+ </div>
186
+ )
187
+ }
@@ -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,22 @@
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 { InlineText, type InlineTextProps, type InlineTextHandle } from './InlineText'
13
+ export {
14
+ CommentHighlights,
15
+ setCommentAnchors,
16
+ commentPlace,
17
+ commentElement,
18
+ anchorOfSelection,
19
+ quoteText,
20
+ type CommentAnchor,
21
+ type CommentHighlightsOptions,
22
+ type CommentPlace,
23
+ type CommentQuote,
24
+ type SelectionAnchor,
25
+ } from './CommentHighlights'
11
26
  export { PastedImages, pastedImages, pastedImageName, type PastedImagesOptions, type ClipboardSource } from './PastedImages'
12
27
  export { PastedMarkdown, type PastedMarkdownOptions } from './PastedMarkdown'
13
28
  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 178 names over 84 files: 119 components, 57 helpers and 2 hooks (InlineText, a title or the line under it edited where it stands (SUM-4), on 16 October; 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: 119, helper: 57, hook: 2 })
58
+ expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(84)
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(80)
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,9 @@ 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
+ | A page's title or the line under it, edited where it stands, as in a document | **InlineText**, from `@estiva-app/ui/editor` — no box; Enter saves and moves on |
76
+ | 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` |
77
+ | 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
78
  | 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
79
  | Add or delete a table's rows and columns in a document being edited | **EditorTableControls**, from `@estiva-app/ui/editor`, inside the EditorBlockHandle |
77
80
  | 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,105 +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
- /**
52
- * The file whose page this message is read on, as under "Mentioned in": it
53
- * gets no card, since you are already on it; its chip and row still draw
54
- * (Miky, 15 Oct, WID-3).
55
- */
56
- here?: Thing;
57
- className?: string;
58
- }
59
- /**
60
- * A message's text, and under it the one file's card. A file with its card
61
- * under the message draws no row in it (a chip in a sentence still draws), so
62
- * a message that is only one link shows its card alone.
63
- */
64
- export declare function PlacedMessage({ lines, removed, onRemove, removeOnHover, oldCompact, here, className }: PlacedMessageProps): import("react").JSX.Element;
65
- /** A sent message whose author can remove its card with ✕, live: try it. */
66
- export declare function SentMessage({ lines, author, className }: {
67
- lines: Line[];
68
- author?: boolean;
69
- className?: string;
70
- }): import("react").JSX.Element;
71
- export interface ChipMenuProps {
72
- thing: Thing;
73
- /**
74
- * A lone-line link offers its card; a chip in a sentence offers Open only,
75
- * and so does a link to what has no card worth choosing (a Folder, a file
76
- * that is not a picture).
77
- */
78
- alone?: boolean;
79
- shownAs?: 'row' | 'card';
80
- onShowAs?: (as: 'row' | 'card') => void;
81
- onOpen?: () => void;
82
- }
83
- /** 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. */
84
- export declare function ChipMenu({ thing, alone, shownAs, onShowAs, onOpen }: ChipMenuProps): import("react").JSX.Element;
85
- /** A document's text with its chip menu, live: click a lone-line link, then choose Row or Card. */
86
- export declare function PlacedDocument({ lines, initialCards, className }: {
87
- lines: Line[];
88
- initialCards?: number[];
89
- className?: string;
90
- }): import("react").JSX.Element;
91
- /**
92
- * A description's and a page's `/` menu with the widget entry (Miky's QA note,
93
- * 15 Oct: "`/` section where you can use `[` to insert a widget"). Format rows
94
- * shortened to three; Insert as the app has it, plus the link.
95
- */
96
- export declare function SlashMenuWithLink({ file, table }: {
97
- file?: boolean;
98
- table?: boolean;
99
- }): import("react").JSX.Element;
100
- /** The `[` list the widget entry opens: the files to link, each led by its kind's icon. */
101
- export declare function LinkPicker({ files, query }: {
102
- files: Thing[];
103
- query?: string;
104
- }): import("react").JSX.Element;
105
- //# 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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"}