@plannotator/ui 0.37.0 → 0.38.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/HANDOFF.md +4 -2
- package/README.md +8 -1
- package/components/ActionMenu.tsx +22 -24
- package/components/AnnotationToolbar.tsx +3 -1
- package/components/ApproveDropdown.tsx +15 -18
- package/components/CommentPopover.tsx +9 -4
- package/components/DecisionControl.tsx +626 -0
- package/components/FloatingQuickLabelPicker.tsx +4 -1
- package/components/KeyboardShortcuts.tsx +1 -0
- package/components/PlanHeaderMenu.tsx +1 -1
- package/components/Settings.tsx +67 -6
- package/components/ToolbarButtons.tsx +31 -9
- package/components/Viewer.tsx +5 -1
- package/components/blocks/AlertBlock.tsx +51 -4
- package/config/reviewView.ts +1 -0
- package/config/settings.ts +48 -3
- package/configure.ts +9 -0
- package/hooks/useDismissablePopover.ts +65 -0
- package/hooks/useVimDocumentFocus.ts +6 -0
- package/package.json +22 -22
- package/shortcuts/decisionControl.shortcuts.ts +31 -0
- package/shortcuts/index.ts +1 -0
- package/shortcuts/plan-review/documentView.shortcuts.ts +13 -0
- package/styles.css +1 -1
- package/theme.css +11 -0
- package/utils/alertTitle.ts +69 -0
- package/utils/decisionSpec.ts +430 -0
- package/utils/platform.ts +16 -0
- package/sprite_package_additional/index.html +0 -34
- package/sprite_package_new/index.html +0 -34
- package/sprite_package_pulluphang/index.html +0 -34
package/HANDOFF.md
CHANGED
|
@@ -94,6 +94,7 @@ Pass any subset of these to `configurePlannotatorUI({ ... })`. Anything omitted
|
|
|
94
94
|
| `loadSettingsFromBackend` | `boolean` | After install, re-hydrate settings from your `storageBackend` | off |
|
|
95
95
|
| `mathRendererLoader` | `() => Promise<MathRenderer>` | How KaTeX is loaded when no renderer is registered before the first math node renders (see "Lazy renderers and eager entries"). Once registered, the package default is never called, not even as a fallback after a rejected load, and `resetMathRenderer()` keeps the registration (0.34.0); a default load already in flight at registration still fills the slot (pre-existing, see `setMathRendererLoader`), so register before the first math render | `utils/math-default-loader`'s `import('katex')`, JS only; CSS stays yours |
|
|
96
96
|
| `identityGenerator` | `() => string` | The synchronous generator behind the default "tater" display name when no `identityProvider` is installed | A built-in 16 x 16 word pool of the same `adjective-noun-tater` shape; Plannotator registers the full dictionary via `utils/identity-tater` |
|
|
97
|
+
| `alertIconRenderer` | `(name: string) => ReactNode | null` | The icon rendered for a GitHub alert whose title line carries `<!-- icon: name -->` (0.38.0; grammar in `utils/alertTitle`). Called only for a title line with an icon comment and no leading emoji; a null return falls back to the type icon | `null` for every name: the type's own icon, the package bundles no icon set |
|
|
97
98
|
|
|
98
99
|
### Interface details worth knowing
|
|
99
100
|
|
|
@@ -237,6 +238,7 @@ Don't import these in a host. Each hits hardcoded Plannotator endpoints:
|
|
|
237
238
|
- `utils/sharing` — Plannotator's public paste service (share-URL feature).
|
|
238
239
|
- `hooks/useUpdateCheck`, `components/MenuVersionSection`, `components/PlanHeaderMenu` — Plannotator release checks.
|
|
239
240
|
- `utils/planAgentInstructions`, `utils/reviewAgentInstructions` — generate agent instructions that curl Plannotator's local API.
|
|
241
|
+
- `components/DecisionControl`, `utils/decisionSpec`, `hooks/useDismissablePopover` — session decision chrome for Plannotator's own approve/deny/exit endpoints (a host's session decisions are its own outcomes against its own backend).
|
|
240
242
|
|
|
241
243
|
If Workspaces ever wants one of these surfaces, the path is the same as everything else: add a seam to the module in a Plannotator PR, don't fork the component.
|
|
242
244
|
|
|
@@ -676,8 +678,8 @@ Additive only, but required: `@plannotator/ui` 0.32.0 imports the new `@plannota
|
|
|
676
678
|
|
|
677
679
|
## Publishing & versioning
|
|
678
680
|
|
|
679
|
-
- The current pair is `@plannotator/ui` `0.
|
|
680
|
-
- Recent pairs, for the consumer's install matrix: ui 0.32.0 on core 0.25.0 (lockstep, `html-anchor`), ui 0.33.0 and ui 0.34.0 on core 0.25.0 (ui only), and ui 0.35.2, ui 0.36.0, and ui 0.
|
|
681
|
+
- The current pair is `@plannotator/ui` `0.38.0` on `@plannotator/core` `0.25.1`. UI 0.38.0 also renders a GitHub alert's bold-only first body line as its title on the icon row (an emoji on that line becomes the icon; `<!-- icon: name -->` is stripped and resolved through the new `alertIconRenderer` seam, null by default; grammar in `utils/alertTitle`, importable by a host editor so it writes the bytes the reader parses; a fenced code block inside an alert body still renders as text, deferred because nesting a `CodeBlock` inside a block interacts with the positional annotation anchors and needs its own design). UI 0.38.0 carries the whole unified decision-control stack: the internal primitives (`DecisionControl`, `utils/decisionSpec`, `hooks/useDismissablePopover` — not host-supported surface, see the unsupported list; `useDismissablePopover` also replaced the hand-rolled dismissal inside `ActionMenu`/`ApproveDropdown`, both likewise unsupported) plus one blessed-barrel addition: `decisionControlShortcuts` on `@plannotator/ui/shortcuts` (pure scope data, fetch-free, same contract as the other scopes). The removal of `ToolbarButtons`' platform-mode `muted` prop is internal — `ToolbarButtons` is not host-supported surface. UI 0.37.0 added the Viewer-owned document-header seam (a new public API, hence the minor bump; 0.36.1 was reserved for it but never published) while retaining the `hideQuickLabel` and `StickyHeaderLane` seams from the 0.35.x and 0.36.0 releases; core 0.25.1 publishes the `annotation-threads` subpath already used by `AnnotationPanel` and `utils/parser`, and UI pins that corrected core exactly.
|
|
682
|
+
- Recent pairs, for the consumer's install matrix: ui 0.32.0 on core 0.25.0 (lockstep, `html-anchor`), ui 0.33.0 and ui 0.34.0 on core 0.25.0 (ui only), and ui 0.35.2, ui 0.36.0, ui 0.37.0, and ui 0.38.0 on core 0.25.1 (0.36.1 was never published). Do not consume ui 0.35.0 externally because its published manifest contains `workspace:*`; do not consume ui 0.35.1 because its exact core 0.25.0 dependency lacks the `annotation-threads` export.
|
|
681
683
|
- When both packages change, **publish `core` first**: ui 0.32.0 imports the `@plannotator/core/html-anchor` subpath, which no earlier published core (0.24.0 and before) has, just as ui 0.29.0 needed core 0.23.0 for `@plannotator/core/annotatable`. Bump core, update UI's exact core dependency to the same new version, and run `bun install` so `bun.lock` records the new workspace versions before packing either package.
|
|
682
684
|
- The HTML annotation seams also changed the guides.show viewer **stylesheet** (five utility rules from `HtmlSurfaceControls`; the viewer JS is unchanged), so `packages/core/guide-viewer-manifest.ts` now pins a CSS hash that exists on guides.show only after the deploy workflow has published this build's `/v1/` assets. A guide exported from this build before that deploy would pin a stylesheet the host does not serve yet: **deploy guides.show before any release that ships this manifest.**
|
|
683
685
|
- UI declares the already published core version exactly in its source manifest. Do not replace it with `workspace:*`: direct publication can preserve that protocol and make the package impossible to install outside this repository. Bun links the local core workspace whenever its version matches the exact dependency. Before publishing, run `bun run --cwd packages/ui smoke:package`; it checks the source and packed manifests, required tarball subpaths, local Bun linking, and a real pnpm install in an external temporary consumer. When both packages change, publish **`core` first, then `ui`**.
|
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ configurePlannotatorUI({
|
|
|
30
30
|
webmcp, // browser-agent (WebMCP) provider policy: { enabled, namePrefix }
|
|
31
31
|
mathRendererLoader, // how KaTeX loads when no renderer is registered before first math render
|
|
32
32
|
identityGenerator, // sync generator behind the default "tater" name (no identityProvider)
|
|
33
|
+
alertIconRenderer, // (name) => ReactNode | null for a GitHub alert title line's <!-- icon: name --> (default: null, the type's icon)
|
|
33
34
|
});
|
|
34
35
|
```
|
|
35
36
|
|
|
@@ -199,6 +200,12 @@ custom scroll element must wrap Viewer in `ScrollViewportProvider` from
|
|
|
199
200
|
Without the provider, CSS page stickiness can still apply, but Viewer cannot
|
|
200
201
|
observe the host scroller to add stuck chrome or calculate anchor clearance.
|
|
201
202
|
|
|
203
|
+
The header reuses Viewer's `[data-sticky-actions]` cluster, so host CSS that
|
|
204
|
+
restyled that selector for the legacy floating bar (negative margins are the
|
|
205
|
+
common case) applies inside the header too and pulls the action cluster out of
|
|
206
|
+
its row. Scope such rules away from the header, e.g.
|
|
207
|
+
`[data-viewer-document-header] [data-sticky-actions] { margin-top: 0; margin-right: 0; }`.
|
|
208
|
+
|
|
202
209
|
The config is intentionally typed rather than a React-node slot. Viewer reuses
|
|
203
210
|
its existing `mode`, `inputMethod`, and `taterMode`; the config supplies only
|
|
204
211
|
the state-change callbacks and optional `hideQuickLabel`. Compact toolstrips
|
|
@@ -244,7 +251,7 @@ npm install @plannotator/ui @plannotator/core
|
|
|
244
251
|
- `@plannotator/core` — pure utils + types, zero deps, browser-safe (CI enforces no `node:` imports). Published.
|
|
245
252
|
- `@plannotator/ui` — React components/hooks + theme + `configure()`. Depends on an exact published `@plannotator/core` version. Published.
|
|
246
253
|
- `@plannotator/shared`, `@plannotator/ai` — stay private to the monorepo; `shared` re-exports `core`'s modules via shims so Plannotator's internals are untouched.
|
|
247
|
-
- Currently `@plannotator/ui` 0.
|
|
254
|
+
- Currently `@plannotator/ui` 0.38.0 depends exactly on `@plannotator/core` 0.25.1. `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.
|
|
248
255
|
|
|
249
256
|
## The one rule
|
|
250
257
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useCallback, useRef, useState } from 'react';
|
|
2
|
+
import { useDismissablePopover } from '../hooks/useDismissablePopover';
|
|
2
3
|
|
|
3
4
|
interface ActionMenuProps {
|
|
4
5
|
className?: string;
|
|
@@ -21,28 +22,15 @@ export const ActionMenu: React.FC<ActionMenuProps> = ({
|
|
|
21
22
|
const [isOpen, setIsOpen] = useState(false);
|
|
22
23
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
34
|
-
if (event.key === 'Escape') {
|
|
35
|
-
setIsOpen(false);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
document.addEventListener('pointerdown', handlePointerDown);
|
|
40
|
-
document.addEventListener('keydown', handleKeyDown);
|
|
41
|
-
return () => {
|
|
42
|
-
document.removeEventListener('pointerdown', handlePointerDown);
|
|
43
|
-
document.removeEventListener('keydown', handleKeyDown);
|
|
44
|
-
};
|
|
45
|
-
}, [isOpen]);
|
|
25
|
+
// Shared dismissal (outside pointerdown + Escape). The hook consumes the
|
|
26
|
+
// dismissing Escape, so closing an open Options menu no longer also runs
|
|
27
|
+
// the host app's own Escape ladder — one Escape, one rung.
|
|
28
|
+
const dismiss = useCallback(() => setIsOpen(false), []);
|
|
29
|
+
useDismissablePopover({
|
|
30
|
+
enabled: isOpen,
|
|
31
|
+
ref: menuRef,
|
|
32
|
+
onDismiss: dismiss,
|
|
33
|
+
});
|
|
46
34
|
|
|
47
35
|
return (
|
|
48
36
|
<div ref={menuRef} className={className ? `relative ${className}` : 'relative'}>
|
|
@@ -53,6 +41,7 @@ export const ActionMenu: React.FC<ActionMenuProps> = ({
|
|
|
53
41
|
|
|
54
42
|
{isOpen && (
|
|
55
43
|
<div
|
|
44
|
+
data-pn-dismissable-popover="true"
|
|
56
45
|
className={panelClassName ?? `absolute top-full right-0 mt-1 ${panelWidth === 'wide' ? 'w-64' : 'w-56'} rounded-lg border border-border bg-popover py-1 shadow-xl z-[70]`}
|
|
57
46
|
>
|
|
58
47
|
{children({ closeMenu: () => setIsOpen(false) })}
|
|
@@ -69,6 +58,12 @@ interface ActionMenuItemProps {
|
|
|
69
58
|
subtitle?: string;
|
|
70
59
|
badge?: React.ReactNode;
|
|
71
60
|
disabled?: boolean;
|
|
61
|
+
/** ARIA menu semantics for hosts that render a real `role="menu"` popover
|
|
62
|
+
* (DecisionControl). Default undefined so existing consumers are unchanged. */
|
|
63
|
+
role?: 'menuitem';
|
|
64
|
+
/** Appended to the row's classes (e.g. a tone token). Default undefined so
|
|
65
|
+
* existing consumers are byte-identical. */
|
|
66
|
+
className?: string;
|
|
72
67
|
}
|
|
73
68
|
|
|
74
69
|
export const ActionMenuItem: React.FC<ActionMenuItemProps> = ({
|
|
@@ -78,13 +73,16 @@ export const ActionMenuItem: React.FC<ActionMenuItemProps> = ({
|
|
|
78
73
|
subtitle,
|
|
79
74
|
badge,
|
|
80
75
|
disabled = false,
|
|
76
|
+
role,
|
|
77
|
+
className,
|
|
81
78
|
}) => (
|
|
82
79
|
<button
|
|
83
80
|
data-pn-touch-target
|
|
84
81
|
type="button"
|
|
85
82
|
onClick={onClick}
|
|
86
83
|
disabled={disabled}
|
|
87
|
-
|
|
84
|
+
role={role}
|
|
85
|
+
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-xs transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-45 disabled:hover:bg-transparent${className ? ` ${className}` : ''}`}
|
|
88
86
|
>
|
|
89
87
|
<span className="text-muted-foreground">{icon}</span>
|
|
90
88
|
{subtitle ? (
|
|
@@ -179,8 +179,9 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
|
|
|
179
179
|
const isCentered = position.left !== undefined;
|
|
180
180
|
const translateX = isCentered ? ' translateX(-50%)' : '';
|
|
181
181
|
|
|
182
|
-
const style: React.CSSProperties = {
|
|
182
|
+
const style: React.CSSProperties & { '--pn-annotation-toolbar-anchor-x'?: string } = {
|
|
183
183
|
top: position.top,
|
|
184
|
+
'--pn-annotation-toolbar-anchor-x': isCentered ? `${position.left}px` : undefined,
|
|
184
185
|
...(isCentered
|
|
185
186
|
? { left: position.left, transform: 'translateX(-50%)' }
|
|
186
187
|
: { right: position.right }),
|
|
@@ -192,6 +193,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
|
|
|
192
193
|
return createPortal(
|
|
193
194
|
<div
|
|
194
195
|
ref={toolbarRef}
|
|
196
|
+
data-pn-annotation-toolbar-centered={isCentered ? 'true' : undefined}
|
|
195
197
|
className="annotation-toolbar fixed z-[100] bg-popover border border-border rounded-lg shadow-2xl"
|
|
196
198
|
style={style}
|
|
197
199
|
onMouseDown={(e) => e.stopPropagation()}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { useState, useRef,
|
|
1
|
+
import React, { useState, useRef, useCallback } from 'react';
|
|
2
2
|
import type { Agent } from '../hooks/useAgents';
|
|
3
|
+
import { useDismissablePopover } from '../hooks/useDismissablePopover';
|
|
3
4
|
import { getAgentSwitchSettings, saveAgentSwitchSettings, type AgentSwitchSettings } from '../utils/agentSwitch';
|
|
4
5
|
|
|
5
6
|
interface ApproveDropdownProps {
|
|
@@ -40,22 +41,15 @@ export const ApproveDropdown: React.FC<ApproveDropdownProps> = ({
|
|
|
40
41
|
const [isOpen, setIsOpen] = useState(false);
|
|
41
42
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
document.addEventListener('pointerdown', handleClickOutside);
|
|
53
|
-
document.addEventListener('keydown', handleEscape);
|
|
54
|
-
return () => {
|
|
55
|
-
document.removeEventListener('pointerdown', handleClickOutside);
|
|
56
|
-
document.removeEventListener('keydown', handleEscape);
|
|
57
|
-
};
|
|
58
|
-
}, []);
|
|
44
|
+
// Shared dismissal (outside pointerdown + Escape), active only while open —
|
|
45
|
+
// the old always-on listeners were no-ops when closed. The hook consumes the
|
|
46
|
+
// dismissing Escape so it cannot double as a host-ladder Escape.
|
|
47
|
+
const dismiss = useCallback(() => setIsOpen(false), []);
|
|
48
|
+
useDismissablePopover({
|
|
49
|
+
enabled: isOpen,
|
|
50
|
+
ref: dropdownRef,
|
|
51
|
+
onDismiss: dismiss,
|
|
52
|
+
});
|
|
59
53
|
|
|
60
54
|
const handleSelect = (newSetting: AgentSwitchSettings) => {
|
|
61
55
|
setSetting(newSetting);
|
|
@@ -120,7 +114,10 @@ export const ApproveDropdown: React.FC<ApproveDropdownProps> = ({
|
|
|
120
114
|
|
|
121
115
|
{/* Dropdown */}
|
|
122
116
|
{isOpen && (
|
|
123
|
-
<div
|
|
117
|
+
<div
|
|
118
|
+
data-pn-dismissable-popover="true"
|
|
119
|
+
className="absolute right-0 top-full mt-1 w-52 rounded-lg border border-border bg-popover shadow-xl z-[70] overflow-hidden py-1"
|
|
120
|
+
>
|
|
124
121
|
<div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-muted-foreground/70 font-medium">
|
|
125
122
|
Switch to agent
|
|
126
123
|
</div>
|
|
@@ -529,6 +529,7 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
|
|
|
529
529
|
hasUnsavedContent ||
|
|
530
530
|
(allowEmptySubmit && initialText.trim().length > 0);
|
|
531
531
|
const canAskAI = !!onAskAI && !askAIDisabled && text.trim().length > 0;
|
|
532
|
+
const showsSkillMenu = skillAc.menu !== null;
|
|
532
533
|
|
|
533
534
|
// Shared by both footers. Disabled once anything is typed or attached so a
|
|
534
535
|
// click can never discard a draft; with content present, Save is the path.
|
|
@@ -570,7 +571,9 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
|
|
|
570
571
|
aria-modal="true"
|
|
571
572
|
aria-label={isGlobal ? 'Global comment' : 'Comment'}
|
|
572
573
|
tabIndex={-1}
|
|
573
|
-
className=
|
|
574
|
+
className={`relative w-full max-w-xl max-h-full min-h-0 bg-popover border border-border rounded-xl shadow-2xl flex flex-col ${
|
|
575
|
+
showsSkillMenu ? 'overflow-visible' : 'overflow-hidden'
|
|
576
|
+
}`}
|
|
574
577
|
style={{
|
|
575
578
|
animation: 'comment-dialog-in 0.15s ease-out',
|
|
576
579
|
}}
|
|
@@ -617,7 +620,9 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
|
|
|
617
620
|
{chipsRow}
|
|
618
621
|
|
|
619
622
|
{/* Textarea */}
|
|
620
|
-
<div className=
|
|
623
|
+
<div className={`relative px-4 py-3 min-h-0 flex-1 ${
|
|
624
|
+
showsSkillMenu ? 'overflow-visible' : 'overflow-y-auto'
|
|
625
|
+
}`}>
|
|
621
626
|
{skillAc.menu && (
|
|
622
627
|
<SkillReferenceMenu
|
|
623
628
|
id={skillListboxId}
|
|
@@ -713,14 +718,14 @@ export const CommentPopover: React.FC<CommentPopoverProps> = ({
|
|
|
713
718
|
left: dragPosition.left,
|
|
714
719
|
width: position.width,
|
|
715
720
|
maxHeight: visibleBounds.height,
|
|
716
|
-
overflowY: 'auto',
|
|
721
|
+
overflowY: showsSkillMenu ? 'visible' : 'auto',
|
|
717
722
|
}
|
|
718
723
|
: {
|
|
719
724
|
top: position.top,
|
|
720
725
|
left: position.left,
|
|
721
726
|
width: position.width,
|
|
722
727
|
maxHeight: position.maxHeight,
|
|
723
|
-
overflowY: 'auto',
|
|
728
|
+
overflowY: showsSkillMenu ? 'visible' : 'auto',
|
|
724
729
|
...(position.flipAbove ? { transform: 'translateY(-100%)' } : {}),
|
|
725
730
|
animation: position.flipAbove
|
|
726
731
|
? 'comment-popover-in-above 0.15s ease-out'
|