@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 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`**: `{ people, emptyNotice?,
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/ui",
3
- "version": "0.43.1",
3
+ "version": "0.43.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./components/*": "./components/*.tsx",