@silvery/term 0.3.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/package.json +54 -0
- package/src/adapters/canvas-adapter.ts +356 -0
- package/src/adapters/dom-adapter.ts +452 -0
- package/src/adapters/flexily-zero-adapter.ts +368 -0
- package/src/adapters/terminal-adapter.ts +305 -0
- package/src/adapters/yoga-adapter.ts +370 -0
- package/src/ansi/ansi.ts +251 -0
- package/src/ansi/constants.ts +76 -0
- package/src/ansi/detection.ts +441 -0
- package/src/ansi/hyperlink.ts +38 -0
- package/src/ansi/index.ts +201 -0
- package/src/ansi/patch-console.ts +159 -0
- package/src/ansi/sgr-codes.ts +34 -0
- package/src/ansi/storybook.ts +209 -0
- package/src/ansi/term.ts +724 -0
- package/src/ansi/types.ts +202 -0
- package/src/ansi/underline.ts +156 -0
- package/src/ansi/utils.ts +65 -0
- package/src/ansi-sanitize.ts +509 -0
- package/src/app.ts +571 -0
- package/src/bound-term.ts +94 -0
- package/src/bracketed-paste.ts +75 -0
- package/src/browser-renderer.ts +174 -0
- package/src/buffer.ts +1984 -0
- package/src/clipboard.ts +74 -0
- package/src/cursor-query.ts +85 -0
- package/src/device-attrs.ts +228 -0
- package/src/devtools.ts +123 -0
- package/src/dom/index.ts +194 -0
- package/src/errors.ts +39 -0
- package/src/focus-reporting.ts +48 -0
- package/src/hit-registry-core.ts +228 -0
- package/src/hit-registry.ts +176 -0
- package/src/index.ts +458 -0
- package/src/input.ts +119 -0
- package/src/inspector.ts +155 -0
- package/src/kitty-detect.ts +95 -0
- package/src/kitty-manager.ts +160 -0
- package/src/layout-engine.ts +296 -0
- package/src/layout.ts +26 -0
- package/src/measurer.ts +74 -0
- package/src/mode-query.ts +106 -0
- package/src/mouse-events.ts +419 -0
- package/src/mouse.ts +83 -0
- package/src/non-tty.ts +223 -0
- package/src/osc-markers.ts +32 -0
- package/src/osc-palette.ts +169 -0
- package/src/output.ts +406 -0
- package/src/pane-manager.ts +248 -0
- package/src/pipeline/CLAUDE.md +587 -0
- package/src/pipeline/content-phase-adapter.ts +976 -0
- package/src/pipeline/content-phase.ts +1765 -0
- package/src/pipeline/helpers.ts +42 -0
- package/src/pipeline/index.ts +416 -0
- package/src/pipeline/layout-phase.ts +686 -0
- package/src/pipeline/measure-phase.ts +198 -0
- package/src/pipeline/measure-stats.ts +21 -0
- package/src/pipeline/output-phase.ts +2593 -0
- package/src/pipeline/render-box.ts +343 -0
- package/src/pipeline/render-helpers.ts +243 -0
- package/src/pipeline/render-text.ts +1255 -0
- package/src/pipeline/types.ts +161 -0
- package/src/pipeline.ts +29 -0
- package/src/pixel-size.ts +119 -0
- package/src/render-adapter.ts +179 -0
- package/src/renderer.ts +1330 -0
- package/src/runtime/create-app.tsx +1845 -0
- package/src/runtime/create-buffer.ts +18 -0
- package/src/runtime/create-runtime.ts +325 -0
- package/src/runtime/diff.ts +56 -0
- package/src/runtime/event-handlers.ts +254 -0
- package/src/runtime/index.ts +119 -0
- package/src/runtime/keys.ts +8 -0
- package/src/runtime/layout.ts +164 -0
- package/src/runtime/run.tsx +318 -0
- package/src/runtime/term-provider.ts +399 -0
- package/src/runtime/terminal-lifecycle.ts +246 -0
- package/src/runtime/tick.ts +219 -0
- package/src/runtime/types.ts +210 -0
- package/src/scheduler.ts +723 -0
- package/src/screenshot.ts +57 -0
- package/src/scroll-region.ts +69 -0
- package/src/scroll-utils.ts +97 -0
- package/src/term-def.ts +267 -0
- package/src/terminal-caps.ts +5 -0
- package/src/terminal-colors.ts +216 -0
- package/src/termtest.ts +224 -0
- package/src/text-sizing.ts +109 -0
- package/src/toolbelt/index.ts +72 -0
- package/src/unicode.ts +1763 -0
- package/src/xterm/index.ts +491 -0
- package/src/xterm/xterm-provider.ts +204 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error types for silvery terminal rendering.
|
|
3
|
+
*
|
|
4
|
+
* Separated from scheduler.ts to allow React-free barrel imports.
|
|
5
|
+
* Keep this file React-free — only plain types allowed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ContentPhaseStats } from "./pipeline/types"
|
|
9
|
+
|
|
10
|
+
/** Structured mismatch data attached to the error (mirrors MismatchDebugContext shape) */
|
|
11
|
+
export interface MismatchErrorData {
|
|
12
|
+
/** Content-phase instrumentation snapshot (nodes visited/rendered/skipped, per-flag breakdown) */
|
|
13
|
+
contentPhaseStats?: ContentPhaseStats
|
|
14
|
+
/** Debug context for the mismatched cell (from debug-mismatch.ts) */
|
|
15
|
+
mismatchContext?: unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Error thrown when SILVERY_CHECK_INCREMENTAL detects a mismatch.
|
|
20
|
+
* This error should NOT be caught by general error handlers - it indicates
|
|
21
|
+
* a bug in incremental rendering that needs to be fixed.
|
|
22
|
+
*
|
|
23
|
+
* When SILVERY_STRICT fires, the error automatically includes:
|
|
24
|
+
* - Content-phase instrumentation (nodes visited/rendered/skipped, per-flag breakdown)
|
|
25
|
+
* - Cell attribution (which node owns the mismatched cell, dirty flags, scroll context)
|
|
26
|
+
*/
|
|
27
|
+
export class IncrementalRenderMismatchError extends Error {
|
|
28
|
+
/** Content-phase instrumentation snapshot */
|
|
29
|
+
contentPhaseStats?: ContentPhaseStats
|
|
30
|
+
/** Debug context for the mismatched cell */
|
|
31
|
+
mismatchContext?: unknown
|
|
32
|
+
|
|
33
|
+
constructor(message: string, data?: MismatchErrorData) {
|
|
34
|
+
super(message)
|
|
35
|
+
this.name = "IncrementalRenderMismatchError"
|
|
36
|
+
this.contentPhaseStats = data?.contentPhaseStats
|
|
37
|
+
this.mismatchContext = data?.mismatchContext
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus Reporting (CSI ?1004h)
|
|
3
|
+
*
|
|
4
|
+
* Enables/disables terminal focus-in/focus-out event reporting.
|
|
5
|
+
* When enabled, the terminal sends CSI I on focus-in and CSI O on focus-out.
|
|
6
|
+
*
|
|
7
|
+
* Protocol:
|
|
8
|
+
* - Enable: CSI ? 1004 h
|
|
9
|
+
* - Disable: CSI ? 1004 l
|
|
10
|
+
* - Focus In: CSI I (\x1b[I)
|
|
11
|
+
* - Focus Out: CSI O (\x1b[O)
|
|
12
|
+
*
|
|
13
|
+
* Supported by: xterm (v282+), Ghostty, Kitty, WezTerm, iTerm2, foot, VTE
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const CSI = "\x1b["
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Enable terminal focus reporting.
|
|
20
|
+
* After enabling, the terminal will send CSI I / CSI O sequences
|
|
21
|
+
* when the terminal window gains or loses focus.
|
|
22
|
+
*/
|
|
23
|
+
export function enableFocusReporting(write: (data: string) => void): void {
|
|
24
|
+
write(`${CSI}?1004h`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Disable terminal focus reporting.
|
|
29
|
+
*/
|
|
30
|
+
export function disableFocusReporting(write: (data: string) => void): void {
|
|
31
|
+
write(`${CSI}?1004l`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a focus event from terminal input.
|
|
36
|
+
*
|
|
37
|
+
* @param input Raw terminal input string
|
|
38
|
+
* @returns Parsed focus event, or null if not a focus sequence
|
|
39
|
+
*/
|
|
40
|
+
export function parseFocusEvent(input: string): { type: "focus-in" | "focus-out" } | null {
|
|
41
|
+
if (input.includes(`${CSI}I`)) {
|
|
42
|
+
return { type: "focus-in" }
|
|
43
|
+
}
|
|
44
|
+
if (input.includes(`${CSI}O`)) {
|
|
45
|
+
return { type: "focus-out" }
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hit Registry Core — Pure logic for mouse hit testing.
|
|
3
|
+
*
|
|
4
|
+
* This module contains the React-free core of the hit registry:
|
|
5
|
+
* types, registry class, z-index constants, and ID counter.
|
|
6
|
+
*
|
|
7
|
+
* React hooks and context live in ./hit-registry (which re-exports everything
|
|
8
|
+
* from here plus adds useHitRegion, useHitRegionCallback, HitRegistryContext).
|
|
9
|
+
*
|
|
10
|
+
* The @silvery/term barrel imports from this file to stay React-free.
|
|
11
|
+
* Consumers who need React hooks should import from @silvery/term/hit-registry.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Target type for hit testing.
|
|
20
|
+
* Each type represents a different clickable element in the UI.
|
|
21
|
+
*/
|
|
22
|
+
export interface HitTarget {
|
|
23
|
+
/** The type of element that was clicked */
|
|
24
|
+
type: "node" | "fold-toggle" | "link" | "column-header" | "scroll-area" | "button"
|
|
25
|
+
/** Column index (for column-header, or items within a column) */
|
|
26
|
+
colIndex?: number
|
|
27
|
+
/** Card index within a column */
|
|
28
|
+
cardIndex?: number
|
|
29
|
+
/** Sub-item index within a card (e.g., checklist items) */
|
|
30
|
+
subIndex?: number
|
|
31
|
+
/** Node ID for node-specific targets */
|
|
32
|
+
nodeId?: string
|
|
33
|
+
/** URL for link targets */
|
|
34
|
+
linkUrl?: string
|
|
35
|
+
/** Custom action identifier */
|
|
36
|
+
action?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A registered hit region with position, size, target, and z-index.
|
|
41
|
+
*/
|
|
42
|
+
export interface HitRegion {
|
|
43
|
+
/** X position on screen (0-indexed column) */
|
|
44
|
+
x: number
|
|
45
|
+
/** Y position on screen (0-indexed row) */
|
|
46
|
+
y: number
|
|
47
|
+
/** Width in columns */
|
|
48
|
+
width: number
|
|
49
|
+
/** Height in rows */
|
|
50
|
+
height: number
|
|
51
|
+
/** The target to return when this region is clicked */
|
|
52
|
+
target: HitTarget
|
|
53
|
+
/** Z-index for layering (higher values are on top) */
|
|
54
|
+
zIndex: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// HitRegistry Class
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Registry for managing hit regions.
|
|
63
|
+
*
|
|
64
|
+
* Components register their screen regions with targets, and the registry
|
|
65
|
+
* resolves mouse clicks to the appropriate target based on position and z-index.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const registry = new HitRegistry();
|
|
70
|
+
*
|
|
71
|
+
* // Register a card region
|
|
72
|
+
* registry.register('card-1', {
|
|
73
|
+
* x: 10, y: 5, width: 30, height: 8,
|
|
74
|
+
* target: { type: 'node', nodeId: 'abc123' },
|
|
75
|
+
* zIndex: 10
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* // Hit test a click
|
|
79
|
+
* const target = registry.hitTest(15, 7);
|
|
80
|
+
* // Returns { type: 'node', nodeId: 'abc123' }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export class HitRegistry {
|
|
84
|
+
private regions = new Map<string, HitRegion>()
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Register a hit region with a unique ID.
|
|
88
|
+
*
|
|
89
|
+
* @param id - Unique identifier for the region (used for unregistration)
|
|
90
|
+
* @param region - The region definition including position, size, target, and z-index
|
|
91
|
+
*/
|
|
92
|
+
register(id: string, region: HitRegion): void {
|
|
93
|
+
this.regions.set(id, region)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Unregister a hit region by ID.
|
|
98
|
+
*
|
|
99
|
+
* @param id - The ID used when registering the region
|
|
100
|
+
*/
|
|
101
|
+
unregister(id: string): void {
|
|
102
|
+
this.regions.delete(id)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Clear all registered regions.
|
|
107
|
+
* Useful when the UI is completely redrawn.
|
|
108
|
+
*/
|
|
109
|
+
clear(): void {
|
|
110
|
+
this.regions.clear()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get the number of registered regions.
|
|
115
|
+
* Useful for debugging.
|
|
116
|
+
*/
|
|
117
|
+
get size(): number {
|
|
118
|
+
return this.regions.size
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Test a screen position and return the highest z-index matching target.
|
|
123
|
+
*
|
|
124
|
+
* @param screenX - X position on screen (0-indexed column)
|
|
125
|
+
* @param screenY - Y position on screen (0-indexed row)
|
|
126
|
+
* @returns The target of the highest z-index region containing the point, or null if none
|
|
127
|
+
*/
|
|
128
|
+
hitTest(screenX: number, screenY: number): HitTarget | null {
|
|
129
|
+
let bestMatch: HitRegion | null = null
|
|
130
|
+
|
|
131
|
+
for (const region of this.regions.values()) {
|
|
132
|
+
// Check if point is within region bounds
|
|
133
|
+
if (
|
|
134
|
+
screenX >= region.x &&
|
|
135
|
+
screenX < region.x + region.width &&
|
|
136
|
+
screenY >= region.y &&
|
|
137
|
+
screenY < region.y + region.height
|
|
138
|
+
) {
|
|
139
|
+
// Keep the highest z-index match
|
|
140
|
+
if (!bestMatch || region.zIndex > bestMatch.zIndex) {
|
|
141
|
+
bestMatch = region
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return bestMatch?.target ?? null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get all regions that contain a point, sorted by z-index (highest first).
|
|
151
|
+
* Useful for debugging or when you need to know all overlapping elements.
|
|
152
|
+
*
|
|
153
|
+
* @param screenX - X position on screen (0-indexed column)
|
|
154
|
+
* @param screenY - Y position on screen (0-indexed row)
|
|
155
|
+
* @returns Array of matching regions, sorted by z-index descending
|
|
156
|
+
*/
|
|
157
|
+
hitTestAll(screenX: number, screenY: number): HitRegion[] {
|
|
158
|
+
const matches: HitRegion[] = []
|
|
159
|
+
|
|
160
|
+
for (const region of this.regions.values()) {
|
|
161
|
+
if (
|
|
162
|
+
screenX >= region.x &&
|
|
163
|
+
screenX < region.x + region.width &&
|
|
164
|
+
screenY >= region.y &&
|
|
165
|
+
screenY < region.y + region.height
|
|
166
|
+
) {
|
|
167
|
+
matches.push(region)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Sort by z-index descending (highest first)
|
|
172
|
+
return matches.sort((a, b) => b.zIndex - a.zIndex)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Debug helper: get all registered regions.
|
|
177
|
+
*/
|
|
178
|
+
getAllRegions(): Map<string, HitRegion> {
|
|
179
|
+
return new Map(this.regions)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// ID Counter
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Generate a unique ID for hit region registration.
|
|
189
|
+
*/
|
|
190
|
+
let hitRegionIdCounter = 0
|
|
191
|
+
export function generateHitRegionId(): string {
|
|
192
|
+
return `hit-${++hitRegionIdCounter}`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Reset the ID counter (useful for testing).
|
|
197
|
+
*/
|
|
198
|
+
export function resetHitRegionIdCounter(): void {
|
|
199
|
+
hitRegionIdCounter = 0
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// Z-Index Constants
|
|
204
|
+
// ============================================================================
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Recommended z-index values for different UI layers.
|
|
208
|
+
*/
|
|
209
|
+
export const Z_INDEX = {
|
|
210
|
+
/** Background elements */
|
|
211
|
+
BACKGROUND: 0,
|
|
212
|
+
/** Column headers */
|
|
213
|
+
COLUMN_HEADER: 5,
|
|
214
|
+
/** Cards in the main view */
|
|
215
|
+
CARD: 10,
|
|
216
|
+
/** Fold toggles (above cards for easier clicking) */
|
|
217
|
+
FOLD_TOGGLE: 15,
|
|
218
|
+
/** Links within cards */
|
|
219
|
+
LINK: 20,
|
|
220
|
+
/** Floating elements */
|
|
221
|
+
FLOATING: 50,
|
|
222
|
+
/** Modal dialogs */
|
|
223
|
+
DIALOG: 100,
|
|
224
|
+
/** Dropdown menus */
|
|
225
|
+
DROPDOWN: 150,
|
|
226
|
+
/** Tooltips */
|
|
227
|
+
TOOLTIP: 200,
|
|
228
|
+
} as const
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hit Registry for Mouse Input
|
|
3
|
+
*
|
|
4
|
+
* Provides a registry for tracking clickable regions in the terminal UI.
|
|
5
|
+
* Components register their screen positions, and mouse clicks are resolved
|
|
6
|
+
* to the appropriate target based on position and z-index.
|
|
7
|
+
*
|
|
8
|
+
* The registry uses z-index ordering to handle overlapping elements:
|
|
9
|
+
* - Dialogs (z-index 100+) take priority over cards
|
|
10
|
+
* - Cards (z-index 10) take priority over background
|
|
11
|
+
* - Background elements (z-index 0) are lowest priority
|
|
12
|
+
*
|
|
13
|
+
* This module re-exports the pure core (types, class, constants) and adds
|
|
14
|
+
* React hooks/context. Import from @silvery/term/hit-registry-core for the
|
|
15
|
+
* React-free subset.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createContext, useContext, useEffect, useRef } from "react"
|
|
19
|
+
import type { Rect } from "@silvery/tea/types"
|
|
20
|
+
|
|
21
|
+
// Re-export everything from core
|
|
22
|
+
export { HitRegistry, generateHitRegionId, resetHitRegionIdCounter, Z_INDEX } from "./hit-registry-core"
|
|
23
|
+
export type { HitTarget, HitRegion } from "./hit-registry-core"
|
|
24
|
+
|
|
25
|
+
// Import for local use
|
|
26
|
+
import { type HitTarget, type HitRegion, HitRegistry, generateHitRegionId } from "./hit-registry-core"
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// React Context
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Context for accessing the HitRegistry.
|
|
34
|
+
* Components use this to register their hit regions.
|
|
35
|
+
*/
|
|
36
|
+
export const HitRegistryContext = createContext<HitRegistry | null>(null)
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Hooks
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Hook to get the HitRegistry from context.
|
|
44
|
+
*
|
|
45
|
+
* @returns The HitRegistry instance, or null if not in a HitRegistryContext
|
|
46
|
+
*/
|
|
47
|
+
export function useHitRegistry(): HitRegistry | null {
|
|
48
|
+
return useContext(HitRegistryContext)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Hook to register a hit region based on component's screen position.
|
|
53
|
+
*
|
|
54
|
+
* Automatically registers on mount and when position changes,
|
|
55
|
+
* and unregisters on unmount.
|
|
56
|
+
*
|
|
57
|
+
* @param target - The target to return when this region is clicked
|
|
58
|
+
* @param rect - The screen rectangle (from useScreenRect or similar)
|
|
59
|
+
* @param zIndex - Z-index for layering (default: 0)
|
|
60
|
+
* @param enabled - Whether the region is active (default: true)
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```tsx
|
|
64
|
+
* function Card({ nodeId }: { nodeId: string }) {
|
|
65
|
+
* const rect = useScreenRect();
|
|
66
|
+
*
|
|
67
|
+
* useHitRegion(
|
|
68
|
+
* { type: 'node', nodeId },
|
|
69
|
+
* rect,
|
|
70
|
+
* 10 // z-index for cards
|
|
71
|
+
* );
|
|
72
|
+
*
|
|
73
|
+
* return <Box>...</Box>;
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function useHitRegion(target: HitTarget, rect: Rect | null, zIndex = 0, enabled = true): void {
|
|
78
|
+
const registry = useContext(HitRegistryContext)
|
|
79
|
+
const idRef = useRef<string | null>(null)
|
|
80
|
+
|
|
81
|
+
// Generate stable ID on first use
|
|
82
|
+
if (idRef.current === null) {
|
|
83
|
+
idRef.current = generateHitRegionId()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!registry || !rect || !enabled) {
|
|
88
|
+
// Clean up if disabled or no registry
|
|
89
|
+
if (idRef.current && registry) {
|
|
90
|
+
registry.unregister(idRef.current)
|
|
91
|
+
}
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const id = idRef.current!
|
|
96
|
+
|
|
97
|
+
// Register the region
|
|
98
|
+
registry.register(id, {
|
|
99
|
+
x: rect.x,
|
|
100
|
+
y: rect.y,
|
|
101
|
+
width: rect.width,
|
|
102
|
+
height: rect.height,
|
|
103
|
+
target,
|
|
104
|
+
zIndex,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Cleanup on unmount or when dependencies change
|
|
108
|
+
return () => {
|
|
109
|
+
registry.unregister(id)
|
|
110
|
+
}
|
|
111
|
+
}, [registry, rect?.x, rect?.y, rect?.width, rect?.height, target, zIndex, enabled])
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Hook to register a hit region using a callback for screen position.
|
|
116
|
+
*
|
|
117
|
+
* Similar to useHitRegion but works with useScreenRectCallback for
|
|
118
|
+
* better performance in large lists (avoids re-renders).
|
|
119
|
+
*
|
|
120
|
+
* @param target - The target to return when this region is clicked
|
|
121
|
+
* @param zIndex - Z-index for layering (default: 0)
|
|
122
|
+
* @param enabled - Whether the region is active (default: true)
|
|
123
|
+
* @returns A callback to pass to useScreenRectCallback
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```tsx
|
|
127
|
+
* function Card({ nodeId }: { nodeId: string }) {
|
|
128
|
+
* const onLayout = useHitRegionCallback(
|
|
129
|
+
* { type: 'node', nodeId },
|
|
130
|
+
* 10 // z-index
|
|
131
|
+
* );
|
|
132
|
+
*
|
|
133
|
+
* useScreenRectCallback(onLayout);
|
|
134
|
+
*
|
|
135
|
+
* return <Box>...</Box>;
|
|
136
|
+
* }
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export function useHitRegionCallback(target: HitTarget, zIndex = 0, enabled = true): (rect: Rect) => void {
|
|
140
|
+
const registry = useContext(HitRegistryContext)
|
|
141
|
+
const idRef = useRef<string | null>(null)
|
|
142
|
+
|
|
143
|
+
// Generate stable ID on first use
|
|
144
|
+
if (idRef.current === null) {
|
|
145
|
+
idRef.current = generateHitRegionId()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Cleanup on unmount
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const id = idRef.current
|
|
151
|
+
return () => {
|
|
152
|
+
if (id && registry) {
|
|
153
|
+
registry.unregister(id)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}, [registry])
|
|
157
|
+
|
|
158
|
+
// Return callback that updates the region
|
|
159
|
+
return (rect: Rect) => {
|
|
160
|
+
if (!registry || !enabled) {
|
|
161
|
+
if (idRef.current && registry) {
|
|
162
|
+
registry.unregister(idRef.current)
|
|
163
|
+
}
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
registry.register(idRef.current!, {
|
|
168
|
+
x: rect.x,
|
|
169
|
+
y: rect.y,
|
|
170
|
+
width: rect.width,
|
|
171
|
+
height: rect.height,
|
|
172
|
+
target,
|
|
173
|
+
zIndex,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
}
|