@plannotator/ui 0.42.0 → 0.43.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.
- package/HANDOFF.md +297 -0
- package/README.md +49 -1
- package/components/AnnotationToolbar.tsx +87 -8
- package/components/CommentPopover.tsx +90 -14
- package/components/MentionPicker.tsx +110 -0
- package/components/SelectionActionsDropdown.tsx +173 -0
- package/components/Viewer.tsx +56 -2
- package/components/html-viewer/HtmlViewer.tsx +24 -1
- package/components/html-viewer/useHtmlAnnotation.ts +4 -1
- package/hooks/useAnnotationHighlighter.ts +22 -3
- package/hooks/useMentionAutocomplete.ts +246 -0
- package/package.json +1 -1
- package/styles.css +1 -1
- package/types.ts +13 -0
- package/utils/mentions.ts +149 -0
- package/utils/selectionActions.ts +64 -0
package/HANDOFF.md
CHANGED
|
@@ -213,6 +213,8 @@ We deliberately did **not** restructure the exports map in this PR (move-don't-r
|
|
|
213
213
|
| `components/HtmlSurfaceControls` | The eye / refresh / pen header controls for an HTML surface, with per-string `labels` overrides. Presentation only. See "HTML annotation parity seams". |
|
|
214
214
|
| `hooks/useHtmlRefresh` | Re-fetch a rendered HTML document through a host-supplied `fetchSnapshot`, remount the viewer on a reload generation, acknowledge the restore report once. See "HTML annotation parity seams". |
|
|
215
215
|
| `shortcuts` (`useHtmlAnnotateShortcuts`, `defineShortcutScope`, the scope registry) | The declarative keyboard-shortcut engine and the per-surface scopes, including the HTML annotate scope (Mod+Shift+A toggles annotate mode, Mod+Shift+X shows/hides the tools). Pure: React plus `utils/platform`; no backend. |
|
|
216
|
+
| `utils/selectionActions` + `components/SelectionActionsDropdown` | The host selection-actions seam: `SelectionAction`, `SelectionActionContext`, the pure `buildSelectionActionContext`, and the dropdown `AnnotationToolbar` opens. Pure React; no backend. *(Blessed in 0.43.0.)* |
|
|
217
|
+
| `utils/mentions` + `components/MentionPicker` + `hooks/useMentionAutocomplete` | The `@` mention seam behind `CommentPopover`'s `mentionSource` (and, since 0.43.1, `Viewer`'s and `HtmlViewer`'s): the pure grammar (`mentionTrigger`, `mentionMatches`, `applyMentionPick`, `survivingMentions`), the portaled picker, and the keyboard state machine. Pure React; no backend. *(Blessed in 0.43.0.)* |
|
|
216
218
|
| `utils/inputMethod` (`getInputMethod`, `saveInputMethod`, `refreshInputMethodStamp`) | The per-surface pinpoint/drag input-method preference with its TTL. Persists through the `storageBackend` seam; no backend of its own. |
|
|
217
219
|
| `utils/codeHighlight` / `utils/codeBlockMark` / `utils/syntaxTheme` | The Shiki-based fence highlighter, swap-surviving annotation marks, and palette→Shiki theme mapping. Replaces all `.hljs` styling. *(Blessed in 0.29.0.)* |
|
|
218
220
|
| `utils/math` (`loadMathRenderer`, `getMathRenderer`, `getMathRendererSource`, `setMathRenderer`, `setMathRendererLoader`, `getMathRendererLoader`, `resetMathRenderer`) and `utils/math-eager` | The math renderer slot and its eager KaTeX registration. Import `utils/math-eager` for synchronous typesetting on the first commit; call `loadMathRenderer()` to pre-warm the lazy path. `resetMathRenderer()` empties the slot and keeps the registered loader; `setMathRendererLoader(null)` drops it. See "Lazy renderers and eager entries". |
|
|
@@ -873,8 +875,303 @@ to fall back on:
|
|
|
873
875
|
|
|
874
876
|
---
|
|
875
877
|
|
|
878
|
+
## Host toolbar seams (0.43.0)
|
|
879
|
+
|
|
880
|
+
Two additive props, ruled in together by Plannotator's owner **on one
|
|
881
|
+
condition: each is an opt-in host capability that changes nothing for
|
|
882
|
+
Plannotator's own users when it is not supplied.** Plannotator is the provider
|
|
883
|
+
of these capabilities; it is not a product that uses them. It passes neither
|
|
884
|
+
prop anywhere, and `packages/editor` / `packages/review-editor` are untouched
|
|
885
|
+
by this release. Core is UNCHANGED at `0.25.5`, so **ui 0.43.0 publishes
|
|
886
|
+
alone**.
|
|
887
|
+
|
|
888
|
+
### 1. `AnnotationToolbar` `selectionActions` — the host's own commands on a selection
|
|
889
|
+
|
|
890
|
+
```ts
|
|
891
|
+
import type { SelectionAction, SelectionActionContext } from "@plannotator/ui/types";
|
|
892
|
+
// (also @plannotator/ui/utils/selectionActions)
|
|
893
|
+
|
|
894
|
+
interface SelectionActionContext {
|
|
895
|
+
text: string; // the toolbar's copy text, else the element's text
|
|
896
|
+
blockId: string; // the enclosing [data-block-id], '' on raw-HTML surfaces
|
|
897
|
+
startOffset: number; // offset of the selection inside that block's text
|
|
898
|
+
endOffset: number; // startOffset + text.length
|
|
899
|
+
element: HTMLElement; // the element the toolbar is anchored to
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
interface SelectionAction {
|
|
903
|
+
id: string;
|
|
904
|
+
label: string;
|
|
905
|
+
detail?: string; // dimmed second line
|
|
906
|
+
icon?: React.ReactNode; // a colored accent bar is drawn when absent
|
|
907
|
+
onSelect(ctx: SelectionActionContext): void;
|
|
908
|
+
}
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
One toolbar button (a wand, `data-selection-actions`) opens the package's own
|
|
912
|
+
dropdown directly below it, in the quick-label picker's placement and chrome
|
|
913
|
+
(`components/SelectionActionsDropdown`: `SelectionActionsDropdown` is the list,
|
|
914
|
+
`FloatingSelectionActionsPicker` the portaled, viewport-clamped, flip-above
|
|
915
|
+
picker; rows carry `data-selection-action="<id>"` under `role="listbox"`).
|
|
916
|
+
Selecting an item calls `onSelect(ctx)` and closes the toolbar exactly as a
|
|
917
|
+
quick label does — **the package creates no annotation**; what an action means
|
|
918
|
+
is entirely the host's business.
|
|
919
|
+
|
|
920
|
+
- **Keyboard:** ArrowDown / ArrowUp move, Enter invokes, Escape closes the
|
|
921
|
+
dropdown (and only the dropdown — the toolbar stays open). Nothing is
|
|
922
|
+
preselected until the first arrow, the package-wide rule, so a stray Enter
|
|
923
|
+
over an open dropdown never fires a host command. Pointer hover highlights a
|
|
924
|
+
row and a click invokes it directly. While the dropdown is open, the
|
|
925
|
+
toolbar's own type-to-comment and Alt+digit listeners stand down, the same
|
|
926
|
+
way they do for `FloatingQuickLabelPicker`.
|
|
927
|
+
- **Where it sits:** the wand takes the slot the quick-labels Zap occupied and
|
|
928
|
+
the Zap moves one place right (`Copy | Delete | Comment | Actions | Quick
|
|
929
|
+
label | 👍 | Cancel`). Chosen over "actions to the right of the Zap" so that
|
|
930
|
+
ONE geometry rule covers both host configurations: with `quickLabels: false`
|
|
931
|
+
— the expected host setup — the wand sits exactly where the Zap sat, and a
|
|
932
|
+
host that keeps both gets its own actions in the primary slot it opted into.
|
|
933
|
+
- `undefined` or `[]` renders no button at all, and the empty array is pinned
|
|
934
|
+
by a test because "the host has no actions right now" is a real state.
|
|
935
|
+
- **The context is derived, not threaded.** `buildSelectionActionContext`
|
|
936
|
+
(`utils/selectionActions`, pure) walks up from the anchor element for
|
|
937
|
+
`[data-block-id]` and computes the offset by splitting the block's text on
|
|
938
|
+
the selection — deliberately the same arithmetic
|
|
939
|
+
`createAnnotationFromSource` uses, so an action sees the coordinates an
|
|
940
|
+
annotation created from that same selection would carry. On a surface with
|
|
941
|
+
no blocks (raw HTML) it degrades to `blockId: ''` and `startOffset: 0`
|
|
942
|
+
(`endOffset` is then the selection's length; an HTML annotation itself
|
|
943
|
+
stores `0`/`0`). The one deliberate deviation from the annotation path is
|
|
944
|
+
a selection the block does not contain — one spanning two blocks — where
|
|
945
|
+
the annotation path reports `blockText.length` and a host gets `0`.
|
|
946
|
+
|
|
947
|
+
### 2. `AnnotationToolbar` `quickLabels` — the opt-out switch
|
|
948
|
+
|
|
949
|
+
`quickLabels?: boolean`, default `true`. `false` hides the Zap picker button
|
|
950
|
+
**and** makes the Alt+digit label shortcuts inert on that toolbar (hiding the
|
|
951
|
+
button while leaving the keys live was the obvious bug; a test pins both). It
|
|
952
|
+
does not touch the one-click 👍, which is a separate affordance, and it does
|
|
953
|
+
not clamp editor mode — a host that persists `'quickLabel'` mode still keeps
|
|
954
|
+
that state out of `Viewer`, exactly as with `AnnotationToolstrip`'s
|
|
955
|
+
`hideQuickLabel` (0.35.0).
|
|
956
|
+
|
|
957
|
+
### 3. `CommentPopover` `mentionSource` — an `@` mention source for the composer
|
|
958
|
+
|
|
959
|
+
```ts
|
|
960
|
+
import type { MentionPerson, MentionSource } from "@plannotator/ui/types";
|
|
961
|
+
// (also @plannotator/ui/utils/mentions)
|
|
962
|
+
|
|
963
|
+
interface MentionPerson {
|
|
964
|
+
readonly id: string; // opaque host id, reported back verbatim
|
|
965
|
+
readonly kind: "user" | "agent"; // agents are never taggable in a comment
|
|
966
|
+
readonly label: string;
|
|
967
|
+
readonly detail: string | null; // right-aligned hint (an email, "Agent")
|
|
968
|
+
readonly canOpen: boolean; // host access data; see onPickBlocked
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
interface MentionSource {
|
|
972
|
+
readonly people: readonly MentionPerson[];
|
|
973
|
+
readonly emptyNotice?: string | null; // honest-empty row
|
|
974
|
+
readonly onMentionsChange?: (ids: readonly string[]) => void;
|
|
975
|
+
readonly onPickBlocked?: (person: MentionPerson) => void;
|
|
976
|
+
}
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
This is the shape the host's own reply box already uses (`MentionPerson` is
|
|
980
|
+
copied field for field from its `plannotator/mention-extension.ts`), so the
|
|
981
|
+
host fills `people` from its existing candidate hook and nothing has to be
|
|
982
|
+
mapped. The **package owns the typing rules and the picker**; the host owns
|
|
983
|
+
the people and what a mention means.
|
|
984
|
+
|
|
985
|
+
- **The grammar is ported, not reinvented** (`utils/mentions`, pure, unit
|
|
986
|
+
tested): the same `MENTION_QUERY_RE = /@[\w .-]*$/`, the same word-boundary
|
|
987
|
+
guard that makes `a@b.com` never open a menu, the same users-only /
|
|
988
|
+
not-already-tagged filtering on label OR detail, the same `@Label ` insertion
|
|
989
|
+
with the caret after it, the same `sanitizeMentionLabel`, and the same
|
|
990
|
+
surviving-token rule — deleting a token untags that person, so the body and
|
|
991
|
+
the reported ids can never disagree about who was named.
|
|
992
|
+
- **The picker** (`components/MentionPicker`) is portaled and `position:
|
|
993
|
+
fixed`, measured from the textarea's rect, above it by default and below
|
|
994
|
+
when there is not 196px of headroom — the composer card clips its own box,
|
|
995
|
+
so a menu positioned inside the textarea's wrapper is cut off. `role=
|
|
996
|
+
"listbox"` with `data-mention-picker`, rows `data-mention-option="<id>"`, the
|
|
997
|
+
empty notice `data-mention-empty` (one non-selectable row; with `people: []`
|
|
998
|
+
and no notice the menu simply stays closed).
|
|
999
|
+
- **Keyboard** (`hooks/useMentionAutocomplete`, modelled on
|
|
1000
|
+
`useSkillReferenceAutocomplete` and living in the same textarea beside it —
|
|
1001
|
+
`handleKeyDown` offers the event to the skill hook first, then this one):
|
|
1002
|
+
nothing is preselected, so Enter is a newline and Tab leaves the field until
|
|
1003
|
+
an arrow engages a row; ArrowDown from none lands on the first row, ArrowUp
|
|
1004
|
+
on the last; a mouse pick uses `mousedown` + `preventDefault` so it beats the
|
|
1005
|
+
blur. **One deliberate difference from the `/` and `$` trigger:** the arrows
|
|
1006
|
+
engage this menu even on a bare `@`, and Escape closes it whenever it is
|
|
1007
|
+
visible. `$` and `/` are ordinary prose characters whose menu must yield the
|
|
1008
|
+
arrows back to caret navigation; `@` at a word boundary is an unambiguous tag
|
|
1009
|
+
gesture, and typing `@` then ArrowDown is how the host's own reply box
|
|
1010
|
+
behaves.
|
|
1011
|
+
- **`onPickBlocked` is the no-access rule.** When a `canOpen: false` person is
|
|
1012
|
+
picked AND the host supplied `onPickBlocked`, the handler fires and
|
|
1013
|
+
**nothing is inserted** (the host shows its own no-access dialog). Without
|
|
1014
|
+
the handler such a person inserts like anyone else — the package never
|
|
1015
|
+
renders a disabled row it cannot explain. Both branches are pinned by tests.
|
|
1016
|
+
- **The ids reach the host two ways.** `onMentionsChange(ids)` fires on every
|
|
1017
|
+
text change with the surviving ids, and `onSubmit` gained an optional THIRD
|
|
1018
|
+
argument: `onSubmit(text, images?, mentions?)`. The third argument is passed
|
|
1019
|
+
**only when `mentionSource` is supplied** — without one the call is the
|
|
1020
|
+
two-argument call it has always been (`arguments.length === 2`, pinned).
|
|
1021
|
+
Hosts can use either; `onMentionsChange` alone is enough for a host that
|
|
1022
|
+
keeps its draft state outside the popover.
|
|
1023
|
+
- Nothing is wired to Plannotator data: there is no mention provider in this
|
|
1024
|
+
repo, and `configurePlannotatorUI` gains no seam for one. A host passes the
|
|
1025
|
+
prop where it renders the composer, the `HtmlViewer` / `Viewer` pattern.
|
|
1026
|
+
|
|
1027
|
+
### Threading points
|
|
1028
|
+
|
|
1029
|
+
- `AnnotationToolbar` (`selectionActions`, `selectionActionsIcon`, `quickLabels`) — the props live here. `selectionActionsIcon?: React.ReactNode` (0.43.1) is the glyph on the wand button, forwarded by `Viewer` (both toolbars) and `HtmlViewer`; absent → the package's own wand, which 0.43.1 also simplified to one thick diagonal with a single star (the six-spark glyph read as noise at 16px). Name, `data-selection-actions`, size and behavior of the button are untouched either way.
|
|
1030
|
+
- `Viewer` forwards both to BOTH of its toolbars (the text-selection toolbar
|
|
1031
|
+
and the code-block hover toolbar).
|
|
1032
|
+
- `HtmlViewer` forwards `selectionActions` to its selection toolbar. It does
|
|
1033
|
+
not take `quickLabels`: that surface is already `commentOnly`, which hides
|
|
1034
|
+
the Zap and the Alt+digit keys outright.
|
|
1035
|
+
- `plan-diff/PlanCleanDiffView` mounts a toolbar too and is deliberately NOT
|
|
1036
|
+
threaded: it is a Plannotator-only surface (the plan-version diff) and is not
|
|
1037
|
+
on the supported-import list. Ask if a host needs it.
|
|
1038
|
+
- `CommentPopover` (`mentionSource`). `Viewer` does NOT forward it: the viewer
|
|
1039
|
+
owns several composers and a per-composer decision belongs to the host that
|
|
1040
|
+
renders them. Ask if you would rather pass it once on `Viewer`.
|
|
1041
|
+
**Answered in 0.43.1 — both viewers now forward it; see the next section.**
|
|
1042
|
+
|
|
1043
|
+
### The no-op guarantee, and how it is pinned
|
|
1044
|
+
|
|
1045
|
+
With neither prop supplied, the rendered DOM of both components is **byte-for-byte
|
|
1046
|
+
what `origin/main` renders**: the same components were mounted on the base commit
|
|
1047
|
+
and on this branch in the same harness and their `outerHTML` diffed to zero
|
|
1048
|
+
(`.annotation-toolbar` + `[data-comment-popover]`, 6376 bytes each, identical).
|
|
1049
|
+
On top of that, committed tests pin the structure rather than a snapshot:
|
|
1050
|
+
the default toolbar's button set and order (`Copy, Delete, Comment, Quick
|
|
1051
|
+
label, Looks good, Cancel`), the absence of `[data-selection-actions]` and of
|
|
1052
|
+
the picker, the exact attribute list on the Zap button, and — for the composer —
|
|
1053
|
+
that typing `@` opens nothing and that submit stays a two-argument call.
|
|
1054
|
+
`useMentionAutocomplete` with no source registers no listener, opens no menu
|
|
1055
|
+
state and returns one frozen empty id array; `AnnotationToolbar` renders no
|
|
1056
|
+
extra element and spreads no extra attributes.
|
|
1057
|
+
|
|
1058
|
+
Tests: `utils/mentions.test.ts` (11, DOM-free),
|
|
1059
|
+
`components/AnnotationToolbar.selectionActions.test.tsx` (8, DOM-gated),
|
|
1060
|
+
`components/CommentPopover.mentionSource.test.tsx` (11, DOM-gated).
|
|
1061
|
+
|
|
1062
|
+
## `mentionSource` on the viewers (0.43.1)
|
|
1063
|
+
|
|
1064
|
+
0.43.0 left `mentionSource` on `CommentPopover` alone, with an open question
|
|
1065
|
+
("ask if you would rather pass it once on `Viewer`"). The answer is yes, so
|
|
1066
|
+
0.43.1 threads it one level up and gives the picked ids somewhere to land.
|
|
1067
|
+
Same ruling as 0.43.0: an opt-in host capability that changes nothing for
|
|
1068
|
+
Plannotator's own users when it is not supplied. `packages/editor` and
|
|
1069
|
+
`packages/review-editor` are untouched by this release; core is UNCHANGED at
|
|
1070
|
+
`0.25.5`, so **ui 0.43.1 publishes alone**.
|
|
1071
|
+
|
|
1072
|
+
### 1. The prop
|
|
1073
|
+
|
|
1074
|
+
`Viewer` and `HtmlViewer` each gain `mentionSource?: MentionSource` — the same
|
|
1075
|
+
type, unchanged, from `@plannotator/ui/types` (also `utils/mentions`) — and
|
|
1076
|
+
each forwards it to EVERY comment composer it mounts:
|
|
1077
|
+
|
|
1078
|
+
- `Viewer` → the text-selection composer (`useAnnotationHighlighter`'s) and the
|
|
1079
|
+
global / code-block one.
|
|
1080
|
+
- `HtmlViewer` → the pinpoint (selection) composer and the global one.
|
|
1081
|
+
|
|
1082
|
+
A host that wants mentions on a surface passes one prop instead of reaching
|
|
1083
|
+
into the viewer's composers. Passing it directly to a `CommentPopover` you
|
|
1084
|
+
mount yourself still works and is unchanged.
|
|
1085
|
+
|
|
1086
|
+
Deliberately NOT threaded: `plan-diff/PlanCleanDiffView`, `CodeFilePopout` and
|
|
1087
|
+
`goal-setup/GoalSetupSurface` mount composers too, but they are Plannotator-only
|
|
1088
|
+
surfaces off the supported-import list — the same line 0.43.0 drew for
|
|
1089
|
+
`selectionActions`. Ask if a host needs one.
|
|
1090
|
+
|
|
1091
|
+
### 2. `Annotation.mentions` — where the ids go
|
|
1092
|
+
|
|
1093
|
+
```ts
|
|
1094
|
+
interface Annotation {
|
|
1095
|
+
// …
|
|
1096
|
+
mentions?: readonly string[]; // opaque host ids, additive
|
|
1097
|
+
}
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
`onSubmit(text, images?, mentions?)` used to stop at the viewer: the third
|
|
1101
|
+
argument was received and dropped. It now rides onto the annotation the viewer
|
|
1102
|
+
hands `onAddAnnotation`, on every creation path behind those composers
|
|
1103
|
+
(`createAnnotationFromSource`, `createAnnotationFromMathSource`, the code-block
|
|
1104
|
+
path, both global comments, and the HTML pinpoint comment).
|
|
1105
|
+
|
|
1106
|
+
**The presence rule** is the whole no-op guarantee, so it is worth stating
|
|
1107
|
+
exactly: the key exists only when a `mentionSource` was supplied AND at least
|
|
1108
|
+
one id survived to submit. No source → no third argument → no key. A source
|
|
1109
|
+
whose tokens the author deleted before submitting → `[]` from the composer →
|
|
1110
|
+
still no key, never an empty array. Every write is a conditional spread
|
|
1111
|
+
(`...(mentions && mentions.length > 0 ? { mentions } : {})`), not `mentions,`,
|
|
1112
|
+
because the bare shorthand would put the key on the object with an `undefined`
|
|
1113
|
+
value and `'mentions' in ann` would start answering true for Plannotator.
|
|
1114
|
+
|
|
1115
|
+
The ids are opaque to the package. Mapping one to a person, notifying them, or
|
|
1116
|
+
rendering an avatar is entirely the host's business — `MentionPerson.id` is
|
|
1117
|
+
reported back verbatim, exactly as it was handed in.
|
|
1118
|
+
|
|
1119
|
+
### 3. What the field does NOT touch
|
|
1120
|
+
|
|
1121
|
+
An id from a host's directory means nothing outside that host, so the field
|
|
1122
|
+
stays out of everything Plannotator produces:
|
|
1123
|
+
|
|
1124
|
+
- **Export.** `exportAnnotations`, `exportAnnotationEntry` and
|
|
1125
|
+
`exportLinkedDocAnnotations` never print it: an annotation carrying
|
|
1126
|
+
`mentions` exports byte-identically to the same annotation without it
|
|
1127
|
+
(`utils/parser.mentions.test.ts`). The readable `@Label` token is in the
|
|
1128
|
+
comment body, which is what the coding agent reads.
|
|
1129
|
+
- **Share links.** Dropped exactly like `htmlAnchor`, `elementContext` and
|
|
1130
|
+
`diagramAnchor` — the compact tuple format never carried extra fields, and a
|
|
1131
|
+
round trip restores `mentions: undefined` (pinned in
|
|
1132
|
+
`utils/sharing.multiTarget.test.ts`).
|
|
1133
|
+
- **External annotations.** `POST /api/external-annotations` builds its rows
|
|
1134
|
+
from an explicit field list and `PATCH` from an allowlist, so a `mentions`
|
|
1135
|
+
key on the wire is dropped as any unknown key is. No change was needed in
|
|
1136
|
+
`@plannotator/core/external-annotation`, in either runtime.
|
|
1137
|
+
- **The feedback archive.** `packages/shared/feedback-archive.ts` copies named
|
|
1138
|
+
fields into its record; `mentions` is not one of them and never reaches
|
|
1139
|
+
`index.jsonl` or a sidecar. No change needed.
|
|
1140
|
+
- **Drafts** carry it for free (annotations are opaque JSON to the draft
|
|
1141
|
+
transport), which is the behavior a host wants: a restored draft still knows
|
|
1142
|
+
who was named.
|
|
1143
|
+
|
|
1144
|
+
### 4. Edit and reply paths
|
|
1145
|
+
|
|
1146
|
+
There is none to thread in these two viewers: both `Viewer` composers and both
|
|
1147
|
+
`HtmlViewer` composers are CREATION composers. Editing an existing comment
|
|
1148
|
+
happens in `AnnotationPanel`'s card, a plain textarea that has never had an
|
|
1149
|
+
`@` picker and takes no `mentionSource`; replies (`inReplyTo`) are created by
|
|
1150
|
+
the WebMCP catalog, not by a composer. So no annotation's `mentions` is
|
|
1151
|
+
rewritten after creation by this package — a host that edits a comment owns
|
|
1152
|
+
the field from then on. If you want the panel editor to pick people too, that
|
|
1153
|
+
is a separate prop on `AnnotationPanel` and worth asking for.
|
|
1154
|
+
|
|
1155
|
+
### No-op guarantee, and how it is pinned
|
|
1156
|
+
|
|
1157
|
+
With no `mentionSource`, both viewers mount the composers they always did, no
|
|
1158
|
+
`@` listener is registered, no picker DOM exists, and the annotation object
|
|
1159
|
+
handed to `onAddAnnotation` has no `mentions` key at all (`'mentions' in ann`
|
|
1160
|
+
is false, asserted rather than `toBeUndefined()` — the difference between the
|
|
1161
|
+
conditional spread and the bare shorthand is invisible to the latter).
|
|
1162
|
+
|
|
1163
|
+
Tests (both DOM-gated, both in the workflow's DOM_TESTS step):
|
|
1164
|
+
`components/Viewer.mentionSource.test.tsx` (5) drives the real Viewer — the
|
|
1165
|
+
selection composer through the Vim toolbar's type-to-comment gesture and the
|
|
1166
|
+
global composer through its button — and
|
|
1167
|
+
`components/html-viewer/HtmlViewer.mentionSource.test.tsx` (3) drives the real
|
|
1168
|
+
HtmlViewer through a bridge pinpoint message and its global button. Plus the two DOM-free pins in
|
|
1169
|
+
§3 above.
|
|
1170
|
+
|
|
876
1171
|
## Publishing & versioning
|
|
877
1172
|
|
|
1173
|
+
- **ui 0.43.1 (`mentionSource` on the viewers): `@plannotator/ui` only — `@plannotator/core` is UNCHANGED at `0.25.5`, so this publishes alone** (core 0.25.5 must already be published). Purely additive over 0.43.0: `mentionSource` on `Viewer` and `HtmlViewer` (forwarded to every comment composer each mounts) and the optional `Annotation.mentions` field the picked ids land on, set only when a source was supplied and a token survived. Nothing is removed, no new modules, no export-, share- or archive-visible change, and Plannotator passes none of it. See "`mentionSource` on the viewers (0.43.1)".
|
|
1174
|
+
- **ui 0.43.0 (host toolbar seams): `@plannotator/ui` only — `@plannotator/core` is UNCHANGED at `0.25.5`, so this publishes alone** (core 0.25.5 must already be published). Purely additive: `selectionActions` + `quickLabels` on `AnnotationToolbar` (forwarded by `Viewer`; `selectionActions` also by `HtmlViewer`), `mentionSource` on `CommentPopover`, an optional third `mentions` argument on that component's `onSubmit`, and the new supported modules `utils/selectionActions`, `utils/mentions`, `components/SelectionActionsDropdown`, `components/MentionPicker`, `hooks/useMentionAutocomplete`. Nothing is removed and Plannotator passes none of it. See "Host toolbar seams (0.43.0)".
|
|
878
1175
|
- **core 0.25.5 / ui 0.42.0 (diagram FILES, `.mmd`/`.mermaid`/`.dot`/`.gv`): additive on both packages, so the next publish is core-first.** `@plannotator/core/annotatable` gains `DiagramRenderKind`, `diagramRenderKindForPath`, `isDiagramRenderKind` and `annotateDiagramRenderKind`, and its built-in annotatable sets now include the four diagram extensions (`shouldStripFrontmatter` returns false for them — Mermaid's `--- … ---` config block is content — and they can no longer be registered through `markdownExtensions`). `@plannotator/ui` gains `diagramDocumentBlocks(text, kind)` on `utils/parser` (the ONE `code` block a whole-file diagram source renders as), `shareableDocumentMarkdown(markdown, renderAs)` on `utils/sharing`, the `DocumentRenderAs` type on `types` (`'markdown' | 'html' | DiagramRenderKind`, re-exporting core's kind), and an optional `Block.diagramSourceLineOffset` that `DiagramBlock` prefers over `Block.startLine` when resolving a diagram comment's `sourceLine` (unset on every parser-produced fence, so fences are byte-identical). `useLinkedDoc`'s `renderAs`/`setRenderAs`/`LinkedDocLoadData.renderAs` widen from `'markdown' | 'html'` to `DocumentRenderAs` — source-compatible for a host that only ever passes the old two, but a host whose own state is typed `'markdown' | 'html'` must widen its setter. Since core changes, **publish `core` first** and update UI's exact core dependency before packing ui.
|
|
879
1176
|
- **The current pair is `@plannotator/ui` `0.41.2` on `@plannotator/core` `0.25.4`.** Core is UNCHANGED from 0.41.1, so 0.41.2 publishes alone (`ui` only; core 0.25.4 must already be published). 0.41.2 ships the palette-derived Mermaid node shadow at a default of 70% (`DEFAULT_MERMAID_SHADOW_AMOUNT`) and the Settings → Display "Diagram Shadow" control (0 / 40 / 70 / 100); see "Node shadow (the one thing that is not a colour)" under "Theme-aware Mermaid diagrams (0.40.0)" for the mapping — no API removal, only additive exports (`buildMermaidShadow`, `DEFAULT_MERMAID_SHADOW_AMOUNT`, `utils/diagramShadow`).
|
|
880
1177
|
- The pair 0.41.1 shipped as was `@plannotator/ui` `0.41.1` on `@plannotator/core` `0.25.4`. Core is UNCHANGED from 0.41.0, so 0.41.1 published alone (`ui` only; core 0.25.4 must already be published). 0.41.1 is two fixes over 0.41.0 with no API change — the diagram engine is loaded lazily by the first diagram fence instead of riding every document read, and a press on the canvas's own controls no longer comments on the part behind them; see "0.41.1 — the engine is lazy, and the controls are not part of the diagram". The 0.41.0 notes below still describe the engine itself.
|
package/README.md
CHANGED
|
@@ -220,6 +220,54 @@ the in-flow host integration.
|
|
|
220
220
|
|
|
221
221
|
One renderer slot and one canvas render every Mermaid and Graphviz diagram — the fences in `Viewer` (`MermaidBlock`, `GraphvizBlock`), their popout, and whatever a host renders itself. `DiagramViewer` from `@plannotator/ui/components/diagram` takes `kind`, `source`, `theme` (`{ colorTheme, mode, shadowAmount? }`), `comments: DiagramComment[]` and `onCreateComment(anchor, text, additionalTargets)`; pass `onSave(source) => Promise<{ status: 'ok' } | { status: 'stale', currentSource }>` to get the Source pane (left of the canvas on desktop, under it on the phone; `readOnlySource` shows it without Save), and `sourceOpen` to toggle it. Zoom, pan and fit are the canvas's (wheel, drag, `+` `-` `0`, arrow keys); a click opens the composer at the part beneath while a drag pans (4 px threshold, 10 px for a finger), nothing highlights on a plain mouse-over (the ring under the pointer needs the platform modifier held), every edge carries an invisible 14 px hit path in one top layer and a click is resolved by priority over everything under the pointer (node, then edge, then cluster; an edge label is its edge), a click on no part comments on the whole diagram (kind `diagram`), sequence diagrams are addressable (actors, messages, notes, frames), a saved comment paints a ring and a numbered badge from the array order, and every comment re-resolves against each render through the engine's finder (id, then label, then unanchored — reported through `onUnanchoredChange`). The anchor is `DiagramAnchor` from `@plannotator/core/diagram-anchor` (`{ v: 1, family, kind, id | from + to, label, sourceLine }`, document lines), and Plannotator stores it as `Annotation.diagramAnchor`. Runtime slots: `utils/mermaid` (as before) and `utils/graphviz` (new, same shape, `@viz-js/viz` pinned `3.30.0`), each lazy on first use or filled by the host. `DiagramPopout` is the full-size viewer in the `PopoutDialog` chrome. See HANDOFF.md § "Diagram engine (0.41.0)" for every export, the adapter, the sanitizer and the migration notes.
|
|
222
222
|
|
|
223
|
+
### Host toolbar seams (0.43.0)
|
|
224
|
+
|
|
225
|
+
Two opt-in props for hosts that want their own commands and their own people
|
|
226
|
+
inside the annotation UI. Both default to today's behavior — pass neither and
|
|
227
|
+
the toolbar and the comment composer render byte-for-byte what they rendered
|
|
228
|
+
in 0.42.0 (proven by diffing the mounted `outerHTML` against the base commit).
|
|
229
|
+
|
|
230
|
+
- **`AnnotationToolbar` `selectionActions`** (forwarded by `Viewer` to both its
|
|
231
|
+
toolbars, and by `HtmlViewer` to the HTML selection toolbar): an array of
|
|
232
|
+
`{ id, label, detail?, icon?, onSelect(ctx) }`. They render as ONE wand
|
|
233
|
+
button (`data-selection-actions`) in the slot the quick-labels Zap occupied,
|
|
234
|
+
opening the package's dropdown below it (arrows + Enter + Escape, nothing
|
|
235
|
+
preselected until the first arrow). `onSelect` receives
|
|
236
|
+
`{ text, blockId, startOffset, endOffset, element }` — the same coordinates
|
|
237
|
+
an annotation created from that selection would carry (`blockId: ''` and
|
|
238
|
+
`startOffset: 0` on a surface with no blocks, such as raw HTML) — and the
|
|
239
|
+
toolbar closes. **The package creates no annotation:** what an action does is yours.
|
|
240
|
+
An empty array renders no button.
|
|
241
|
+
- **`AnnotationToolbar` `selectionActionsIcon`** (forwarded by `Viewer` and
|
|
242
|
+
`HtmlViewer`): the glyph on the `selectionActions` button, so a host can match
|
|
243
|
+
the wand it draws elsewhere. Absent means the package's own wand; nothing else
|
|
244
|
+
about the button changes.
|
|
245
|
+
- **`AnnotationToolbar` `quickLabels`** (default `true`): `false` hides the Zap
|
|
246
|
+
picker and makes the Alt+digit label shortcuts inert on that toolbar. The
|
|
247
|
+
one-click 👍 is unaffected, and mode state is still yours to clamp.
|
|
248
|
+
- **`CommentPopover` `mentionSource`**: `{ people, emptyNotice?,
|
|
249
|
+
onMentionsChange?, onPickBlocked? }` over `MentionPerson`
|
|
250
|
+
(`{ id, kind, label, detail, canOpen }`). Typing `@` at a word boundary opens
|
|
251
|
+
a portaled picker measured from the textarea; picking inserts the readable
|
|
252
|
+
`@Label ` token, and the ids ride `onMentionsChange(ids)` plus an optional
|
|
253
|
+
third `onSubmit(text, images?, mentions?)` argument that is passed **only**
|
|
254
|
+
when a source is supplied. Deleting a token untags that person. A
|
|
255
|
+
`canOpen: false` person inserts nothing when you supply `onPickBlocked` (your
|
|
256
|
+
no-access dialog) and inserts normally when you do not. Not wired to any
|
|
257
|
+
Plannotator data and not a `configurePlannotatorUI` seam — pass it where you
|
|
258
|
+
render the composer.
|
|
259
|
+
- **`Viewer` / `HtmlViewer` `mentionSource`** (0.43.1): the same prop on the two
|
|
260
|
+
viewers, forwarded to every comment composer each of them mounts, so a host
|
|
261
|
+
wires mentions once per surface instead of per composer. The ids the author
|
|
262
|
+
kept ride onto the created annotation as `Annotation.mentions`
|
|
263
|
+
(`readonly string[]`, present only when a source was supplied and at least
|
|
264
|
+
one token survived). It is host data: the package never renders, exports,
|
|
265
|
+
shares or archives it.
|
|
266
|
+
|
|
267
|
+
See HANDOFF.md § "Host toolbar seams (0.43.0)" for the grammar, the keyboard
|
|
268
|
+
rules and the threading points, and § "mentionSource on the viewers (0.43.1)"
|
|
269
|
+
for the two viewer props and the annotation field.
|
|
270
|
+
|
|
223
271
|
### WebMCP provider (`@plannotator/ui/webmcp`; 0.32.0)
|
|
224
272
|
|
|
225
273
|
The engine that lets a browser-integrated agent (Chrome/Edge WebMCP, `document.modelContext`) call in-page tools on a document surface. Feature-detected once; a browser without the API sees no registration, no DOM, no network, no timers. Seam: `configurePlannotatorUI({ webmcp: { enabled, namePrefix } })`, default enabled with the `plannotator.` prefix; pass `enabled: false` to keep a host page tool-free, or your own prefix to namespace the tools beside your own. There is deliberately no confirmation seam: the catalog is read-and-comment only (no approve / submit / close tools), and the agent may only edit or remove comments stamped `source: "browser-agent"`.
|
|
@@ -255,7 +303,7 @@ npm install @plannotator/ui @plannotator/core
|
|
|
255
303
|
- `@plannotator/core` — pure utils + types, zero deps, browser-safe (CI enforces no `node:` imports). Published.
|
|
256
304
|
- `@plannotator/ui` — React components/hooks + theme + `configure()`. Depends on an exact published `@plannotator/core` version. Published.
|
|
257
305
|
- `@plannotator/shared`, `@plannotator/ai` — stay private to the monorepo; `shared` re-exports `core`'s modules via shims so Plannotator's internals are untouched.
|
|
258
|
-
- Currently `@plannotator/ui` 0.
|
|
306
|
+
- Currently `@plannotator/ui` 0.43.0 depends exactly on `@plannotator/core` 0.25.5. `core` is bumped only when something under `packages/core` changes, so `ui` can advance alone. Keep the published core version exact in `packages/ui/package.json`; do not use a `workspace:` protocol there, because a directly published manifest must remain installable outside this monorepo. Bun still links the matching local workspace during development. When both packages change, publish `core` first, then build and publish the UI tarball. See HANDOFF.md "Publishing & versioning" for the verification command.
|
|
259
307
|
|
|
260
308
|
## The one rule
|
|
261
309
|
|
|
@@ -6,6 +6,8 @@ import { type QuickLabel, getQuickLabels, THUMBS_UP_LABEL } from "../utils/quick
|
|
|
6
6
|
import { copyTextToClipboard } from "../utils/clipboard";
|
|
7
7
|
import { acquireTypeToCommentCapture } from "../shortcuts/plan-review/annotationMode.shortcuts";
|
|
8
8
|
import { FloatingQuickLabelPicker } from "./FloatingQuickLabelPicker";
|
|
9
|
+
import { FloatingSelectionActionsPicker } from "./SelectionActionsDropdown";
|
|
10
|
+
import { buildSelectionActionContext, type SelectionAction } from "../utils/selectionActions";
|
|
9
11
|
|
|
10
12
|
type PositionMode = 'center-above' | 'top-right';
|
|
11
13
|
|
|
@@ -33,6 +35,27 @@ interface AnnotationToolbarProps {
|
|
|
33
35
|
* the one label affordance restored to these surfaces. Markdown surfaces
|
|
34
36
|
* keep the full toolbar. */
|
|
35
37
|
commentOnly?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Opt-in host capability: the host's own commands for this selection,
|
|
40
|
+
* rendered as ONE wand button (`data-selection-actions`) that opens the
|
|
41
|
+
* package's dropdown below it. Selecting an item calls `onSelect` with the
|
|
42
|
+
* selection context and closes the toolbar, exactly as a quick label does;
|
|
43
|
+
* the package creates no annotation — the host decides what an action means.
|
|
44
|
+
* Absent or empty → no button, no dropdown: today's toolbar.
|
|
45
|
+
*/
|
|
46
|
+
selectionActions?: SelectionAction[];
|
|
47
|
+
/**
|
|
48
|
+
* Opt-in host capability: the glyph on the `selectionActions` button, so a
|
|
49
|
+
* host can match the wand it draws elsewhere. Absent → the package's own
|
|
50
|
+
* wand. Nothing else about the button changes (name, data attribute, size).
|
|
51
|
+
*/
|
|
52
|
+
selectionActionsIcon?: React.ReactNode;
|
|
53
|
+
/**
|
|
54
|
+
* Whether the package's own quick labels are offered (default true).
|
|
55
|
+
* `false` hides the Zap picker button and the Alt+digit label shortcuts on
|
|
56
|
+
* this toolbar. The one-click 👍 is a separate affordance and is unaffected.
|
|
57
|
+
*/
|
|
58
|
+
quickLabels?: boolean;
|
|
36
59
|
/** Hide the copy button (set when a keyboard copy handler exists) */
|
|
37
60
|
hideCopyButton?: boolean;
|
|
38
61
|
/** Close toolbar when element scrolls out of viewport */
|
|
@@ -51,6 +74,9 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
|
|
|
51
74
|
onClose,
|
|
52
75
|
onRequestComment,
|
|
53
76
|
onQuickLabel,
|
|
77
|
+
selectionActions,
|
|
78
|
+
selectionActionsIcon,
|
|
79
|
+
quickLabels: quickLabelsEnabled = true,
|
|
54
80
|
copyText,
|
|
55
81
|
commentOnly = false,
|
|
56
82
|
hideCopyButton = false,
|
|
@@ -62,10 +88,24 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
|
|
|
62
88
|
const [position, setPosition] = useState<{ top: number; left?: number; right?: number } | null>(null);
|
|
63
89
|
const [copied, setCopied] = useState(false);
|
|
64
90
|
const [showQuickLabels, setShowQuickLabels] = useState(false);
|
|
91
|
+
const [showSelectionActions, setShowSelectionActions] = useState(false);
|
|
65
92
|
const toolbarRef = useRef<HTMLDivElement>(null);
|
|
66
93
|
const zapButtonRef = useRef<HTMLButtonElement>(null);
|
|
94
|
+
const actionsButtonRef = useRef<HTMLButtonElement>(null);
|
|
67
95
|
const quickLabels = useMemo(() => getQuickLabels(), []);
|
|
68
96
|
|
|
97
|
+
// A picker counts as open only while the button that opens it is actually
|
|
98
|
+
// rendered. Without this, a host whose `selectionActions` go empty (or who
|
|
99
|
+
// flips `quickLabels` off) while its dropdown is open unmounts the dropdown
|
|
100
|
+
// and its Escape handler, but leaves the flag set — and the flag is what
|
|
101
|
+
// stands the toolbar's own Escape, type-to-comment and outside-dismiss
|
|
102
|
+
// listeners down, wedging the toolbar until the ✕ is clicked. Both flags are
|
|
103
|
+
// false whenever the props are absent, so Plannotator's toolbar is
|
|
104
|
+
// unchanged.
|
|
105
|
+
const hasSelectionActions = !!selectionActions && selectionActions.length > 0;
|
|
106
|
+
const selectionActionsOpen = showSelectionActions && hasSelectionActions;
|
|
107
|
+
const quickLabelsOpen = showQuickLabels && !commentOnly && quickLabelsEnabled;
|
|
108
|
+
|
|
69
109
|
useEffect(() => { setCopied(false); }, [element]);
|
|
70
110
|
|
|
71
111
|
const handleCopy = async () => {
|
|
@@ -119,8 +159,8 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
|
|
|
119
159
|
if (e.defaultPrevented || e.isComposing) return;
|
|
120
160
|
if (isEditableElement(e.target) || isEditableElement(document.activeElement)) return;
|
|
121
161
|
|
|
122
|
-
// When picker is open, let
|
|
123
|
-
if (
|
|
162
|
+
// When a picker is open, let it own all keyboard input
|
|
163
|
+
if (quickLabelsOpen || selectionActionsOpen) return;
|
|
124
164
|
|
|
125
165
|
if (e.key === "Escape") {
|
|
126
166
|
onClose();
|
|
@@ -132,7 +172,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
|
|
|
132
172
|
const isDigit = (e.code >= 'Digit1' && e.code <= 'Digit9') || e.code === 'Digit0';
|
|
133
173
|
if (isDigit && !e.ctrlKey && !e.metaKey && e.altKey) {
|
|
134
174
|
e.preventDefault();
|
|
135
|
-
if (!commentOnly) {
|
|
175
|
+
if (!commentOnly && quickLabelsEnabled) {
|
|
136
176
|
const digit = parseInt(e.code.slice(5), 10);
|
|
137
177
|
const index = digit === 0 ? 9 : digit - 1;
|
|
138
178
|
if (index < quickLabels.length) {
|
|
@@ -158,10 +198,10 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
|
|
|
158
198
|
window.removeEventListener("keydown", handleKeyDown);
|
|
159
199
|
releaseCapture();
|
|
160
200
|
};
|
|
161
|
-
}, [onClose, onRequestComment, onQuickLabel, quickLabels,
|
|
201
|
+
}, [onClose, onRequestComment, onQuickLabel, quickLabels, quickLabelsOpen, selectionActionsOpen, commentOnly, quickLabelsEnabled]);
|
|
162
202
|
|
|
163
203
|
useDismissOnOutsideAndEscape({
|
|
164
|
-
enabled: !
|
|
204
|
+
enabled: !quickLabelsOpen && !selectionActionsOpen,
|
|
165
205
|
ref: toolbarRef,
|
|
166
206
|
onDismiss: onClose,
|
|
167
207
|
});
|
|
@@ -236,9 +276,37 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
|
|
|
236
276
|
label="Comment"
|
|
237
277
|
className="text-annotation-comment hover:bg-annotation-comment/10"
|
|
238
278
|
/>
|
|
279
|
+
{hasSelectionActions && (
|
|
280
|
+
<ToolbarButton
|
|
281
|
+
ref={actionsButtonRef}
|
|
282
|
+
onClick={() => setShowSelectionActions(prev => !prev)}
|
|
283
|
+
icon={selectionActionsIcon ?? <WandIcon />}
|
|
284
|
+
label="Actions"
|
|
285
|
+
className={selectionActionsOpen ? "text-primary bg-primary/10" : "text-primary hover:bg-primary/10"}
|
|
286
|
+
dataAttributes={{ 'data-selection-actions': 'true' }}
|
|
287
|
+
/>
|
|
288
|
+
)}
|
|
289
|
+
{selectionActionsOpen && actionsButtonRef.current && (
|
|
290
|
+
<FloatingSelectionActionsPicker
|
|
291
|
+
anchorEl={actionsButtonRef.current}
|
|
292
|
+
actions={selectionActions!}
|
|
293
|
+
onSelect={(action) => {
|
|
294
|
+
setShowSelectionActions(false);
|
|
295
|
+
// Read at invoke time, not on every render: a toolbar with no
|
|
296
|
+
// host actions must not walk the element's text at all.
|
|
297
|
+
const actionText = copyText
|
|
298
|
+
?? element.querySelector('code')?.textContent
|
|
299
|
+
?? element.textContent
|
|
300
|
+
?? '';
|
|
301
|
+
action.onSelect(buildSelectionActionContext(element, actionText));
|
|
302
|
+
onClose();
|
|
303
|
+
}}
|
|
304
|
+
onDismiss={() => setShowSelectionActions(false)}
|
|
305
|
+
/>
|
|
306
|
+
)}
|
|
239
307
|
{onQuickLabel && (
|
|
240
308
|
<>
|
|
241
|
-
{!commentOnly && (
|
|
309
|
+
{!commentOnly && quickLabelsEnabled && (
|
|
242
310
|
<ToolbarButton
|
|
243
311
|
ref={zapButtonRef}
|
|
244
312
|
onClick={() => setShowQuickLabels(prev => !prev)}
|
|
@@ -253,7 +321,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
|
|
|
253
321
|
label="Looks good"
|
|
254
322
|
className="hover:bg-green-500/10"
|
|
255
323
|
/>
|
|
256
|
-
{
|
|
324
|
+
{quickLabelsOpen && zapButtonRef.current && (
|
|
257
325
|
<FloatingQuickLabelPicker
|
|
258
326
|
anchorEl={zapButtonRef.current}
|
|
259
327
|
onSelect={(label) => {
|
|
@@ -309,6 +377,14 @@ const ZapIcon = () => (
|
|
|
309
377
|
</svg>
|
|
310
378
|
);
|
|
311
379
|
|
|
380
|
+
// One thick diagonal wand with a single four-point star at its tip: the
|
|
381
|
+
// earlier glyph carried six sparks and read as noise at 16px.
|
|
382
|
+
const WandIcon = () => (
|
|
383
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
384
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M3 21L13.5 10.5M17 3v7M13.5 6.5h7" />
|
|
385
|
+
</svg>
|
|
386
|
+
);
|
|
387
|
+
|
|
312
388
|
const CloseIcon = () => (
|
|
313
389
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
314
390
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
@@ -320,7 +396,9 @@ const ToolbarButton = React.forwardRef<HTMLButtonElement, {
|
|
|
320
396
|
icon: React.ReactNode;
|
|
321
397
|
label: string;
|
|
322
398
|
className: string;
|
|
323
|
-
|
|
399
|
+
/** Extra DOM attributes (e.g. data-selection-actions). Absent adds nothing. */
|
|
400
|
+
dataAttributes?: Record<string, string>;
|
|
401
|
+
}>(({ onClick, icon, label, className, dataAttributes }, ref) => (
|
|
324
402
|
<button
|
|
325
403
|
ref={ref}
|
|
326
404
|
// Icon-only controls: the markers are inert outside the compact touch
|
|
@@ -331,6 +409,7 @@ const ToolbarButton = React.forwardRef<HTMLButtonElement, {
|
|
|
331
409
|
onClick={onClick}
|
|
332
410
|
title={label}
|
|
333
411
|
className={`p-1.5 rounded-md transition-colors ${className}`}
|
|
412
|
+
{...dataAttributes}
|
|
334
413
|
>
|
|
335
414
|
{icon}
|
|
336
415
|
</button>
|