@estiva-app/ui 0.44.1 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/SelectionToolbar.d.ts +62 -0
- package/dist/SelectionToolbar.d.ts.map +1 -0
- package/dist/SuggestionMenu.d.ts +75 -0
- package/dist/SuggestionMenu.d.ts.map +1 -0
- package/dist/editor.d.ts +12 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +357 -0
- package/dist/editor.js.map +7 -0
- package/dist/eslint/index.js +2 -2
- package/dist/eslint/index.js.map +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +201 -99
- package/dist/index.js.map +4 -4
- package/dist/registry/{app-YMABQII6.js → app-LDGOH6UW.js} +3 -3
- package/dist/registry/{build-3EANARLQ.js → build-P6X3PTZP.js} +6 -2
- package/dist/registry/build.d.ts +4 -0
- package/dist/registry/build.d.ts.map +1 -1
- package/dist/registry/{chunk-SIB44CES.js → chunk-P55P2QFD.js} +9 -4
- package/dist/registry/chunk-P55P2QFD.js.map +7 -0
- package/dist/registry/{chunk-EVJW26UR.js → chunk-TQ6UQER3.js} +10 -10
- package/dist/registry/chunk-TQ6UQER3.js.map +7 -0
- package/dist/registry/cli.js +2 -2
- package/dist/registry/index.js +2 -2
- package/dist/shortcuts.d.ts +27 -0
- package/dist/shortcuts.d.ts.map +1 -0
- package/dist/suggestionPopup.d.ts +41 -0
- package/dist/suggestionPopup.d.ts.map +1 -0
- package/package.json +27 -2
- package/registry.json +341 -3
- package/src/SelectionToolbar.mdx +78 -0
- package/src/SelectionToolbar.stories.tsx +63 -0
- package/src/SelectionToolbar.test.ts +105 -0
- package/src/SelectionToolbar.tsx +487 -0
- package/src/SuggestionMenu.mdx +107 -0
- package/src/SuggestionMenu.stories.tsx +202 -0
- package/src/SuggestionMenu.test.tsx +128 -0
- package/src/SuggestionMenu.tsx +189 -0
- package/src/editor.ts +11 -0
- package/src/eslint/no-rebuilt-behaviour.test.ts +1 -1
- package/src/eslint/no-rebuilt-behaviour.ts +2 -2
- package/src/how.test.ts +1 -1
- package/src/index.ts +2 -0
- package/src/keptSelection.test.ts +67 -0
- package/src/registry/app.ts +8 -8
- package/src/registry/build.ts +10 -2
- package/src/registry/registry.test.ts +14 -9
- package/src/shortcuts.ts +40 -0
- package/src/suggestionPopup.tsx +119 -0
- package/stories/Choosing.mdx +2 -0
- package/dist/registry/chunk-EVJW26UR.js.map +0 -7
- package/dist/registry/chunk-SIB44CES.js.map +0 -7
- /package/dist/registry/{app-YMABQII6.js.map → app-LDGOH6UW.js.map} +0 -0
- /package/dist/registry/{build-3EANARLQ.js.map → build-P6X3PTZP.js.map} +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Editor } from '@tiptap/react';
|
|
2
|
+
import { Extension } from '@tiptap/core';
|
|
3
|
+
import { PluginKey } from '@tiptap/pm/state';
|
|
4
|
+
/**
|
|
5
|
+
* The selection, kept visible while something else has the focus — Peek's
|
|
6
|
+
* `src/extensions/keptSelection.ts` (PEE-19), moved into the package with the
|
|
7
|
+
* `SelectionToolbar` that needs it (UIG-31, Katerina's ruling 29 September).
|
|
8
|
+
*
|
|
9
|
+
* The toolbar's address field takes the focus, and with it the document's
|
|
10
|
+
* one selection, so the words being linked lost their highlight the moment
|
|
11
|
+
* the field opened (measured 2026-09-07). Linear keeps it, and Katerina asked
|
|
12
|
+
* for the same. The editor's own selection never moved — ProseMirror keeps it
|
|
13
|
+
* while unfocused — so this only *draws* it: an inline decoration over the
|
|
14
|
+
* range, in `bg-selected` — the token Peek's composer paints `::selection` in,
|
|
15
|
+
* mapped through edits and cleared when the field closes.
|
|
16
|
+
*
|
|
17
|
+
* A decoration rather than a second selection, because a document has one
|
|
18
|
+
* and the field needs it. Editor-internal by design (the editor owns its own
|
|
19
|
+
* drawing); nothing here floats, focuses or handles a key.
|
|
20
|
+
*/
|
|
21
|
+
export interface KeptRange {
|
|
22
|
+
from: number;
|
|
23
|
+
to: number;
|
|
24
|
+
}
|
|
25
|
+
/** The plugin's key: read what is being kept, or dispatch a change to it. */
|
|
26
|
+
export declare const keptSelectionKey: PluginKey<KeptRange | null>;
|
|
27
|
+
/** The extension an editor registers so a selection can stay drawn while a field has the focus. */
|
|
28
|
+
export declare const KeptSelection: Extension<any, any>;
|
|
29
|
+
/** Draw the selection as it is now, until `releaseSelection`. */
|
|
30
|
+
export declare function keepSelection(editor: Editor): void;
|
|
31
|
+
/** Stop drawing it. Nothing kept, nothing dispatched. */
|
|
32
|
+
export declare function releaseSelection(editor: Editor): void;
|
|
33
|
+
/** What is being drawn, or null. */
|
|
34
|
+
export declare function keptSelection(editor: Editor): KeptRange | null;
|
|
35
|
+
/** A mark the strip can toggle — the suite's inline vocabulary (SPEC §13.1) less link and reference. */
|
|
36
|
+
export type MarkId = 'bold' | 'italic' | 'underline' | 'code';
|
|
37
|
+
/** The address the selection sits in, or '' — what the link field opens with. */
|
|
38
|
+
export declare function currentLink(editor: Editor): string;
|
|
39
|
+
/**
|
|
40
|
+
* What somebody typed into the link field → an `href`, or null when it cannot
|
|
41
|
+
* be one. A bare host (`google.com`) gets `https://`. A scheme is kept only when
|
|
42
|
+
* SPEC §13.1 lets a reader follow it (http, https, mailto); `javascript:` and
|
|
43
|
+
* the like are refused here. `localhost:5173` is a host and a port: a scheme is
|
|
44
|
+
* never followed by a digit.
|
|
45
|
+
*/
|
|
46
|
+
export declare function normalizeHref(raw: string): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Apply the link field to the selection. Empty removes the link; an address
|
|
49
|
+
* sets it, over the whole link when the caret sits inside one. `false` means
|
|
50
|
+
* the text could not be an address, and the field should stay open.
|
|
51
|
+
*/
|
|
52
|
+
export declare function applyLink(editor: Editor, raw: string): boolean;
|
|
53
|
+
export interface SelectionToolbarProps {
|
|
54
|
+
/** The editor whose selection the strip follows. `null` while it loads. */
|
|
55
|
+
editor: Editor | null;
|
|
56
|
+
/** Which marks the strip toggles, in order. Default bold, italic, underline. */
|
|
57
|
+
marks?: readonly MarkId[];
|
|
58
|
+
/** The Link button and its address field. Needs a link mark in the editor and `KeptSelection`. Default off. */
|
|
59
|
+
link?: boolean;
|
|
60
|
+
}
|
|
61
|
+
export declare function SelectionToolbar({ editor, marks, link }: SelectionToolbarProps): import("react").JSX.Element | null;
|
|
62
|
+
//# sourceMappingURL=SelectionToolbar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SelectionToolbar.d.ts","sourceRoot":"","sources":["../src/SelectionToolbar.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAU,SAAS,EAAE,MAAM,kBAAkB,CAAA;AASpD;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;CACX;AAED,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,6BAAmD,CAAA;AAEhF,mGAAmG;AACnG,eAAO,MAAM,aAAa,qBA8BxB,CAAA;AAEF,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,QAG3C;AAED,yDAAyD;AACzD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,QAG9C;AAED,oCAAoC;AACpC,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAE9D;AAED,wGAAwG;AACxG,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAA;AAkB7D,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGlD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMxD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAQ9D;AA2ED,MAAM,WAAW,qBAAqB;IACpC,2EAA2E;IAC3E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,gFAAgF;IAChF,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB,+GAA+G;IAC/G,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,KAAuC,EAAE,IAAY,EAAE,EAAE,qBAAqB,sCAqQxH"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type ReactNode, type Ref } from 'react';
|
|
2
|
+
import { type MenuItemProps } from './Menu';
|
|
3
|
+
/**
|
|
4
|
+
* The list a text editor opens as you type — `/` for commands, `@` for people,
|
|
5
|
+
* `[` for a reference. UIG-31: Peek's four type-ahead menus, made one part.
|
|
6
|
+
*
|
|
7
|
+
* **Why it is not a `Combobox` or a `Menu`.** Base UI's lists need a text field
|
|
8
|
+
* of their own, and the text being completed is an editor, not an `<input>`.
|
|
9
|
+
* So focus never leaves the text: the editor hands its arrow keys and Enter to
|
|
10
|
+
* this list through `handle`, and the list tells the editor which row is
|
|
11
|
+
* active (`aria-activedescendant`), the pattern Base UI's `Combobox` builds.
|
|
12
|
+
* If Base UI ever adds a list that can be driven from outside its own field,
|
|
13
|
+
* this part moves onto it.
|
|
14
|
+
*
|
|
15
|
+
* **What it owns**, all of it written once here instead of in every menu:
|
|
16
|
+
*
|
|
17
|
+
* - the box: the package `Popover`, anchored to the caret, above it and below
|
|
18
|
+
* when there is no room, clamped to the window, scrolling in the package's
|
|
19
|
+
* `ScrollArea` past `maxHeight`;
|
|
20
|
+
* - the highlighted row: ↑ and ↓ (wrapping at the ends), Enter to choose, the
|
|
21
|
+
* pointer moving the highlight, a press choosing without taking focus from
|
|
22
|
+
* the text, the highlight back on the first row whenever the list changes,
|
|
23
|
+
* and the highlighted row scrolled into view;
|
|
24
|
+
* - what a screen reader is told: the list is a `listbox`, each row an
|
|
25
|
+
* `option`, and the editor says it completes into this list and which row
|
|
26
|
+
* is active — all removed again when the list closes.
|
|
27
|
+
*
|
|
28
|
+
* What stays with the app: which rows exist, what choosing one does, and what
|
|
29
|
+
* each row shows (its label, face, keys, hint) — `row` returns the package
|
|
30
|
+
* `MenuItem`'s own props. With nothing to show there is no box at all.
|
|
31
|
+
*/
|
|
32
|
+
export interface SuggestionMenuRow {
|
|
33
|
+
label: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
leading?: ReactNode;
|
|
36
|
+
/** What you would type — drawn as keys at the right, e.g. `#` for a heading. */
|
|
37
|
+
shortcut?: string;
|
|
38
|
+
/** Drawn at the right while the row is highlighted, e.g. `<EnterHint />`. */
|
|
39
|
+
hint?: ReactNode;
|
|
40
|
+
/** `tall` for a row with a face and a second line — `MenuItem`'s own sizes. */
|
|
41
|
+
size?: MenuItemProps['size'];
|
|
42
|
+
}
|
|
43
|
+
export interface SuggestionMenuSection<T> {
|
|
44
|
+
/** The section's heading. Without one, the rows stand alone. */
|
|
45
|
+
label?: string;
|
|
46
|
+
items: readonly T[];
|
|
47
|
+
/** On the heading row, as `MenuSection`'s. */
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
/** What the editor calls while the list is open. */
|
|
51
|
+
export interface SuggestionMenuHandle {
|
|
52
|
+
/** ↑, ↓ and Enter. `true` when the list used the key, so the editor must not. */
|
|
53
|
+
onKeyDown: (event: KeyboardEvent) => boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface SuggestionMenuProps<T> {
|
|
56
|
+
/** The caret's rect, from the editor. `null` closes the box. */
|
|
57
|
+
rect: DOMRect | null;
|
|
58
|
+
/** The editor's own element — told which row is active. Absent in a story. */
|
|
59
|
+
editorElement?: HTMLElement | null;
|
|
60
|
+
/** Names the list — "Commands", "People". */
|
|
61
|
+
ariaLabel: string;
|
|
62
|
+
/** The box's width, e.g. `w-[300px]`. */
|
|
63
|
+
width: string;
|
|
64
|
+
/** The cap before the rows scroll, e.g. `max-h-[400px]`. */
|
|
65
|
+
maxHeight: string;
|
|
66
|
+
sections: readonly SuggestionMenuSection<T>[];
|
|
67
|
+
/** A stable key for a row. */
|
|
68
|
+
itemKey: (item: T) => string;
|
|
69
|
+
/** What a row shows. */
|
|
70
|
+
row: (item: T) => SuggestionMenuRow;
|
|
71
|
+
onSelect: (item: T) => void;
|
|
72
|
+
handle?: Ref<SuggestionMenuHandle>;
|
|
73
|
+
}
|
|
74
|
+
export declare function SuggestionMenu<T>({ rect, editorElement, ariaLabel, width, maxHeight, sections, itemKey, row, onSelect, handle }: SuggestionMenuProps<T>): import("react").JSX.Element | null;
|
|
75
|
+
//# sourceMappingURL=SuggestionMenu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SuggestionMenu.d.ts","sourceRoot":"","sources":["../src/SuggestionMenu.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAyE,KAAK,SAAS,EAAE,KAAK,GAAG,EAAE,MAAM,OAAO,CAAA;AACvH,OAAO,EAAwC,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAA;AAGjF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CAC7B;AAED,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,SAAS,CAAC,EAAE,CAAA;IACnB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,oDAAoD;AACpD,MAAM,WAAW,oBAAoB;IACnC,iFAAiF;IACjF,SAAS,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,OAAO,CAAA;CAC7C;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,gEAAgE;IAChE,IAAI,EAAE,OAAO,GAAG,IAAI,CAAA;IACpB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,WAAW,GAAG,IAAI,CAAA;IAClC,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAA;IACjB,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,SAAS,qBAAqB,CAAC,CAAC,CAAC,EAAE,CAAA;IAC7C,8BAA8B;IAC9B,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAA;IAC5B,wBAAwB;IACxB,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,iBAAiB,CAAA;IACnC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAA;IAC3B,MAAM,CAAC,EAAE,GAAG,CAAC,oBAAoB,CAAC,CAAA;CACnC;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,mBAAmB,CAAC,CAAC,CAAC,sCA6GvJ"}
|
package/dist/editor.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@estiva-app/ui/editor` — the parts that work inside a Tiptap editor (UIG-31,
|
|
3
|
+
* Katerina's ruling 29 September: one editor corner in the package, option A).
|
|
4
|
+
*
|
|
5
|
+
* Its own entry because it imports Tiptap, which only an app with an editor
|
|
6
|
+
* installs: the main entry stays free of it, and the Tiptap peers are optional,
|
|
7
|
+
* as the eslint ones are. The rest of the package it draws with — `Popover`,
|
|
8
|
+
* `Toolbar`, `MenuItem`, `SuggestionMenu` — is the main entry's own, not a copy.
|
|
9
|
+
*/
|
|
10
|
+
export { KeptSelection, SelectionToolbar, normalizeHref, type MarkId, type SelectionToolbarProps } from './SelectionToolbar';
|
|
11
|
+
export { suggestionPopup, isSuggestionActive, isSuggestionOpen, type SuggestionPopupOptions } from './suggestionPopup';
|
|
12
|
+
//# sourceMappingURL=editor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,KAAK,MAAM,EAAE,KAAK,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAC5H,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,KAAK,sBAAsB,EAAE,MAAM,mBAAmB,CAAA"}
|
package/dist/editor.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// src/SelectionToolbar.tsx
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Extension } from "@tiptap/core";
|
|
4
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
5
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
6
|
+
import { IconBold, IconCode, IconItalic, IconLink, IconUnderline } from "@tabler/icons-react";
|
|
7
|
+
import { Form } from "@estiva-app/ui";
|
|
8
|
+
import { Popover } from "@estiva-app/ui";
|
|
9
|
+
import { TextInput } from "@estiva-app/ui";
|
|
10
|
+
import { Toolbar, ToolbarButton } from "@estiva-app/ui";
|
|
11
|
+
import { shortcutLabel } from "@estiva-app/ui";
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
var keptSelectionKey = new PluginKey("keptSelection");
|
|
14
|
+
var KeptSelection = Extension.create({
|
|
15
|
+
name: "keptSelection",
|
|
16
|
+
addProseMirrorPlugins() {
|
|
17
|
+
return [
|
|
18
|
+
new Plugin({
|
|
19
|
+
key: keptSelectionKey,
|
|
20
|
+
state: {
|
|
21
|
+
init: () => null,
|
|
22
|
+
apply(tr, previous) {
|
|
23
|
+
const meta = tr.getMeta(keptSelectionKey);
|
|
24
|
+
if (meta !== void 0) return meta;
|
|
25
|
+
if (!previous) return null;
|
|
26
|
+
return { from: tr.mapping.map(previous.from), to: tr.mapping.map(previous.to) };
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
props: {
|
|
30
|
+
decorations(state) {
|
|
31
|
+
const range = keptSelectionKey.getState(state);
|
|
32
|
+
if (!range || range.from === range.to) return null;
|
|
33
|
+
return DecorationSet.create(state.doc, [
|
|
34
|
+
Decoration.inline(range.from, range.to, { class: "kept-selection bg-bg-selected" })
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
function keepSelection(editor) {
|
|
43
|
+
const { from, to } = editor.state.selection;
|
|
44
|
+
editor.view.dispatch(editor.state.tr.setMeta(keptSelectionKey, { from, to }));
|
|
45
|
+
}
|
|
46
|
+
function releaseSelection(editor) {
|
|
47
|
+
if (editor.isDestroyed || !keptSelectionKey.getState(editor.state)) return;
|
|
48
|
+
editor.view.dispatch(editor.state.tr.setMeta(keptSelectionKey, null));
|
|
49
|
+
}
|
|
50
|
+
var MARKS = {
|
|
51
|
+
bold: { label: "Bold", keys: "Mod-B" },
|
|
52
|
+
italic: { label: "Italic", keys: "Mod-I" },
|
|
53
|
+
underline: { label: "Underline", keys: "Mod-U" },
|
|
54
|
+
code: { label: "Code", keys: "Mod-E" }
|
|
55
|
+
};
|
|
56
|
+
function toggleMark(editor, id) {
|
|
57
|
+
return editor.chain().focus().toggleMark(id).run();
|
|
58
|
+
}
|
|
59
|
+
function currentLink(editor) {
|
|
60
|
+
const href = editor.getAttributes("link").href;
|
|
61
|
+
return typeof href === "string" ? href : "";
|
|
62
|
+
}
|
|
63
|
+
function normalizeHref(raw) {
|
|
64
|
+
const s = raw.trim();
|
|
65
|
+
if (!s || /\s/.test(s)) return null;
|
|
66
|
+
if (/^(https?:|mailto:)/i.test(s)) return s;
|
|
67
|
+
if (/^[a-z][a-z0-9+.-]*:(?!\d)/i.test(s)) return null;
|
|
68
|
+
return `https://${s}`;
|
|
69
|
+
}
|
|
70
|
+
function applyLink(editor, raw) {
|
|
71
|
+
const chain = editor.chain().focus().extendMarkRange("link");
|
|
72
|
+
if (!raw.trim()) return chain.unsetMark("link", { extendEmptyMarkRange: true }).setMeta("preventAutolink", true).run();
|
|
73
|
+
const href = normalizeHref(raw);
|
|
74
|
+
if (!href) return false;
|
|
75
|
+
return chain.setMark("link", { href }).setMeta("preventAutolink", true).run();
|
|
76
|
+
}
|
|
77
|
+
var ICONS = {
|
|
78
|
+
bold: IconBold,
|
|
79
|
+
italic: IconItalic,
|
|
80
|
+
underline: IconUnderline,
|
|
81
|
+
code: IconCode
|
|
82
|
+
};
|
|
83
|
+
var SETTLE_MS = 250;
|
|
84
|
+
var GAP = 8;
|
|
85
|
+
var POPOVER_GAP = 4;
|
|
86
|
+
function SelectionToolbar({ editor, marks = ["bold", "italic", "underline"], link = false }) {
|
|
87
|
+
const [anchor, setAnchor] = useState(null);
|
|
88
|
+
const [mode, setMode] = useState("marks");
|
|
89
|
+
const [url, setUrl] = useState("");
|
|
90
|
+
const [, setTick] = useState(0);
|
|
91
|
+
const timer = useRef(void 0);
|
|
92
|
+
const pointerDown = useRef(false);
|
|
93
|
+
const field = useRef(null);
|
|
94
|
+
const draft = useRef(null);
|
|
95
|
+
const modeRef = useRef("marks");
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
modeRef.current = mode;
|
|
98
|
+
}, [mode]);
|
|
99
|
+
const linkOn = useRef(link);
|
|
100
|
+
linkOn.current = link;
|
|
101
|
+
const hide = useCallback(() => {
|
|
102
|
+
clearTimeout(timer.current);
|
|
103
|
+
draft.current = null;
|
|
104
|
+
if (editor) releaseSelection(editor);
|
|
105
|
+
setAnchor(null);
|
|
106
|
+
setMode("marks");
|
|
107
|
+
}, [editor]);
|
|
108
|
+
const commit = useCallback(() => {
|
|
109
|
+
const text = draft.current;
|
|
110
|
+
if (text === null || !editor) return;
|
|
111
|
+
draft.current = null;
|
|
112
|
+
if (text.trim() && applyLink(editor, text)) {
|
|
113
|
+
editor.commands.setTextSelection(editor.state.selection.to);
|
|
114
|
+
}
|
|
115
|
+
hide();
|
|
116
|
+
}, [editor, hide]);
|
|
117
|
+
const settle = useCallback(() => {
|
|
118
|
+
if (!editor || editor.isDestroyed) return;
|
|
119
|
+
if (modeRef.current === "link") return;
|
|
120
|
+
const { state, view } = editor;
|
|
121
|
+
const { empty, from, to } = state.selection;
|
|
122
|
+
if (empty || from === to || !view.hasFocus() || pointerDown.current) {
|
|
123
|
+
setAnchor(null);
|
|
124
|
+
setMode("marks");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const start = view.coordsAtPos(from);
|
|
128
|
+
const end = view.coordsAtPos(to);
|
|
129
|
+
const centre = (Math.min(start.left, end.left) + Math.max(start.right, end.right)) / 2;
|
|
130
|
+
const inLink = linkOn.current && editor.isActive("link");
|
|
131
|
+
const address = inLink ? currentLink(editor) : "";
|
|
132
|
+
draft.current = inLink ? address : null;
|
|
133
|
+
if (inLink) keepSelection(editor);
|
|
134
|
+
setMode(inLink ? "link" : "marks");
|
|
135
|
+
setUrl(address);
|
|
136
|
+
setAnchor({ top: start.top, centre });
|
|
137
|
+
}, [editor]);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!editor) return;
|
|
140
|
+
const schedule = () => {
|
|
141
|
+
if (modeRef.current === "link") return;
|
|
142
|
+
setTick((t) => t + 1);
|
|
143
|
+
clearTimeout(timer.current);
|
|
144
|
+
if (editor.state.selection.empty) {
|
|
145
|
+
setAnchor(null);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
timer.current = setTimeout(settle, SETTLE_MS);
|
|
149
|
+
};
|
|
150
|
+
const onBlur = () => {
|
|
151
|
+
if (modeRef.current !== "link") hide();
|
|
152
|
+
};
|
|
153
|
+
const startsSelection = (e) => e.target instanceof Node && editor.view.dom.contains(e.target);
|
|
154
|
+
const onPointerDown = (e) => {
|
|
155
|
+
if (!startsSelection(e)) return;
|
|
156
|
+
commit();
|
|
157
|
+
pointerDown.current = true;
|
|
158
|
+
hide();
|
|
159
|
+
};
|
|
160
|
+
const onPointerUp = () => {
|
|
161
|
+
if (!pointerDown.current) return;
|
|
162
|
+
pointerDown.current = false;
|
|
163
|
+
schedule();
|
|
164
|
+
};
|
|
165
|
+
editor.on("selectionUpdate", schedule);
|
|
166
|
+
editor.on("transaction", schedule);
|
|
167
|
+
editor.on("blur", onBlur);
|
|
168
|
+
document.addEventListener("mousedown", onPointerDown, true);
|
|
169
|
+
document.addEventListener("mouseup", onPointerUp, true);
|
|
170
|
+
return () => {
|
|
171
|
+
clearTimeout(timer.current);
|
|
172
|
+
editor.off("selectionUpdate", schedule);
|
|
173
|
+
editor.off("transaction", schedule);
|
|
174
|
+
editor.off("blur", onBlur);
|
|
175
|
+
document.removeEventListener("mousedown", onPointerDown, true);
|
|
176
|
+
document.removeEventListener("mouseup", onPointerUp, true);
|
|
177
|
+
};
|
|
178
|
+
}, [editor, settle, hide, commit]);
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (mode !== "link" || !anchor) return;
|
|
181
|
+
const frame = requestAnimationFrame(() => {
|
|
182
|
+
field.current?.focus();
|
|
183
|
+
field.current?.select();
|
|
184
|
+
});
|
|
185
|
+
return () => cancelAnimationFrame(frame);
|
|
186
|
+
}, [mode, anchor]);
|
|
187
|
+
if (!editor || !anchor) return null;
|
|
188
|
+
const openField = () => {
|
|
189
|
+
const address = currentLink(editor);
|
|
190
|
+
draft.current = address;
|
|
191
|
+
keepSelection(editor);
|
|
192
|
+
setUrl(address);
|
|
193
|
+
setMode("link");
|
|
194
|
+
};
|
|
195
|
+
const submit = () => {
|
|
196
|
+
if (!url.trim()) {
|
|
197
|
+
applyLink(editor, "");
|
|
198
|
+
hide();
|
|
199
|
+
editor.commands.focus();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
commit();
|
|
203
|
+
};
|
|
204
|
+
const discard = () => {
|
|
205
|
+
hide();
|
|
206
|
+
editor.commands.focus();
|
|
207
|
+
};
|
|
208
|
+
const anchorRect = new DOMRect(anchor.centre, anchor.top - (GAP - POPOVER_GAP), 0, 0);
|
|
209
|
+
return /* @__PURE__ */ jsx(
|
|
210
|
+
Popover,
|
|
211
|
+
{
|
|
212
|
+
anchor: anchorRect,
|
|
213
|
+
open: true,
|
|
214
|
+
side: "top",
|
|
215
|
+
align: "center",
|
|
216
|
+
onOpenChange: (next) => {
|
|
217
|
+
if (!next) commit();
|
|
218
|
+
},
|
|
219
|
+
ariaLabel: "Text formatting",
|
|
220
|
+
className: "w-auto min-w-0",
|
|
221
|
+
contentClassName: "p-1",
|
|
222
|
+
children: mode === "link" ? (
|
|
223
|
+
/* One field, so no strip: a toolbar of one control is a toolbar in
|
|
224
|
+
name only, and `ToolbarInput` cannot take the ref this field is
|
|
225
|
+
focused by (ADOPTION B25). */
|
|
226
|
+
// The package Form (UIG-7): Enter applies the link. `free` and `flex`: one field, which keeps its own height.
|
|
227
|
+
/* @__PURE__ */ jsx(Form, { onSubmit: submit, layout: "free", className: "flex", children: /* @__PURE__ */ jsx(
|
|
228
|
+
TextInput,
|
|
229
|
+
{
|
|
230
|
+
ref: field,
|
|
231
|
+
value: url,
|
|
232
|
+
onChange: (e) => {
|
|
233
|
+
setUrl(e.target.value);
|
|
234
|
+
draft.current = e.target.value;
|
|
235
|
+
},
|
|
236
|
+
onKeyDown: (e) => {
|
|
237
|
+
if (e.key === "Escape") {
|
|
238
|
+
e.stopPropagation();
|
|
239
|
+
discard();
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
placeholder: "Enter link URL",
|
|
243
|
+
"aria-label": "Link URL",
|
|
244
|
+
className: "w-[264px]"
|
|
245
|
+
}
|
|
246
|
+
) })
|
|
247
|
+
) : (
|
|
248
|
+
/* Four marks and a link button: one Tab stop, ← → along them (D29). */
|
|
249
|
+
/* @__PURE__ */ jsxs(Toolbar, { "aria-label": "Text formatting", surface: false, children: [
|
|
250
|
+
marks.map((id) => {
|
|
251
|
+
const Icon = ICONS[id];
|
|
252
|
+
const active = editor.isActive(id);
|
|
253
|
+
return /* @__PURE__ */ jsx(
|
|
254
|
+
ToolbarButton,
|
|
255
|
+
{
|
|
256
|
+
"aria-label": MARKS[id].label,
|
|
257
|
+
pressed: active,
|
|
258
|
+
tooltip: MARKS[id].label,
|
|
259
|
+
tooltipShortcut: shortcutLabel(MARKS[id].keys),
|
|
260
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
261
|
+
onClick: () => toggleMark(editor, id),
|
|
262
|
+
children: /* @__PURE__ */ jsx(Icon, { size: 16, stroke: 1.5 })
|
|
263
|
+
},
|
|
264
|
+
id
|
|
265
|
+
);
|
|
266
|
+
}),
|
|
267
|
+
link && /* @__PURE__ */ jsx(
|
|
268
|
+
ToolbarButton,
|
|
269
|
+
{
|
|
270
|
+
"aria-label": "Link",
|
|
271
|
+
tooltip: "Link",
|
|
272
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
273
|
+
onClick: openField,
|
|
274
|
+
children: /* @__PURE__ */ jsx(IconLink, { size: 16, stroke: 1.5 })
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
] })
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/suggestionPopup.tsx
|
|
284
|
+
import { forwardRef, useRef as useRef2, useImperativeHandle } from "react";
|
|
285
|
+
import { ReactRenderer } from "@tiptap/react";
|
|
286
|
+
import { SuggestionMenu } from "@estiva-app/ui";
|
|
287
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
288
|
+
var openCount = 0;
|
|
289
|
+
var lastClose = 0;
|
|
290
|
+
function isSuggestionActive() {
|
|
291
|
+
return openCount > 0 || Date.now() - lastClose < 100;
|
|
292
|
+
}
|
|
293
|
+
function isSuggestionOpen() {
|
|
294
|
+
return openCount > 0;
|
|
295
|
+
}
|
|
296
|
+
var Popup = forwardRef(function Popup2({ options, ...props }, handle) {
|
|
297
|
+
const menu = useRef2(null);
|
|
298
|
+
useImperativeHandle(handle, () => ({ onKeyDown: (event) => menu.current?.onKeyDown(event) ?? false }));
|
|
299
|
+
const sections = options.sections ? options.sections(props.items, props.query) : [{ items: props.items }];
|
|
300
|
+
return /* @__PURE__ */ jsx2(
|
|
301
|
+
SuggestionMenu,
|
|
302
|
+
{
|
|
303
|
+
handle: menu,
|
|
304
|
+
rect: props.clientRect?.() ?? null,
|
|
305
|
+
editorElement: props.editor.view.dom,
|
|
306
|
+
ariaLabel: options.ariaLabel,
|
|
307
|
+
width: options.width,
|
|
308
|
+
maxHeight: options.maxHeight,
|
|
309
|
+
sections,
|
|
310
|
+
itemKey: options.itemKey,
|
|
311
|
+
row: options.row,
|
|
312
|
+
onSelect: (item) => props.command(item)
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
function suggestionPopup(options) {
|
|
317
|
+
return () => {
|
|
318
|
+
let component = null;
|
|
319
|
+
let container = null;
|
|
320
|
+
const exit = () => {
|
|
321
|
+
if (!component) return;
|
|
322
|
+
openCount = Math.max(0, openCount - 1);
|
|
323
|
+
lastClose = Date.now();
|
|
324
|
+
component.destroy();
|
|
325
|
+
container?.remove();
|
|
326
|
+
component = null;
|
|
327
|
+
container = null;
|
|
328
|
+
};
|
|
329
|
+
return {
|
|
330
|
+
onStart: (props) => {
|
|
331
|
+
openCount++;
|
|
332
|
+
component = new ReactRenderer(Popup, { props: { ...props, options }, editor: props.editor });
|
|
333
|
+
container = document.createElement("div");
|
|
334
|
+
document.body.appendChild(container);
|
|
335
|
+
container.appendChild(component.element);
|
|
336
|
+
},
|
|
337
|
+
onUpdate: (props) => component?.updateProps({ ...props, options }),
|
|
338
|
+
onKeyDown: ({ event }) => {
|
|
339
|
+
if (event.key === "Escape") {
|
|
340
|
+
exit();
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
return component?.ref?.onKeyDown(event) ?? false;
|
|
344
|
+
},
|
|
345
|
+
onExit: exit
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
export {
|
|
350
|
+
KeptSelection,
|
|
351
|
+
SelectionToolbar,
|
|
352
|
+
isSuggestionActive,
|
|
353
|
+
isSuggestionOpen,
|
|
354
|
+
normalizeHref,
|
|
355
|
+
suggestionPopup
|
|
356
|
+
};
|
|
357
|
+
//# sourceMappingURL=editor.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/SelectionToolbar.tsx", "../src/suggestionPopup.tsx"],
|
|
4
|
+
"sourcesContent": ["import { useCallback, useEffect, useRef, useState } from 'react'\nimport type { Editor } from '@tiptap/react'\nimport { Extension } from '@tiptap/core'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\nimport { IconBold, IconCode, IconItalic, IconLink, IconUnderline } from '@tabler/icons-react'\nimport { Form } from './Form'\nimport { Popover } from './Popover'\nimport { TextInput } from './TextInput'\nimport { Toolbar, ToolbarButton } from './Toolbar'\nimport { shortcutLabel } from './shortcuts'\n\n/**\n * The selection, kept visible while something else has the focus \u2014 Peek's\n * `src/extensions/keptSelection.ts` (PEE-19), moved into the package with the\n * `SelectionToolbar` that needs it (UIG-31, Katerina's ruling 29 September).\n *\n * The toolbar's address field takes the focus, and with it the document's\n * one selection, so the words being linked lost their highlight the moment\n * the field opened (measured 2026-09-07). Linear keeps it, and Katerina asked\n * for the same. The editor's own selection never moved \u2014 ProseMirror keeps it\n * while unfocused \u2014 so this only *draws* it: an inline decoration over the\n * range, in `bg-selected` \u2014 the token Peek's composer paints `::selection` in,\n * mapped through edits and cleared when the field closes.\n *\n * A decoration rather than a second selection, because a document has one\n * and the field needs it. Editor-internal by design (the editor owns its own\n * drawing); nothing here floats, focuses or handles a key.\n */\n\nexport interface KeptRange {\n from: number\n to: number\n}\n\n/** The plugin's key: read what is being kept, or dispatch a change to it. */\nexport const keptSelectionKey = new PluginKey<KeptRange | null>('keptSelection')\n\n/** The extension an editor registers so a selection can stay drawn while a field has the focus. */\nexport const KeptSelection = Extension.create({\n name: 'keptSelection',\n\n addProseMirrorPlugins() {\n return [\n new Plugin<KeptRange | null>({\n key: keptSelectionKey,\n state: {\n init: () => null,\n apply(tr, previous) {\n const meta = tr.getMeta(keptSelectionKey) as KeptRange | null | undefined\n if (meta !== undefined) return meta\n if (!previous) return null\n // Follow the words through an edit, so applying the link to them\n // (or typing before them) does not leave the highlight behind.\n return { from: tr.mapping.map(previous.from), to: tr.mapping.map(previous.to) }\n },\n },\n props: {\n decorations(state) {\n const range = keptSelectionKey.getState(state)\n if (!range || range.from === range.to) return null\n return DecorationSet.create(state.doc, [\n Decoration.inline(range.from, range.to, { class: 'kept-selection bg-bg-selected' }),\n ])\n },\n },\n }),\n ]\n },\n})\n\n/** Draw the selection as it is now, until `releaseSelection`. */\nexport function keepSelection(editor: Editor) {\n const { from, to } = editor.state.selection\n editor.view.dispatch(editor.state.tr.setMeta(keptSelectionKey, { from, to }))\n}\n\n/** Stop drawing it. Nothing kept, nothing dispatched. */\nexport function releaseSelection(editor: Editor) {\n if (editor.isDestroyed || !keptSelectionKey.getState(editor.state)) return\n editor.view.dispatch(editor.state.tr.setMeta(keptSelectionKey, null))\n}\n\n/** What is being drawn, or null. */\nexport function keptSelection(editor: Editor): KeptRange | null {\n return keptSelectionKey.getState(editor.state) ?? null\n}\n\n/** A mark the strip can toggle \u2014 the suite's inline vocabulary (SPEC \u00A713.1) less link and reference. */\nexport type MarkId = 'bold' | 'italic' | 'underline' | 'code'\n\nconst MARKS: Record<MarkId, { label: string; keys: string }> = {\n bold: { label: 'Bold', keys: 'Mod-B' },\n italic: { label: 'Italic', keys: 'Mod-I' },\n underline: { label: 'Underline', keys: 'Mod-U' },\n code: { label: 'Code', keys: 'Mod-E' },\n}\n\n/**\n * Toggle one mark over the current selection. The core `toggleMark`, which\n * each mark's own `toggleBold` and the like wrap: the package names no Tiptap\n * extension, only the marks the suite's format has (SPEC \u00A713.1).\n */\nfunction toggleMark(editor: Editor, id: MarkId): boolean {\n return editor.chain().focus().toggleMark(id).run()\n}\n\n/** The address the selection sits in, or '' \u2014 what the link field opens with. */\nexport function currentLink(editor: Editor): string {\n const href: unknown = editor.getAttributes('link').href\n return typeof href === 'string' ? href : ''\n}\n\n/**\n * What somebody typed into the link field \u2192 an `href`, or null when it cannot\n * be one. A bare host (`google.com`) gets `https://`. A scheme is kept only when\n * SPEC \u00A713.1 lets a reader follow it (http, https, mailto); `javascript:` and\n * the like are refused here. `localhost:5173` is a host and a port: a scheme is\n * never followed by a digit.\n */\nexport function normalizeHref(raw: string): string | null {\n const s = raw.trim()\n if (!s || /\\s/.test(s)) return null\n if (/^(https?:|mailto:)/i.test(s)) return s\n if (/^[a-z][a-z0-9+.-]*:(?!\\d)/i.test(s)) return null\n return `https://${s}`\n}\n\n/**\n * Apply the link field to the selection. Empty removes the link; an address\n * sets it, over the whole link when the caret sits inside one. `false` means\n * the text could not be an address, and the field should stay open.\n */\nexport function applyLink(editor: Editor, raw: string): boolean {\n const chain = editor.chain().focus().extendMarkRange('link')\n // The core commands the Link extension's `setLink` and `unsetLink` wrap, with\n // the same meta; `normalizeHref` has already refused what it would refuse.\n if (!raw.trim()) return chain.unsetMark('link', { extendEmptyMarkRange: true }).setMeta('preventAutolink', true).run()\n const href = normalizeHref(raw)\n if (!href) return false\n return chain.setMark('link', { href }).setMeta('preventAutolink', true).run()\n}\n\n/**\n * Bold / italic / underline / link over selected text \u2014 Peek's\n * `SelectionToolbar` (PEE-18, PEE-19), moved into the package at Katerina's\n * word (29 September, UIG-31) when Ship's description became its second user.\n * Unchanged but for two props: `marks`, which buttons the strip has (Peek's\n * three by default; Ship adds code), and `link`, whether it has the Link button\n * and its field (Peek's editor has a link mark; an editor without one leaves it\n * off). The editor must register `KeptSelection` when `link` is on.\n *\n * The marks apply to a run you have already written, which is why they are\n * not in the `/` menu: typing `/` replaces the selection, so by definition\n * there is nothing selected to apply them to.\n *\n * **The chrome is the shared `Popover`, anchored** (P26). It was a `Menu`, and\n * it never was one: a menu gives its contents roving focus and typeahead,\n * which would fight the link field this panel exists for. An anchored\n * `Popover` is the mode built for exactly this \u2014 it takes no focus, so the\n * caret stays in the text. It is laid out as a row instead of a column\n * through `className`, which `cn` merges last. The panel still portals out of\n * the composer, so no overflow clips it, and Floating UI clamps it on screen.\n *\n * **The anchor is the selection's own point** since 0.12.2. This toolbar is\n * *centred* over the selection (her design, PEE-19, after Linear), and the\n * package's `Popover` aligns left or right only \u2014 Base UI has `center`, the\n * package does not expose it (ADOPTION **B21**). So the placement arithmetic\n * and the hand-measured `SIZE` box stay for now, and the rect handed over is\n * positioned so the panel lands on the same pixel it always has: the\n * package's own 4px gap is subtracted from this file's 8px, and the rect's\n * left edge is the panel's. When `align=\"center\"` exists, `SIZE`, `GAP` and\n * `placement` all go and the anchor becomes the selection's own rect.\n *\n * **Two faces** (PEE-19 \u2014 Katerina, 2026-09-07, after Linear). `marks` is the\n * row of four buttons. `link` is one text field for the address: it opens from\n * the Link button, and opens by itself when the settled selection sits inside\n * a link, with that link's address in it. There is no open, embed or delete\n * button, by her call: the field is the whole control.\n *\n * **What applies the field** (her rule, same day): Enter, and clicking away \u2014\n * anywhere outside the toolbar, the editor included. Enter on an empty field\n * removes the link; clicking away with an empty field changes nothing, since\n * removing is deliberate and leaving is not. Escape drops what was typed and\n * puts the caret back. Once applied, the caret lands after the link, and\n * typing goes on as plain text \u2014 the mark is not inclusive (editorKit).\n *\n * While the field has focus the editor has not, so everything below that\n * hides the toolbar on blur or on an empty editor selection stands down in\n * `link` mode. The editor keeps its selection while unfocused; `focus()` in\n * the apply command puts it back.\n *\n * **Why it waits.** Showing on every selection change put the toolbar under the\n * pointer mid-drag, and made it blink on a plain click \u2014 ProseMirror restores\n * the previous selection when the editor takes focus and collapses it a tick\n * later, so there is a real, non-empty range in between. Both go away by\n * refusing to show while the pointer is down and re-reading the selection after\n * a short settle rather than trusting the event that woke us.\n */\n\nconst ICONS: Record<MarkId, typeof IconBold> = {\n bold: IconBold,\n italic: IconItalic,\n underline: IconUnderline,\n code: IconCode,\n}\n\ntype Mode = 'marks' | 'link'\n\n/** How long a selection must stand still before the toolbar appears. */\nconst SETTLE_MS = 250\nconst GAP = 8\n/** The gap the package's `Popover` adds of its own, subtracted above so the\n * panel keeps this file's 8px. Read from `Popover.tsx`, not guessed. */\nconst POPOVER_GAP = 4\n\nexport interface SelectionToolbarProps {\n /** The editor whose selection the strip follows. `null` while it loads. */\n editor: Editor | null\n /** Which marks the strip toggles, in order. Default bold, italic, underline. */\n marks?: readonly MarkId[]\n /** The Link button and its address field. Needs a link mark in the editor and `KeptSelection`. Default off. */\n link?: boolean\n}\n\nexport function SelectionToolbar({ editor, marks = ['bold', 'italic', 'underline'], link = false }: SelectionToolbarProps) {\n /** The selection's top edge and horizontal centre; the face decides the rest. */\n const [anchor, setAnchor] = useState<{ top: number; centre: number } | null>(null)\n const [mode, setMode] = useState<Mode>('marks')\n const [url, setUrl] = useState('')\n // Re-render as marks turn on and off while the caret moves through the\n // selection, so the pressed states stay honest.\n const [, setTick] = useState(0)\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)\n const pointerDown = useRef(false)\n const field = useRef<HTMLInputElement>(null)\n /** What the field holds while it is open; null while it is closed. The\n * listeners below are bound once, so they read this rather than state. */\n const draft = useRef<string | null>(null)\n // Read by the same listeners. Kept in an effect, declared before the focus\n // effect below so the ref already says `link` when the field's focus blurs\n // the editor.\n const modeRef = useRef<Mode>('marks')\n useEffect(() => {\n modeRef.current = mode\n }, [mode])\n // Read by `settle`, which is bound once: words already a link open the field only when there is one.\n const linkOn = useRef(link)\n linkOn.current = link\n\n const hide = useCallback(() => {\n clearTimeout(timer.current)\n draft.current = null\n if (editor) releaseSelection(editor)\n setAnchor(null)\n setMode('marks')\n }, [editor])\n\n /**\n * Leaving the field applies it (Enter or a click away). An address links the\n * words and the caret moves past them; an empty field on the way out is a\n * shrug, not a removal. Idempotent: the outside click reaches this twice,\n * once from the editor press and once from the Menu's own listener.\n */\n const commit = useCallback(() => {\n const text = draft.current\n if (text === null || !editor) return\n draft.current = null\n if (text.trim() && applyLink(editor, text)) {\n editor.commands.setTextSelection(editor.state.selection.to)\n }\n hide()\n }, [editor, hide])\n\n /** Read the selection as it is *now* and place the toolbar, or hide it. */\n const settle = useCallback(() => {\n if (!editor || editor.isDestroyed) return\n // Keeping the selection drawn is itself a transaction, which schedules\n // this; by then the field has the focus, and reading `hasFocus()` here\n // would close it.\n if (modeRef.current === 'link') return\n const { state, view } = editor\n const { empty, from, to } = state.selection\n if (empty || from === to || !view.hasFocus() || pointerDown.current) {\n setAnchor(null)\n setMode('marks')\n return\n }\n const start = view.coordsAtPos(from)\n const end = view.coordsAtPos(to)\n const centre = (Math.min(start.left, end.left) + Math.max(start.right, end.right)) / 2\n // Words that are already a link open on the field, with their address in it.\n const inLink = linkOn.current && editor.isActive('link')\n const address = inLink ? currentLink(editor) : ''\n draft.current = inLink ? address : null\n // The field is about to take the focus; the words stay highlighted.\n if (inLink) keepSelection(editor)\n setMode(inLink ? 'link' : 'marks')\n setUrl(address)\n setAnchor({ top: start.top, centre })\n }, [editor])\n\n useEffect(() => {\n if (!editor) return\n\n const schedule = () => {\n // The field owns the keyboard and the focus; the editor's own events\n // stand down until it closes.\n if (modeRef.current === 'link') return\n setTick((t) => t + 1)\n clearTimeout(timer.current)\n // A collapsed selection hides at once \u2014 waiting to hide is what made the\n // click blink. Only *showing* is deferred.\n if (editor.state.selection.empty) {\n setAnchor(null)\n return\n }\n timer.current = setTimeout(settle, SETTLE_MS)\n }\n\n // The field taking focus is the one blur that must not hide us.\n const onBlur = () => {\n if (modeRef.current !== 'link') hide()\n }\n\n // Only a press *inside the editor* starts a selection. A press on the\n // toolbar itself must not hide it \u2014 doing so unmounted the button before\n // its click could land, so nothing ever applied.\n const startsSelection = (e: MouseEvent) =>\n e.target instanceof Node && editor.view.dom.contains(e.target)\n\n const onPointerDown = (e: MouseEvent) => {\n if (!startsSelection(e)) return\n // A press in the editor while the field is open is a click away: apply\n // first, before the editor moves the caret under the press.\n commit()\n pointerDown.current = true\n hide()\n }\n const onPointerUp = () => {\n if (!pointerDown.current) return\n pointerDown.current = false\n schedule()\n }\n\n editor.on('selectionUpdate', schedule)\n editor.on('transaction', schedule)\n editor.on('blur', onBlur)\n // @estiva-escape(no-hand-rolled-behaviour): watches presses in the editor to follow a text selection; the Popover it opens in has no trigger to own that (Peek's UIG-8 escape, moved here with the part, UIG-31)\n document.addEventListener('mousedown', onPointerDown, true)\n document.addEventListener('mouseup', onPointerUp, true)\n\n return () => {\n clearTimeout(timer.current)\n editor.off('selectionUpdate', schedule)\n editor.off('transaction', schedule)\n editor.off('blur', onBlur)\n document.removeEventListener('mousedown', onPointerDown, true)\n document.removeEventListener('mouseup', onPointerUp, true)\n }\n }, [editor, settle, hide, commit])\n\n // The field takes focus once it is on screen. Not `autoFocus`, and not in\n // the same frame: the Menu renders once hidden to measure itself, and a\n // hidden input cannot take focus \u2014 measured 2026-09-07, the field opened\n // unfocused on a re-selected link. An address already in it is selected\n // whole, so typing replaces it.\n useEffect(() => {\n if (mode !== 'link' || !anchor) return\n const frame = requestAnimationFrame(() => {\n field.current?.focus()\n field.current?.select()\n })\n return () => cancelAnimationFrame(frame)\n }, [mode, anchor])\n\n if (!editor || !anchor) return null\n\n const openField = () => {\n const address = currentLink(editor)\n draft.current = address\n keepSelection(editor)\n setUrl(address)\n setMode('link')\n }\n /** Enter. Unlike a click away, an empty field here means remove. */\n const submit = () => {\n if (!url.trim()) {\n applyLink(editor, '')\n hide()\n editor.commands.focus()\n return\n }\n commit()\n }\n const discard = () => {\n hide()\n editor.commands.focus()\n }\n\n /* A zero-sized rect at the selection's own top edge and centre. The panel's\n width is no longer this file's business: `align=\"center\"` puts the\n panel's middle over this point (B21, package 0.12.2), so the measured\n `SIZE` table and the placement arithmetic that stood in for it are gone.\n The top is this file's GAP less the package's own 4, so the panel keeps\n the 8px it has always had. */\n const anchorRect = new DOMRect(anchor.centre, anchor.top - (GAP - POPOVER_GAP), 0, 0)\n\n return (\n <Popover\n anchor={anchorRect}\n open\n side=\"top\"\n align=\"center\"\n onOpenChange={(next) => {\n if (!next) commit()\n }}\n ariaLabel=\"Text formatting\"\n // The panel gives up its column; the strip inside draws the row. The 4px\n // around it is `contentClassName`'s: on `className` it was added to the\n // package's 8px from 0.12.6 on, and the toolbar grew 8px a side\n // (Katerina, 2026-09-13; migration docs D68, Finding 60). This is the\n // package's own `Popover/A toolbar` story, class for class.\n className=\"w-auto min-w-0\"\n contentClassName=\"p-1\"\n >\n {mode === 'link' ? (\n /* One field, so no strip: a toolbar of one control is a toolbar in\n name only, and `ToolbarInput` cannot take the ref this field is\n focused by (ADOPTION B25). */\n // The package Form (UIG-7): Enter applies the link. `free` and `flex`: one field, which keeps its own height.\n <Form onSubmit={submit} layout=\"free\" className=\"flex\">\n <TextInput\n ref={field}\n value={url}\n onChange={(e) => {\n setUrl(e.target.value)\n draft.current = e.target.value\n }}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n // Before the Menu's own Escape, which would apply instead.\n e.stopPropagation()\n discard()\n }\n }}\n placeholder=\"Enter link URL\"\n aria-label=\"Link URL\"\n className=\"w-[264px]\"\n />\n </Form>\n ) : (\n /* Four marks and a link button: one Tab stop, \u2190 \u2192 along them (D29). */\n <Toolbar aria-label=\"Text formatting\" surface={false}>\n {marks.map((id) => {\n const Icon = ICONS[id]\n const active = editor.isActive(id)\n return (\n <ToolbarButton\n key={id}\n aria-label={MARKS[id].label}\n pressed={active}\n tooltip={MARKS[id].label}\n tooltipShortcut={shortcutLabel(MARKS[id].keys)}\n // Keep the selection: focus must not leave the editor on press.\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => toggleMark(editor, id)}\n >\n <Icon size={16} stroke={1.5} />\n </ToolbarButton>\n )\n })}\n {link && (\n <ToolbarButton\n aria-label=\"Link\"\n tooltip=\"Link\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={openField}\n >\n <IconLink size={16} stroke={1.5} />\n </ToolbarButton>\n )}\n </Toolbar>\n )}\n </Popover>\n )\n}\n", "// @estiva-escape(component-has-a-page, component-has-a-story): draws nothing of its own \u2014 it opens a SuggestionMenu from a Tiptap suggestion plugin, and SuggestionMenu's page and stories show it (UIG-31)\nimport { forwardRef, useRef, useImperativeHandle } from 'react'\nimport { ReactRenderer } from '@tiptap/react'\nimport type { SuggestionKeyDownProps, SuggestionOptions, SuggestionProps } from '@tiptap/suggestion'\nimport { SuggestionMenu, type SuggestionMenuHandle, type SuggestionMenuRow, type SuggestionMenuSection } from './SuggestionMenu'\n\n/*\n * Is a type-ahead list open? \u2014 Peek's counter (`mention.tsx`), moved here with\n * the lists it counts (UIG-31).\n *\n * A counter rather than a boolean: two suggestion plugins can overlap for a\n * moment while one hands over to the other (`/` choosing \"Mention\" types `@`).\n * An editor asks before it acts on Enter or Escape, so a key that chose a row\n * does not also send the message or cancel the edit.\n */\nlet openCount = 0\nlet lastClose = 0\n\n/**\n * Open, or closed less than 100ms ago \u2014 what an Enter handler asks. Choosing a\n * row with Enter closes the list, and that same Enter must not also send.\n */\nexport function isSuggestionActive(): boolean {\n return openCount > 0 || Date.now() - lastClose < 100\n}\n\n/**\n * Open *now*, with no grace period \u2014 what an Escape handler asks. The first\n * Escape closes the list, the second is somebody's deliberate press.\n */\nexport function isSuggestionOpen(): boolean {\n return openCount > 0\n}\n\n/** How a list looks and what fills it \u2014 everything but the editor's own wiring. */\nexport interface SuggestionPopupOptions<T> {\n /** Names the list \u2014 \"Commands\", \"People\". */\n ariaLabel: string\n /** The box's width, e.g. `w-[300px]`. */\n width: string\n /** The cap before the rows scroll, e.g. `max-h-[400px]`. */\n maxHeight: string\n /** The plugin's items, and the typed query, \u2192 the sections to draw. One unlabeled section by default. */\n sections?: (items: T[], query: string) => readonly SuggestionMenuSection<T>[]\n itemKey: (item: T) => string\n row: (item: T) => SuggestionMenuRow\n}\n\ntype Props<T> = SuggestionProps<T> & { options: SuggestionPopupOptions<T> }\n\n// `forwardRef`, so Tiptap's `ReactRenderer` hands its `ref` through whichever\n// way it detects a component that takes one.\nconst Popup = forwardRef<SuggestionMenuHandle, Props<unknown>>(function Popup({ options, ...props }, handle) {\n const menu = useRef<SuggestionMenuHandle>(null)\n useImperativeHandle(handle, () => ({ onKeyDown: (event) => menu.current?.onKeyDown(event) ?? false }))\n const sections = options.sections ? options.sections(props.items, props.query) : [{ items: props.items }]\n return (\n <SuggestionMenu\n handle={menu}\n rect={props.clientRect?.() ?? null}\n editorElement={props.editor.view.dom}\n ariaLabel={options.ariaLabel}\n width={options.width}\n maxHeight={options.maxHeight}\n sections={sections}\n itemKey={options.itemKey}\n row={options.row}\n onSelect={(item) => props.command(item)}\n />\n )\n})\n\n/**\n * The `render` of a Tiptap suggestion plugin, drawn as a `SuggestionMenu` \u2014\n * the popup controller Peek wrote four times (`mention.tsx`, `slashCommands.tsx`),\n * once.\n *\n * Suggestion({ editor, char: '/', items, command,\n * render: suggestionPopup({ ariaLabel: 'Commands', width: 'w-[300px]',\n * maxHeight: 'max-h-[400px]', itemKey, row, sections }) })\n *\n * It mounts the list when the query starts and removes it when it ends: the\n * plugin owns the list's life. Escape closes it and is used up; \u2191, \u2193 and Enter\n * go to the list; every other key stays the editor's. The list portals itself\n * (the package `Popover`), so the mount point is only a mount point.\n */\nexport function suggestionPopup<T>(options: SuggestionPopupOptions<T>): NonNullable<SuggestionOptions<T>['render']> {\n return () => {\n let component: ReactRenderer<SuggestionMenuHandle, Props<T>> | null = null\n let container: HTMLDivElement | null = null\n const exit = () => {\n if (!component) return\n openCount = Math.max(0, openCount - 1)\n lastClose = Date.now()\n component.destroy()\n container?.remove()\n component = null\n container = null\n }\n return {\n onStart: (props) => {\n openCount++\n component = new ReactRenderer<SuggestionMenuHandle, Props<T>>(Popup as never, { props: { ...props, options }, editor: props.editor })\n container = document.createElement('div')\n document.body.appendChild(container)\n container.appendChild(component.element)\n },\n onUpdate: (props) => component?.updateProps({ ...props, options }),\n onKeyDown: ({ event }: SuggestionKeyDownProps) => {\n if (event.key === 'Escape') {\n exit()\n return true\n }\n return component?.ref?.onKeyDown(event) ?? false\n },\n onExit: exit,\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["useRef", "jsx", "Popup"]
|
|
7
|
+
}
|
package/dist/eslint/index.js
CHANGED
|
@@ -1652,7 +1652,7 @@ var BASE_UI_PARTS = {
|
|
|
1652
1652
|
};
|
|
1653
1653
|
var LIST_PART = {
|
|
1654
1654
|
use: "Select",
|
|
1655
|
-
more: " To pick several, `ChipInput`; to search and act, `CommandPalette`; to tick several in a list, `Checkbox` with `row`."
|
|
1655
|
+
more: " To pick several, `ChipInput`; to search and act, `CommandPalette`; to tick several in a list, `Checkbox` with `row`; for the list a text editor opens as you type, `SuggestionMenu`."
|
|
1656
1656
|
};
|
|
1657
1657
|
var MESSAGE_PART = { use: "FieldLine", more: " For a notice, `Banner`; for a message that comes and goes, `Toast`." };
|
|
1658
1658
|
var ROLE_PARTS = {
|
|
@@ -1758,7 +1758,7 @@ var OWNED_BEHAVIOURS = [
|
|
|
1758
1758
|
id: "walking",
|
|
1759
1759
|
behaviour: "Moves through its items with the arrow keys",
|
|
1760
1760
|
baseUi: ["Menu", "Select", "Combobox", "Autocomplete", "Tabs", "Toolbar"],
|
|
1761
|
-
owners: ["Menu", "Select", "ChipInput", "CommandPalette", "Tabs", "Toolbar"],
|
|
1761
|
+
owners: ["Menu", "Select", "ChipInput", "CommandPalette", "SuggestionMenu", "Tabs", "Toolbar"],
|
|
1762
1762
|
reads: `a key compared by hand with ${WALKING_KEYS.join(", ")}`
|
|
1763
1763
|
},
|
|
1764
1764
|
{
|