@cueloop/client 0.1.0-alpha.47 → 0.1.0-alpha.49
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/package.json +5 -5
- package/src/App.prototype.test.tsx +30 -0
- package/src/App.tsx +1 -0
- package/src/components/PrototypeSheet.tsx +35 -0
- package/src/kitty-image.test.ts +20 -0
- package/src/kitty-image.ts +5 -4
- package/src/prototype-browser.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.49",
|
|
4
4
|
"description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
30
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
31
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
32
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
29
|
+
"@cueloop/daemon": "0.1.0-alpha.49",
|
|
30
|
+
"@cueloop/extension-api": "0.1.0-alpha.49",
|
|
31
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.49",
|
|
32
|
+
"@cueloop/schema": "0.1.0-alpha.49",
|
|
33
33
|
"@opentui/core": "^0.5.2",
|
|
34
34
|
"@opentui/keymap": "0.5.2",
|
|
35
35
|
"@opentui/react": "^0.5.2",
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
typeText,
|
|
22
22
|
waitForState,
|
|
23
23
|
waitForText,
|
|
24
|
+
waitForTextGone,
|
|
24
25
|
} from "./test-support";
|
|
25
26
|
|
|
26
27
|
const HTML = "<main><div class='card'><h2>Pro</h2><p>$24/mo</p></div></main>";
|
|
@@ -33,6 +34,7 @@ const FAKE_ELEMENT: PrototypeElement = {
|
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
let rendered = false;
|
|
37
|
+
let scrollDeltas: number[] = [];
|
|
36
38
|
const fakeRenderer: PrototypeRenderer = {
|
|
37
39
|
viewport: { width: 1280, height: 800 },
|
|
38
40
|
screenshot: async () => {
|
|
@@ -41,6 +43,10 @@ const fakeRenderer: PrototypeRenderer = {
|
|
|
41
43
|
},
|
|
42
44
|
elementAt: async () => FAKE_ELEMENT,
|
|
43
45
|
highlight: async () => undefined,
|
|
46
|
+
scrollBy: async (deltaY) => {
|
|
47
|
+
scrollDeltas.push(deltaY);
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
44
50
|
close: async () => undefined,
|
|
45
51
|
};
|
|
46
52
|
|
|
@@ -63,6 +69,7 @@ beforeEach(() => {
|
|
|
63
69
|
},
|
|
64
70
|
});
|
|
65
71
|
rendered = false;
|
|
72
|
+
scrollDeltas = [];
|
|
66
73
|
setPrototypeRendererFactory(async () => fakeRenderer);
|
|
67
74
|
});
|
|
68
75
|
afterEach(() => {
|
|
@@ -122,4 +129,27 @@ describe("prototype review", () => {
|
|
|
122
129
|
expect(frame).toContain("tighten the padding");
|
|
123
130
|
expect(frame).toContain("COMMENT");
|
|
124
131
|
});
|
|
132
|
+
|
|
133
|
+
test("wheel over the preview scrolls the page down, then up, and clears a selection", async () => {
|
|
134
|
+
// Arrange
|
|
135
|
+
const setup = await renderApp();
|
|
136
|
+
await waitForState(setup, () => rendered);
|
|
137
|
+
await settle(setup);
|
|
138
|
+
|
|
139
|
+
// open a selection, then scroll: the scroll drops it (it would drift once
|
|
140
|
+
// the page moves under it)
|
|
141
|
+
await setup.mockMouse.click(6, 6);
|
|
142
|
+
await waitForText(setup, "comment");
|
|
143
|
+
await setup.mockMouse.scroll(6, 6, "down");
|
|
144
|
+
await waitForState(setup, () => scrollDeltas.length === 1);
|
|
145
|
+
await waitForTextGone(setup, "comment");
|
|
146
|
+
|
|
147
|
+
// Act - scroll back up
|
|
148
|
+
await setup.mockMouse.scroll(6, 6, "up");
|
|
149
|
+
await waitForState(setup, () => scrollDeltas.length === 2);
|
|
150
|
+
|
|
151
|
+
// Assert - down was a positive delta, up was negative
|
|
152
|
+
expect(scrollDeltas[0]!).toBeGreaterThan(0);
|
|
153
|
+
expect(scrollDeltas[1]!).toBeLessThan(0);
|
|
154
|
+
});
|
|
125
155
|
});
|
package/src/App.tsx
CHANGED
|
@@ -1040,6 +1040,7 @@ export function App({
|
|
|
1040
1040
|
controller.annotatePrototype(element.selector, element.quote, body)
|
|
1041
1041
|
}
|
|
1042
1042
|
onComposingChange={setPrototypeComposing}
|
|
1043
|
+
hidden={menuOpen || menuDialog !== null}
|
|
1043
1044
|
/>
|
|
1044
1045
|
) : isDiff ? (
|
|
1045
1046
|
// the sheet dims to reading-quiet colors while the wizard has focus
|
|
@@ -40,6 +40,9 @@ export interface PrototypeSheetProps {
|
|
|
40
40
|
/** Signals when the inline compose owns the keyboard, so the app suppresses
|
|
41
41
|
* its global keymap and the compose textarea receives the typed note. */
|
|
42
42
|
onComposingChange?: (active: boolean) => void;
|
|
43
|
+
/** True while an app overlay (menu, settings) covers the sheet; the image is
|
|
44
|
+
* removed so those overlays are not shown through the graphics layer. */
|
|
45
|
+
hidden?: boolean;
|
|
43
46
|
theme?: Theme;
|
|
44
47
|
}
|
|
45
48
|
|
|
@@ -47,6 +50,8 @@ type SheetStatus = "loading" | "ready" | "unsupported" | "error";
|
|
|
47
50
|
|
|
48
51
|
const POPOVER_ROWS = 3;
|
|
49
52
|
const PROTOTYPE_IMAGE_ID = 811;
|
|
53
|
+
/** Page pixels scrolled per wheel notch. */
|
|
54
|
+
const SCROLL_STEP = 240;
|
|
50
55
|
// The capture viewport width in CSS pixels; its height matches the region's
|
|
51
56
|
// cell aspect so the rendered image fills the box instead of letterboxing. The
|
|
52
57
|
// aspect comes from the terminal's reported cell pixels, falling back to a
|
|
@@ -77,6 +82,7 @@ export function PrototypeSheet({
|
|
|
77
82
|
canComment,
|
|
78
83
|
onCommentElement,
|
|
79
84
|
onComposingChange,
|
|
85
|
+
hidden = false,
|
|
80
86
|
theme,
|
|
81
87
|
}: PrototypeSheetProps): React.ReactNode {
|
|
82
88
|
const tokens = useComponentTheme(theme);
|
|
@@ -89,6 +95,10 @@ export function PrototypeSheet({
|
|
|
89
95
|
const transmittedRef = useRef<Uint8Array | null>(null);
|
|
90
96
|
const paintRef = useRef<() => void>(() => undefined);
|
|
91
97
|
const launchRef = useRef<() => void>(() => undefined);
|
|
98
|
+
const hiddenRef = useRef(hidden);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
hiddenRef.current = hidden;
|
|
101
|
+
});
|
|
92
102
|
|
|
93
103
|
const [status, setStatus] = useState<SheetStatus>("loading");
|
|
94
104
|
const [errorMessage, setErrorMessage] = useState<string>("");
|
|
@@ -117,6 +127,15 @@ export function PrototypeSheet({
|
|
|
117
127
|
typeof rawWrite === "function" ? (chunk: string) => rawWrite.call(renderer, chunk) : null;
|
|
118
128
|
const paint = (): void => {
|
|
119
129
|
if (!write) return;
|
|
130
|
+
// an app overlay is covering the sheet: pull the image so it does not show
|
|
131
|
+
// through the overlay's cells, and re-transmit once the overlay closes
|
|
132
|
+
if (hiddenRef.current) {
|
|
133
|
+
if (transmittedRef.current !== null) {
|
|
134
|
+
deleteKittyImage(write, PROTOTYPE_IMAGE_ID);
|
|
135
|
+
transmittedRef.current = null;
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
120
139
|
const region = regionOf(regionRef.current);
|
|
121
140
|
const png = pngRef.current;
|
|
122
141
|
if (!region || !png) return;
|
|
@@ -238,17 +257,32 @@ export function PrototypeSheet({
|
|
|
238
257
|
});
|
|
239
258
|
};
|
|
240
259
|
|
|
260
|
+
const onRegionScroll = (event: { scroll?: { direction: string } }): void => {
|
|
261
|
+
const browser = browserRef.current;
|
|
262
|
+
if (status !== "ready" || !browser) return;
|
|
263
|
+
const delta = event.scroll?.direction === "up" ? -SCROLL_STEP : SCROLL_STEP;
|
|
264
|
+
// a selection would drift once the page scrolls under it, so drop it
|
|
265
|
+
clearSelection();
|
|
266
|
+
void browser
|
|
267
|
+
.scrollBy(delta)
|
|
268
|
+
.then(() => refresh())
|
|
269
|
+
.catch(() => undefined);
|
|
270
|
+
};
|
|
271
|
+
|
|
241
272
|
const commit = (body: string): void => {
|
|
242
273
|
if (!canComment) return;
|
|
243
274
|
if (selected && body.trim()) onCommentElement(selected, body.trim());
|
|
244
275
|
clearSelection();
|
|
245
276
|
};
|
|
246
277
|
|
|
278
|
+
// the overlay carries an opaque fill so the popover and compose card read as
|
|
279
|
+
// solid cards over the image, matching the plan surface
|
|
247
280
|
const overlayStyle = {
|
|
248
281
|
position: "absolute" as const,
|
|
249
282
|
left: overlayCell?.left ?? 0,
|
|
250
283
|
top: Math.max(0, overlayCell?.top ?? 0),
|
|
251
284
|
flexDirection: "column" as const,
|
|
285
|
+
backgroundColor: tokens.elevated,
|
|
252
286
|
};
|
|
253
287
|
|
|
254
288
|
return (
|
|
@@ -268,6 +302,7 @@ export function PrototypeSheet({
|
|
|
268
302
|
style={{ flexGrow: 1 }}
|
|
269
303
|
onSizeChange={launch}
|
|
270
304
|
onMouseDown={onRegionMouseDown}
|
|
305
|
+
onMouseScroll={onRegionScroll}
|
|
271
306
|
/>
|
|
272
307
|
{status !== "ready" ? (
|
|
273
308
|
<box style={{ position: "absolute", left: 2, top: 1 }}>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { placeKittyImage, transmitKittyImage } from "./kitty-image";
|
|
3
|
+
|
|
4
|
+
const REGION = { column: 2, row: 3, columns: 40, rows: 12 };
|
|
5
|
+
|
|
6
|
+
describe("kitty image stacking", () => {
|
|
7
|
+
test("places the prototype below cells with painted backgrounds", () => {
|
|
8
|
+
// Arrange
|
|
9
|
+
const writes: string[] = [];
|
|
10
|
+
|
|
11
|
+
// Act
|
|
12
|
+
transmitKittyImage((chunk) => writes.push(chunk), new Uint8Array([1]), REGION, 811);
|
|
13
|
+
placeKittyImage((chunk) => writes.push(chunk), REGION, 811);
|
|
14
|
+
|
|
15
|
+
// Assert
|
|
16
|
+
expect(writes).toHaveLength(2);
|
|
17
|
+
expect(writes[0]).toContain("z=-1073741825");
|
|
18
|
+
expect(writes[1]).toContain("z=-1073741825");
|
|
19
|
+
});
|
|
20
|
+
});
|
package/src/kitty-image.ts
CHANGED
|
@@ -19,6 +19,9 @@ export type OrderedWrite = (chunk: string) => void;
|
|
|
19
19
|
|
|
20
20
|
const ESCAPE_STRING_TERMINATOR = "\x1b\\";
|
|
21
21
|
const CHUNK_BYTES = 4096;
|
|
22
|
+
// The protocol reserves values below INT32_MIN/2 for images that must sit
|
|
23
|
+
// beneath cells with painted backgrounds, such as the marker popover.
|
|
24
|
+
const IMAGE_Z_INDEX = -1_073_741_825;
|
|
22
25
|
|
|
23
26
|
/** Save the cursor, run the placement at the region's corner, restore it. */
|
|
24
27
|
function atRegion(region: CellRegion, body: string): string {
|
|
@@ -43,9 +46,7 @@ export function transmitKittyImage(
|
|
|
43
46
|
): void {
|
|
44
47
|
if (region.columns < 1 || region.rows < 1) return;
|
|
45
48
|
const base64 = Buffer.from(png).toString("base64");
|
|
46
|
-
|
|
47
|
-
// compose card (ordinary cells) still paint over it
|
|
48
|
-
const controlKeys = `a=T,f=100,i=${imageId},q=2,C=1,z=-1,c=${region.columns},r=${region.rows}`;
|
|
49
|
+
const controlKeys = `a=T,f=100,i=${imageId},q=2,C=1,z=${IMAGE_Z_INDEX},c=${region.columns},r=${region.rows}`;
|
|
49
50
|
let sequence = "";
|
|
50
51
|
for (let offset = 0; offset < base64.length; offset += CHUNK_BYTES) {
|
|
51
52
|
const chunk = base64.slice(offset, offset + CHUNK_BYTES);
|
|
@@ -62,7 +63,7 @@ export function transmitKittyImage(
|
|
|
62
63
|
export function placeKittyImage(write: OrderedWrite, region: CellRegion, imageId: number): void {
|
|
63
64
|
if (region.columns < 1 || region.rows < 1) return;
|
|
64
65
|
const command = graphicsCommand(
|
|
65
|
-
`a=p,i=${imageId},q=2,C=1,z
|
|
66
|
+
`a=p,i=${imageId},q=2,C=1,z=${IMAGE_Z_INDEX},c=${region.columns},r=${region.rows}`,
|
|
66
67
|
"",
|
|
67
68
|
);
|
|
68
69
|
write(atRegion(region, command));
|
package/src/prototype-browser.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface PrototypeRenderer {
|
|
|
31
31
|
screenshot(): Promise<Uint8Array>;
|
|
32
32
|
elementAt(cssX: number, cssY: number): Promise<PrototypeElement | null>;
|
|
33
33
|
highlight(selector: string | null): Promise<void>;
|
|
34
|
+
/** Scroll the page by a pixel delta; returns whether the scroll position moved. */
|
|
35
|
+
scrollBy(deltaY: number): Promise<boolean>;
|
|
34
36
|
close(): Promise<void>;
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -130,6 +132,9 @@ export async function launchPrototypeRenderer(options: LaunchOptions): Promise<P
|
|
|
130
132
|
async highlight(selector) {
|
|
131
133
|
await page.evaluate(highlightScript, selector);
|
|
132
134
|
},
|
|
135
|
+
async scrollBy(deltaY) {
|
|
136
|
+
return (await page.evaluate(scrollByScript, deltaY)) as boolean;
|
|
137
|
+
},
|
|
133
138
|
async close() {
|
|
134
139
|
await browser.close();
|
|
135
140
|
},
|
|
@@ -201,6 +206,12 @@ function elementAtScript(x: number, y: number): unknown {
|
|
|
201
206
|
return { selector, quote, box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
|
|
202
207
|
}
|
|
203
208
|
|
|
209
|
+
function scrollByScript(deltaY: number): boolean {
|
|
210
|
+
const before = window.scrollY;
|
|
211
|
+
window.scrollBy(0, deltaY);
|
|
212
|
+
return window.scrollY !== before;
|
|
213
|
+
}
|
|
214
|
+
|
|
204
215
|
function highlightScript(selector: string | null): void {
|
|
205
216
|
const ATTRIBUTE = "data-cueloop-selected";
|
|
206
217
|
const STYLE_ID = "cueloop-highlight-style";
|