@plannotator/ui 0.43.1 → 0.43.2
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 +3 -0
- package/README.md +3 -1
- package/components/CommentPopover.tsx +2 -0
- package/components/MentionPicker.tsx +43 -0
- package/hooks/useMentionAutocomplete.ts +3 -1
- package/package.json +1 -1
- package/styles.css +1 -1
- package/utils/mentions.ts +8 -0
package/HANDOFF.md
CHANGED
|
@@ -956,6 +956,8 @@ that state out of `Viewer`, exactly as with `AnnotationToolstrip`'s
|
|
|
956
956
|
|
|
957
957
|
### 3. `CommentPopover` `mentionSource` — an `@` mention source for the composer
|
|
958
958
|
|
|
959
|
+
> **0.43.2 additions (opt-in, byte-identical when absent):** `MentionSource.heading?: string | null` draws one non-selectable heading row (`data-mention-heading`, e.g. "People in this workspace") above the list; `MentionPerson.avatar?: { url?, initials?, tint? }` draws an avatar (`data-mention-avatar="image" | "initials"`) before the label — an `<img>` when `url` is set, else `initials` (defaulting to the label's first letter) on a disc tinted with `tint` (any CSS color; absent means the muted surface). Neither the grammar, the keyboard state machine, `onMentionsChange`, `onPickBlocked` nor `Annotation.mentions` changes.
|
|
960
|
+
|
|
959
961
|
```ts
|
|
960
962
|
import type { MentionPerson, MentionSource } from "@plannotator/ui/types";
|
|
961
963
|
// (also @plannotator/ui/utils/mentions)
|
|
@@ -971,6 +973,7 @@ interface MentionPerson {
|
|
|
971
973
|
interface MentionSource {
|
|
972
974
|
readonly people: readonly MentionPerson[];
|
|
973
975
|
readonly emptyNotice?: string | null; // honest-empty row
|
|
976
|
+
readonly heading?: string | null; // 0.43.2: heading row above the list, absent → none
|
|
974
977
|
readonly onMentionsChange?: (ids: readonly string[]) => void;
|
|
975
978
|
readonly onPickBlocked?: (person: MentionPerson) => void;
|
|
976
979
|
}
|
package/README.md
CHANGED
|
@@ -245,7 +245,9 @@ in 0.42.0 (proven by diffing the mounted `outerHTML` against the base commit).
|
|
|
245
245
|
- **`AnnotationToolbar` `quickLabels`** (default `true`): `false` hides the Zap
|
|
246
246
|
picker and makes the Alt+digit label shortcuts inert on that toolbar. The
|
|
247
247
|
one-click 👍 is unaffected, and mode state is still yours to clamp.
|
|
248
|
-
- **`CommentPopover` `mentionSource
|
|
248
|
+
- **`CommentPopover` `mentionSource`** (0.43.2 adds `heading?` above the list
|
|
249
|
+
and an optional `avatar?: { url?, initials?, tint? }` per person, drawn before
|
|
250
|
+
the label; both absent → the 0.43.1 rows): `{ people, emptyNotice?,
|
|
249
251
|
onMentionsChange?, onPickBlocked? }` over `MentionPerson`
|
|
250
252
|
(`{ id, kind, label, detail, canOpen }`). Typing `@` at a word boundary opens
|
|
251
253
|
a portaled picker measured from the textarea; picking inserts the readable
|
|
@@ -694,6 +694,7 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
|
|
|
694
694
|
id={mentionListboxId}
|
|
695
695
|
people={mentionAc.menu.items}
|
|
696
696
|
emptyNotice={mentionAc.menu.emptyNotice}
|
|
697
|
+
heading={mentionAc.menu.heading}
|
|
697
698
|
active={mentionAc.menu.activeIndex}
|
|
698
699
|
anchor={mentionAc.menu.anchor}
|
|
699
700
|
onPick={(person) => {
|
|
@@ -858,6 +859,7 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
|
|
|
858
859
|
id={mentionListboxId}
|
|
859
860
|
people={mentionAc.menu.items}
|
|
860
861
|
emptyNotice={mentionAc.menu.emptyNotice}
|
|
862
|
+
heading={mentionAc.menu.heading}
|
|
861
863
|
active={mentionAc.menu.activeIndex}
|
|
862
864
|
anchor={mentionAc.menu.anchor}
|
|
863
865
|
onPick={(person) => {
|
|
@@ -34,10 +34,45 @@ export function mentionAnchorOf(element: HTMLElement | null): MentionAnchor | nu
|
|
|
34
34
|
return { top: rect.top, bottom: rect.bottom, left: rect.left, width: rect.width };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** The optional avatar column: an image, else initials on a tinted disc. */
|
|
38
|
+
function MentionAvatar({
|
|
39
|
+
avatar,
|
|
40
|
+
label,
|
|
41
|
+
}: {
|
|
42
|
+
readonly avatar: NonNullable<MentionPerson['avatar']>;
|
|
43
|
+
readonly label: string;
|
|
44
|
+
}) {
|
|
45
|
+
if (avatar.url) {
|
|
46
|
+
return (
|
|
47
|
+
<img
|
|
48
|
+
data-mention-avatar="image"
|
|
49
|
+
src={avatar.url}
|
|
50
|
+
alt=""
|
|
51
|
+
aria-hidden="true"
|
|
52
|
+
className="h-4 w-4 shrink-0 rounded-full object-cover"
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const initials = (avatar.initials ?? label.trim().charAt(0)).slice(0, 2).toUpperCase();
|
|
57
|
+
return (
|
|
58
|
+
<span
|
|
59
|
+
data-mention-avatar="initials"
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
style={avatar.tint ? { backgroundColor: avatar.tint } : undefined}
|
|
62
|
+
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[8px] font-semibold leading-none ${
|
|
63
|
+
avatar.tint ? 'text-white' : 'bg-muted text-foreground/80'
|
|
64
|
+
}`}
|
|
65
|
+
>
|
|
66
|
+
{initials}
|
|
67
|
+
</span>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
37
71
|
export function MentionPicker({
|
|
38
72
|
id,
|
|
39
73
|
people,
|
|
40
74
|
emptyNotice,
|
|
75
|
+
heading,
|
|
41
76
|
active,
|
|
42
77
|
anchor,
|
|
43
78
|
onPick,
|
|
@@ -48,6 +83,8 @@ export function MentionPicker({
|
|
|
48
83
|
readonly people: readonly MentionPerson[];
|
|
49
84
|
/** Shown as one non-selectable row when `people` is empty. */
|
|
50
85
|
readonly emptyNotice?: string | null;
|
|
86
|
+
/** Optional heading above the list. Absent → nothing rendered. */
|
|
87
|
+
readonly heading?: string | null;
|
|
51
88
|
/** Index of the arrow-focused row, or null for "nothing preselected". */
|
|
52
89
|
readonly active: number | null;
|
|
53
90
|
readonly anchor: MentionAnchor | null;
|
|
@@ -70,6 +107,11 @@ export function MentionPicker({
|
|
|
70
107
|
style={{ position: 'fixed', left: anchor.left, width: anchor.width, ...style }}
|
|
71
108
|
className="z-[120] max-h-48 overflow-y-auto rounded-md border border-border bg-popover p-1 shadow-xl"
|
|
72
109
|
>
|
|
110
|
+
{heading && (
|
|
111
|
+
<p data-mention-heading className="px-2 pb-1 pt-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
112
|
+
{heading}
|
|
113
|
+
</p>
|
|
114
|
+
)}
|
|
73
115
|
{people.length === 0 ? (
|
|
74
116
|
<p data-mention-empty className="px-2 py-1 text-[11px] text-muted-foreground">
|
|
75
117
|
{emptyNotice}
|
|
@@ -94,6 +136,7 @@ export function MentionPicker({
|
|
|
94
136
|
active === index ? 'bg-muted text-foreground' : 'text-foreground/85 hover:bg-muted/60'
|
|
95
137
|
}`}
|
|
96
138
|
>
|
|
139
|
+
{person.avatar && <MentionAvatar avatar={person.avatar} label={person.label} />}
|
|
97
140
|
<span className="min-w-0 flex-1 truncate">{person.label}</span>
|
|
98
141
|
{person.detail && (
|
|
99
142
|
// The NAME is what the person reads; the detail gives way first.
|
|
@@ -15,6 +15,7 @@ export interface MentionMenuState {
|
|
|
15
15
|
items: readonly MentionPerson[];
|
|
16
16
|
/** Shown as one non-selectable row when `items` is empty. */
|
|
17
17
|
emptyNotice: string | null;
|
|
18
|
+
heading: string | null;
|
|
18
19
|
/** Explicitly activated row, or null — the menu opens with NOTHING active. */
|
|
19
20
|
activeIndex: number | null;
|
|
20
21
|
/** Measured textarea rect the portaled picker is placed from. */
|
|
@@ -112,6 +113,7 @@ export function useMentionAutocomplete(options: {
|
|
|
112
113
|
);
|
|
113
114
|
|
|
114
115
|
const emptyNotice = source?.emptyNotice ?? null;
|
|
116
|
+
const heading = source?.heading ?? null;
|
|
115
117
|
const open =
|
|
116
118
|
trigger !== null
|
|
117
119
|
&& trigger.from !== dismissedStart
|
|
@@ -236,7 +238,7 @@ export function useMentionAutocomplete(options: {
|
|
|
236
238
|
|
|
237
239
|
return {
|
|
238
240
|
menu: open && trigger
|
|
239
|
-
? { items, emptyNotice, activeIndex: boundedActive, anchor, query: trigger.query }
|
|
241
|
+
? { items, emptyNotice, heading, activeIndex: boundedActive, anchor, query: trigger.query }
|
|
240
242
|
: null,
|
|
241
243
|
onKeyDown,
|
|
242
244
|
onSelect: readCaret,
|