@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.
@@ -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 };