@grida/hud 0.1.0 → 0.2.1
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/README.md +657 -28
- package/dist/core/index.d.mts +181 -0
- package/dist/core/index.d.ts +181 -0
- package/dist/core/index.js +301 -0
- package/dist/core/index.mjs +291 -0
- package/dist/cursor-CxS8EMvm.d.mts +64 -0
- package/dist/cursor-CxS8EMvm.d.ts +64 -0
- package/dist/cursor-DW-uAPVE.mjs +57 -0
- package/dist/cursor-FGiJBdU-.js +80 -0
- package/dist/cursors/index.d.mts +98 -0
- package/dist/cursors/index.d.ts +98 -0
- package/dist/cursors/index.js +188 -0
- package/dist/cursors/index.mjs +185 -0
- package/dist/index-BrfEdWbQ.d.ts +3140 -0
- package/dist/index-Cmbe2X5b.d.mts +3140 -0
- package/dist/index.d.mts +107 -2
- package/dist/index.d.ts +107 -2
- package/dist/index.js +66 -1
- package/dist/index.mjs +3 -2
- package/dist/overlay-CVV4s3IL.d.ts +241 -0
- package/dist/overlay-dsG32baA.d.mts +241 -0
- package/dist/primitives/bedrock.d.mts +47 -0
- package/dist/primitives/bedrock.d.ts +47 -0
- package/dist/primitives/bedrock.js +71 -0
- package/dist/primitives/bedrock.mjs +65 -0
- package/dist/react.d.mts +3 -2
- package/dist/react.d.ts +2 -1
- package/dist/react.js +8 -3
- package/dist/react.mjs +8 -3
- package/dist/surface-BHQVvRFC.js +7356 -0
- package/dist/surface-NHSzUR8r.mjs +6902 -0
- package/dist/types-3wwFisZs.d.mts +296 -0
- package/dist/types-3wwFisZs.d.ts +296 -0
- package/package.json +18 -2
- package/dist/index-CBqCh-ZM.d.mts +0 -734
- package/dist/index-DRBeSiI2.d.ts +0 -734
- package/dist/surface-CNlBaEXn.js +0 -1890
- package/dist/surface-hUEeEVdL.mjs +0 -1808
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { n as CursorRenderer } from "../cursor-CxS8EMvm.js";
|
|
2
|
+
|
|
3
|
+
//#region cursors/renderer.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Build the default cursor renderer.
|
|
6
|
+
*
|
|
7
|
+
* Stateless — every call regenerates the data URL from scratch. This is
|
|
8
|
+
* cheap (`template_*` is a string-concat, `btoa` of ~600 B is
|
|
9
|
+
* sub-millisecond) AND the upstream `SurfaceState.setCursor` already
|
|
10
|
+
* gates re-emit via `cursorEquals`, which buckets `baseAngle` to
|
|
11
|
+
* `CURSOR_ANGLE_BUCKET_RAD`. Net result: this function is invoked at
|
|
12
|
+
* most once per real bucket change — caching here would be redundant
|
|
13
|
+
* and, sized wrong (the previous 64-entry LRU thrashed after ~32° of
|
|
14
|
+
* drag), actively harmful.
|
|
15
|
+
*
|
|
16
|
+
* Hosts install via `surface.setCursorRenderer(...)`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* import { cursors } from "@grida/hud/cursors";
|
|
20
|
+
* surface.setCursorRenderer(cursors.defaultRenderer());
|
|
21
|
+
*/
|
|
22
|
+
declare function defaultRenderer(): CursorRenderer;
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region cursors/encode.d.ts
|
|
25
|
+
/**
|
|
26
|
+
* SVG → `data:` URL encoding for CSS `cursor:` values.
|
|
27
|
+
*
|
|
28
|
+
* Uses base64 (`btoa`) for two reasons:
|
|
29
|
+
* - Cross-browser support is universal; URL-encoded SVG has historically had
|
|
30
|
+
* parser inconsistencies on `#` and `<`.
|
|
31
|
+
* - It's what the main editor's `cursor-data.ts` uses, so cursor visuals
|
|
32
|
+
* compare byte-identical when we A/B during the eventual migration.
|
|
33
|
+
*
|
|
34
|
+
* Browser-only — `btoa` is part of the WHATWG HTML spec, available on
|
|
35
|
+
* Worker / Window. The package's `platform: "neutral"` tsdown config means
|
|
36
|
+
* we don't pull a Node polyfill; consumers using SSR must avoid evaluating
|
|
37
|
+
* the renderer on the server (the Surface itself is client-side).
|
|
38
|
+
*/
|
|
39
|
+
/** Encode an SVG string as `data:image/svg+xml;base64,...`. */
|
|
40
|
+
declare function svgDataUrl(svg: string): string;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region cursors/templates.d.ts
|
|
43
|
+
/**
|
|
44
|
+
* SVG cursor templates — pure string templates parameterized by angle.
|
|
45
|
+
*
|
|
46
|
+
* Two families:
|
|
47
|
+
*
|
|
48
|
+
* 1. **Rotate arrow** — a curved double-arrow. Built from one template
|
|
49
|
+
* (`template_rotate`) rotated by `angle_deg` around the SVG's
|
|
50
|
+
* hotspot. Per-corner orientation comes from caller-supplied
|
|
51
|
+
* initial offsets (NW: −45°, NE: +45°, SW: −135°, SE: +135°), in
|
|
52
|
+
* Phase A. In Phase B, the host's selection rotation is added on
|
|
53
|
+
* top of that.
|
|
54
|
+
*
|
|
55
|
+
* 2. **Resize arrow** — a straight double-arrow, also built from one
|
|
56
|
+
* template (`template_resize`) rotated per cardinal direction. The
|
|
57
|
+
* 8 standard directions (N/NE/E/SE/S/SW/W/NW) map to multiples of
|
|
58
|
+
* 45° from the canonical horizontal arrow.
|
|
59
|
+
*
|
|
60
|
+
* **Fixed Grida palette** — black fill, white stroke. Cursors are not
|
|
61
|
+
* color-themed; hosts wanting custom colors install a custom
|
|
62
|
+
* `CursorRenderer` (see `renderer.ts`).
|
|
63
|
+
*
|
|
64
|
+
* **Hotspots** are documented per template — the click point inside
|
|
65
|
+
* the SVG that the OS aligns with the actual pointer position. Hotspots
|
|
66
|
+
* are fixed (SVG center) because the SVG rotates around the same point;
|
|
67
|
+
* the cursor stays anchored to the pointer regardless of angle.
|
|
68
|
+
*/
|
|
69
|
+
/** Rotate cursor SVG. `angle_deg` rotates the arrow around (13, 12). */
|
|
70
|
+
declare function template_rotate(angle_deg: number): string;
|
|
71
|
+
/**
|
|
72
|
+
* Resize cursor SVG. `angle_deg` rotates the arrow around (16, 16).
|
|
73
|
+
*
|
|
74
|
+
* For the canonical horizontal arrow pass `0`. For the 8 standard
|
|
75
|
+
* cardinal directions, see `DIRECTION_ANGLE_DEG` in `renderer.ts`.
|
|
76
|
+
*/
|
|
77
|
+
declare function template_resize(angle_deg: number): string;
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region cursors/index.d.ts
|
|
80
|
+
/**
|
|
81
|
+
* Convenience namespace export. All cursor utilities are also available
|
|
82
|
+
* as named imports — but `cursors.defaultRenderer()` reads more cleanly
|
|
83
|
+
* at host call sites, mirroring the main editor's `cursors.*` style.
|
|
84
|
+
*/
|
|
85
|
+
declare const cursors: {
|
|
86
|
+
readonly defaultRenderer: typeof defaultRenderer;
|
|
87
|
+
readonly svgDataUrl: typeof svgDataUrl;
|
|
88
|
+
readonly templates: {
|
|
89
|
+
readonly rotate: typeof template_rotate;
|
|
90
|
+
readonly resize: typeof template_resize;
|
|
91
|
+
};
|
|
92
|
+
readonly hotspots: {
|
|
93
|
+
readonly rotate: readonly [number, number];
|
|
94
|
+
readonly resize: readonly [number, number];
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
//#endregion
|
|
98
|
+
export { type CursorRenderer, cursors, defaultRenderer, svgDataUrl };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_cursor = require("../cursor-FGiJBdU-.js");
|
|
3
|
+
//#region cursors/encode.ts
|
|
4
|
+
/**
|
|
5
|
+
* SVG → `data:` URL encoding for CSS `cursor:` values.
|
|
6
|
+
*
|
|
7
|
+
* Uses base64 (`btoa`) for two reasons:
|
|
8
|
+
* - Cross-browser support is universal; URL-encoded SVG has historically had
|
|
9
|
+
* parser inconsistencies on `#` and `<`.
|
|
10
|
+
* - It's what the main editor's `cursor-data.ts` uses, so cursor visuals
|
|
11
|
+
* compare byte-identical when we A/B during the eventual migration.
|
|
12
|
+
*
|
|
13
|
+
* Browser-only — `btoa` is part of the WHATWG HTML spec, available on
|
|
14
|
+
* Worker / Window. The package's `platform: "neutral"` tsdown config means
|
|
15
|
+
* we don't pull a Node polyfill; consumers using SSR must avoid evaluating
|
|
16
|
+
* the renderer on the server (the Surface itself is client-side).
|
|
17
|
+
*/
|
|
18
|
+
/** Encode an SVG string as `data:image/svg+xml;base64,...`. */
|
|
19
|
+
function svgDataUrl(svg) {
|
|
20
|
+
return `data:image/svg+xml;base64,${btoa(svg)}`;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region cursors/templates.ts
|
|
24
|
+
/**
|
|
25
|
+
* SVG cursor templates — pure string templates parameterized by angle.
|
|
26
|
+
*
|
|
27
|
+
* Two families:
|
|
28
|
+
*
|
|
29
|
+
* 1. **Rotate arrow** — a curved double-arrow. Built from one template
|
|
30
|
+
* (`template_rotate`) rotated by `angle_deg` around the SVG's
|
|
31
|
+
* hotspot. Per-corner orientation comes from caller-supplied
|
|
32
|
+
* initial offsets (NW: −45°, NE: +45°, SW: −135°, SE: +135°), in
|
|
33
|
+
* Phase A. In Phase B, the host's selection rotation is added on
|
|
34
|
+
* top of that.
|
|
35
|
+
*
|
|
36
|
+
* 2. **Resize arrow** — a straight double-arrow, also built from one
|
|
37
|
+
* template (`template_resize`) rotated per cardinal direction. The
|
|
38
|
+
* 8 standard directions (N/NE/E/SE/S/SW/W/NW) map to multiples of
|
|
39
|
+
* 45° from the canonical horizontal arrow.
|
|
40
|
+
*
|
|
41
|
+
* **Fixed Grida palette** — black fill, white stroke. Cursors are not
|
|
42
|
+
* color-themed; hosts wanting custom colors install a custom
|
|
43
|
+
* `CursorRenderer` (see `renderer.ts`).
|
|
44
|
+
*
|
|
45
|
+
* **Hotspots** are documented per template — the click point inside
|
|
46
|
+
* the SVG that the OS aligns with the actual pointer position. Hotspots
|
|
47
|
+
* are fixed (SVG center) because the SVG rotates around the same point;
|
|
48
|
+
* the cursor stays anchored to the pointer regardless of angle.
|
|
49
|
+
*/
|
|
50
|
+
/** Rotate cursor SVG. `angle_deg` rotates the arrow around (13, 12). */
|
|
51
|
+
function template_rotate(angle_deg) {
|
|
52
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="26" height="24" fill="none"><g filter="url(#a)" transform="rotate(${angle_deg}, 13, 12), scale(0.75)"><path fill="#000" fill-rule="evenodd" d="M23 15.5h-6.43l2.65-2.79A7.72 7.72 0 0 0 13 9.5a7.72 7.72 0 0 0-6.22 3.21l2.65 2.79H3V8.75l1.74 1.83A10.5 10.5 0 0 1 13 6.5c3.32 0 6.3 1.59 8.26 4.08L23 8.75v6.75Z" clip-rule="evenodd"/><path stroke="#fff" stroke-width=".75" d="M23 15.88h.38V7.8l-.65.68-1.45 1.52A10.85 10.85 0 0 0 13 6.13c-3.3 0-6.25 1.5-8.28 3.88L3.27 8.5l-.64-.68v8.07h7.67l-.6-.64-2.43-2.55A7.32 7.32 0 0 1 13 9.88c2.3 0 4.36 1.08 5.73 2.8l-2.43 2.56-.6.63H23Z"/></g><defs><filter id="a" width="25.1" height="14.1" x=".45" y="4.95" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation=".9"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.65 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_443_204"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_443_204" result="shape"/></filter></defs></svg>`;
|
|
53
|
+
}
|
|
54
|
+
/** Hotspot for `template_rotate` — CSS-px offset from SVG top-left. */
|
|
55
|
+
const ROTATE_HOTSPOT = [12, 12];
|
|
56
|
+
/**
|
|
57
|
+
* Resize cursor SVG. `angle_deg` rotates the arrow around (16, 16).
|
|
58
|
+
*
|
|
59
|
+
* For the canonical horizontal arrow pass `0`. For the 8 standard
|
|
60
|
+
* cardinal directions, see `DIRECTION_ANGLE_DEG` in `renderer.ts`.
|
|
61
|
+
*/
|
|
62
|
+
function template_resize(angle_deg) {
|
|
63
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><g filter="url(#b)" transform="rotate(${angle_deg}, 16, 16)"><path fill="#000" stroke="#fff" stroke-width="1" stroke-linejoin="round" d="M7 16 L11 12 L11 14.5 L21 14.5 L21 12 L25 16 L21 20 L21 17.5 L11 17.5 L11 20 Z"/></g><defs><filter id="b" width="22" height="13" x="5" y="10" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation=".8"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.55 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_resize"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_resize" result="shape"/></filter></defs></svg>`;
|
|
64
|
+
}
|
|
65
|
+
/** Hotspot for `template_resize` — CSS-px offset from SVG top-left. */
|
|
66
|
+
const RESIZE_HOTSPOT = [16, 16];
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region cursors/renderer.ts
|
|
69
|
+
/**
|
|
70
|
+
* Default cursor renderer — maps `CursorIcon` → CSS `cursor:` value.
|
|
71
|
+
*
|
|
72
|
+
* Two responsibilities:
|
|
73
|
+
*
|
|
74
|
+
* 1. **Static-keyword passthrough** for cursors that don't need angle
|
|
75
|
+
* info (text, grab, crosshair, etc.) — defer to the canonical
|
|
76
|
+
* `cursorToCss` mapping shipped from `event/cursor.ts` so the
|
|
77
|
+
* fallback keyword stays the single source of truth.
|
|
78
|
+
*
|
|
79
|
+
* 2. **SVG generation** for rotation-sensitive cursors (rotate +
|
|
80
|
+
* 8 resize directions). Each cardinal direction maps to a fixed
|
|
81
|
+
* base angle; when the icon carries a `baseAngle` (radians), it
|
|
82
|
+
* composes with the base. The renderer is stateless — the upstream
|
|
83
|
+
* `SurfaceState.setCursor` already gates re-emit at the
|
|
84
|
+
* `CURSOR_ANGLE_BUCKET_RAD` level via `cursorEquals`, so this
|
|
85
|
+
* function is invoked at most once per real change.
|
|
86
|
+
*
|
|
87
|
+
* Output shape per icon:
|
|
88
|
+
*
|
|
89
|
+
* "url(data:image/svg+xml;base64,...) HOTSPOT_X HOTSPOT_Y, FALLBACK_KEYWORD"
|
|
90
|
+
*
|
|
91
|
+
* `FALLBACK_KEYWORD` is what `cursorToCss` would have returned. This
|
|
92
|
+
* means a browser that fails to load the data URL (truly impossible for
|
|
93
|
+
* inline base64, but the spec requires a fallback) lands on the
|
|
94
|
+
* existing native cursor — never on `auto`.
|
|
95
|
+
*/
|
|
96
|
+
const RESIZE_BASE_ANGLE_DEG = {
|
|
97
|
+
e: 0,
|
|
98
|
+
w: 0,
|
|
99
|
+
n: 90,
|
|
100
|
+
s: 90,
|
|
101
|
+
ne: -45,
|
|
102
|
+
sw: -45,
|
|
103
|
+
nw: 45,
|
|
104
|
+
se: 45
|
|
105
|
+
};
|
|
106
|
+
const ROTATE_BASE_ANGLE_DEG = {
|
|
107
|
+
nw: -45,
|
|
108
|
+
ne: 45,
|
|
109
|
+
se: 135,
|
|
110
|
+
sw: -135
|
|
111
|
+
};
|
|
112
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
113
|
+
/**
|
|
114
|
+
* Build the default cursor renderer.
|
|
115
|
+
*
|
|
116
|
+
* Stateless — every call regenerates the data URL from scratch. This is
|
|
117
|
+
* cheap (`template_*` is a string-concat, `btoa` of ~600 B is
|
|
118
|
+
* sub-millisecond) AND the upstream `SurfaceState.setCursor` already
|
|
119
|
+
* gates re-emit via `cursorEquals`, which buckets `baseAngle` to
|
|
120
|
+
* `CURSOR_ANGLE_BUCKET_RAD`. Net result: this function is invoked at
|
|
121
|
+
* most once per real bucket change — caching here would be redundant
|
|
122
|
+
* and, sized wrong (the previous 64-entry LRU thrashed after ~32° of
|
|
123
|
+
* drag), actively harmful.
|
|
124
|
+
*
|
|
125
|
+
* Hosts install via `surface.setCursorRenderer(...)`.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* import { cursors } from "@grida/hud/cursors";
|
|
129
|
+
* surface.setCursorRenderer(cursors.defaultRenderer());
|
|
130
|
+
*/
|
|
131
|
+
function defaultRenderer() {
|
|
132
|
+
return function render(icon) {
|
|
133
|
+
if (typeof icon === "string") return require_cursor.cursorToCss(icon);
|
|
134
|
+
if (icon.kind === "rotate") return build_rotate_css(icon.corner, bucket_to_deg(require_cursor.angleBucket(icon.baseAngle)));
|
|
135
|
+
return build_resize_css(icon.direction, bucket_to_deg(require_cursor.angleBucket(icon.baseAngle)));
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function bucket_to_deg(bucket) {
|
|
139
|
+
return bucket * require_cursor.CURSOR_ANGLE_BUCKET_RAD * RAD_TO_DEG;
|
|
140
|
+
}
|
|
141
|
+
function build_cursor_css(template, hotspot, fallback, base_deg, extra_deg) {
|
|
142
|
+
return `url(${svgDataUrl(template(base_deg + extra_deg))}) ${hotspot[0]} ${hotspot[1]}, ${fallback}`;
|
|
143
|
+
}
|
|
144
|
+
function build_rotate_css(corner, extra_angle_deg) {
|
|
145
|
+
return build_cursor_css(template_rotate, ROTATE_HOTSPOT, "crosshair", ROTATE_BASE_ANGLE_DEG[corner], extra_angle_deg);
|
|
146
|
+
}
|
|
147
|
+
function build_resize_css(direction, extra_angle_deg) {
|
|
148
|
+
return build_cursor_css(template_resize, RESIZE_HOTSPOT, `${direction}-resize`, RESIZE_BASE_ANGLE_DEG[direction], extra_angle_deg);
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region cursors/index.ts
|
|
152
|
+
/**
|
|
153
|
+
* `@grida/hud/cursors` — opt-in default cursor renderer.
|
|
154
|
+
*
|
|
155
|
+
* Hosts wire it once at construction:
|
|
156
|
+
*
|
|
157
|
+
* import { cursors } from "@grida/hud/cursors";
|
|
158
|
+
* surface.setCursorRenderer(cursors.defaultRenderer());
|
|
159
|
+
*
|
|
160
|
+
* Tree-shake invariant: nothing in `surface/`, `event/`, or
|
|
161
|
+
* `primitives/` may import from this directory. Hosts that don't import
|
|
162
|
+
* the subpath pay zero bundle cost. See `__tests__/cursors.test.ts` for
|
|
163
|
+
* the import-graph assertion that enforces this.
|
|
164
|
+
*
|
|
165
|
+
* Templates and the encoder are re-exported for hosts that want to
|
|
166
|
+
* render cursor previews in sidebar UI without going through the Surface.
|
|
167
|
+
*/
|
|
168
|
+
/**
|
|
169
|
+
* Convenience namespace export. All cursor utilities are also available
|
|
170
|
+
* as named imports — but `cursors.defaultRenderer()` reads more cleanly
|
|
171
|
+
* at host call sites, mirroring the main editor's `cursors.*` style.
|
|
172
|
+
*/
|
|
173
|
+
const cursors = {
|
|
174
|
+
defaultRenderer,
|
|
175
|
+
svgDataUrl,
|
|
176
|
+
templates: {
|
|
177
|
+
rotate: template_rotate,
|
|
178
|
+
resize: template_resize
|
|
179
|
+
},
|
|
180
|
+
hotspots: {
|
|
181
|
+
rotate: ROTATE_HOTSPOT,
|
|
182
|
+
resize: RESIZE_HOTSPOT
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
//#endregion
|
|
186
|
+
exports.cursors = cursors;
|
|
187
|
+
exports.defaultRenderer = defaultRenderer;
|
|
188
|
+
exports.svgDataUrl = svgDataUrl;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { i as cursorToCss, n as angleBucket, t as CURSOR_ANGLE_BUCKET_RAD } from "../cursor-DW-uAPVE.mjs";
|
|
2
|
+
//#region cursors/encode.ts
|
|
3
|
+
/**
|
|
4
|
+
* SVG → `data:` URL encoding for CSS `cursor:` values.
|
|
5
|
+
*
|
|
6
|
+
* Uses base64 (`btoa`) for two reasons:
|
|
7
|
+
* - Cross-browser support is universal; URL-encoded SVG has historically had
|
|
8
|
+
* parser inconsistencies on `#` and `<`.
|
|
9
|
+
* - It's what the main editor's `cursor-data.ts` uses, so cursor visuals
|
|
10
|
+
* compare byte-identical when we A/B during the eventual migration.
|
|
11
|
+
*
|
|
12
|
+
* Browser-only — `btoa` is part of the WHATWG HTML spec, available on
|
|
13
|
+
* Worker / Window. The package's `platform: "neutral"` tsdown config means
|
|
14
|
+
* we don't pull a Node polyfill; consumers using SSR must avoid evaluating
|
|
15
|
+
* the renderer on the server (the Surface itself is client-side).
|
|
16
|
+
*/
|
|
17
|
+
/** Encode an SVG string as `data:image/svg+xml;base64,...`. */
|
|
18
|
+
function svgDataUrl(svg) {
|
|
19
|
+
return `data:image/svg+xml;base64,${btoa(svg)}`;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region cursors/templates.ts
|
|
23
|
+
/**
|
|
24
|
+
* SVG cursor templates — pure string templates parameterized by angle.
|
|
25
|
+
*
|
|
26
|
+
* Two families:
|
|
27
|
+
*
|
|
28
|
+
* 1. **Rotate arrow** — a curved double-arrow. Built from one template
|
|
29
|
+
* (`template_rotate`) rotated by `angle_deg` around the SVG's
|
|
30
|
+
* hotspot. Per-corner orientation comes from caller-supplied
|
|
31
|
+
* initial offsets (NW: −45°, NE: +45°, SW: −135°, SE: +135°), in
|
|
32
|
+
* Phase A. In Phase B, the host's selection rotation is added on
|
|
33
|
+
* top of that.
|
|
34
|
+
*
|
|
35
|
+
* 2. **Resize arrow** — a straight double-arrow, also built from one
|
|
36
|
+
* template (`template_resize`) rotated per cardinal direction. The
|
|
37
|
+
* 8 standard directions (N/NE/E/SE/S/SW/W/NW) map to multiples of
|
|
38
|
+
* 45° from the canonical horizontal arrow.
|
|
39
|
+
*
|
|
40
|
+
* **Fixed Grida palette** — black fill, white stroke. Cursors are not
|
|
41
|
+
* color-themed; hosts wanting custom colors install a custom
|
|
42
|
+
* `CursorRenderer` (see `renderer.ts`).
|
|
43
|
+
*
|
|
44
|
+
* **Hotspots** are documented per template — the click point inside
|
|
45
|
+
* the SVG that the OS aligns with the actual pointer position. Hotspots
|
|
46
|
+
* are fixed (SVG center) because the SVG rotates around the same point;
|
|
47
|
+
* the cursor stays anchored to the pointer regardless of angle.
|
|
48
|
+
*/
|
|
49
|
+
/** Rotate cursor SVG. `angle_deg` rotates the arrow around (13, 12). */
|
|
50
|
+
function template_rotate(angle_deg) {
|
|
51
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="26" height="24" fill="none"><g filter="url(#a)" transform="rotate(${angle_deg}, 13, 12), scale(0.75)"><path fill="#000" fill-rule="evenodd" d="M23 15.5h-6.43l2.65-2.79A7.72 7.72 0 0 0 13 9.5a7.72 7.72 0 0 0-6.22 3.21l2.65 2.79H3V8.75l1.74 1.83A10.5 10.5 0 0 1 13 6.5c3.32 0 6.3 1.59 8.26 4.08L23 8.75v6.75Z" clip-rule="evenodd"/><path stroke="#fff" stroke-width=".75" d="M23 15.88h.38V7.8l-.65.68-1.45 1.52A10.85 10.85 0 0 0 13 6.13c-3.3 0-6.25 1.5-8.28 3.88L3.27 8.5l-.64-.68v8.07h7.67l-.6-.64-2.43-2.55A7.32 7.32 0 0 1 13 9.88c2.3 0 4.36 1.08 5.73 2.8l-2.43 2.56-.6.63H23Z"/></g><defs><filter id="a" width="25.1" height="14.1" x=".45" y="4.95" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation=".9"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.65 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_443_204"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_443_204" result="shape"/></filter></defs></svg>`;
|
|
52
|
+
}
|
|
53
|
+
/** Hotspot for `template_rotate` — CSS-px offset from SVG top-left. */
|
|
54
|
+
const ROTATE_HOTSPOT = [12, 12];
|
|
55
|
+
/**
|
|
56
|
+
* Resize cursor SVG. `angle_deg` rotates the arrow around (16, 16).
|
|
57
|
+
*
|
|
58
|
+
* For the canonical horizontal arrow pass `0`. For the 8 standard
|
|
59
|
+
* cardinal directions, see `DIRECTION_ANGLE_DEG` in `renderer.ts`.
|
|
60
|
+
*/
|
|
61
|
+
function template_resize(angle_deg) {
|
|
62
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><g filter="url(#b)" transform="rotate(${angle_deg}, 16, 16)"><path fill="#000" stroke="#fff" stroke-width="1" stroke-linejoin="round" d="M7 16 L11 12 L11 14.5 L21 14.5 L21 12 L25 16 L21 20 L21 17.5 L11 17.5 L11 20 Z"/></g><defs><filter id="b" width="22" height="13" x="5" y="10" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation=".8"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.55 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_resize"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_resize" result="shape"/></filter></defs></svg>`;
|
|
63
|
+
}
|
|
64
|
+
/** Hotspot for `template_resize` — CSS-px offset from SVG top-left. */
|
|
65
|
+
const RESIZE_HOTSPOT = [16, 16];
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region cursors/renderer.ts
|
|
68
|
+
/**
|
|
69
|
+
* Default cursor renderer — maps `CursorIcon` → CSS `cursor:` value.
|
|
70
|
+
*
|
|
71
|
+
* Two responsibilities:
|
|
72
|
+
*
|
|
73
|
+
* 1. **Static-keyword passthrough** for cursors that don't need angle
|
|
74
|
+
* info (text, grab, crosshair, etc.) — defer to the canonical
|
|
75
|
+
* `cursorToCss` mapping shipped from `event/cursor.ts` so the
|
|
76
|
+
* fallback keyword stays the single source of truth.
|
|
77
|
+
*
|
|
78
|
+
* 2. **SVG generation** for rotation-sensitive cursors (rotate +
|
|
79
|
+
* 8 resize directions). Each cardinal direction maps to a fixed
|
|
80
|
+
* base angle; when the icon carries a `baseAngle` (radians), it
|
|
81
|
+
* composes with the base. The renderer is stateless — the upstream
|
|
82
|
+
* `SurfaceState.setCursor` already gates re-emit at the
|
|
83
|
+
* `CURSOR_ANGLE_BUCKET_RAD` level via `cursorEquals`, so this
|
|
84
|
+
* function is invoked at most once per real change.
|
|
85
|
+
*
|
|
86
|
+
* Output shape per icon:
|
|
87
|
+
*
|
|
88
|
+
* "url(data:image/svg+xml;base64,...) HOTSPOT_X HOTSPOT_Y, FALLBACK_KEYWORD"
|
|
89
|
+
*
|
|
90
|
+
* `FALLBACK_KEYWORD` is what `cursorToCss` would have returned. This
|
|
91
|
+
* means a browser that fails to load the data URL (truly impossible for
|
|
92
|
+
* inline base64, but the spec requires a fallback) lands on the
|
|
93
|
+
* existing native cursor — never on `auto`.
|
|
94
|
+
*/
|
|
95
|
+
const RESIZE_BASE_ANGLE_DEG = {
|
|
96
|
+
e: 0,
|
|
97
|
+
w: 0,
|
|
98
|
+
n: 90,
|
|
99
|
+
s: 90,
|
|
100
|
+
ne: -45,
|
|
101
|
+
sw: -45,
|
|
102
|
+
nw: 45,
|
|
103
|
+
se: 45
|
|
104
|
+
};
|
|
105
|
+
const ROTATE_BASE_ANGLE_DEG = {
|
|
106
|
+
nw: -45,
|
|
107
|
+
ne: 45,
|
|
108
|
+
se: 135,
|
|
109
|
+
sw: -135
|
|
110
|
+
};
|
|
111
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
112
|
+
/**
|
|
113
|
+
* Build the default cursor renderer.
|
|
114
|
+
*
|
|
115
|
+
* Stateless — every call regenerates the data URL from scratch. This is
|
|
116
|
+
* cheap (`template_*` is a string-concat, `btoa` of ~600 B is
|
|
117
|
+
* sub-millisecond) AND the upstream `SurfaceState.setCursor` already
|
|
118
|
+
* gates re-emit via `cursorEquals`, which buckets `baseAngle` to
|
|
119
|
+
* `CURSOR_ANGLE_BUCKET_RAD`. Net result: this function is invoked at
|
|
120
|
+
* most once per real bucket change — caching here would be redundant
|
|
121
|
+
* and, sized wrong (the previous 64-entry LRU thrashed after ~32° of
|
|
122
|
+
* drag), actively harmful.
|
|
123
|
+
*
|
|
124
|
+
* Hosts install via `surface.setCursorRenderer(...)`.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* import { cursors } from "@grida/hud/cursors";
|
|
128
|
+
* surface.setCursorRenderer(cursors.defaultRenderer());
|
|
129
|
+
*/
|
|
130
|
+
function defaultRenderer() {
|
|
131
|
+
return function render(icon) {
|
|
132
|
+
if (typeof icon === "string") return cursorToCss(icon);
|
|
133
|
+
if (icon.kind === "rotate") return build_rotate_css(icon.corner, bucket_to_deg(angleBucket(icon.baseAngle)));
|
|
134
|
+
return build_resize_css(icon.direction, bucket_to_deg(angleBucket(icon.baseAngle)));
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function bucket_to_deg(bucket) {
|
|
138
|
+
return bucket * CURSOR_ANGLE_BUCKET_RAD * RAD_TO_DEG;
|
|
139
|
+
}
|
|
140
|
+
function build_cursor_css(template, hotspot, fallback, base_deg, extra_deg) {
|
|
141
|
+
return `url(${svgDataUrl(template(base_deg + extra_deg))}) ${hotspot[0]} ${hotspot[1]}, ${fallback}`;
|
|
142
|
+
}
|
|
143
|
+
function build_rotate_css(corner, extra_angle_deg) {
|
|
144
|
+
return build_cursor_css(template_rotate, ROTATE_HOTSPOT, "crosshair", ROTATE_BASE_ANGLE_DEG[corner], extra_angle_deg);
|
|
145
|
+
}
|
|
146
|
+
function build_resize_css(direction, extra_angle_deg) {
|
|
147
|
+
return build_cursor_css(template_resize, RESIZE_HOTSPOT, `${direction}-resize`, RESIZE_BASE_ANGLE_DEG[direction], extra_angle_deg);
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region cursors/index.ts
|
|
151
|
+
/**
|
|
152
|
+
* `@grida/hud/cursors` — opt-in default cursor renderer.
|
|
153
|
+
*
|
|
154
|
+
* Hosts wire it once at construction:
|
|
155
|
+
*
|
|
156
|
+
* import { cursors } from "@grida/hud/cursors";
|
|
157
|
+
* surface.setCursorRenderer(cursors.defaultRenderer());
|
|
158
|
+
*
|
|
159
|
+
* Tree-shake invariant: nothing in `surface/`, `event/`, or
|
|
160
|
+
* `primitives/` may import from this directory. Hosts that don't import
|
|
161
|
+
* the subpath pay zero bundle cost. See `__tests__/cursors.test.ts` for
|
|
162
|
+
* the import-graph assertion that enforces this.
|
|
163
|
+
*
|
|
164
|
+
* Templates and the encoder are re-exported for hosts that want to
|
|
165
|
+
* render cursor previews in sidebar UI without going through the Surface.
|
|
166
|
+
*/
|
|
167
|
+
/**
|
|
168
|
+
* Convenience namespace export. All cursor utilities are also available
|
|
169
|
+
* as named imports — but `cursors.defaultRenderer()` reads more cleanly
|
|
170
|
+
* at host call sites, mirroring the main editor's `cursors.*` style.
|
|
171
|
+
*/
|
|
172
|
+
const cursors = {
|
|
173
|
+
defaultRenderer,
|
|
174
|
+
svgDataUrl,
|
|
175
|
+
templates: {
|
|
176
|
+
rotate: template_rotate,
|
|
177
|
+
resize: template_resize
|
|
178
|
+
},
|
|
179
|
+
hotspots: {
|
|
180
|
+
rotate: ROTATE_HOTSPOT,
|
|
181
|
+
resize: RESIZE_HOTSPOT
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
//#endregion
|
|
185
|
+
export { cursors, defaultRenderer, svgDataUrl };
|