@plannotator/ui 0.35.1 → 0.36.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 +3 -2
- package/README.md +24 -1
- package/components/StickyHeaderLane.tsx +54 -18
- package/package.json +2 -2
package/HANDOFF.md
CHANGED
|
@@ -196,6 +196,7 @@ We deliberately did **not** restructure the exports map in this PR (move-don't-r
|
|
|
196
196
|
| `components/CommentPopover` | Anchor capture + comment entry. Ask-AI UI renders only if you pass `onAskAI`. |
|
|
197
197
|
| `components/AnnotationPanel` | Renders from your annotation state; no fetches of its own. |
|
|
198
198
|
| `components/AnnotationToolstrip` | The annotation mode toolstrip (Select / Pinpoint / Markup / Comment / Redline / Label). **Pass `showHelpLink={false}` in a host** — the default help modal embeds Plannotator's own YouTube walkthroughs. `hideQuickLabel` omits only the Label button (`StickyHeaderLane` forwards it, so the pinned scroll header matches); it hides the control, it does **not** clamp the mode — keep host mode state out of `'quickLabel'` (including preferences restored through `utils/editorMode`, which accepts it from storage) or text selection silently opens the quick-label picker with no visible cause. `hideInputMethodSwitch` likewise omits the pinpoint/drag switch. *(Blessed in 0.35.0.)* |
|
|
199
|
+
| `components/StickyHeaderLane` | The measured compact toolstrip + badge lane aligned beside `Viewer`'s `[data-sticky-actions]` cluster. Defaults remain hidden/inert at rest and visible only while stuck, including the incumbent hidden chrome during its fade. `visibility="always"` exposes the zero-height, absolutely positioned lane at rest without sticky chrome or document padding; the host must reserve a clear header-height region so its controls do not cover or intercept document content. `sticky={false}` uses normal-flow positioning, creates no intersection observer, and scrolls away; pass the same value to `Viewer.stickyActions` and pair it with `visibility="always"` because the default stuck-only visibility would otherwise stay hidden. The measured Viewer-actions width is intentionally retained because both clusters still share the lane at rest. Wide active-label, tight icon-only, and narrow stacked fallbacks remain measurement-driven, and `hideQuickLabel` still forwards to the compact toolstrip. |
|
|
199
200
|
| `components/ThemeProvider` | Color-mode context. |
|
|
200
201
|
| `theme-modes` (`THEME_MODES`, `Mode`) | The supported Light/Dark/System catalog and mode type. `Mode` also remains exported from `components/ThemeProvider` for compatibility with existing consumers. |
|
|
201
202
|
| `components/ImageThumbnail` / `getImageSrc` | Routes through `imageSrcResolver`. |
|
|
@@ -675,8 +676,8 @@ Additive only, but required: `@plannotator/ui` 0.32.0 imports the new `@plannota
|
|
|
675
676
|
|
|
676
677
|
## Publishing & versioning
|
|
677
678
|
|
|
678
|
-
- The current pair is `@plannotator/ui` `0.35.
|
|
679
|
-
- Recent pairs, for the consumer's install matrix: ui 0.31.0 on core 0.24.0 (lockstep), ui 0.32.0 on core 0.25.0 (lockstep, `html-anchor`),
|
|
679
|
+
- The current pair is `@plannotator/ui` `0.35.2` on `@plannotator/core` `0.25.1`. Core 0.25.1 publishes the `annotation-threads` subpath already used by `AnnotationPanel` and `utils/parser`; UI 0.35.2 pins that corrected core exactly. The UI behavior remains the `hideQuickLabel` seam introduced in 0.35.0.
|
|
680
|
+
- Recent pairs, for the consumer's install matrix: ui 0.31.0 on core 0.24.0 (lockstep), 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 on core 0.25.1. 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.
|
|
680
681
|
- 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.
|
|
681
682
|
- 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.**
|
|
682
683
|
- 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
|
@@ -137,6 +137,29 @@ The srcdoc then carries one classic `<script src>` in the exact place the inline
|
|
|
137
137
|
- **`showHelpLink={false}`** for hosts: the default help modal embeds Plannotator's own video walkthroughs.
|
|
138
138
|
- **`hideInputMethodSwitch`** omits the pinpoint/drag input-method switch.
|
|
139
139
|
|
|
140
|
+
#### Sticky header lane host props
|
|
141
|
+
|
|
142
|
+
`components/StickyHeaderLane` is the measured compact companion to `Viewer`'s
|
|
143
|
+
`[data-sticky-actions]` cluster. Its defaults preserve Plannotator's ghost-header
|
|
144
|
+
behavior: hidden and inert at rest, then visible with card chrome once stuck.
|
|
145
|
+
|
|
146
|
+
- **`visibility="always"`** keeps the existing measured left lane visible and
|
|
147
|
+
interactive at rest as well as while stuck. Resting lanes have no background,
|
|
148
|
+
border, backdrop, shadow, or new document padding; stuck lanes retain the
|
|
149
|
+
incumbent chrome. The lane remains zero-height and absolutely positioned, so
|
|
150
|
+
the host must reserve a clear header-height region; otherwise its visible
|
|
151
|
+
controls can cover and intercept interaction with document content below.
|
|
152
|
+
- **`sticky={false}`** uses non-sticky positioning, creates no intersection
|
|
153
|
+
observer, and scrolls away normally. Pass the same value to
|
|
154
|
+
`Viewer.stickyActions` so the left lane and right action cluster follow one
|
|
155
|
+
policy. By itself it leaves the default stuck-only lane permanently hidden;
|
|
156
|
+
combine it with `visibility="always"` for a visible non-sticky header. The
|
|
157
|
+
measured Viewer-actions width is still reserved because both clusters share
|
|
158
|
+
the lane at rest before they scroll away together.
|
|
159
|
+
- Wide, tight icon-only, and narrow stacked layouts continue to derive from the
|
|
160
|
+
wrapper width and the measured action-cluster width. `hideQuickLabel` is still
|
|
161
|
+
forwarded to the compact toolstrip.
|
|
162
|
+
|
|
140
163
|
### WebMCP provider (`@plannotator/ui/webmcp`; 0.32.0)
|
|
141
164
|
|
|
142
165
|
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"`.
|
|
@@ -172,7 +195,7 @@ npm install @plannotator/ui @plannotator/core
|
|
|
172
195
|
- `@plannotator/core` — pure utils + types, zero deps, browser-safe (CI enforces no `node:` imports). Published.
|
|
173
196
|
- `@plannotator/ui` — React components/hooks + theme + `configure()`. Depends on an exact published `@plannotator/core` version. Published.
|
|
174
197
|
- `@plannotator/shared`, `@plannotator/ai` — stay private to the monorepo; `shared` re-exports `core`'s modules via shims so Plannotator's internals are untouched.
|
|
175
|
-
- Currently `@plannotator/ui` 0.35.
|
|
198
|
+
- Currently `@plannotator/ui` 0.35.2 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.
|
|
176
199
|
|
|
177
200
|
## The one rule
|
|
178
201
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* StickyHeaderLane — compact "ghost" header that
|
|
2
|
+
* StickyHeaderLane — compact "ghost" header that can pin as the user scrolls
|
|
3
3
|
* past the AnnotationToolstrip.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* and badge cluster
|
|
5
|
+
* By default, the lane is invisible and non-interactive at rest, leaving the
|
|
6
|
+
* original toolstrip and badge cluster as the visible source of truth. Hosts
|
|
7
|
+
* can keep this measured lane visible at rest or let it scroll normally.
|
|
7
8
|
*
|
|
8
|
-
* Layout is driven by two ResizeObserver measurements — the
|
|
9
|
+
* Layout is driven by two ResizeObserver measurements — the lane
|
|
9
10
|
* wrapper's actual width AND the Viewer action button cluster's actual
|
|
10
11
|
* width — so the bar fits exactly into the space between its left edge
|
|
11
12
|
* and the buttons, with no fixed pixel reserves.
|
|
@@ -58,7 +59,11 @@ const GAP = 16;
|
|
|
58
59
|
const WIDE_BAR_WIDTH = 460;
|
|
59
60
|
const MIN_BAR_WIDTH = 300;
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
/** Controls when the compact header lane is visible and interactive. */
|
|
63
|
+
export type StickyHeaderLaneVisibility = 'stuck' | 'always';
|
|
64
|
+
|
|
65
|
+
/** Props for the measured compact annotation and document header lane. */
|
|
66
|
+
export interface StickyHeaderLaneProps {
|
|
62
67
|
// Toolstrip state
|
|
63
68
|
inputMethod: InputMethod;
|
|
64
69
|
onInputMethodChange: (method: InputMethod) => void;
|
|
@@ -68,6 +73,21 @@ interface StickyHeaderLaneProps {
|
|
|
68
73
|
/** Omit the Quick Label tool in the compact toolstrip (mirrors AnnotationToolstripProps.hideQuickLabel). */
|
|
69
74
|
hideQuickLabel?: boolean;
|
|
70
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Show the lane only after it sticks, or keep it visible at rest too.
|
|
78
|
+
* Defaults to `'stuck'`, preserving the incumbent ghost-header behavior.
|
|
79
|
+
* Hosts using `'always'` must reserve a clear header-height region because
|
|
80
|
+
* the lane remains zero-height and absolutely positioned over its sibling.
|
|
81
|
+
*/
|
|
82
|
+
visibility?: StickyHeaderLaneVisibility;
|
|
83
|
+
/**
|
|
84
|
+
* Keep the lane pinned while its scroll viewport moves. Pass the same value
|
|
85
|
+
* to `Viewer.stickyActions` so both measured header lanes share one policy.
|
|
86
|
+
* Defaults to true. Pair false with `visibility="always"`; the default
|
|
87
|
+
* stuck-only visibility cannot become visible when stickiness is disabled.
|
|
88
|
+
*/
|
|
89
|
+
sticky?: boolean;
|
|
90
|
+
|
|
71
91
|
// Badge state
|
|
72
92
|
repoInfo?: { display: string; branch?: string } | null;
|
|
73
93
|
planDiffStats?: PlanDiffStats | null;
|
|
@@ -91,6 +111,7 @@ interface StickyHeaderLaneProps {
|
|
|
91
111
|
remountToken?: string;
|
|
92
112
|
}
|
|
93
113
|
|
|
114
|
+
/** Render the shared, measured header lane beside the Viewer's action cluster. */
|
|
94
115
|
export const StickyHeaderLane: React.FC<StickyHeaderLaneProps> = ({
|
|
95
116
|
inputMethod,
|
|
96
117
|
onInputMethodChange,
|
|
@@ -98,6 +119,8 @@ export const StickyHeaderLane: React.FC<StickyHeaderLaneProps> = ({
|
|
|
98
119
|
onModeChange,
|
|
99
120
|
taterMode,
|
|
100
121
|
hideQuickLabel,
|
|
122
|
+
visibility = 'stuck',
|
|
123
|
+
sticky = true,
|
|
101
124
|
repoInfo,
|
|
102
125
|
planDiffStats,
|
|
103
126
|
isPlanDiffActive,
|
|
@@ -115,6 +138,12 @@ export const StickyHeaderLane: React.FC<StickyHeaderLaneProps> = ({
|
|
|
115
138
|
const [wrapperWidth, setWrapperWidth] = useState(0);
|
|
116
139
|
const [actionsWidth, setActionsWidth] = useState(0);
|
|
117
140
|
const scrollViewport = useScrollViewport();
|
|
141
|
+
const laneIsStuck = sticky && isStuck;
|
|
142
|
+
const isVisible = visibility === 'always' || laneIsStuck;
|
|
143
|
+
// Preserve the incumbent ghost lane exactly: its chrome remains mounted
|
|
144
|
+
// while the whole hidden bar fades out. Only the new always-visible mode
|
|
145
|
+
// removes chrome at rest for the supported chrome-free presentation.
|
|
146
|
+
const showChrome = visibility === 'always' ? laneIsStuck : true;
|
|
118
147
|
|
|
119
148
|
// Space available for the bar in the shared lane = wrapper width, minus
|
|
120
149
|
// the bar's left offset, minus the action buttons' measured width, minus
|
|
@@ -175,6 +204,10 @@ export const StickyHeaderLane: React.FC<StickyHeaderLaneProps> = ({
|
|
|
175
204
|
// doubles up with the still-visible toolstrip. Root is the OverlayScrollArea
|
|
176
205
|
// viewport from context, NOT <main> (which doesn't actually scroll).
|
|
177
206
|
useEffect(() => {
|
|
207
|
+
if (!sticky) {
|
|
208
|
+
setIsStuck(false);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
178
211
|
if (!sentinelRef.current || !scrollViewport) return;
|
|
179
212
|
const observer = new IntersectionObserver(
|
|
180
213
|
([entry]) => setIsStuck(!entry.isIntersecting),
|
|
@@ -186,17 +219,18 @@ export const StickyHeaderLane: React.FC<StickyHeaderLaneProps> = ({
|
|
|
186
219
|
);
|
|
187
220
|
observer.observe(sentinelRef.current);
|
|
188
221
|
return () => observer.disconnect();
|
|
189
|
-
}, [scrollViewport]);
|
|
222
|
+
}, [scrollViewport, sticky]);
|
|
190
223
|
|
|
191
224
|
return (
|
|
192
225
|
<>
|
|
193
|
-
{/* Sentinel —
|
|
194
|
-
column
|
|
195
|
-
|
|
196
|
-
<div ref={sentinelRef} aria-hidden="true" className="h-0 w-0" />
|
|
226
|
+
{/* Sentinel — present only for sticky positioning. It sits at the top of
|
|
227
|
+
the column and activates the stuck state after scrolling out of the
|
|
228
|
+
OverlayScrollArea viewport. */}
|
|
229
|
+
{sticky && <div ref={sentinelRef} aria-hidden="true" className="h-0 w-0" />}
|
|
197
230
|
|
|
198
|
-
{/*
|
|
199
|
-
|
|
231
|
+
{/* Zero-height wrapper — sticky by default, relative when sticky is
|
|
232
|
+
disabled so the absolutely positioned lane scrolls in normal flow.
|
|
233
|
+
It never pushes document content down.
|
|
200
234
|
The Viewer's outer wrapper uses z-50, so the sticky lane must
|
|
201
235
|
sit above that to paint over the card.
|
|
202
236
|
|
|
@@ -209,8 +243,8 @@ export const StickyHeaderLane: React.FC<StickyHeaderLaneProps> = ({
|
|
|
209
243
|
<div
|
|
210
244
|
ref={wrapperRef}
|
|
211
245
|
data-sticky-header-lane="true"
|
|
212
|
-
className={
|
|
213
|
-
isNarrow ? 'top-[52px] md:top-[60px]' : 'top-3'
|
|
246
|
+
className={`${sticky ? 'sticky' : 'relative'} z-[60] w-full self-center pointer-events-none ${
|
|
247
|
+
sticky ? (isNarrow ? 'top-[52px] md:top-[60px]' : 'top-3') : ''
|
|
214
248
|
}`}
|
|
215
249
|
style={maxWidth == null ? { height: 0 } : { maxWidth, height: 0 }}
|
|
216
250
|
>
|
|
@@ -227,11 +261,13 @@ export const StickyHeaderLane: React.FC<StickyHeaderLaneProps> = ({
|
|
|
227
261
|
is the final safety net so any overflow clips inside the chrome
|
|
228
262
|
rather than leaking out.
|
|
229
263
|
|
|
230
|
-
`inert` removes the bar from the tab order
|
|
264
|
+
`inert` removes the bar from the tab order whenever it is hidden. */}
|
|
231
265
|
<div
|
|
232
|
-
inert={!
|
|
233
|
-
className={`absolute left-3 md:left-5 top-0 inline-flex flex-wrap items-center gap-x-3 gap-y-1 min-w-0 overflow-hidden rounded-lg py-1 md:py-1.5
|
|
234
|
-
|
|
266
|
+
inert={!isVisible || undefined}
|
|
267
|
+
className={`absolute left-3 md:left-5 top-0 inline-flex flex-wrap items-center gap-x-3 gap-y-1 min-w-0 overflow-hidden rounded-lg py-1 md:py-1.5 ${
|
|
268
|
+
showChrome ? 'bg-card/95 backdrop-blur-sm shadow-sm border border-border/30' : ''
|
|
269
|
+
} motion-reduce:transform-none ${
|
|
270
|
+
isVisible
|
|
235
271
|
? 'opacity-100 translate-y-0 pointer-events-auto'
|
|
236
272
|
: 'opacity-0 -translate-y-1 pointer-events-none'
|
|
237
273
|
}`}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plannotator/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./components/*": "./components/*.tsx",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"@lezer/highlight": "^1.2.3",
|
|
75
75
|
"@pierre/diffs": "1.3.2",
|
|
76
76
|
"@plannotator/atomic-editor": "^0.8.0",
|
|
77
|
-
"@plannotator/core": "0.25.
|
|
77
|
+
"@plannotator/core": "0.25.1",
|
|
78
78
|
"@plannotator/markdown-editor": "^0.4.0",
|
|
79
79
|
"@plannotator/web-highlighter": "^0.8.1",
|
|
80
80
|
"@tanstack/react-table": "^8.21.3",
|