import type { Integration, IntegrationContext } from "../contracts/index.ts"; import { compileCss } from "../views/css/index.ts"; /** The states the utilities know. */ export const STATES = [ "open", "closed", "active", "disabled", "loading", "checked", "selected", ] as const; /** The size of the grid that tracks the pointer. */ export interface TrackerOptions { /** Columns of the grid. */ columns?: number; /** Rows of the grid. */ rows?: number; /** Extra grid detail, multiplied into each axis. */ detail?: number; /** Enables the CSS grid to receive pointer events. Disabled by default. */ capture?: boolean; /** Stable name for a tracker mounted inside one component. */ scope?: string; } /** The list of a range, used by the loops of the stylesheet. */ function range(count: number): string { return Array.from({ length: count }, (_, index) => index + 1).join(" "); } /** * The stylesheet of the integration. * * The mouse position is tracked by a grid of cells, the parent hack moves the * custom properties of the root element: every cell knows its position, the * `:has()` selector of the parent picks the hovered one. */ export function reactStylesheet(options: TrackerOptions = {}): string { const detail = Math.max(1, options.detail ?? 1); const columns = (options.columns ?? 10) * detail; const rows = (options.rows ?? 10) * detail; const states = STATES.join(" "); return ` $columns: ${columns}; $rows: ${rows}; @each $state in ${states} { .state-#{$state} { --muddle-state: #{$state}; } [data-state="#{$state}"] { --muddle-state: #{$state}; } } [data-state] { --muddle-state-opacity: 1; --muddle-state-visibility: visible; } [data-state="closed"] { --muddle-state-opacity: 0; --muddle-state-visibility: hidden; } [data-state="disabled"] { --muddle-state-opacity: 0.5; } [data-state="loading"] { --muddle-state-opacity: 0.7; } .state-open { --muddle-state-opacity: 1; --muddle-state-visibility: visible; } .state-closed { --muddle-state-opacity: 0; --muddle-state-visibility: hidden; } .peer:checked ~ [data-state="checked"] { display: block; } .peer:not(:checked) ~ [data-state="unchecked"] { display: none; } `; } /** CSS for one explicitly mounted tracker. */ export function mouseTrackerCss(options: TrackerOptions = {}): string { const detail = Math.max(1, options.detail ?? 1); const columns = (options.columns ?? 10) * detail; const rows = (options.rows ?? 10) * detail; const scope = options.scope ?? "default"; const selector = `[data-muddle-tracker="${scope}"]`; const rules = Array.from({ length: columns * rows }, (_, index) => { const x = Math.floor(index / rows) + 1; const y = (index % rows) + 1; return `${selector}:has(.mouse-tracker__cell:nth-child(${index + 1}):hover) { --muddle-mouse-x: ${x / columns * 100}%; --muddle-mouse-y: ${y / rows * 100}%; }`; }); return `${selector} { position: relative; --muddle-mouse-x: 50%; --muddle-mouse-y: 50%; }\n${selector} > .mouse-tracker { position: absolute; inset: 0; display: grid; grid-template-columns: repeat(${columns}, 1fr); grid-template-rows: repeat(${rows}, 1fr); pointer-events: none; }\n${selector}[data-muddle-capture="true"] > .mouse-tracker .mouse-tracker__cell { pointer-events: auto; }\n${rules.join("\n")}`; } /** The compiled stylesheet of the integration. */ export function reactCss(options: TrackerOptions = {}): string { return compileCss(reactStylesheet(options)); } /** The markup of the grid that tracks the pointer. */ export function mouseTrackerMarkup(options: TrackerOptions = {}): string { const detail = Math.max(1, options.detail ?? 1); const columns = (options.columns ?? 10) * detail; const rows = (options.rows ?? 10) * detail; const cells = Array.from( { length: columns * rows }, () => '
', ).join(""); const scope = options.scope ?? "default"; return `