@unpunnyfuns/d3-blockmap 0.1.0 → 0.2.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/axis.ts","../src/constants.ts","../src/dom.ts","../src/view.ts","../src/bins.ts","../src/brush.ts","../src/colors.ts","../src/interaction.ts","../src/renderer.ts","../src/bitset.ts","../src/ranges.ts","../src/state.ts","../src/tooltip.ts","../src/blockMap.ts"],"sourcesContent":["/**\n * Block-number axis under the canvas: a linear scale, nice tick positions,\n * and absolutely positioned labels. Hand-rolled rather than pulling in\n * d3-scale/d3-axis, which assume SVG; only the tick-step maths comes from D3.\n */\n\nimport { tickStep } from \"d3-array\";\nimport {\n CSS_VAR_AXIS_COLOR,\n DECIMAL_BASE,\n EDGE_ANCHOR_RATIO,\n MIN_TICKS,\n TICK_SPACING_PX,\n} from \"./constants\";\nimport { ensureRelative } from \"./dom\";\nimport { createScale } from \"./view\";\nimport type { BlockView } from \"./view\";\n\nexport type AxisOptions = {\n format?: (block: number, step: number) => string;\n tickCount?: number;\n};\n\nexport type Tick = { block: number; x: number; step: number };\n\nexport type Axis = {\n update(view: BlockView, cssWidth: number): void;\n destroy(): void;\n};\n\n// Data attributes rather than classes, so consumers can style the axis\n// without the library shipping any CSS.\nconst AXIS_ATTRIBUTE = \"data-blockmap-axis\";\nconst TICK_ATTRIBUTE = \"data-blockmap-tick\";\nconst FULL = \"100%\";\n\n// A label is centred on its tick unless that would overflow the container,\n// in which case it is pinned to the edge.\nconst CENTER = \"translateX(-50%)\";\nconst LEFT_EDGE = \"translateX(0)\";\nconst RIGHT_EDGE = \"translateX(-100%)\";\n\n// Compact notation (\"1.2M\") only starts at four digits; its units step in\n// powers of a thousand, and beyond three fraction digits it stops being\n// shorter than the full grouped number.\nconst COMPACT_THRESHOLD = 1000;\nconst MAGNITUDE_STEP = 3;\nconst MAX_COMPACT_FRACTION_DIGITS = 3;\n\n// Intl.NumberFormat construction is expensive and ticks are rebuilt on every\n// view change, so formatters are made once and cached per digit count.\nconst groupedFormat = new Intl.NumberFormat(\"en-US\");\nconst compactFormatters = new Map<number, Intl.NumberFormat>();\n\nconst compactFormatterFor = (digits: number): Intl.NumberFormat => {\n const cached = compactFormatters.get(digits);\n if (cached) return cached;\n\n const formatter = new Intl.NumberFormat(\"en-US\", {\n notation: \"compact\",\n maximumFractionDigits: digits,\n });\n compactFormatters.set(digits, formatter);\n return formatter;\n};\n\n/**\n * Precision follows the tick step, not the value: at a step of 100k within\n * millions, \"1.1M\" is right, while rounding to \"1M\" would print the same\n * label twice. `unit` is the compact unit in play (1e3, 1e6, ...) and\n * log10(unit / step) is how many of its fraction digits the step occupies.\n * When that exceeds three, compact notation no longer saves anything, so\n * fall back to grouped digits.\n */\nexport const defaultFormat = (block: number, step = 1): string => {\n if (block < COMPACT_THRESHOLD) return groupedFormat.format(block);\n\n const magnitude =\n Math.floor(Math.log10(Math.max(Math.abs(block), 1)) / MAGNITUDE_STEP) * MAGNITUDE_STEP;\n const unit = DECIMAL_BASE ** magnitude;\n const digits = Math.max(0, Math.ceil(Math.log10(unit / step)));\n\n if (digits > MAX_COMPACT_FRACTION_DIGITS) return groupedFormat.format(block);\n return compactFormatterFor(digits).format(block);\n};\n\n/**\n * d3-array's tickStep returns a human-friendly step - 1, 2 or 5 times a\n * power of ten - that yields approximately `count` ticks across the domain,\n * which is why ticks land on round block numbers as you zoom. Blocks are\n * whole, so a step below 1 is floored to 1, and ticks start at the first\n * multiple of the step at or above the view's start.\n */\nexport const computeTicks = (view: BlockView, cssWidth: number, tickCount?: number): Tick[] => {\n if (cssWidth <= 0) return [];\n\n const count = tickCount ?? Math.max(MIN_TICKS, Math.floor(cssWidth / TICK_SPACING_PX));\n const scale = createScale(view, cssWidth);\n const rawStep = tickStep(view.start, view.end, count);\n const step = Math.max(1, rawStep);\n\n const first = Math.ceil(view.start / step) * step;\n const result: Tick[] = [];\n for (let value = first; value <= view.end; value += step) {\n result.push({ block: value, x: scale.x(value), step });\n }\n return result;\n};\n\n/**\n * Half the tick spacing is treated as the edge zone: a label that close to a\n * side would be clipped if centred, so it is pinned instead. In a container\n * narrower than the tick spacing both tests can pass, and the nearer edge\n * wins.\n */\nexport const anchorFor = (x: number, cssWidth: number): string => {\n const edge = TICK_SPACING_PX * EDGE_ANCHOR_RATIO;\n const nearLeft = x < edge;\n const nearRight = cssWidth - x < edge;\n\n if (nearLeft && nearRight) return x < cssWidth - x ? LEFT_EDGE : RIGHT_EDGE;\n if (nearLeft) return LEFT_EDGE;\n if (nearRight) return RIGHT_EDGE;\n return CENTER;\n};\n\nexport const createAxis = (container: HTMLElement, options: AxisOptions): Axis => {\n const format = options.format ?? defaultFormat;\n ensureRelative(container);\n\n // `top: 100%` hangs the axis below the canvas without reserving layout\n // space; it is aria-hidden because the tooltip and getProgress already\n // expose the same numbers; pointer-events none keeps it out of hit testing.\n const root = document.createElement(\"div\");\n root.setAttribute(AXIS_ATTRIBUTE, \"\");\n root.setAttribute(\"aria-hidden\", \"true\");\n Object.assign(root.style, {\n position: \"absolute\",\n left: \"0\",\n right: \"0\",\n top: FULL,\n pointerEvents: \"none\",\n userSelect: \"none\",\n color: `var(${CSS_VAR_AXIS_COLOR}, currentColor)`,\n });\n container.append(root);\n\n return {\n // Rebuilds every label on each update. There are only a handful of ticks,\n // so a single replaceChildren beats diffing, and it cannot leave stale\n // labels behind after a zoom.\n update: (view, cssWidth) => {\n const spans = computeTicks(view, cssWidth, options.tickCount).map((tick) => {\n const span = document.createElement(\"span\");\n span.setAttribute(TICK_ATTRIBUTE, \"\");\n span.dataset.block = String(tick.block);\n span.textContent = format(tick.block, tick.step);\n Object.assign(span.style, {\n display: \"block\",\n position: \"absolute\",\n top: \"0\",\n left: `${tick.x}px`,\n transform: anchorFor(tick.x, cssWidth),\n whiteSpace: \"nowrap\",\n });\n return span;\n });\n root.replaceChildren(...spans);\n },\n\n destroy: () => {\n root.remove();\n },\n };\n};\n","/**\n * Shared numeric and string literals, kept in one place because the\n * no-magic-numbers lint rule forbids them inline. Values only: no logic,\n * no imports, so every module can depend on this without cycles.\n */\n\n// Bit addressing inside the bitset's Uint8Array: `index >>> BYTE_SHIFT`\n// selects the byte, `index & BYTE_MASK` the bit within it.\nexport const BITS_PER_BYTE = 8;\nexport const BYTE_SHIFT = 3;\nexport const BYTE_MASK = 0b111;\nexport const FULL_BYTE = 0xff;\n\n// RGBA pixel layout, shared by the colour lookup table and the canvas\n// ImageData buffer. 256 entries means one per 1/255 step of coverage.\nexport const BYTES_PER_PIXEL = 4;\nexport const OPAQUE_ALPHA = 255;\nexport const COLOR_TABLE_SIZE = 256;\n\n// The canvas is a single pixel row stretched vertically by CSS, so one bin\n// is one column and there is never a second row to write.\nexport const CANVAS_ROWS = 1;\n\n// Tooltip readout: percentage precision and the cursor offset.\nexport const PERCENT = 100;\nexport const PERCENT_FRACTION_DIGITS = 1;\nexport const TOOLTIP_OFFSET_PX = 8;\n\n// Custom properties read off the container so colours stay themeable in CSS;\n// DEFAULT_THEME is the fallback when a property is unset or unparseable.\nexport const CSS_VAR_MISSING = \"--blockmap-missing\";\nexport const CSS_VAR_PARTIAL_LOW = \"--blockmap-partial-low\";\nexport const CSS_VAR_PARTIAL_HIGH = \"--blockmap-partial-high\";\nexport const CSS_VAR_PARTIAL = \"--blockmap-partial\";\nexport const CSS_VAR_COMPLETE = \"--blockmap-complete\";\nexport const CSS_VAR_HIGHLIGHT = \"--blockmap-highlight\";\nexport const CSS_VAR_BRUSH = \"--blockmap-brush\";\n\nexport const DEFAULT_THEME = {\n missing: \"#2b2b2b\",\n partialLow: \"#5b3a00\",\n partialHigh: \"#e0a500\",\n partial: \"#e0a500\",\n complete: \"#2ecc71\",\n highlight: \"#ff4d4f\",\n brush: \"#ffffff\",\n} as const;\n\n// The brush selection paints over the bar, so its fill stays translucent\n// while its stroke uses the colour at full strength.\nexport const BRUSH_FILL_OPACITY = 0.3;\n\n// Theme re-reads are triggered by these attribute mutations on <html> (a\n// class swap, an inline style, a data-theme toggle) and by the colour scheme.\nexport const THEME_ATTRIBUTES = [\"class\", \"style\", \"data-theme\"] as const;\nexport const DARK_SCHEME_QUERY = \"(prefers-color-scheme: dark)\";\n\n// Interaction tuning: one wheel notch multiplies the view length by 1.25\n// (out) or 0.8 (in), a pointer must travel 3px before it counts as a drag\n// rather than a click, and MIN_VIEW_LENGTH keeps the view from collapsing\n// to zero blocks.\nexport const ZOOM_STEP = 1.25;\nexport const DRAG_THRESHOLD_PX = 3;\nexport const BLINK_INTERVAL_MS = 500;\nexport const REDUCED_MOTION_QUERY = \"(prefers-reduced-motion: reduce)\";\nexport const MIN_VIEW_LENGTH = 1;\n\n// Keyboard panning moves the view by a fraction of its own length: 10%\n// normally, 50% with Shift held, for a faster traverse of a large view.\nexport const KEY_PAN_FRACTION = 0.1;\nexport const KEY_PAN_FRACTION_FAST = 0.5;\n\n// The default aria-label reports a whole percentage, no decimal places.\nexport const ARIA_LABEL_DEFAULT_PERCENT_DIGITS = 0;\n\n// Describes the canvas's role to assistive tech beyond plain \"img\", and\n// lists the keys a focused, interactive map responds to.\nexport const ARIA_ROLEDESCRIPTION = \"zoomable block map\";\nexport const ARIA_KEYSHORTCUTS = \"Plus - ArrowLeft ArrowRight Home\";\n\n// Axis layout: aim for one tick per 80px, never fewer than two, and treat a\n// tick within half that spacing of an edge as needing an edge-aligned label.\nexport const CSS_VAR_AXIS_COLOR = \"--blockmap-axis-color\";\nexport const TICK_SPACING_PX = 80;\nexport const MIN_TICKS = 2;\nexport const EDGE_ANCHOR_RATIO = 0.5;\nexport const DECIMAL_BASE = 10;\n\n// Limits: ranges arrive as flat [start, end] pairs, and block indices must\n// stay addressable as unsigned 32-bit integers.\nexport const RANGE_PAIR = 2;\nexport const MAX_BLOCKS = 0xffffffff;\n","/**\n * The DOM odds and ends several modules need: the container tweak the tooltip,\n * axis and brush overlay all depend on, and the pointer-to-canvas coordinate\n * conversion the tooltip, click and interaction all work in. Kept here so\n * none of them has to import another, and so there is a single place to\n * reason about mutating the host container's style.\n */\n\n/**\n * The tooltip and axis are absolutely positioned children, so the container\n * must be a positioned ancestor or they would escape to the nearest one up\n * the tree. An empty string covers stub environments whose getComputedStyle\n * reports nothing rather than the \"static\" default.\n */\nexport const ensureRelative = (container: HTMLElement): void => {\n const position = getComputedStyle(container).position;\n if (position === \"static\" || !position) container.style.position = \"relative\";\n};\n\n/**\n * Pointer position within the canvas, in css pixels. Listeners sit on the\n * container, so offsetX would be relative to whichever child was hit, while\n * the canvas rect is the frame the renderer paints in. The rect is read per\n * event because scrolling or a layout change moves it without any resize the\n * map hears about.\n */\nexport const canvasX = (canvas: HTMLCanvasElement, event: { clientX: number }): number =>\n event.clientX - canvas.getBoundingClientRect().left;\n","/**\n * The visible block window as a half-open [start, end) range, with the\n * clamp/zoom/pan arithmetic that keeps it legal. Pure functions over plain\n * objects: no DOM, no event handling, so the maths is testable on its own.\n */\n\nimport { MIN_VIEW_LENGTH } from \"./constants\";\n\nexport type BlockView = { start: number; end: number };\n\nexport type BlockScale = {\n domain: readonly [number, number];\n range: readonly [number, number];\n x(block: number): number;\n block(x: number): number;\n};\n\nexport const fullView = (totalBlocks: number): BlockView => ({ start: 0, end: totalBlocks });\n\nexport const viewLength = (view: BlockView): number => view.end - view.start;\n\nexport const sameView = (a: BlockView, b: BlockView): boolean =>\n a.start === b.start && a.end === b.end;\n\n/**\n * Linear block-to-pixel mapping for the view, with `block` as its inverse.\n * It is the one projection between blocks and css pixels: the axis, the\n * brush and getScale all go through it. The zero-width guards keep a\n * container that has not been laid out yet from producing NaN coordinates.\n */\nexport const createScale = (view: BlockView, cssWidth: number): BlockScale => {\n const length = viewLength(view);\n\n return {\n domain: [view.start, view.end],\n range: [0, cssWidth],\n x: (block) => (cssWidth === 0 ? 0 : ((block - view.start) / length) * cssWidth),\n block: (x) => (cssWidth === 0 ? view.start : view.start + (x / cssWidth) * length),\n };\n};\n\n/**\n * Normalises any candidate view: swaps inverted bounds, rounds to whole\n * blocks, enforces a minimum length, then slides the window inside\n * [0, totalBlocks) while preserving its length rather than truncating it.\n */\nexport const clampView = (view: BlockView, totalBlocks: number): BlockView => {\n let start = Math.round(Math.min(view.start, view.end));\n let end = Math.round(Math.max(view.start, view.end));\n if (end - start < MIN_VIEW_LENGTH) end = start + MIN_VIEW_LENGTH;\n\n const length = Math.min(end - start, totalBlocks);\n start = Math.max(0, Math.min(start, totalBlocks - length));\n return { start, end: start + length };\n};\n\n/**\n * The one check a caller's view goes through before it reaches the state:\n * non-finite bounds cannot be clamped into anything meaningful, so they are\n * no view at all, and everything else is clamped.\n */\nexport const legalView = (view: BlockView, totalBlocks: number): BlockView | undefined =>\n Number.isFinite(view.start) && Number.isFinite(view.end)\n ? clampView(view, totalBlocks)\n : undefined;\n\n/**\n * Scales the view by `factor` while holding `anchor` (the block under the\n * cursor) at the same fractional offset, so the block you point at does not\n * drift. Rounding follows the gesture's direction: zooming in (factor < 1)\n * floors and zooming out ceils, so every notch changes the length by at\n * least one block instead of rounding back to where it started.\n */\nexport const zoomView = (\n view: BlockView,\n anchor: number,\n factor: number,\n totalBlocks: number,\n): BlockView => {\n const length = viewLength(view);\n const scaled = length * factor;\n const nextLength = Math.max(\n MIN_VIEW_LENGTH,\n Math.min(totalBlocks, factor < 1 ? Math.floor(scaled) : Math.ceil(scaled)),\n );\n\n const ratio = (anchor - view.start) / length;\n const start = Math.round(anchor - ratio * nextLength);\n return clampView({ start, end: start + nextLength }, totalBlocks);\n};\n\n/**\n * Shifts the window by `deltaBlocks`, keeping its length. Callers pass the\n * view captured at pointerdown plus the total pointer travel, not the\n * previous frame's view, so accumulated rounding cannot make a drag creep.\n */\nexport const panView = (view: BlockView, deltaBlocks: number, totalBlocks: number): BlockView => {\n const length = viewLength(view);\n const start = Math.round(view.start + deltaBlocks);\n return clampView({ start, end: start + length }, totalBlocks);\n};\n","/**\n * Maps the current view's block range onto one bin per canvas column and\n * keeps a completed-block count per bin. Pure index arithmetic plus a\n * Uint32Array: no DOM, no colours, no knowledge of how bins are painted.\n */\n\nimport type { BlockRange } from \"./ranges\";\nimport type { BlockView } from \"./view\";\n\nexport type Bins = {\n readonly binCount: number;\n readonly counts: Uint32Array;\n binOf(index: number): number;\n rangeOf(bin: number): BlockRange;\n sizeOf(bin: number): number;\n rebuild(countInRange: (start: number, end: number) => number): void;\n addRange(start: number, end: number): void;\n binsTouching(start: number, end: number): number[];\n recountBins(bins: Iterable<number>, countInRange: (start: number, end: number) => number): void;\n};\n\n/**\n * Discrete mode, fewer blocks in the view than columns: the block `block`\n * places from the view's start covers columns [discreteColumn(block),\n * discreteColumn(block + 1)). The same floor split for both ends is what lets\n * consecutive blocks tile the row with no gaps.\n */\nexport const discreteColumn = (block: number, length: number, binCount: number): number =>\n Math.floor((block * binCount) / length);\n\n/**\n * Inverse of discreteColumn, counted from the view's start: the block under a\n * column is the number of block boundaries that fit within the first\n * `column + 1` columns, minus one. Floor and ceil must agree like this or the\n * tooltip names a neighbour of the block the renderer painted there.\n */\nexport const discreteBlock = (column: number, length: number, binCount: number): number =>\n Math.ceil(((column + 1) * length) / binCount) - 1;\n\n/**\n * Callers may hand in ranges that extend past the zoomed-in view; intersect\n * first so bin arithmetic never sees an out-of-range index.\n */\nconst clipToView = (\n rawStart: number,\n rawEnd: number,\n viewStart: number,\n viewEnd: number,\n): BlockRange => [Math.max(rawStart, viewStart), Math.min(rawEnd, viewEnd)];\n\nexport const createBins = (view: BlockView, binCount: number): Bins => {\n const { start: viewStart, end: viewEnd } = view;\n const length = viewEnd - viewStart;\n const counts = new Uint32Array(binCount);\n\n // Bins divide the view evenly in real terms, so boundaries rarely land on\n // integers and bin sizes differ by one block.\n const binOf = (index: number): number => Math.floor(((index - viewStart) * binCount) / length);\n\n /**\n * Exact inverse of binOf. Because binOf floors, bin b holds every index\n * whose scaled position is in [b, b + 1); the smallest integer at or above\n * b * length / binCount is its first block, hence ceil on both ends. Using\n * floor here would overlap neighbouring bins by a block.\n */\n const rangeOf = (bin: number): BlockRange => [\n viewStart + Math.ceil((bin * length) / binCount),\n viewStart + Math.ceil(((bin + 1) * length) / binCount),\n ];\n\n const sizeOf = (bin: number): number => {\n const [start, end] = rangeOf(bin);\n return end - start;\n };\n\n return {\n binCount,\n counts,\n binOf,\n rangeOf,\n sizeOf,\n\n // Full recount from the bitset, used after a resize or a view change.\n rebuild: (countInRange) => {\n counts.fill(0);\n if (binCount === 0) return;\n\n for (let bin = 0; bin < binCount; bin += 1) {\n const [start, end] = rangeOf(bin);\n counts[bin] = countInRange(start, end);\n }\n },\n\n /**\n * Adds a wholly-new range's blocks to the bin counts without consulting\n * the bitset: the first and last bins get the part of themselves the\n * range covers, the bins between get their full size.\n */\n addRange: (rawStart, rawEnd) => {\n const [start, end] = clipToView(rawStart, rawEnd, viewStart, viewEnd);\n if (binCount === 0 || end <= start) return;\n\n const first = binOf(start);\n const last = binOf(end - 1);\n\n if (first === last) {\n counts[first] += end - start;\n return;\n }\n\n counts[first] += rangeOf(first)[1] - start;\n for (let bin = first + 1; bin < last; bin += 1) counts[bin] += sizeOf(bin);\n counts[last] += end - rangeOf(last)[0];\n },\n\n // Which bins a range overlaps, so callers can mark exactly those dirty.\n binsTouching: (rawStart, rawEnd) => {\n const [start, end] = clipToView(rawStart, rawEnd, viewStart, viewEnd);\n if (binCount === 0 || end <= start) return [];\n\n const first = binOf(start);\n const last = binOf(end - 1);\n const touched: number[] = [];\n for (let bin = first; bin <= last; bin += 1) touched.push(bin);\n return touched;\n },\n\n recountBins: (bins, countInRange) => {\n for (const bin of bins) {\n const [start, end] = rangeOf(bin);\n counts[bin] = countInRange(start, end);\n }\n },\n };\n};\n","/**\n * d3-brush wrapped so a selection is kept in block space rather than in\n * pixels: every update re-projects the stored blocks through the current\n * view, which is what makes a selection survive zoom, pan and resize. The\n * projection maths is exported separately so it can be tested without a DOM.\n */\n\nimport { brushX } from \"d3-brush\";\nimport type { BrushSelection, D3BrushEvent } from \"d3-brush\";\nimport { select } from \"d3-selection\";\nimport { BRUSH_FILL_OPACITY, DEFAULT_THEME } from \"./constants\";\nimport { ensureRelative } from \"./dom\";\nimport type { BlockRange } from \"./ranges\";\nimport { createScale } from \"./view\";\nimport type { BlockView } from \"./view\";\n\nexport type BrushOverlayDeps = {\n getView(): BlockView;\n getWidth(): number;\n getHeight(): number;\n getTotalBlocks(): number;\n onBrush(selection: BlockRange | null): void;\n onBrushEnd(selection: BlockRange | null): void;\n};\n\nexport type BrushOverlay = {\n update(): void;\n setSelection(selection: BlockRange | null): void;\n getSelection(): BlockRange | null;\n setColor(color: string): void;\n destroy(): void;\n};\n\n// A data attribute rather than a class, matching the axis, so consumers can\n// style or query the overlay without the library shipping any CSS.\nconst BRUSH_ATTRIBUTE = \"data-blockmap-brush\";\nconst SVG_NAMESPACE = \"http://www.w3.org/2000/svg\";\nconst SELECTION_CLASS = \".selection\";\n// Everything d3 attaches outside our own subtree during a gesture: the brush's\n// window listeners plus d3-drag's page-wide suppression of text selection.\nconst GESTURE_LISTENERS =\n \"mousemove.brush mouseup.brush keydown.brush keyup.brush dragstart.drag selectstart.drag\";\n\n// Only the properties the filter inspects: d3-brush calls it for both mouse\n// and touch starts, and TouchEvent has no `button`.\ntype BrushGestureEvent = {\n ctrlKey: boolean;\n metaKey: boolean;\n shiftKey: boolean;\n button?: number;\n};\n\nconst clamp = (value: number, max: number): number => Math.max(0, Math.min(max, value));\n\n/**\n * Blocks to pixels within the view, clipped to the container so a selection\n * reaching past either edge still paints up to it. Null means \"nothing to\n * draw\": the selection lies entirely outside the view, or the container has\n * not been laid out yet and every coordinate would collapse to zero.\n */\nexport const toPixels = (\n selection: BlockRange,\n view: BlockView,\n cssWidth: number,\n): [number, number] | null => {\n if (cssWidth === 0) return null;\n if (selection[1] <= view.start || selection[0] >= view.end) return null;\n\n const scale = createScale(view, cssWidth);\n return [clamp(scale.x(selection[0]), cssWidth), clamp(scale.x(selection[1]), cssWidth)];\n};\n\n/**\n * Pixels back to blocks, snapped to whole ones. A zero-length result would\n * be an invisible selection, so it widens by a block, and at the very end of\n * the map it has to widen backwards instead. A map with no blocks is the one\n * case that cannot yield a non-empty range.\n */\nexport const toBlocks = (\n pixels: readonly [number, number],\n view: BlockView,\n cssWidth: number,\n totalBlocks: number,\n): BlockRange => {\n const scale = createScale(view, cssWidth);\n const start = clamp(Math.round(scale.block(pixels[0])), totalBlocks);\n let end = clamp(Math.round(scale.block(pixels[1])), totalBlocks);\n\n if (end <= start) {\n end = Math.min(start + 1, totalBlocks);\n if (end <= start) return [Math.max(0, end - 1), end];\n }\n return [start, end];\n};\n\n// Caller input is trusted no further than the API promises: rounded, clamped,\n// and anything that is not a forward range is simply no selection.\nconst normalize = (selection: BlockRange | null, totalBlocks: number): BlockRange | null => {\n if (!selection) return null;\n const start = clamp(Math.round(selection[0]), totalBlocks);\n const end = clamp(Math.round(selection[1]), totalBlocks);\n return end > start ? [start, end] : null;\n};\n\n// brushX only ever reports a one-dimensional selection, but the shared\n// BrushSelection type also covers the two-dimensional shape.\nconst xSpan = (selection: BrushSelection | null): readonly [number, number] | null => {\n if (!selection) return null;\n const [x0, x1] = selection;\n if (typeof x0 !== \"number\" || typeof x1 !== \"number\") return null;\n return [x0, x1];\n};\n\n// Two selections are the same when both are empty or both cover the same\n// blocks; used to tell a no-op gesture apart from a real change.\nconst sameRange = (a: BlockRange | null, b: BlockRange | null): boolean =>\n a === b || (a !== null && b !== null && a[0] === b[0] && a[1] === b[1]);\n\nexport const createBrushOverlay = (\n container: HTMLElement,\n deps: BrushOverlayDeps,\n): BrushOverlay => {\n ensureRelative(container);\n\n // Stretched over the container rather than sized in pixels, so a resize\n // needs no attribute update; overflow stays visible so the handles at the\n // edges are not clipped.\n const svg = document.createElementNS(SVG_NAMESPACE, \"svg\");\n svg.setAttribute(BRUSH_ATTRIBUTE, \"\");\n Object.assign(svg.style, {\n position: \"absolute\",\n inset: \"0\",\n width: \"100%\",\n height: \"100%\",\n touchAction: \"none\",\n overflow: \"visible\",\n display: \"block\",\n });\n container.append(svg);\n\n const g = select(svg).append(\"g\");\n\n // A mousedown on the overlay would otherwise move focus off the canvas -\n // an svg is not focusable, so the browser walks up to the body - and the\n // keyboard gestures would stop working after the first brush. Capturing, so\n // the default is cancelled whatever d3-brush decides to do with the event.\n const onMouseDown = (event: MouseEvent): void => {\n event.preventDefault();\n };\n svg.addEventListener(\"mousedown\", onMouseDown, true);\n\n // Touch is declined so a touch drag falls through to the map's own pan: a\n // brush and a pan are the same gesture, and a touch user has no modifier\n // key to tell them apart. A held modifier means the gesture belongs to\n // panning, and a non-primary button to the context menu, so the brush\n // declines both of those too.\n const brush = brushX()\n .touchable(() => false)\n .filter(\n (event: BrushGestureEvent) =>\n !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.button,\n );\n\n let selection: BlockRange | null = null;\n let color: string = DEFAULT_THEME.brush;\n // d3-brush emits start/brush/end for programmatic moves too; they carry no\n // sourceEvent, and this flag covers a future version that supplies one.\n let silent = false;\n // A gesture is in flight, which is the only state in which d3 holds the\n // page-wide window listeners that destroy has to clean up by hand.\n let active = false;\n\n // The .selection rect does not exist until the behaviour has been applied,\n // and d3-brush gives it default colours as it creates it, so repainting\n // after every call is what makes setColor order-independent.\n const paint = (): void => {\n g.select(SELECTION_CLASS)\n .attr(\"fill\", color)\n .attr(\"stroke\", color)\n .attr(\"fill-opacity\", BRUSH_FILL_OPACITY);\n };\n\n const update = (): void => {\n const width = deps.getWidth();\n brush.extent([\n [0, 0],\n [width, deps.getHeight()],\n ]);\n g.call(brush);\n paint();\n\n silent = true;\n try {\n g.call(brush.move, selection ? toPixels(selection, deps.getView(), width) : null);\n } finally {\n silent = false;\n }\n };\n\n const blocksFrom = (pixels: BrushSelection | null): BlockRange | null => {\n const span = xSpan(pixels);\n if (!span) return null;\n return toBlocks(span, deps.getView(), deps.getWidth(), deps.getTotalBlocks());\n };\n\n brush.on(\"start\", (event: D3BrushEvent<unknown>) => {\n if (!event.sourceEvent || silent) return;\n active = true;\n });\n\n brush.on(\"brush\", (event: D3BrushEvent<unknown>) => {\n if (!event.sourceEvent || silent) return;\n deps.onBrush(blocksFrom(event.selection));\n });\n\n /**\n * Release is where the selection becomes block space: snap, store, then\n * re-project so the rectangle sits on the block boundaries it reports.\n * d3-brush emits `end` for a bare press just as for a drag, so a gesture\n * that leaves the selection as it found it - a stray click on the map,\n * either half of a double click, a drag back to where it started - reports\n * nothing; a press that cleared a selection still does, because the\n * selection genuinely changed. The rectangle is re-projected either way,\n * so a nudge that rounds back to the same blocks snaps back onto them.\n */\n brush.on(\"end\", (event: D3BrushEvent<unknown>) => {\n if (!event.sourceEvent || silent) return;\n active = false;\n\n const next = normalize(blocksFrom(event.selection), deps.getTotalBlocks());\n const changed = !sameRange(selection, next);\n\n selection = next;\n update();\n if (changed) deps.onBrushEnd(selection);\n });\n\n update();\n\n return {\n update,\n\n setSelection: (next) => {\n selection = normalize(next, deps.getTotalBlocks());\n update();\n },\n\n getSelection: () => (selection ? [selection[0], selection[1]] : null),\n\n setColor: (next) => {\n color = next;\n paint();\n },\n\n // d3-brush only drops its window listeners and re-enables dragging from\n // inside its own gesture end, so destroying mid-drag has to do it by\n // hand or they outlive the map. Every targeted browser has\n // onselectstart, so d3-drag suppresses selection with a listener rather\n // than an inline style and removing the listeners is the whole cleanup.\n // Those listeners are removed by name, which would take another\n // component's brush or drag down with them, so this only runs for a\n // gesture that is actually in flight.\n destroy: () => {\n brush.on(\"start\", null).on(\"brush\", null).on(\"end\", null);\n g.on(\".brush\", null);\n svg.removeEventListener(\"mousedown\", onMouseDown, true);\n if (active) select(window).on(GESTURE_LISTENERS, null);\n svg.remove();\n },\n };\n};\n","/**\n * Turns the container's CSS custom properties into a 256-entry RGBA lookup\n * table, so painting a bin is an array slice rather than a colour\n * computation. Owns theme reading and colour maths only; it never touches\n * the canvas or the bin counts.\n */\n\nimport { rgb } from \"d3-color\";\nimport { interpolateRgb, piecewise } from \"d3-interpolate\";\nimport {\n BYTES_PER_PIXEL,\n COLOR_TABLE_SIZE,\n CSS_VAR_BRUSH,\n CSS_VAR_COMPLETE,\n CSS_VAR_HIGHLIGHT,\n CSS_VAR_MISSING,\n CSS_VAR_PARTIAL,\n CSS_VAR_PARTIAL_HIGH,\n CSS_VAR_PARTIAL_LOW,\n DARK_SCHEME_QUERY,\n DEFAULT_THEME,\n OPAQUE_ALPHA,\n THEME_ATTRIBUTES,\n} from \"./constants\";\n\nexport type Theme = {\n missing: string;\n partialLow: string;\n partialHigh: string;\n partial: string;\n complete: string;\n highlight: string;\n brush: string;\n};\n\nexport type ColorScale = (fraction: number) => Uint8ClampedArray;\n\nexport type PartialStyle = \"gradient\" | \"coverage\";\n\nconst THEME_VARS: Record<keyof Theme, string> = {\n missing: CSS_VAR_MISSING,\n partialLow: CSS_VAR_PARTIAL_LOW,\n partialHigh: CSS_VAR_PARTIAL_HIGH,\n partial: CSS_VAR_PARTIAL,\n complete: CSS_VAR_COMPLETE,\n highlight: CSS_VAR_HIGHLIGHT,\n brush: CSS_VAR_BRUSH,\n};\n\n// Entry 0 is reserved for \"nothing complete\" and the last entry for \"all\n// complete\"; the partial ramp occupies everything strictly between, so the\n// two certainties are never approximated by a nearby ramp colour.\nconst LAST_INDEX = COLOR_TABLE_SIZE - 1;\nconst FIRST_PARTIAL_INDEX = 1;\nconst LAST_PARTIAL_INDEX = LAST_INDEX - 1;\n\n/**\n * d3-color's rgb() accepts any CSS colour string and yields NaN channels for\n * input it cannot parse, and NaN is not displayable - so displayable() works\n * as the parse check. The installed d3-color returns a non-displayable colour\n * rather than throwing; the catch is defensive, so a future version that does\n * throw cannot break theme reading.\n */\nconst parseOr = (value: string, fallback: string): string => {\n try {\n return rgb(value).displayable() ? value : fallback;\n } catch {\n return fallback;\n }\n};\n\nconst writeColor = (table: Uint8ClampedArray, index: number, css: string): void => {\n const color = rgb(css);\n const offset = index * BYTES_PER_PIXEL;\n table.set([color.r, color.g, color.b, OPAQUE_ALPHA], offset);\n};\n\n// Reads the theme off the element's computed style, so a cascade change or a\n// dark-mode switch is picked up without the caller passing colours in.\nexport const readTheme = (el: HTMLElement): Theme => {\n const style = getComputedStyle(el);\n const read = (key: keyof Theme): string =>\n style.getPropertyValue(THEME_VARS[key]).trim() || DEFAULT_THEME[key];\n\n return {\n missing: read(\"missing\"),\n partialLow: read(\"partialLow\"),\n partialHigh: read(\"partialHigh\"),\n partial: read(\"partial\"),\n complete: read(\"complete\"),\n highlight: read(\"highlight\"),\n brush: read(\"brush\"),\n };\n};\n\n/**\n * Builds the ramp once and samples it into the table. Both d3-interpolate\n * helpers return a function of t in [0, 1] that yields a CSS colour string:\n * interpolateRgb blends two colours channel-wise in sRGB, and piecewise\n * chains several such interpolators over equal sub-intervals, so \"coverage\"\n * puts `partial` exactly at t = 0.5 between missing and complete. \"gradient\"\n * ignores coverage endpoints and runs low -> high instead.\n */\nexport const createColorScale = (theme: Theme, partial: PartialStyle = \"coverage\"): ColorScale => {\n const table = new Uint8ClampedArray(COLOR_TABLE_SIZE * BYTES_PER_PIXEL);\n const missing = parseOr(theme.missing, DEFAULT_THEME.missing);\n const complete = parseOr(theme.complete, DEFAULT_THEME.complete);\n const partialScale =\n partial === \"coverage\"\n ? piecewise(interpolateRgb, [\n missing,\n parseOr(theme.partial, DEFAULT_THEME.partial),\n complete,\n ])\n : interpolateRgb(\n parseOr(theme.partialLow, DEFAULT_THEME.partialLow),\n parseOr(theme.partialHigh, DEFAULT_THEME.partialHigh),\n );\n\n writeColor(table, 0, missing);\n writeColor(table, LAST_INDEX, complete);\n for (let i = FIRST_PARTIAL_INDEX; i <= LAST_PARTIAL_INDEX; i += 1) {\n writeColor(table, i, partialScale(i / LAST_INDEX));\n }\n\n /**\n * Nudges rounding away from the two reserved entries: a bin with a single\n * completed block out of a million must not read as empty, and one missing\n * block must not read as done. Returns a view into the table, not a copy,\n * so a full repaint allocates nothing - callers must use it immediately.\n */\n return (fraction) => {\n let index = Math.round(fraction * LAST_INDEX);\n if (fraction > 0 && index < FIRST_PARTIAL_INDEX) index = FIRST_PARTIAL_INDEX;\n if (fraction < 1 && index > LAST_PARTIAL_INDEX) index = LAST_PARTIAL_INDEX;\n\n const offset = index * BYTES_PER_PIXEL;\n return table.subarray(offset, offset + BYTES_PER_PIXEL);\n };\n};\n\n// Standalone RGBA quad rather than a table entry: the highlight replaces a\n// bin's colour wholesale and so needs no ramp.\nexport const highlightColor = (theme: Theme): Uint8ClampedArray => {\n const color = rgb(parseOr(theme.highlight, DEFAULT_THEME.highlight));\n return Uint8ClampedArray.of(color.r, color.g, color.b, OPAQUE_ALPHA);\n};\n\n/**\n * Custom properties do not fire change events, so theme switches are\n * inferred: a class/style/data-theme mutation on <html> covers app-driven\n * switching, and the colour-scheme media query covers the OS.\n */\nexport const watchTheme = (onChange: () => void): (() => void) => {\n const observer = new MutationObserver(onChange);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [...THEME_ATTRIBUTES],\n });\n\n const media = matchMedia(DARK_SCHEME_QUERY);\n media.addEventListener(\"change\", onChange);\n\n return () => {\n observer.disconnect();\n media.removeEventListener(\"change\", onChange);\n };\n};\n","/**\n * Translates pointer, wheel and keyboard input into view changes:\n * ctrl/meta-wheel zooms, drag pans, double click resets, and the keys below\n * mirror those gestures for anyone who can't use a pointer. Pointer, wheel\n * and dblclick listeners sit on the surface - the container - rather than\n * the canvas, so an overlay stacked above the canvas (the brush) cannot\n * swallow them; keydown stays on the canvas because that is the focusable\n * element. Positions therefore come from clientX minus the canvas rect\n * instead of offsetX, which would be relative to whichever child the event\n * happened to hit. It computes the new view with the pure helpers in view.ts\n * and hands it to deps.setView; it never touches state, pixels or the\n * tooltip directly.\n */\n\nimport { DRAG_THRESHOLD_PX, KEY_PAN_FRACTION, KEY_PAN_FRACTION_FAST, ZOOM_STEP } from \"./constants\";\nimport { canvasX } from \"./dom\";\nimport { panView, viewLength, zoomView } from \"./view\";\nimport type { BlockView } from \"./view\";\n\nexport type InteractionDeps = {\n getView(): BlockView;\n setView(view: BlockView): void;\n resetView(): void;\n getBinCount(): number;\n getDpr(): number;\n getTotalBlocks(): number;\n // True while another gesture owns the plain drag - the brush - so a mouse\n // or pen pan only happens with Shift held.\n panRequiresShift(): boolean;\n onDragStart(): void;\n onDragEnd(): void;\n};\n\nconst PRIMARY_BUTTON = 0;\nconst TOUCH_POINTER = \"touch\";\nconst GRABBING = \"grabbing\";\nconst NO_POINTER = -1;\nconst HALF = 2;\n\n// `=` and `_` are the unshifted/shifted neighbours of `+` and `-` on a US\n// keyboard layout, so both read as the same gesture without requiring Shift.\nconst ZOOM_IN_KEYS = new Set([\"+\", \"=\"]);\nconst ZOOM_OUT_KEYS = new Set([\"-\", \"_\"]);\nconst RESET_KEYS = new Set([\"0\", \"Home\"]);\n\n/**\n * The view as it was at pointerdown, plus where the pointer went down.\n * `active` flips once the pointer has moved past the drag threshold, which is\n * what separates a pan from a click or a double click.\n */\ntype DragState = { downX: number; view: BlockView; pointerId: number; active: boolean };\n\nexport const attachInteraction = (\n surface: HTMLElement,\n canvas: HTMLCanvasElement,\n deps: InteractionDeps,\n): (() => void) => {\n let drag: DragState | undefined;\n\n // CSS pixels to blocks: scale to device pixels (one per column), take that\n // as a fraction of the columns across, and spend it over the view length.\n const columnsToBlocks = (px: number, view: BlockView): number =>\n ((px * deps.getDpr()) / deps.getBinCount()) * viewLength(view);\n\n /**\n * Requiring ctrl/meta leaves plain wheel events to scroll the page and\n * matches the trackpad pinch gesture, which browsers report as ctrl+wheel.\n * preventDefault - which needs a non-passive listener - stops the browser\n * applying its own page zoom on top. Scrolling down zooms out.\n */\n const onWheel = (event: WheelEvent): void => {\n if (!(event.ctrlKey || event.metaKey)) return;\n if (drag?.active) return;\n if (deps.getBinCount() === 0) return;\n event.preventDefault();\n\n const view = deps.getView();\n const anchor = view.start + columnsToBlocks(canvasX(canvas, event), view);\n const factor = event.deltaY > 0 ? ZOOM_STEP : 1 / ZOOM_STEP;\n deps.setView(zoomView(view, anchor, factor, deps.getTotalBlocks()));\n };\n\n /**\n * Records the baseline but claims nothing yet: a press that never moves\n * must still reach dblclick and leave the cursor alone. Focus moves to the\n * canvas on every primary press, before the shift check, so the keyboard\n * gestures stay reachable even when the press belongs to another gesture;\n * preventScroll keeps a map below the fold from jumping into view. The\n * guard covers test doubles and elements without the focus API.\n */\n const onPointerDown = (event: PointerEvent): void => {\n if (event.button !== PRIMARY_BUTTON) return;\n\n if (document.activeElement !== canvas && typeof canvas.focus === \"function\") {\n canvas.focus({ preventScroll: true });\n }\n\n // The brush only claims mouse and pen drags, so a touch drag is the only\n // pan a touch user has and never needs the modifier they cannot hold.\n if (deps.panRequiresShift() && event.pointerType !== TOUCH_POINTER && !event.shiftKey) return;\n\n drag = {\n downX: canvasX(canvas, event),\n view: deps.getView(),\n pointerId: event.pointerId ?? NO_POINTER,\n active: false,\n };\n };\n\n /**\n * Panning is always computed from the pointerdown baseline and the total\n * travel, never incrementally from the last frame, so rounding cannot make\n * a slow drag creep. Capture is taken on the first real movement so the\n * gesture survives the pointer leaving the surface, and the delta is\n * negated because dragging right moves the content right, i.e. the view\n * left. Capture and the cursor go on the surface because that is the\n * element the listeners are on; capturing the canvas would retarget the\n * remaining events to an element that never hears them.\n */\n const onPointerMove = (event: PointerEvent): void => {\n if (!drag) return;\n\n const dx = canvasX(canvas, event) - drag.downX;\n if (!drag.active) {\n if (Math.abs(dx) < DRAG_THRESHOLD_PX) return;\n drag.active = true;\n if (typeof surface.setPointerCapture === \"function\" && drag.pointerId !== NO_POINTER) {\n surface.setPointerCapture(drag.pointerId);\n }\n surface.style.cursor = GRABBING;\n deps.onDragStart();\n }\n\n if (deps.getBinCount() === 0) return;\n deps.setView(panView(drag.view, -columnsToBlocks(dx, drag.view), deps.getTotalBlocks()));\n };\n\n // Only a drag that became active took capture and changed the cursor, so\n // only that one has anything to undo. The optional-call guards cover\n // environments and test doubles without the pointer capture API.\n const endDrag = (): void => {\n if (!drag) return;\n\n if (drag.active) {\n const hasCapture =\n drag.pointerId !== NO_POINTER && (surface.hasPointerCapture?.(drag.pointerId) ?? true);\n if (typeof surface.releasePointerCapture === \"function\" && hasCapture) {\n surface.releasePointerCapture(drag.pointerId);\n }\n surface.style.cursor = \"\";\n deps.onDragEnd();\n }\n\n drag = undefined;\n };\n\n const onDblClick = (): void => deps.resetView();\n\n /**\n * Ctrl/meta/alt are left alone so browser shortcuts - page zoom, Alt+arrow\n * history navigation - are never intercepted; Shift is a gesture modifier\n * here, not a browser one, so it stays allowed. A drag in progress also\n * returns early, matching onWheel, since the pointer already owns the\n * gesture. Zoom keys anchor on the view's centre rather than a pointer\n * position, since a keyboard gesture has none. Pan keys move by a fraction\n * of the current view length rather than a fixed block count, so the step\n * scales with how far zoomed in the view already is. Only handled keys are\n * prevented, so everything else - Tab, unmodified browser shortcuts -\n * still works.\n */\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.ctrlKey || event.metaKey || event.altKey) return;\n if (drag?.active) return;\n\n const view = deps.getView();\n const length = viewLength(view);\n const centre = view.start + length / HALF;\n const total = deps.getTotalBlocks();\n\n if (ZOOM_IN_KEYS.has(event.key)) {\n deps.setView(zoomView(view, centre, 1 / ZOOM_STEP, total));\n } else if (ZOOM_OUT_KEYS.has(event.key)) {\n deps.setView(zoomView(view, centre, ZOOM_STEP, total));\n } else if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n const fraction = event.shiftKey ? KEY_PAN_FRACTION_FAST : KEY_PAN_FRACTION;\n const delta = length * fraction * (event.key === \"ArrowLeft\" ? -1 : 1);\n deps.setView(panView(view, delta, total));\n } else if (RESET_KEYS.has(event.key)) {\n deps.resetView();\n } else {\n return;\n }\n event.preventDefault();\n };\n\n surface.addEventListener(\"wheel\", onWheel, { passive: false });\n surface.addEventListener(\"pointerdown\", onPointerDown);\n surface.addEventListener(\"pointermove\", onPointerMove);\n surface.addEventListener(\"pointerup\", endDrag);\n surface.addEventListener(\"pointercancel\", endDrag);\n surface.addEventListener(\"dblclick\", onDblClick);\n canvas.addEventListener(\"keydown\", onKeyDown);\n\n // Teardown ends any drag in flight first, so capture and the grabbing\n // cursor are released even if the map is destroyed mid-gesture.\n return () => {\n endDrag();\n surface.removeEventListener(\"wheel\", onWheel);\n surface.removeEventListener(\"pointerdown\", onPointerDown);\n surface.removeEventListener(\"pointermove\", onPointerMove);\n surface.removeEventListener(\"pointerup\", endDrag);\n surface.removeEventListener(\"pointercancel\", endDrag);\n surface.removeEventListener(\"dblclick\", onDblClick);\n canvas.removeEventListener(\"keydown\", onKeyDown);\n };\n};\n","/**\n * Owns the one-row ImageData buffer and the repaint schedule: dirty bins are\n * collected, then flushed once per animation frame. It reads the model\n * through RenderSource and is handed a ColorScale, so it knows nothing about\n * themes, pointers or the DOM beyond a 2d context. The scheduler and timer\n * are injected so tests decide when frames and blinks run.\n */\n\nimport { discreteColumn } from \"./bins\";\nimport type { ColorScale } from \"./colors\";\nimport { BLINK_INTERVAL_MS, BYTES_PER_PIXEL, CANVAS_ROWS } from \"./constants\";\nimport { viewLength } from \"./view\";\nimport type { BlockView } from \"./view\";\n\nexport type ImageDataLike = {\n readonly data: Uint8ClampedArray;\n readonly width: number;\n readonly height: number;\n};\n\nexport type Canvas2dLike = {\n createImageData(width: number, height: number): ImageDataLike;\n putImageData(image: ImageDataLike, dx: number, dy: number): void;\n};\n\nexport type RenderSource = {\n readonly binCount: number;\n getView(): BlockView;\n fraction(bin: number): number;\n has(index: number): boolean;\n};\n\n// request must call back later, never during the call itself, as\n// requestAnimationFrame does: the frame handle it returns is the only record\n// that a frame is pending.\nexport type Scheduler = {\n request(callback: () => void): number;\n cancel(handle: number): void;\n};\n\nexport type IntervalTimer = {\n set(callback: () => void, ms: number): number;\n clear(id: number): void;\n};\n\nexport type Renderer = {\n setColors(scale: ColorScale): void;\n setHighlight(on: boolean, color: Uint8ClampedArray, blink: boolean): void;\n setHighlightColor(color: Uint8ClampedArray): void;\n markDirty(bins: ArrayLike<number>): void;\n markAllDirty(): void;\n destroy(): void;\n};\n\nconst NO_FRAME = 0;\nconst NO_TIMER = 0;\n\n// A bin is fully covered at fraction 1; used both as the discrete-mode\n// \"block is present\" value and as the test that exempts finished bins from\n// the highlight.\nconst COMPLETE = 1;\n\nexport const RAF_SCHEDULER: Scheduler = {\n request: (callback) => requestAnimationFrame(callback),\n cancel: (handle) => cancelAnimationFrame(handle),\n};\n\nexport const WINDOW_TIMER: IntervalTimer = {\n set: (callback, ms) => window.setInterval(callback, ms),\n clear: (id) => window.clearInterval(id),\n};\n\nexport const createRenderer = (\n ctx: Canvas2dLike,\n source: RenderSource,\n scheduler: Scheduler = RAF_SCHEDULER,\n timer: IntervalTimer = WINDOW_TIMER,\n): Renderer => {\n let image: ImageDataLike | undefined;\n let scale: ColorScale | undefined;\n let frame = NO_FRAME;\n let allDirty = false;\n let highlight: Uint8ClampedArray | undefined;\n let phase = false;\n let blinkId = NO_TIMER;\n const dirty = new Set<number>();\n\n // A new buffer starts fully transparent, so a width change invalidates\n // every column, not just the ones that were already dirty.\n const ensureImage = (): ImageDataLike | undefined => {\n const { binCount } = source;\n if (binCount === 0) return undefined;\n\n if (!image || image.width !== binCount) {\n image = ctx.createImageData(binCount, CANVAS_ROWS);\n allDirty = true;\n }\n return image;\n };\n\n /**\n * While the highlight is on and the blink phase is lit, unfinished bins are\n * painted in the highlight colour; completed bins keep their real colour so\n * progress stays readable underneath. With blinking disabled, phase stays\n * true for the lifetime of the highlight and it simply holds steady.\n */\n const colorFor = (colors: ColorScale, fraction: number): Uint8ClampedArray =>\n highlight && phase && fraction < COMPLETE ? highlight : colors(fraction);\n\n const writeColumn = (target: ImageDataLike, column: number, color: Uint8ClampedArray): void => {\n target.data.set(color, column * BYTES_PER_PIXEL);\n };\n\n /**\n * Zoomed in past one block per column: each block owns a span of columns,\n * so iterate blocks and paint their span. The colour is binary - a block is\n * either present or not.\n */\n const paintDiscrete = (target: ImageDataLike, colors: ColorScale): void => {\n const { binCount } = source;\n const view = source.getView();\n const length = viewLength(view);\n\n for (let block = 0; block < length; block += 1) {\n const color = colorFor(colors, source.has(view.start + block) ? COMPLETE : 0);\n const start = discreteColumn(block, length, binCount);\n const end = discreteColumn(block + 1, length, binCount);\n for (let column = start; column < end; column += 1) writeColumn(target, column, color);\n }\n };\n\n // The usual case: one column per bin, repainting only what changed unless a\n // resize, theme change or blink invalidated everything. A dirty bin can be\n // out of range if it was recorded before the buffer shrank.\n const paintBins = (target: ImageDataLike, colors: ColorScale): void => {\n if (allDirty) {\n for (let bin = 0; bin < source.binCount; bin += 1)\n writeColumn(target, bin, colorFor(colors, source.fraction(bin)));\n return;\n }\n\n for (const bin of dirty) {\n if (bin >= target.width) continue;\n writeColumn(target, bin, colorFor(colors, source.fraction(bin)));\n }\n };\n\n /**\n * The single flush point. Fewer blocks than columns means discrete mode;\n * otherwise bins. The flags are reset on every path, including the early\n * return, since there is no buffer or palette to apply the pending work to\n * and whatever restores them (a resize, a theme) marks everything dirty.\n */\n const paint = (): void => {\n frame = NO_FRAME;\n\n const target = ensureImage();\n if (!target || !scale) {\n dirty.clear();\n allDirty = false;\n return;\n }\n\n if (viewLength(source.getView()) < source.binCount) paintDiscrete(target, scale);\n else paintBins(target, scale);\n ctx.putImageData(target, 0, 0);\n\n dirty.clear();\n allDirty = false;\n };\n\n // Many updates can arrive between frames; this coalesces them into one paint.\n const schedule = (): void => {\n if (frame !== NO_FRAME) return;\n frame = scheduler.request(paint);\n };\n\n const markAllDirty = (): void => {\n allDirty = true;\n schedule();\n };\n\n const stopBlink = (): void => {\n if (blinkId !== NO_TIMER) timer.clear(blinkId);\n blinkId = NO_TIMER;\n };\n\n return {\n setColors: (next) => {\n scale = next;\n markAllDirty();\n },\n\n // Restarts cleanly on every call: the old interval is always cleared, so\n // repeated enables cannot stack up blink timers. Turning the highlight\n // off when it was never on is a no-op, avoiding a pointless repaint.\n setHighlight: (on, color, blink) => {\n if (!on && highlight === undefined) return;\n\n stopBlink();\n highlight = on ? color : undefined;\n phase = on;\n\n if (on && blink) {\n blinkId = timer.set(() => {\n phase = !phase;\n markAllDirty();\n }, BLINK_INTERVAL_MS);\n }\n\n markAllDirty();\n },\n\n // Theme changed mid-highlight; the blink phase keeps running untouched.\n setHighlightColor: (color) => {\n if (highlight === undefined) return;\n\n highlight = color;\n markAllDirty();\n },\n\n markDirty: (bins) => {\n for (let i = 0; i < bins.length; i += 1) dirty.add(bins[i]);\n if (bins.length > 0) schedule();\n },\n\n markAllDirty,\n\n destroy: () => {\n stopBlink();\n if (frame !== NO_FRAME) scheduler.cancel(frame);\n frame = NO_FRAME;\n dirty.clear();\n },\n };\n};\n","/**\n * One bit per block, packed into a Uint8Array: 8 million blocks cost 1 MB\n * instead of the 8 MB a Uint8Array of flags would. Owns only the bits and a\n * running population count; it knows nothing about views, bins or pixels.\n */\n\nimport { BITS_PER_BYTE, BYTE_MASK, BYTE_SHIFT, FULL_BYTE } from \"./constants\";\n\nexport type Bitset = {\n has(index: number): boolean;\n set(index: number): boolean;\n fill(start: number, end: number): void;\n clearRange(start: number, end: number): void;\n countRange(start: number, end: number): number;\n clear(): void;\n setCount(next: number): void;\n readonly count: number;\n};\n\n/**\n * Mask with bits [from, to) set within a single byte. `(1 << to) - 1` is all\n * bits below `to`; clearing the bits below `from` leaves the half-open window.\n * `to` may be 8, where `1 << 8` is still 256 so the subtraction gives 0xff.\n */\nconst maskRange = (from: number, to: number): number => ((1 << to) - 1) & ~((1 << from) - 1);\n\n/**\n * Population count per byte value, built once by recurrence: the bits of `i`\n * are the bits of `i >> 1` plus its own low bit. Lets countRange tally whole\n * bytes with a table lookup instead of a per-bit loop.\n */\nconst POPCOUNT = new Uint8Array(1 << BITS_PER_BYTE);\nfor (let i = 1; i < POPCOUNT.length; i += 1) {\n POPCOUNT[i] = POPCOUNT[i >> 1] + (i & 1);\n}\n\nexport const createBitset = (size: number): Bitset => {\n const bytes = new Uint8Array(Math.ceil(size / BITS_PER_BYTE));\n let count = 0;\n\n return {\n has: (index) => (bytes[index >>> BYTE_SHIFT] & (1 << (index & BYTE_MASK))) !== 0,\n\n /**\n * Returns false when the bit was already set, so callers can skip the\n * dirty-marking work for blocks that did not actually change.\n */\n set: (index) => {\n const byte = index >>> BYTE_SHIFT;\n const mask = 1 << (index & BYTE_MASK);\n if ((bytes[byte] & mask) !== 0) return false;\n\n bytes[byte] |= mask;\n count += 1;\n return true;\n },\n\n /**\n * Head/middle/tail: OR a partial mask into the first byte, memset the\n * whole bytes in between, OR a partial mask into the last. `end - 1` is\n * the last included bit, so `toBit` is its offset plus one.\n */\n fill: (start, end) => {\n if (end <= start) return;\n\n const firstByte = start >>> BYTE_SHIFT;\n const lastByte = (end - 1) >>> BYTE_SHIFT;\n const fromBit = start & BYTE_MASK;\n const toBit = ((end - 1) & BYTE_MASK) + 1;\n\n if (firstByte === lastByte) {\n bytes[firstByte] |= maskRange(fromBit, toBit);\n return;\n }\n\n bytes[firstByte] |= maskRange(fromBit, BITS_PER_BYTE);\n bytes.fill(FULL_BYTE, firstByte + 1, lastByte);\n bytes[lastByte] |= maskRange(0, toBit);\n },\n\n // Same head/middle/tail shape as fill, with the masks inverted.\n clearRange: (start, end) => {\n if (end <= start) return;\n\n const firstByte = start >>> BYTE_SHIFT;\n const lastByte = (end - 1) >>> BYTE_SHIFT;\n const fromBit = start & BYTE_MASK;\n const toBit = ((end - 1) & BYTE_MASK) + 1;\n\n if (firstByte === lastByte) {\n bytes[firstByte] &= ~maskRange(fromBit, toBit);\n return;\n }\n\n bytes[firstByte] &= ~maskRange(fromBit, BITS_PER_BYTE);\n bytes.fill(0, firstByte + 1, lastByte);\n bytes[lastByte] &= ~maskRange(0, toBit);\n },\n\n // Head and tail are masked before counting; the middle bytes are counted\n // whole, one table lookup each.\n countRange: (start, end) => {\n if (end <= start) return 0;\n\n const firstByte = start >>> BYTE_SHIFT;\n const lastByte = (end - 1) >>> BYTE_SHIFT;\n const fromBit = start & BYTE_MASK;\n const toBit = ((end - 1) & BYTE_MASK) + 1;\n\n if (firstByte === lastByte) {\n return POPCOUNT[bytes[firstByte] & maskRange(fromBit, toBit)];\n }\n\n let total = POPCOUNT[bytes[firstByte] & maskRange(fromBit, BITS_PER_BYTE)];\n for (let b = firstByte + 1; b < lastByte; b += 1) total += POPCOUNT[bytes[b]];\n total += POPCOUNT[bytes[lastByte] & maskRange(0, toBit)];\n return total;\n },\n\n clear: () => {\n bytes.fill(0);\n count = 0;\n },\n\n /**\n * fill and clearRange do not maintain `count`, because recounting a\n * million-bit span per call would defeat the point. Callers that use them\n * compute the delta themselves and write the total back here.\n */\n setCount: (next) => {\n count = next;\n },\n\n get count() {\n return count;\n },\n };\n};\n","/**\n * Normalises caller-supplied block ranges into one canonical form: a flat\n * Uint32Array of sorted, non-overlapping [start, end) pairs. Everything\n * downstream can then assume that shape; this module does no state keeping.\n */\n\nimport { RANGE_PAIR } from \"./constants\";\n\n// A half-open [start, end) pair: both what callers hand in as range input and\n// what the map reports back from hit tests and the brush.\nexport type BlockRange = readonly [start: number, end: number];\nexport type RangeInput = ArrayLike<BlockRange> | ArrayLike<number>;\n\ntype Pair = [number, number];\n\n/**\n * Two accepted input shapes: a flat [s, e, s, e, ...] list (typically a\n * typed array straight off the wire) or an array of [start, end] tuples.\n * A typed array is always flat; otherwise a numeric first element decides.\n */\nconst isFlatPairs = (input: RangeInput): input is ArrayLike<number> =>\n ArrayBuffer.isView(input) || typeof input[0] === \"number\";\n\nconst readPairs = (input: RangeInput): Pair[] => {\n const pairs: Pair[] = [];\n\n if (isFlatPairs(input)) {\n const pairCount = Math.floor(input.length / RANGE_PAIR);\n for (let i = 0; i < pairCount; i += 1) {\n pairs.push([input[i * RANGE_PAIR], input[i * RANGE_PAIR + 1]]);\n }\n return pairs;\n }\n\n for (let i = 0; i < input.length; i += 1) {\n const tuple = input[i];\n pairs.push([tuple[0], tuple[1]]);\n }\n return pairs;\n};\n\n/**\n * Drops non-integer and empty ranges, clamps to [0, totalBlocks), then sorts\n * by start and merges anything that overlaps or touches. Merging matters\n * because replaceRanges trusts that shape: it sums the range lengths for the\n * progress total and adds each range into the bin counts arithmetically, so\n * an overlap would be counted twice.\n */\nexport const normalizeRanges = (input: RangeInput, totalBlocks: number): Uint32Array => {\n const clamped: Pair[] = [];\n for (const [rawStart, rawEnd] of readPairs(input)) {\n if (!Number.isInteger(rawStart) || !Number.isInteger(rawEnd)) continue;\n const start = Math.max(0, rawStart);\n const end = Math.min(totalBlocks, rawEnd);\n if (end > start) clamped.push([start, end]);\n }\n clamped.sort((a, b) => a[0] - b[0]);\n\n // merged holds the flat result, so its last element is the previous end;\n // a start at or below it extends that pair instead of opening a new one.\n const merged: number[] = [];\n for (const [start, end] of clamped) {\n const lastIndex = merged.length - 1;\n if (merged.length > 0 && start <= merged[lastIndex]) {\n merged[lastIndex] = Math.max(merged[lastIndex], end);\n } else {\n merged.push(start, end);\n }\n }\n return Uint32Array.from(merged);\n};\n\n// Total blocks covered; only correct for normalized (non-overlapping) input.\nexport const rangesLength = (flat: Uint32Array): number => {\n let total = 0;\n for (let i = 0; i < flat.length; i += RANGE_PAIR) total += flat[i + 1] - flat[i];\n return total;\n};\n","/**\n * The model: which blocks are complete (bitset), how they fold into the\n * current view's bins, and which bins changed. Deliberately free of DOM, D3\n * and canvas imports so it stays portable - it could run in a worker and\n * post dirty-bin arrays back to the renderer unchanged.\n */\n\nimport { createBitset } from \"./bitset\";\nimport { createBins } from \"./bins\";\nimport type { Bins } from \"./bins\";\nimport { MAX_BLOCKS, RANGE_PAIR } from \"./constants\";\nimport { normalizeRanges, rangesLength } from \"./ranges\";\nimport type { BlockRange, RangeInput } from \"./ranges\";\nimport { fullView, sameView } from \"./view\";\nimport type { BlockView } from \"./view\";\n\nexport type Progress = { completed: number; total: number };\n\n/**\n * Dirty arrays are ascending and deduplicated.\n */\nexport type BlockState = {\n readonly totalBlocks: number;\n readonly binCount: number;\n resize(binCount: number): void;\n setView(view: BlockView): boolean;\n getView(): BlockView;\n add(indices: ArrayLike<number>): Uint32Array;\n replace(indices: ArrayLike<number>): void;\n replaceRanges(input: RangeInput): void;\n addRanges(input: RangeInput): Uint32Array;\n removeRanges(input: RangeInput): Uint32Array;\n has(index: number): boolean;\n fraction(bin: number): number;\n countOf(bin: number): number;\n rangeOf(bin: number): BlockRange;\n progress(): Progress;\n};\n\n// Shared immutable \"nothing changed\" result, so the common case allocates no\n// array at all.\nconst EMPTY_DIRTY = new Uint32Array(0);\n\nconst isValidIndex = (index: number, totalBlocks: number): boolean =>\n Number.isInteger(index) && index >= 0 && index < totalBlocks;\n\nexport const createBlockState = (totalBlocks: number): BlockState => {\n if (!Number.isInteger(totalBlocks) || totalBlocks < 1) {\n throw new RangeError(`totalBlocks must be a positive integer, got ${totalBlocks}`);\n }\n\n // The bitset addresses bytes with `index >>> BYTE_SHIFT`, and the unsigned\n // shift coerces its operand to a uint32: an index past 2^32 - 1 would wrap\n // round and corrupt an unrelated block, so reject it up front.\n if (totalBlocks > MAX_BLOCKS) {\n throw new RangeError(\n `totalBlocks must fit in an unsigned 32-bit integer (<= ${MAX_BLOCKS}), got ${totalBlocks}`,\n );\n }\n\n const bitset = createBitset(totalBlocks);\n let view: BlockView = fullView(totalBlocks);\n\n // Bins are created empty: binCount is unknown until the container has been\n // measured, so nothing can be binned before the first resize.\n let bins: Bins = createBins(view, 0);\n\n const inView = (index: number): boolean => index >= view.start && index < view.end;\n\n /**\n * Incremental add of individual indices. bitset.set reports whether the bit\n * actually flipped, so an already-complete block costs no bin update and\n * does not report its bin dirty. A Set then Uint32Array.from().sort() gives\n * the ascending, deduplicated result the renderer expects; typed-array sort\n * is numeric, so no comparator is needed.\n */\n const add = (indices: ArrayLike<number>): Uint32Array => {\n const hasBins = bins.binCount > 0;\n const dirty = new Set<number>();\n\n for (let i = 0; i < indices.length; i += 1) {\n const index = indices[i];\n if (!isValidIndex(index, totalBlocks) || !bitset.set(index)) continue;\n if (!hasBins || !inView(index)) continue;\n\n const bin = bins.binOf(index);\n bins.counts[bin] += 1;\n dirty.add(bin);\n }\n\n return dirty.size === 0 ? EMPTY_DIRTY : Uint32Array.from(dirty).sort();\n };\n\n /**\n * Shared body of addRanges and removeRanges. Incoming ranges may overlap\n * bits that are already in the target state, so the change per bin cannot\n * be derived from the range bounds alone - hence countRange before each\n * mutation for the global delta, and a recount of every touched bin from\n * the bitset afterwards. The alternative, recounting all bins, would cost a\n * full pass over millions of blocks per update.\n */\n const applyRanges = (input: RangeInput, complete: boolean): Uint32Array => {\n const flat = normalizeRanges(input, totalBlocks);\n const dirty = new Set<number>();\n let delta = 0;\n\n for (let i = 0; i < flat.length; i += RANGE_PAIR) {\n const start = flat[i];\n const end = flat[i + 1];\n const present = bitset.countRange(start, end);\n\n if (complete) {\n bitset.fill(start, end);\n delta += end - start - present;\n } else {\n bitset.clearRange(start, end);\n delta -= present;\n }\n\n for (const bin of bins.binsTouching(start, end)) dirty.add(bin);\n }\n\n bins.recountBins(dirty, bitset.countRange);\n bitset.setCount(bitset.count + delta);\n\n return dirty.size === 0 ? EMPTY_DIRTY : Uint32Array.from(dirty).sort();\n };\n\n return {\n totalBlocks,\n get binCount() {\n return bins.binCount;\n },\n // Column count changed: the bin boundaries move, so rebuild from the\n // bitset rather than trying to redistribute the old counts.\n resize: (binCount) => {\n bins = createBins(view, binCount);\n bins.rebuild(bitset.countRange);\n },\n\n // Takes a view legalView has already checked, so the façade validates once\n // per request rather than again at commit. Returns whether anything moved,\n // so callers can skip a repaint.\n setView: (next) => {\n if (sameView(next, view)) return false;\n\n view = { ...next };\n bins = createBins(view, bins.binCount);\n bins.rebuild(bitset.countRange);\n return true;\n },\n\n // A copy, because `view` is live and handed to external callbacks.\n getView: () => ({ ...view }),\n\n add,\n\n replace: (indices) => {\n bitset.clear();\n bins.counts.fill(0);\n add(indices);\n },\n\n /**\n * Ranges are already merged and disjoint, so each one's blocks are new to\n * the bitset: bins.addRange can add sizes arithmetically and the total is\n * just the summed range lengths, with no counting pass.\n */\n replaceRanges: (input) => {\n const flat = normalizeRanges(input, totalBlocks);\n bitset.clear();\n bins.counts.fill(0);\n\n for (let i = 0; i < flat.length; i += RANGE_PAIR) {\n bitset.fill(flat[i], flat[i + 1]);\n bins.addRange(flat[i], flat[i + 1]);\n }\n\n bitset.setCount(rangesLength(flat));\n },\n\n addRanges: (input) => applyRanges(input, true),\n removeRanges: (input) => applyRanges(input, false),\n\n has: bitset.has,\n\n // Coverage in [0, 1], which is what the colour scale is indexed by.\n fraction: (bin) => {\n const size = bins.sizeOf(bin);\n return size === 0 ? 0 : bins.counts[bin] / size;\n },\n\n countOf: (bin) => bins.counts[bin],\n rangeOf: (bin) => bins.rangeOf(bin),\n progress: () => ({ completed: bitset.count, total: totalBlocks }),\n };\n};\n","/**\n * Hover readout: the label strings and the single absolutely positioned div\n * that shows them. The formatters take a range and a count rather than the\n * state object, so they are pure and testable; this module does no hit\n * testing - the caller decides which bin or block is under the pointer.\n */\n\nimport { DECIMAL_BASE, PERCENT, PERCENT_FRACTION_DIGITS, TOOLTIP_OFFSET_PX } from \"./constants\";\nimport { ensureRelative } from \"./dom\";\nimport type { BlockRange } from \"./ranges\";\n\nexport type Tooltip = {\n show(x: number, text: string): void;\n hide(): void;\n destroy(): void;\n};\n\nconst LOCALE = \"en-US\";\nconst TOOLTIP_ATTRIBUTE = \"data-blockmap-tooltip\";\nconst SEPARATOR = \" · \";\nconst EN_DASH = \"–\";\n\nconst countFormat = new Intl.NumberFormat(LOCALE);\n\n// Smallest percentage one fraction digit can show, and the fixed-point\n// strings that rounding collapses onto at either end of the range.\nconst MIN_PARTIAL_STEP = 1 / DECIMAL_BASE ** PERCENT_FRACTION_DIGITS;\nconst MAX_PARTIAL = (PERCENT - MIN_PARTIAL_STEP).toFixed(PERCENT_FRACTION_DIGITS);\nconst MIN_PARTIAL = MIN_PARTIAL_STEP.toFixed(PERCENT_FRACTION_DIGITS);\nconst MAX_FIXED = PERCENT.toFixed(PERCENT_FRACTION_DIGITS);\nconst MIN_FIXED = (0).toFixed(PERCENT_FRACTION_DIGITS);\n\n/**\n * At a million blocks per bin, one missing block rounds to 100.0% and one\n * present block to 0.0%, which would read as finished or untouched. Only an\n * exact count gets a bare 0% or 100%; anything that merely rounds there is\n * reported as a bound instead, \">99.9%\" or \"<0.1%\".\n */\nconst formatPercent = (count: number, size: number): string => {\n if (count === 0) return \"0%\";\n if (count === size) return \"100%\";\n\n const value = (count / size) * PERCENT;\n const text = value.toFixed(PERCENT_FRACTION_DIGITS);\n\n if (text === MAX_FIXED) {\n return `>${MAX_PARTIAL}%`;\n }\n if (text === MIN_FIXED) {\n return `<${MIN_PARTIAL}%`;\n }\n return `${text}%`;\n};\n\n// Ranges are half-open, so the label's last block is end - 1, and a bin that\n// covers a single block is named rather than given a degenerate range.\nexport const formatBinTooltip = ([start, end]: BlockRange, count: number): string => {\n const size = end - start;\n const label =\n size === 1\n ? `Block ${countFormat.format(start)}`\n : `Blocks ${countFormat.format(start)}${EN_DASH}${countFormat.format(end - 1)}`;\n return `${label}${SEPARATOR}${countFormat.format(count)} / ${countFormat.format(size)} (${formatPercent(count, size)})`;\n};\n\n// Used when zoomed in far enough that a column is one block, where coverage\n// is binary and a percentage would be noise.\nexport const formatBlockTooltip = (index: number, complete: boolean): string =>\n `Block ${countFormat.format(index)}${SEPARATOR}${complete ? \"complete\" : \"missing\"}`;\n\nexport const createTooltip = (container: HTMLElement): Tooltip => {\n // translateY(-100%) lifts the box clear of the canvas, and pointerEvents\n // none keeps it from intercepting the pointermove events on the canvas\n // that position it in the first place.\n const el = document.createElement(\"div\");\n el.setAttribute(TOOLTIP_ATTRIBUTE, \"\");\n Object.assign(el.style, {\n position: \"absolute\",\n top: \"0\",\n left: \"0\",\n display: \"none\",\n pointerEvents: \"none\",\n whiteSpace: \"nowrap\",\n transform: \"translateY(-100%)\",\n });\n ensureRelative(container);\n container.append(el);\n\n return {\n // x is in container-relative CSS pixels; the offset keeps the label from\n // sitting directly under the cursor.\n show: (x, text) => {\n el.textContent = text;\n el.style.left = `${x + TOOLTIP_OFFSET_PX}px`;\n el.style.display = \"block\";\n },\n\n hide: () => {\n el.style.display = \"none\";\n },\n\n destroy: () => {\n el.remove();\n },\n };\n};\n","/**\n * The façade: creates the canvas, wires state, renderer, tooltip, axis,\n * brush overlay and interaction together, and owns everything with a\n * lifetime - observers, media queries, the pending-view frame. The pieces it\n * composes stay unaware of each other; all cross-cutting policy (when to\n * repaint, what a gesture means for the model) lives here.\n */\n\nimport { createAxis } from \"./axis\";\nimport type { Axis, AxisOptions } from \"./axis\";\nimport { discreteBlock } from \"./bins\";\nimport { createBrushOverlay } from \"./brush\";\nimport type { BrushOverlay } from \"./brush\";\nimport { createColorScale, highlightColor, readTheme, watchTheme } from \"./colors\";\nimport type { PartialStyle, Theme } from \"./colors\";\nimport {\n ARIA_KEYSHORTCUTS,\n ARIA_LABEL_DEFAULT_PERCENT_DIGITS,\n ARIA_ROLEDESCRIPTION,\n CANVAS_ROWS,\n DRAG_THRESHOLD_PX,\n PERCENT,\n REDUCED_MOTION_QUERY,\n} from \"./constants\";\nimport { canvasX } from \"./dom\";\nimport { attachInteraction } from \"./interaction\";\nimport { createRenderer } from \"./renderer\";\nimport type { Renderer } from \"./renderer\";\nimport type { BlockRange, RangeInput } from \"./ranges\";\nimport { createBlockState } from \"./state\";\nimport type { Progress } from \"./state\";\nimport { createTooltip, formatBinTooltip, formatBlockTooltip } from \"./tooltip\";\nimport { createScale, fullView, legalView, sameView, viewLength } from \"./view\";\nimport type { BlockScale, BlockView } from \"./view\";\n\nexport type BlockMapOptions = {\n totalBlocks: number;\n initialBlocks?: ArrayLike<number>;\n initialRanges?: RangeInput;\n partial?: PartialStyle;\n view?: BlockView;\n interactive?: boolean;\n axis?: boolean | AxisOptions;\n brush?: boolean;\n onViewChange?: (view: BlockView) => void;\n onBlockClick?: (event: BlockClickEvent) => void;\n onBrush?: (selection: BlockRange | null) => void;\n onBrushEnd?: (selection: BlockRange | null) => void;\n label?: (progress: Progress) => string;\n};\n\n// Reported from a click on the surface: block/range/completed/total mirror\n// whichever mode located the pointer, discrete block or binned column, and\n// originalEvent carries the native MouseEvent for modifier keys or\n// preventDefault.\nexport type BlockClickEvent = {\n block: number;\n range: BlockRange;\n completed: number;\n total: number;\n originalEvent: MouseEvent;\n};\n\nexport type BlockMap = {\n setBlocks(indices: ArrayLike<number>): void;\n setRanges(ranges: RangeInput): void;\n addBlocks(indices: ArrayLike<number>): void;\n addRanges(ranges: RangeInput): void;\n removeRanges(ranges: RangeInput): void;\n setView(view: BlockView): void;\n resetView(): void;\n getView(): BlockView;\n getScale(): BlockScale;\n setBrush(selection: BlockRange | null): void;\n getBrush(): BlockRange | null;\n setHighlight(on: boolean): void;\n getProgress(): Progress;\n destroy(): void;\n};\n\nconst FULL = \"100%\";\nconst CONTEXT_ID = \"2d\";\nconst NO_FRAME = 0;\nconst LOCALE = \"en-US\";\nconst ARIA_LABEL_ATTRIBUTE = \"aria-label\";\n\nconst numberFormat = new Intl.NumberFormat(LOCALE);\n\n// What a screen reader announces when nothing else is specified: grouped\n// counts so large totals read in thousands rather than a wall of digits.\n// The percentage is floored, not rounded, so it only reads 100% once every\n// block actually is, and reports a last block still missing as 99%.\nexport const defaultLabel = (progress: Progress): string => {\n const percent = Math.floor((progress.completed / progress.total) * PERCENT).toFixed(\n ARIA_LABEL_DEFAULT_PERCENT_DIGITS,\n );\n return `Download progress: ${numberFormat.format(progress.completed)} of ${numberFormat.format(progress.total)} blocks (${percent}%)`;\n};\n\n// devicePixelRatio fires no change event, so the current ratio is watched as\n// a media query that stops matching when it moves.\nconst dprQuery = (dpr: number): string => `(resolution: ${dpr}dppx)`;\n\nexport const createBlockMap = (container: HTMLElement, options: BlockMapOptions): BlockMap => {\n const state = createBlockState(options.totalBlocks);\n const initialView = options.view && legalView(options.view, state.totalBlocks);\n if (initialView) state.setView(initialView);\n\n // One pixel row stretched to fill the container: width is set in layout\n // once the container has been measured. touchAction none so a drag pans\n // the view instead of scrolling the page.\n const canvas = document.createElement(\"canvas\");\n Object.assign(canvas.style, {\n display: \"block\",\n width: FULL,\n height: FULL,\n touchAction: \"none\",\n });\n canvas.height = CANVAS_ROWS;\n canvas.setAttribute(\"role\", \"img\");\n canvas.setAttribute(\"aria-roledescription\", ARIA_ROLEDESCRIPTION);\n // Only a canvas the keyboard can reach needs to be in the tab order or\n // advertise key shortcuts; a non-interactive map has nothing for arrow\n // keys or zoom keys to do.\n if (options.interactive !== false) {\n canvas.tabIndex = 0;\n canvas.setAttribute(\"aria-keyshortcuts\", ARIA_KEYSHORTCUTS);\n }\n container.append(canvas);\n\n // Remove the canvas before throwing, so a failed construction leaves no\n // orphan element in the caller's container.\n const ctx = canvas.getContext(CONTEXT_ID);\n if (!ctx) {\n canvas.remove();\n throw new Error(`Could not acquire a ${CONTEXT_ID} canvas context`);\n }\n\n const renderer: Renderer = createRenderer(ctx, state);\n\n // Declared before the children below because the overlay measures the\n // container through these the moment it is created.\n let destroyed = false;\n let cssWidth = 0;\n let cssHeight = 0;\n let dpr = devicePixelRatio;\n let dprMedia: MediaQueryList | undefined;\n let theme: Theme = readTheme(container);\n let highlightOn = false;\n let dragging = false;\n // Where the last press went down. NaN until the first one, so a click with\n // no press before it - a synthetic one - is never taken for a drag's end.\n let downX = Number.NaN;\n\n // Appended straight after the canvas so it covers the bar, and before the\n // tooltip and the axis so those keep painting over it. The committed view\n // is what the canvas shows, so the overlay projects against that rather\n // than a view change still waiting for its frame.\n const overlay: BrushOverlay | undefined =\n options.brush === true\n ? createBrushOverlay(container, {\n getView: () => state.getView(),\n getWidth: () => cssWidth,\n getHeight: () => cssHeight,\n getTotalBlocks: () => state.totalBlocks,\n onBrush: (selection) => options.onBrush?.(selection),\n onBrushEnd: (selection) => options.onBrushEnd?.(selection),\n })\n : undefined;\n\n const tooltip = createTooltip(container);\n\n // `axis: true` means defaults, an object means those options, and both\n // false and absent mean no axis at all.\n const axisOptions: AxisOptions | undefined =\n options.axis === true\n ? {}\n : options.axis === false || options.axis === undefined\n ? undefined\n : options.axis;\n const axis: Axis | undefined = axisOptions ? createAxis(container, axisOptions) : undefined;\n\n // Created once and read live, rather than re-queried per call, so the\n // change listener below can be attached to this exact MediaQueryList.\n const motionMedia = matchMedia(REDUCED_MOTION_QUERY);\n const blinkAllowed = (): boolean => !motionMedia.matches;\n\n const applyHighlight = (): void => {\n renderer.setHighlight(highlightOn, highlightColor(theme), blinkAllowed());\n };\n\n // The preference can flip while the highlight is already running and its\n // blink timer mid-flight; only a fresh call to applyHighlight picks up the\n // new blinkAllowed() value, and only the active highlight needs it.\n const onMotionChange = (): void => {\n if (highlightOn) applyHighlight();\n };\n motionMedia.addEventListener(\"change\", onMotionChange);\n\n // Re-reads the custom properties and rebuilds the 256-entry table; the\n // highlight colour is separate from that table, so it needs its own push.\n const applyTheme = (): void => {\n theme = readTheme(container);\n renderer.setColors(createColorScale(theme, options.partial));\n if (highlightOn) renderer.setHighlightColor(highlightColor(theme));\n overlay?.setColor(theme.brush);\n };\n\n // Reads the current progress through options.label when given, defaultLabel\n // otherwise, and writes it straight to the attribute. Every mutator calls it\n // directly: one count and one string per call is cheap next to the mutation\n // itself, and the label never lags the state.\n const writeLabel = (): void => {\n canvas.setAttribute(ARIA_LABEL_ATTRIBUTE, (options.label ?? defaultLabel)(state.progress()));\n };\n\n // The axis and the brush overlay are both laid out in css pixels against\n // the current view, so neither can be refreshed without the other.\n const updateChrome = (): void => {\n axis?.update(state.getView(), cssWidth);\n overlay?.update();\n };\n\n let pendingView: BlockView | undefined;\n let viewFrame = NO_FRAME;\n\n // Reports the requested view, not the committed one, so a read straight\n // after setView sees what the caller just asked for.\n const getView = (): BlockView => ({ ...(pendingView ?? state.getView()) });\n\n /**\n * Applies at most one view change per frame. Rebinning a view change is the\n * expensive part - it recounts every bin - so a wheel spin or a drag that\n * produces several candidates per frame only pays for the last one.\n */\n const commitView = (): void => {\n viewFrame = NO_FRAME;\n const next = pendingView;\n pendingView = undefined;\n if (!next || !state.setView(next)) return;\n\n renderer.markAllDirty();\n updateChrome();\n options.onViewChange?.(state.getView());\n };\n\n /**\n * Repeated calls within a frame overwrite the target rather than queueing\n * more frames. Comparing against the pending view means a gesture that ends\n * where it started is dropped without a repaint.\n */\n const setView = (view: BlockView): void => {\n const legal = legalView(view, state.totalBlocks);\n if (!legal || sameView(legal, pendingView ?? state.getView())) return;\n\n pendingView = legal;\n if (viewFrame !== NO_FRAME) return;\n viewFrame = requestAnimationFrame(commitView);\n };\n\n const resetView = (): void => setView(fullView(state.totalBlocks));\n\n /**\n * One bin per device pixel, so the map never shows more detail than the\n * screen can resolve. A zero css width means the container has not been\n * measured yet (or has collapsed), so there is nothing to lay out and the\n * chrome is left alone rather than refreshed against a width of zero. Only\n * a bin count change resizes the canvas: assigning canvas.width clears the\n * backing store, so a no-op resize would blank the canvas until the next\n * frame. The chrome is placed in css pixels, which can move while the bin\n * count stays put, so it is always refreshed.\n */\n const layout = (): void => {\n if (cssWidth === 0) return;\n\n const binCount = Math.round(cssWidth * dpr);\n if (binCount !== state.binCount) {\n canvas.width = binCount;\n state.resize(binCount);\n renderer.markAllDirty();\n }\n updateChrome();\n };\n\n const onDprChange = (): void => {\n dpr = devicePixelRatio;\n layout();\n watchDpr();\n };\n\n // The query only tells us when the ratio stops being what it was, so it is\n // a one-shot listener that is re-armed at the new ratio each time - page\n // zoom and dragging the window to another display both land here.\n const watchDpr = (): void => {\n dprMedia?.removeEventListener(\"change\", onDprChange);\n dprMedia = matchMedia(dprQuery(dpr));\n dprMedia.addEventListener(\"change\", onDprChange, { once: true });\n };\n\n const resizeObserver = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (!entry) return;\n cssWidth = entry.contentRect.width;\n cssHeight = entry.contentRect.height;\n layout();\n });\n resizeObserver.observe(container);\n\n type Hit = {\n block: number;\n range: BlockRange;\n completed: number;\n total: number;\n };\n\n /**\n * Hit test shared by the tooltip and the click listener, mirroring how the\n * renderer chose to paint: fewer blocks than columns means each block spans\n * several columns and the readout names a single block, otherwise a column\n * is a bin and the readout is a coverage count. An x outside the canvas hit\n * nothing: the listeners are on the container, so container padding or a\n * brush handle sticking out past an edge would otherwise report the nearest\n * column as a phantom hit. Within the canvas the column is still clamped,\n * since x scaled by dpr can round onto the column past the last one.\n * Returns undefined before the first layout too, when there are no bins.\n */\n const locate = (x: number): Hit | undefined => {\n const { binCount } = state;\n if (binCount === 0) return undefined;\n if (x < 0 || x >= cssWidth) return undefined;\n\n const column = Math.min(binCount - 1, Math.max(0, Math.floor(x * dpr)));\n const view = state.getView();\n const length = viewLength(view);\n\n if (length < binCount) {\n const block = view.start + discreteBlock(column, length, binCount);\n return {\n block,\n range: [block, block + 1],\n completed: state.has(block) ? 1 : 0,\n total: 1,\n };\n }\n\n const [start, end] = state.rangeOf(column);\n return {\n block: start,\n range: [start, end],\n completed: state.countOf(column),\n total: end - start,\n };\n };\n\n // Suppressed while dragging, where the pointer means pan rather than hover.\n // A pointer inside the container but off the canvas - padding, or a brush\n // handle poking past the edge - hides the readout rather than leaving a\n // stale one behind.\n const onPointerMove = (event: PointerEvent): void => {\n if (dragging) return;\n\n const x = canvasX(canvas, event);\n const hit = locate(x);\n if (!hit) {\n tooltip.hide();\n return;\n }\n\n const text =\n viewLength(state.getView()) < state.binCount\n ? formatBlockTooltip(hit.block, hit.completed === 1)\n : formatBinTooltip(hit.range, hit.completed);\n tooltip.show(x, text);\n };\n\n const onPointerLeave = (): void => tooltip.hide();\n\n const onPointerDown = (event: PointerEvent): void => {\n downX = event.clientX;\n };\n\n // A click that lands as far from its press as a drag has to travel is the\n // end of a pan or a brush, not an intent to pick a block. Distance alone\n // decides, and each press is read by at most one click, so no gesture can\n // leave state behind for the next one.\n // Registered regardless of `interactive`, like the tooltip listeners above,\n // since a non-interactive map can still report what was clicked.\n const onClick = (event: MouseEvent): void => {\n const travelled = Math.abs(event.clientX - downX);\n downX = Number.NaN;\n if (travelled >= DRAG_THRESHOLD_PX) return;\n if (!options.onBlockClick) return;\n\n const hit = locate(canvasX(canvas, event));\n if (!hit) return;\n\n options.onBlockClick({ ...hit, originalEvent: event });\n };\n\n container.addEventListener(\"pointermove\", onPointerMove);\n container.addEventListener(\"pointerleave\", onPointerLeave);\n container.addEventListener(\"pointerdown\", onPointerDown);\n container.addEventListener(\"click\", onClick);\n\n // Interaction reads the live view and total through getters rather than\n // captured values, since both change under it as zoom and resize happen.\n const detachInteraction =\n options.interactive === false\n ? undefined\n : attachInteraction(container, canvas, {\n getView,\n setView,\n resetView,\n getBinCount: () => state.binCount,\n getDpr: () => dpr,\n getTotalBlocks: () => state.totalBlocks,\n panRequiresShift: () => overlay !== undefined,\n onDragStart: () => {\n dragging = true;\n tooltip.hide();\n },\n onDragEnd: () => {\n dragging = false;\n },\n });\n\n const unwatchTheme = watchTheme(applyTheme);\n watchDpr();\n applyTheme();\n\n // Ranges first, then loose blocks on top: initialRanges replaces the whole\n // bitset, so the opposite order would discard the individual blocks.\n if (options.initialRanges) state.replaceRanges(options.initialRanges);\n if (options.initialBlocks) renderer.markDirty(state.add(options.initialBlocks));\n writeLabel();\n\n // Every mutating method runs through this, so a late async update from a\n // caller that has already torn the map down is a no-op rather than a write\n // to a detached canvas.\n const live =\n <A extends unknown[]>(method: (...args: A) => void) =>\n (...args: A): void => {\n if (!destroyed) method(...args);\n };\n\n return {\n setBlocks: live((indices) => {\n state.replace(indices);\n renderer.markAllDirty();\n writeLabel();\n }),\n setRanges: live((ranges) => {\n state.replaceRanges(ranges);\n renderer.markAllDirty();\n writeLabel();\n }),\n\n // The incremental paths repaint only the bins the state reports changed.\n addBlocks: live((indices) => {\n renderer.markDirty(state.add(indices));\n writeLabel();\n }),\n addRanges: live((ranges) => {\n renderer.markDirty(state.addRanges(ranges));\n writeLabel();\n }),\n removeRanges: live((ranges) => {\n renderer.markDirty(state.removeRanges(ranges));\n writeLabel();\n }),\n\n setView: live(setView),\n resetView: live(resetView),\n getView,\n\n // Built from the committed view, so the scale always matches what is on\n // screen rather than a change still waiting for its frame.\n getScale: () => createScale(state.getView(), cssWidth),\n\n // Selections are only meaningful while there is an overlay to draw them:\n // without one setBrush has nothing to store and getBrush nothing to\n // report. A destroyed map reports none either, rather than whatever the\n // detached overlay was last holding.\n setBrush: live((selection) => overlay?.setSelection(selection)),\n getBrush: () => (destroyed ? null : (overlay?.getSelection() ?? null)),\n\n setHighlight: live((on) => {\n highlightOn = on;\n applyHighlight();\n }),\n getProgress: () => (destroyed ? { completed: 0, total: state.totalBlocks } : state.progress()),\n\n // Idempotent, and unwinds in the reverse order of construction: pending\n // frame, observers, listeners, owned children, then the canvas itself.\n destroy: live(() => {\n destroyed = true;\n\n if (viewFrame !== NO_FRAME) cancelAnimationFrame(viewFrame);\n viewFrame = NO_FRAME;\n pendingView = undefined;\n\n resizeObserver.disconnect();\n unwatchTheme();\n dprMedia?.removeEventListener(\"change\", onDprChange);\n motionMedia.removeEventListener(\"change\", onMotionChange);\n container.removeEventListener(\"pointermove\", onPointerMove);\n container.removeEventListener(\"pointerleave\", onPointerLeave);\n container.removeEventListener(\"pointerdown\", onPointerDown);\n container.removeEventListener(\"click\", onClick);\n detachInteraction?.();\n\n renderer.destroy();\n tooltip.destroy();\n axis?.destroy();\n overlay?.destroy();\n canvas.remove();\n }),\n };\n};\n"],"mappings":";AAMA,SAAS,gBAAgB;;;ACElB,IAAM,gBAAgB;AACtB,IAAM,aAAa;AACnB,IAAM,YAAY;AAClB,IAAM,YAAY;AAIlB,IAAM,kBAAkB;AACxB,IAAM,eAAe;AACrB,IAAM,mBAAmB;AAIzB,IAAM,cAAc;AAGpB,IAAM,UAAU;AAChB,IAAM,0BAA0B;AAChC,IAAM,oBAAoB;AAI1B,IAAM,kBAAkB;AACxB,IAAM,sBAAsB;AAC5B,IAAM,uBAAuB;AAC7B,IAAM,kBAAkB;AACxB,IAAM,mBAAmB;AACzB,IAAM,oBAAoB;AAC1B,IAAM,gBAAgB;AAEtB,IAAM,gBAAgB;AAAA,EAC3B,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,SAAS;AAAA,EACT,UAAU;AAAA,EACV,WAAW;AAAA,EACX,OAAO;AACT;AAIO,IAAM,qBAAqB;AAI3B,IAAM,mBAAmB,CAAC,SAAS,SAAS,YAAY;AACxD,IAAM,oBAAoB;AAM1B,IAAM,YAAY;AAClB,IAAM,oBAAoB;AAC1B,IAAM,oBAAoB;AAC1B,IAAM,uBAAuB;AAC7B,IAAM,kBAAkB;AAIxB,IAAM,mBAAmB;AACzB,IAAM,wBAAwB;AAG9B,IAAM,oCAAoC;AAI1C,IAAM,uBAAuB;AAC7B,IAAM,oBAAoB;AAI1B,IAAM,qBAAqB;AAC3B,IAAM,kBAAkB;AACxB,IAAM,YAAY;AAClB,IAAM,oBAAoB;AAC1B,IAAM,eAAe;AAIrB,IAAM,aAAa;AACnB,IAAM,aAAa;;;AC7EnB,IAAM,iBAAiB,CAAC,cAAiC;AAC9D,QAAM,WAAW,iBAAiB,SAAS,EAAE;AAC7C,MAAI,aAAa,YAAY,CAAC,SAAU,WAAU,MAAM,WAAW;AACrE;AASO,IAAM,UAAU,CAAC,QAA2B,UACjD,MAAM,UAAU,OAAO,sBAAsB,EAAE;;;ACV1C,IAAM,WAAW,CAAC,iBAAoC,EAAE,OAAO,GAAG,KAAK,YAAY;AAEnF,IAAM,aAAa,CAAC,SAA4B,KAAK,MAAM,KAAK;AAEhE,IAAM,WAAW,CAAC,GAAc,MACrC,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE;AAQ9B,IAAM,cAAc,CAAC,MAAiB,aAAiC;AAC5E,QAAM,SAAS,WAAW,IAAI;AAE9B,SAAO;AAAA,IACL,QAAQ,CAAC,KAAK,OAAO,KAAK,GAAG;AAAA,IAC7B,OAAO,CAAC,GAAG,QAAQ;AAAA,IACnB,GAAG,CAAC,UAAW,aAAa,IAAI,KAAM,QAAQ,KAAK,SAAS,SAAU;AAAA,IACtE,OAAO,CAAC,MAAO,aAAa,IAAI,KAAK,QAAQ,KAAK,QAAS,IAAI,WAAY;AAAA,EAC7E;AACF;AAOO,IAAM,YAAY,CAAC,MAAiB,gBAAmC;AAC5E,MAAI,QAAQ,KAAK,MAAM,KAAK,IAAI,KAAK,OAAO,KAAK,GAAG,CAAC;AACrD,MAAI,MAAM,KAAK,MAAM,KAAK,IAAI,KAAK,OAAO,KAAK,GAAG,CAAC;AACnD,MAAI,MAAM,QAAQ,gBAAiB,OAAM,QAAQ;AAEjD,QAAM,SAAS,KAAK,IAAI,MAAM,OAAO,WAAW;AAChD,UAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,cAAc,MAAM,CAAC;AACzD,SAAO,EAAE,OAAO,KAAK,QAAQ,OAAO;AACtC;AAOO,IAAM,YAAY,CAAC,MAAiB,gBACzC,OAAO,SAAS,KAAK,KAAK,KAAK,OAAO,SAAS,KAAK,GAAG,IACnD,UAAU,MAAM,WAAW,IAC3B;AASC,IAAM,WAAW,CACtB,MACA,QACA,QACA,gBACc;AACd,QAAM,SAAS,WAAW,IAAI;AAC9B,QAAM,SAAS,SAAS;AACxB,QAAM,aAAa,KAAK;AAAA,IACtB;AAAA,IACA,KAAK,IAAI,aAAa,SAAS,IAAI,KAAK,MAAM,MAAM,IAAI,KAAK,KAAK,MAAM,CAAC;AAAA,EAC3E;AAEA,QAAM,SAAS,SAAS,KAAK,SAAS;AACtC,QAAM,QAAQ,KAAK,MAAM,SAAS,QAAQ,UAAU;AACpD,SAAO,UAAU,EAAE,OAAO,KAAK,QAAQ,WAAW,GAAG,WAAW;AAClE;AAOO,IAAM,UAAU,CAAC,MAAiB,aAAqB,gBAAmC;AAC/F,QAAM,SAAS,WAAW,IAAI;AAC9B,QAAM,QAAQ,KAAK,MAAM,KAAK,QAAQ,WAAW;AACjD,SAAO,UAAU,EAAE,OAAO,KAAK,QAAQ,OAAO,GAAG,WAAW;AAC9D;;;AHpEA,IAAM,iBAAiB;AACvB,IAAM,iBAAiB;AACvB,IAAM,OAAO;AAIb,IAAM,SAAS;AACf,IAAM,YAAY;AAClB,IAAM,aAAa;AAKnB,IAAM,oBAAoB;AAC1B,IAAM,iBAAiB;AACvB,IAAM,8BAA8B;AAIpC,IAAM,gBAAgB,IAAI,KAAK,aAAa,OAAO;AACnD,IAAM,oBAAoB,oBAAI,IAA+B;AAE7D,IAAM,sBAAsB,CAAC,WAAsC;AACjE,QAAM,SAAS,kBAAkB,IAAI,MAAM;AAC3C,MAAI,OAAQ,QAAO;AAEnB,QAAM,YAAY,IAAI,KAAK,aAAa,SAAS;AAAA,IAC/C,UAAU;AAAA,IACV,uBAAuB;AAAA,EACzB,CAAC;AACD,oBAAkB,IAAI,QAAQ,SAAS;AACvC,SAAO;AACT;AAUO,IAAM,gBAAgB,CAAC,OAAe,OAAO,MAAc;AAChE,MAAI,QAAQ,kBAAmB,QAAO,cAAc,OAAO,KAAK;AAEhE,QAAM,YACJ,KAAK,MAAM,KAAK,MAAM,KAAK,IAAI,KAAK,IAAI,KAAK,GAAG,CAAC,CAAC,IAAI,cAAc,IAAI;AAC1E,QAAM,OAAO,gBAAgB;AAC7B,QAAM,SAAS,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK,MAAM,OAAO,IAAI,CAAC,CAAC;AAE7D,MAAI,SAAS,4BAA6B,QAAO,cAAc,OAAO,KAAK;AAC3E,SAAO,oBAAoB,MAAM,EAAE,OAAO,KAAK;AACjD;AASO,IAAM,eAAe,CAAC,MAAiB,UAAkB,cAA+B;AAC7F,MAAI,YAAY,EAAG,QAAO,CAAC;AAE3B,QAAM,QAAQ,aAAa,KAAK,IAAI,WAAW,KAAK,MAAM,WAAW,eAAe,CAAC;AACrF,QAAM,QAAQ,YAAY,MAAM,QAAQ;AACxC,QAAM,UAAU,SAAS,KAAK,OAAO,KAAK,KAAK,KAAK;AACpD,QAAM,OAAO,KAAK,IAAI,GAAG,OAAO;AAEhC,QAAM,QAAQ,KAAK,KAAK,KAAK,QAAQ,IAAI,IAAI;AAC7C,QAAM,SAAiB,CAAC;AACxB,WAAS,QAAQ,OAAO,SAAS,KAAK,KAAK,SAAS,MAAM;AACxD,WAAO,KAAK,EAAE,OAAO,OAAO,GAAG,MAAM,EAAE,KAAK,GAAG,KAAK,CAAC;AAAA,EACvD;AACA,SAAO;AACT;AAQO,IAAM,YAAY,CAAC,GAAW,aAA6B;AAChE,QAAM,OAAO,kBAAkB;AAC/B,QAAM,WAAW,IAAI;AACrB,QAAM,YAAY,WAAW,IAAI;AAEjC,MAAI,YAAY,UAAW,QAAO,IAAI,WAAW,IAAI,YAAY;AACjE,MAAI,SAAU,QAAO;AACrB,MAAI,UAAW,QAAO;AACtB,SAAO;AACT;AAEO,IAAM,aAAa,CAAC,WAAwB,YAA+B;AAChF,QAAM,SAAS,QAAQ,UAAU;AACjC,iBAAe,SAAS;AAKxB,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,aAAa,gBAAgB,EAAE;AACpC,OAAK,aAAa,eAAe,MAAM;AACvC,SAAO,OAAO,KAAK,OAAO;AAAA,IACxB,UAAU;AAAA,IACV,MAAM;AAAA,IACN,OAAO;AAAA,IACP,KAAK;AAAA,IACL,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,OAAO,OAAO,kBAAkB;AAAA,EAClC,CAAC;AACD,YAAU,OAAO,IAAI;AAErB,SAAO;AAAA;AAAA;AAAA;AAAA,IAIL,QAAQ,CAAC,MAAM,aAAa;AAC1B,YAAM,QAAQ,aAAa,MAAM,UAAU,QAAQ,SAAS,EAAE,IAAI,CAAC,SAAS;AAC1E,cAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,aAAK,aAAa,gBAAgB,EAAE;AACpC,aAAK,QAAQ,QAAQ,OAAO,KAAK,KAAK;AACtC,aAAK,cAAc,OAAO,KAAK,OAAO,KAAK,IAAI;AAC/C,eAAO,OAAO,KAAK,OAAO;AAAA,UACxB,SAAS;AAAA,UACT,UAAU;AAAA,UACV,KAAK;AAAA,UACL,MAAM,GAAG,KAAK,CAAC;AAAA,UACf,WAAW,UAAU,KAAK,GAAG,QAAQ;AAAA,UACrC,YAAY;AAAA,QACd,CAAC;AACD,eAAO;AAAA,MACT,CAAC;AACD,WAAK,gBAAgB,GAAG,KAAK;AAAA,IAC/B;AAAA,IAEA,SAAS,MAAM;AACb,WAAK,OAAO;AAAA,IACd;AAAA,EACF;AACF;;;AInJO,IAAM,iBAAiB,CAAC,OAAe,QAAgB,aAC5D,KAAK,MAAO,QAAQ,WAAY,MAAM;AAQjC,IAAM,gBAAgB,CAAC,QAAgB,QAAgB,aAC5D,KAAK,MAAO,SAAS,KAAK,SAAU,QAAQ,IAAI;AAMlD,IAAM,aAAa,CACjB,UACA,QACA,WACA,YACe,CAAC,KAAK,IAAI,UAAU,SAAS,GAAG,KAAK,IAAI,QAAQ,OAAO,CAAC;AAEnE,IAAM,aAAa,CAAC,MAAiB,aAA2B;AACrE,QAAM,EAAE,OAAO,WAAW,KAAK,QAAQ,IAAI;AAC3C,QAAM,SAAS,UAAU;AACzB,QAAM,SAAS,IAAI,YAAY,QAAQ;AAIvC,QAAM,QAAQ,CAAC,UAA0B,KAAK,OAAQ,QAAQ,aAAa,WAAY,MAAM;AAQ7F,QAAM,UAAU,CAAC,QAA4B;AAAA,IAC3C,YAAY,KAAK,KAAM,MAAM,SAAU,QAAQ;AAAA,IAC/C,YAAY,KAAK,MAAO,MAAM,KAAK,SAAU,QAAQ;AAAA,EACvD;AAEA,QAAM,SAAS,CAAC,QAAwB;AACtC,UAAM,CAAC,OAAO,GAAG,IAAI,QAAQ,GAAG;AAChC,WAAO,MAAM;AAAA,EACf;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAGA,SAAS,CAAC,iBAAiB;AACzB,aAAO,KAAK,CAAC;AACb,UAAI,aAAa,EAAG;AAEpB,eAAS,MAAM,GAAG,MAAM,UAAU,OAAO,GAAG;AAC1C,cAAM,CAAC,OAAO,GAAG,IAAI,QAAQ,GAAG;AAChC,eAAO,GAAG,IAAI,aAAa,OAAO,GAAG;AAAA,MACvC;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,UAAU,CAAC,UAAU,WAAW;AAC9B,YAAM,CAAC,OAAO,GAAG,IAAI,WAAW,UAAU,QAAQ,WAAW,OAAO;AACpE,UAAI,aAAa,KAAK,OAAO,MAAO;AAEpC,YAAM,QAAQ,MAAM,KAAK;AACzB,YAAM,OAAO,MAAM,MAAM,CAAC;AAE1B,UAAI,UAAU,MAAM;AAClB,eAAO,KAAK,KAAK,MAAM;AACvB;AAAA,MACF;AAEA,aAAO,KAAK,KAAK,QAAQ,KAAK,EAAE,CAAC,IAAI;AACrC,eAAS,MAAM,QAAQ,GAAG,MAAM,MAAM,OAAO,EAAG,QAAO,GAAG,KAAK,OAAO,GAAG;AACzE,aAAO,IAAI,KAAK,MAAM,QAAQ,IAAI,EAAE,CAAC;AAAA,IACvC;AAAA;AAAA,IAGA,cAAc,CAAC,UAAU,WAAW;AAClC,YAAM,CAAC,OAAO,GAAG,IAAI,WAAW,UAAU,QAAQ,WAAW,OAAO;AACpE,UAAI,aAAa,KAAK,OAAO,MAAO,QAAO,CAAC;AAE5C,YAAM,QAAQ,MAAM,KAAK;AACzB,YAAM,OAAO,MAAM,MAAM,CAAC;AAC1B,YAAM,UAAoB,CAAC;AAC3B,eAAS,MAAM,OAAO,OAAO,MAAM,OAAO,EAAG,SAAQ,KAAK,GAAG;AAC7D,aAAO;AAAA,IACT;AAAA,IAEA,aAAa,CAAC,MAAM,iBAAiB;AACnC,iBAAW,OAAO,MAAM;AACtB,cAAM,CAAC,OAAO,GAAG,IAAI,QAAQ,GAAG;AAChC,eAAO,GAAG,IAAI,aAAa,OAAO,GAAG;AAAA,MACvC;AAAA,IACF;AAAA,EACF;AACF;;;AC/HA,SAAS,cAAc;AAEvB,SAAS,cAAc;AA0BvB,IAAM,kBAAkB;AACxB,IAAM,gBAAgB;AACtB,IAAM,kBAAkB;AAGxB,IAAM,oBACJ;AAWF,IAAM,QAAQ,CAAC,OAAe,QAAwB,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,CAAC;AAQ/E,IAAM,WAAW,CACtB,WACA,MACA,aAC4B;AAC5B,MAAI,aAAa,EAAG,QAAO;AAC3B,MAAI,UAAU,CAAC,KAAK,KAAK,SAAS,UAAU,CAAC,KAAK,KAAK,IAAK,QAAO;AAEnE,QAAM,QAAQ,YAAY,MAAM,QAAQ;AACxC,SAAO,CAAC,MAAM,MAAM,EAAE,UAAU,CAAC,CAAC,GAAG,QAAQ,GAAG,MAAM,MAAM,EAAE,UAAU,CAAC,CAAC,GAAG,QAAQ,CAAC;AACxF;AAQO,IAAM,WAAW,CACtB,QACA,MACA,UACA,gBACe;AACf,QAAM,QAAQ,YAAY,MAAM,QAAQ;AACxC,QAAM,QAAQ,MAAM,KAAK,MAAM,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,WAAW;AACnE,MAAI,MAAM,MAAM,KAAK,MAAM,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,WAAW;AAE/D,MAAI,OAAO,OAAO;AAChB,UAAM,KAAK,IAAI,QAAQ,GAAG,WAAW;AACrC,QAAI,OAAO,MAAO,QAAO,CAAC,KAAK,IAAI,GAAG,MAAM,CAAC,GAAG,GAAG;AAAA,EACrD;AACA,SAAO,CAAC,OAAO,GAAG;AACpB;AAIA,IAAM,YAAY,CAAC,WAA8B,gBAA2C;AAC1F,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,QAAQ,MAAM,KAAK,MAAM,UAAU,CAAC,CAAC,GAAG,WAAW;AACzD,QAAM,MAAM,MAAM,KAAK,MAAM,UAAU,CAAC,CAAC,GAAG,WAAW;AACvD,SAAO,MAAM,QAAQ,CAAC,OAAO,GAAG,IAAI;AACtC;AAIA,IAAM,QAAQ,CAAC,cAAuE;AACpF,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,CAAC,IAAI,EAAE,IAAI;AACjB,MAAI,OAAO,OAAO,YAAY,OAAO,OAAO,SAAU,QAAO;AAC7D,SAAO,CAAC,IAAI,EAAE;AAChB;AAIA,IAAM,YAAY,CAAC,GAAsB,MACvC,MAAM,KAAM,MAAM,QAAQ,MAAM,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC;AAEhE,IAAM,qBAAqB,CAChC,WACA,SACiB;AACjB,iBAAe,SAAS;AAKxB,QAAM,MAAM,SAAS,gBAAgB,eAAe,KAAK;AACzD,MAAI,aAAa,iBAAiB,EAAE;AACpC,SAAO,OAAO,IAAI,OAAO;AAAA,IACvB,UAAU;AAAA,IACV,OAAO;AAAA,IACP,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,UAAU;AAAA,IACV,SAAS;AAAA,EACX,CAAC;AACD,YAAU,OAAO,GAAG;AAEpB,QAAM,IAAI,OAAO,GAAG,EAAE,OAAO,GAAG;AAMhC,QAAM,cAAc,CAAC,UAA4B;AAC/C,UAAM,eAAe;AAAA,EACvB;AACA,MAAI,iBAAiB,aAAa,aAAa,IAAI;AAOnD,QAAM,QAAQ,OAAO,EAClB,UAAU,MAAM,KAAK,EACrB;AAAA,IACC,CAAC,UACC,CAAC,MAAM,WAAW,CAAC,MAAM,WAAW,CAAC,MAAM,YAAY,CAAC,MAAM;AAAA,EAClE;AAEF,MAAI,YAA+B;AACnC,MAAI,QAAgB,cAAc;AAGlC,MAAI,SAAS;AAGb,MAAI,SAAS;AAKb,QAAM,QAAQ,MAAY;AACxB,MAAE,OAAO,eAAe,EACrB,KAAK,QAAQ,KAAK,EAClB,KAAK,UAAU,KAAK,EACpB,KAAK,gBAAgB,kBAAkB;AAAA,EAC5C;AAEA,QAAM,SAAS,MAAY;AACzB,UAAM,QAAQ,KAAK,SAAS;AAC5B,UAAM,OAAO;AAAA,MACX,CAAC,GAAG,CAAC;AAAA,MACL,CAAC,OAAO,KAAK,UAAU,CAAC;AAAA,IAC1B,CAAC;AACD,MAAE,KAAK,KAAK;AACZ,UAAM;AAEN,aAAS;AACT,QAAI;AACF,QAAE,KAAK,MAAM,MAAM,YAAY,SAAS,WAAW,KAAK,QAAQ,GAAG,KAAK,IAAI,IAAI;AAAA,IAClF,UAAE;AACA,eAAS;AAAA,IACX;AAAA,EACF;AAEA,QAAM,aAAa,CAAC,WAAqD;AACvE,UAAM,OAAO,MAAM,MAAM;AACzB,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO,SAAS,MAAM,KAAK,QAAQ,GAAG,KAAK,SAAS,GAAG,KAAK,eAAe,CAAC;AAAA,EAC9E;AAEA,QAAM,GAAG,SAAS,CAAC,UAAiC;AAClD,QAAI,CAAC,MAAM,eAAe,OAAQ;AAClC,aAAS;AAAA,EACX,CAAC;AAED,QAAM,GAAG,SAAS,CAAC,UAAiC;AAClD,QAAI,CAAC,MAAM,eAAe,OAAQ;AAClC,SAAK,QAAQ,WAAW,MAAM,SAAS,CAAC;AAAA,EAC1C,CAAC;AAYD,QAAM,GAAG,OAAO,CAAC,UAAiC;AAChD,QAAI,CAAC,MAAM,eAAe,OAAQ;AAClC,aAAS;AAET,UAAM,OAAO,UAAU,WAAW,MAAM,SAAS,GAAG,KAAK,eAAe,CAAC;AACzE,UAAM,UAAU,CAAC,UAAU,WAAW,IAAI;AAE1C,gBAAY;AACZ,WAAO;AACP,QAAI,QAAS,MAAK,WAAW,SAAS;AAAA,EACxC,CAAC;AAED,SAAO;AAEP,SAAO;AAAA,IACL;AAAA,IAEA,cAAc,CAAC,SAAS;AACtB,kBAAY,UAAU,MAAM,KAAK,eAAe,CAAC;AACjD,aAAO;AAAA,IACT;AAAA,IAEA,cAAc,MAAO,YAAY,CAAC,UAAU,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI;AAAA,IAEhE,UAAU,CAAC,SAAS;AAClB,cAAQ;AACR,YAAM;AAAA,IACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUA,SAAS,MAAM;AACb,YAAM,GAAG,SAAS,IAAI,EAAE,GAAG,SAAS,IAAI,EAAE,GAAG,OAAO,IAAI;AACxD,QAAE,GAAG,UAAU,IAAI;AACnB,UAAI,oBAAoB,aAAa,aAAa,IAAI;AACtD,UAAI,OAAQ,QAAO,MAAM,EAAE,GAAG,mBAAmB,IAAI;AACrD,UAAI,OAAO;AAAA,IACb;AAAA,EACF;AACF;;;ACvQA,SAAS,WAAW;AACpB,SAAS,gBAAgB,iBAAiB;AA+B1C,IAAM,aAA0C;AAAA,EAC9C,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,SAAS;AAAA,EACT,UAAU;AAAA,EACV,WAAW;AAAA,EACX,OAAO;AACT;AAKA,IAAM,aAAa,mBAAmB;AACtC,IAAM,sBAAsB;AAC5B,IAAM,qBAAqB,aAAa;AASxC,IAAM,UAAU,CAAC,OAAe,aAA6B;AAC3D,MAAI;AACF,WAAO,IAAI,KAAK,EAAE,YAAY,IAAI,QAAQ;AAAA,EAC5C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,aAAa,CAAC,OAA0B,OAAe,QAAsB;AACjF,QAAM,QAAQ,IAAI,GAAG;AACrB,QAAM,SAAS,QAAQ;AACvB,QAAM,IAAI,CAAC,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,GAAG,MAAM;AAC7D;AAIO,IAAM,YAAY,CAAC,OAA2B;AACnD,QAAM,QAAQ,iBAAiB,EAAE;AACjC,QAAM,OAAO,CAAC,QACZ,MAAM,iBAAiB,WAAW,GAAG,CAAC,EAAE,KAAK,KAAK,cAAc,GAAG;AAErE,SAAO;AAAA,IACL,SAAS,KAAK,SAAS;AAAA,IACvB,YAAY,KAAK,YAAY;AAAA,IAC7B,aAAa,KAAK,aAAa;AAAA,IAC/B,SAAS,KAAK,SAAS;AAAA,IACvB,UAAU,KAAK,UAAU;AAAA,IACzB,WAAW,KAAK,WAAW;AAAA,IAC3B,OAAO,KAAK,OAAO;AAAA,EACrB;AACF;AAUO,IAAM,mBAAmB,CAAC,OAAc,UAAwB,eAA2B;AAChG,QAAM,QAAQ,IAAI,kBAAkB,mBAAmB,eAAe;AACtE,QAAM,UAAU,QAAQ,MAAM,SAAS,cAAc,OAAO;AAC5D,QAAM,WAAW,QAAQ,MAAM,UAAU,cAAc,QAAQ;AAC/D,QAAM,eACJ,YAAY,aACR,UAAU,gBAAgB;AAAA,IACxB;AAAA,IACA,QAAQ,MAAM,SAAS,cAAc,OAAO;AAAA,IAC5C;AAAA,EACF,CAAC,IACD;AAAA,IACE,QAAQ,MAAM,YAAY,cAAc,UAAU;AAAA,IAClD,QAAQ,MAAM,aAAa,cAAc,WAAW;AAAA,EACtD;AAEN,aAAW,OAAO,GAAG,OAAO;AAC5B,aAAW,OAAO,YAAY,QAAQ;AACtC,WAAS,IAAI,qBAAqB,KAAK,oBAAoB,KAAK,GAAG;AACjE,eAAW,OAAO,GAAG,aAAa,IAAI,UAAU,CAAC;AAAA,EACnD;AAQA,SAAO,CAAC,aAAa;AACnB,QAAI,QAAQ,KAAK,MAAM,WAAW,UAAU;AAC5C,QAAI,WAAW,KAAK,QAAQ,oBAAqB,SAAQ;AACzD,QAAI,WAAW,KAAK,QAAQ,mBAAoB,SAAQ;AAExD,UAAM,SAAS,QAAQ;AACvB,WAAO,MAAM,SAAS,QAAQ,SAAS,eAAe;AAAA,EACxD;AACF;AAIO,IAAM,iBAAiB,CAAC,UAAoC;AACjE,QAAM,QAAQ,IAAI,QAAQ,MAAM,WAAW,cAAc,SAAS,CAAC;AACnE,SAAO,kBAAkB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY;AACrE;AAOO,IAAM,aAAa,CAAC,aAAuC;AAChE,QAAM,WAAW,IAAI,iBAAiB,QAAQ;AAC9C,WAAS,QAAQ,SAAS,iBAAiB;AAAA,IACzC,YAAY;AAAA,IACZ,iBAAiB,CAAC,GAAG,gBAAgB;AAAA,EACvC,CAAC;AAED,QAAM,QAAQ,WAAW,iBAAiB;AAC1C,QAAM,iBAAiB,UAAU,QAAQ;AAEzC,SAAO,MAAM;AACX,aAAS,WAAW;AACpB,UAAM,oBAAoB,UAAU,QAAQ;AAAA,EAC9C;AACF;;;ACtIA,IAAM,iBAAiB;AACvB,IAAM,gBAAgB;AACtB,IAAM,WAAW;AACjB,IAAM,aAAa;AACnB,IAAM,OAAO;AAIb,IAAM,eAAe,oBAAI,IAAI,CAAC,KAAK,GAAG,CAAC;AACvC,IAAM,gBAAgB,oBAAI,IAAI,CAAC,KAAK,GAAG,CAAC;AACxC,IAAM,aAAa,oBAAI,IAAI,CAAC,KAAK,MAAM,CAAC;AASjC,IAAM,oBAAoB,CAC/B,SACA,QACA,SACiB;AACjB,MAAI;AAIJ,QAAM,kBAAkB,CAAC,IAAY,SACjC,KAAK,KAAK,OAAO,IAAK,KAAK,YAAY,IAAK,WAAW,IAAI;AAQ/D,QAAM,UAAU,CAAC,UAA4B;AAC3C,QAAI,EAAE,MAAM,WAAW,MAAM,SAAU;AACvC,QAAI,MAAM,OAAQ;AAClB,QAAI,KAAK,YAAY,MAAM,EAAG;AAC9B,UAAM,eAAe;AAErB,UAAM,OAAO,KAAK,QAAQ;AAC1B,UAAM,SAAS,KAAK,QAAQ,gBAAgB,QAAQ,QAAQ,KAAK,GAAG,IAAI;AACxE,UAAM,SAAS,MAAM,SAAS,IAAI,YAAY,IAAI;AAClD,SAAK,QAAQ,SAAS,MAAM,QAAQ,QAAQ,KAAK,eAAe,CAAC,CAAC;AAAA,EACpE;AAUA,QAAM,gBAAgB,CAAC,UAA8B;AACnD,QAAI,MAAM,WAAW,eAAgB;AAErC,QAAI,SAAS,kBAAkB,UAAU,OAAO,OAAO,UAAU,YAAY;AAC3E,aAAO,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,IACtC;AAIA,QAAI,KAAK,iBAAiB,KAAK,MAAM,gBAAgB,iBAAiB,CAAC,MAAM,SAAU;AAEvF,WAAO;AAAA,MACL,OAAO,QAAQ,QAAQ,KAAK;AAAA,MAC5B,MAAM,KAAK,QAAQ;AAAA,MACnB,WAAW,MAAM,aAAa;AAAA,MAC9B,QAAQ;AAAA,IACV;AAAA,EACF;AAYA,QAAM,gBAAgB,CAAC,UAA8B;AACnD,QAAI,CAAC,KAAM;AAEX,UAAM,KAAK,QAAQ,QAAQ,KAAK,IAAI,KAAK;AACzC,QAAI,CAAC,KAAK,QAAQ;AAChB,UAAI,KAAK,IAAI,EAAE,IAAI,kBAAmB;AACtC,WAAK,SAAS;AACd,UAAI,OAAO,QAAQ,sBAAsB,cAAc,KAAK,cAAc,YAAY;AACpF,gBAAQ,kBAAkB,KAAK,SAAS;AAAA,MAC1C;AACA,cAAQ,MAAM,SAAS;AACvB,WAAK,YAAY;AAAA,IACnB;AAEA,QAAI,KAAK,YAAY,MAAM,EAAG;AAC9B,SAAK,QAAQ,QAAQ,KAAK,MAAM,CAAC,gBAAgB,IAAI,KAAK,IAAI,GAAG,KAAK,eAAe,CAAC,CAAC;AAAA,EACzF;AAKA,QAAM,UAAU,MAAY;AAC1B,QAAI,CAAC,KAAM;AAEX,QAAI,KAAK,QAAQ;AACf,YAAM,aACJ,KAAK,cAAc,eAAe,QAAQ,oBAAoB,KAAK,SAAS,KAAK;AACnF,UAAI,OAAO,QAAQ,0BAA0B,cAAc,YAAY;AACrE,gBAAQ,sBAAsB,KAAK,SAAS;AAAA,MAC9C;AACA,cAAQ,MAAM,SAAS;AACvB,WAAK,UAAU;AAAA,IACjB;AAEA,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,MAAY,KAAK,UAAU;AAc9C,QAAM,YAAY,CAAC,UAA+B;AAChD,QAAI,MAAM,WAAW,MAAM,WAAW,MAAM,OAAQ;AACpD,QAAI,MAAM,OAAQ;AAElB,UAAM,OAAO,KAAK,QAAQ;AAC1B,UAAM,SAAS,WAAW,IAAI;AAC9B,UAAM,SAAS,KAAK,QAAQ,SAAS;AACrC,UAAM,QAAQ,KAAK,eAAe;AAElC,QAAI,aAAa,IAAI,MAAM,GAAG,GAAG;AAC/B,WAAK,QAAQ,SAAS,MAAM,QAAQ,IAAI,WAAW,KAAK,CAAC;AAAA,IAC3D,WAAW,cAAc,IAAI,MAAM,GAAG,GAAG;AACvC,WAAK,QAAQ,SAAS,MAAM,QAAQ,WAAW,KAAK,CAAC;AAAA,IACvD,WAAW,MAAM,QAAQ,eAAe,MAAM,QAAQ,cAAc;AAClE,YAAM,WAAW,MAAM,WAAW,wBAAwB;AAC1D,YAAM,QAAQ,SAAS,YAAY,MAAM,QAAQ,cAAc,KAAK;AACpE,WAAK,QAAQ,QAAQ,MAAM,OAAO,KAAK,CAAC;AAAA,IAC1C,WAAW,WAAW,IAAI,MAAM,GAAG,GAAG;AACpC,WAAK,UAAU;AAAA,IACjB,OAAO;AACL;AAAA,IACF;AACA,UAAM,eAAe;AAAA,EACvB;AAEA,UAAQ,iBAAiB,SAAS,SAAS,EAAE,SAAS,MAAM,CAAC;AAC7D,UAAQ,iBAAiB,eAAe,aAAa;AACrD,UAAQ,iBAAiB,eAAe,aAAa;AACrD,UAAQ,iBAAiB,aAAa,OAAO;AAC7C,UAAQ,iBAAiB,iBAAiB,OAAO;AACjD,UAAQ,iBAAiB,YAAY,UAAU;AAC/C,SAAO,iBAAiB,WAAW,SAAS;AAI5C,SAAO,MAAM;AACX,YAAQ;AACR,YAAQ,oBAAoB,SAAS,OAAO;AAC5C,YAAQ,oBAAoB,eAAe,aAAa;AACxD,YAAQ,oBAAoB,eAAe,aAAa;AACxD,YAAQ,oBAAoB,aAAa,OAAO;AAChD,YAAQ,oBAAoB,iBAAiB,OAAO;AACpD,YAAQ,oBAAoB,YAAY,UAAU;AAClD,WAAO,oBAAoB,WAAW,SAAS;AAAA,EACjD;AACF;;;ACjKA,IAAM,WAAW;AACjB,IAAM,WAAW;AAKjB,IAAM,WAAW;AAEV,IAAM,gBAA2B;AAAA,EACtC,SAAS,CAAC,aAAa,sBAAsB,QAAQ;AAAA,EACrD,QAAQ,CAAC,WAAW,qBAAqB,MAAM;AACjD;AAEO,IAAM,eAA8B;AAAA,EACzC,KAAK,CAAC,UAAU,OAAO,OAAO,YAAY,UAAU,EAAE;AAAA,EACtD,OAAO,CAAC,OAAO,OAAO,cAAc,EAAE;AACxC;AAEO,IAAM,iBAAiB,CAC5B,KACA,QACA,YAAuB,eACvB,QAAuB,iBACV;AACb,MAAI;AACJ,MAAI;AACJ,MAAI,QAAQ;AACZ,MAAI,WAAW;AACf,MAAI;AACJ,MAAI,QAAQ;AACZ,MAAI,UAAU;AACd,QAAM,QAAQ,oBAAI,IAAY;AAI9B,QAAM,cAAc,MAAiC;AACnD,UAAM,EAAE,SAAS,IAAI;AACrB,QAAI,aAAa,EAAG,QAAO;AAE3B,QAAI,CAAC,SAAS,MAAM,UAAU,UAAU;AACtC,cAAQ,IAAI,gBAAgB,UAAU,WAAW;AACjD,iBAAW;AAAA,IACb;AACA,WAAO;AAAA,EACT;AAQA,QAAM,WAAW,CAAC,QAAoB,aACpC,aAAa,SAAS,WAAW,WAAW,YAAY,OAAO,QAAQ;AAEzE,QAAM,cAAc,CAAC,QAAuB,QAAgB,UAAmC;AAC7F,WAAO,KAAK,IAAI,OAAO,SAAS,eAAe;AAAA,EACjD;AAOA,QAAM,gBAAgB,CAAC,QAAuB,WAA6B;AACzE,UAAM,EAAE,SAAS,IAAI;AACrB,UAAM,OAAO,OAAO,QAAQ;AAC5B,UAAM,SAAS,WAAW,IAAI;AAE9B,aAAS,QAAQ,GAAG,QAAQ,QAAQ,SAAS,GAAG;AAC9C,YAAM,QAAQ,SAAS,QAAQ,OAAO,IAAI,KAAK,QAAQ,KAAK,IAAI,WAAW,CAAC;AAC5E,YAAM,QAAQ,eAAe,OAAO,QAAQ,QAAQ;AACpD,YAAM,MAAM,eAAe,QAAQ,GAAG,QAAQ,QAAQ;AACtD,eAAS,SAAS,OAAO,SAAS,KAAK,UAAU,EAAG,aAAY,QAAQ,QAAQ,KAAK;AAAA,IACvF;AAAA,EACF;AAKA,QAAM,YAAY,CAAC,QAAuB,WAA6B;AACrE,QAAI,UAAU;AACZ,eAAS,MAAM,GAAG,MAAM,OAAO,UAAU,OAAO;AAC9C,oBAAY,QAAQ,KAAK,SAAS,QAAQ,OAAO,SAAS,GAAG,CAAC,CAAC;AACjE;AAAA,IACF;AAEA,eAAW,OAAO,OAAO;AACvB,UAAI,OAAO,OAAO,MAAO;AACzB,kBAAY,QAAQ,KAAK,SAAS,QAAQ,OAAO,SAAS,GAAG,CAAC,CAAC;AAAA,IACjE;AAAA,EACF;AAQA,QAAM,QAAQ,MAAY;AACxB,YAAQ;AAER,UAAM,SAAS,YAAY;AAC3B,QAAI,CAAC,UAAU,CAAC,OAAO;AACrB,YAAM,MAAM;AACZ,iBAAW;AACX;AAAA,IACF;AAEA,QAAI,WAAW,OAAO,QAAQ,CAAC,IAAI,OAAO,SAAU,eAAc,QAAQ,KAAK;AAAA,QAC1E,WAAU,QAAQ,KAAK;AAC5B,QAAI,aAAa,QAAQ,GAAG,CAAC;AAE7B,UAAM,MAAM;AACZ,eAAW;AAAA,EACb;AAGA,QAAM,WAAW,MAAY;AAC3B,QAAI,UAAU,SAAU;AACxB,YAAQ,UAAU,QAAQ,KAAK;AAAA,EACjC;AAEA,QAAM,eAAe,MAAY;AAC/B,eAAW;AACX,aAAS;AAAA,EACX;AAEA,QAAM,YAAY,MAAY;AAC5B,QAAI,YAAY,SAAU,OAAM,MAAM,OAAO;AAC7C,cAAU;AAAA,EACZ;AAEA,SAAO;AAAA,IACL,WAAW,CAAC,SAAS;AACnB,cAAQ;AACR,mBAAa;AAAA,IACf;AAAA;AAAA;AAAA;AAAA,IAKA,cAAc,CAAC,IAAI,OAAO,UAAU;AAClC,UAAI,CAAC,MAAM,cAAc,OAAW;AAEpC,gBAAU;AACV,kBAAY,KAAK,QAAQ;AACzB,cAAQ;AAER,UAAI,MAAM,OAAO;AACf,kBAAU,MAAM,IAAI,MAAM;AACxB,kBAAQ,CAAC;AACT,uBAAa;AAAA,QACf,GAAG,iBAAiB;AAAA,MACtB;AAEA,mBAAa;AAAA,IACf;AAAA;AAAA,IAGA,mBAAmB,CAAC,UAAU;AAC5B,UAAI,cAAc,OAAW;AAE7B,kBAAY;AACZ,mBAAa;AAAA,IACf;AAAA,IAEA,WAAW,CAAC,SAAS;AACnB,eAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,EAAG,OAAM,IAAI,KAAK,CAAC,CAAC;AAC1D,UAAI,KAAK,SAAS,EAAG,UAAS;AAAA,IAChC;AAAA,IAEA;AAAA,IAEA,SAAS,MAAM;AACb,gBAAU;AACV,UAAI,UAAU,SAAU,WAAU,OAAO,KAAK;AAC9C,cAAQ;AACR,YAAM,MAAM;AAAA,IACd;AAAA,EACF;AACF;;;ACnNA,IAAM,YAAY,CAAC,MAAc,QAAyB,KAAK,MAAM,IAAK,GAAG,KAAK,QAAQ;AAO1F,IAAM,WAAW,IAAI,WAAW,KAAK,aAAa;AAClD,SAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK,GAAG;AAC3C,WAAS,CAAC,IAAI,SAAS,KAAK,CAAC,KAAK,IAAI;AACxC;AAEO,IAAM,eAAe,CAAC,SAAyB;AACpD,QAAM,QAAQ,IAAI,WAAW,KAAK,KAAK,OAAO,aAAa,CAAC;AAC5D,MAAI,QAAQ;AAEZ,SAAO;AAAA,IACL,KAAK,CAAC,WAAW,MAAM,UAAU,UAAU,IAAK,MAAM,QAAQ,gBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA,IAM/E,KAAK,CAAC,UAAU;AACd,YAAM,OAAO,UAAU;AACvB,YAAM,OAAO,MAAM,QAAQ;AAC3B,WAAK,MAAM,IAAI,IAAI,UAAU,EAAG,QAAO;AAEvC,YAAM,IAAI,KAAK;AACf,eAAS;AACT,aAAO;AAAA,IACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,MAAM,CAAC,OAAO,QAAQ;AACpB,UAAI,OAAO,MAAO;AAElB,YAAM,YAAY,UAAU;AAC5B,YAAM,WAAY,MAAM,MAAO;AAC/B,YAAM,UAAU,QAAQ;AACxB,YAAM,SAAU,MAAM,IAAK,aAAa;AAExC,UAAI,cAAc,UAAU;AAC1B,cAAM,SAAS,KAAK,UAAU,SAAS,KAAK;AAC5C;AAAA,MACF;AAEA,YAAM,SAAS,KAAK,UAAU,SAAS,aAAa;AACpD,YAAM,KAAK,WAAW,YAAY,GAAG,QAAQ;AAC7C,YAAM,QAAQ,KAAK,UAAU,GAAG,KAAK;AAAA,IACvC;AAAA;AAAA,IAGA,YAAY,CAAC,OAAO,QAAQ;AAC1B,UAAI,OAAO,MAAO;AAElB,YAAM,YAAY,UAAU;AAC5B,YAAM,WAAY,MAAM,MAAO;AAC/B,YAAM,UAAU,QAAQ;AACxB,YAAM,SAAU,MAAM,IAAK,aAAa;AAExC,UAAI,cAAc,UAAU;AAC1B,cAAM,SAAS,KAAK,CAAC,UAAU,SAAS,KAAK;AAC7C;AAAA,MACF;AAEA,YAAM,SAAS,KAAK,CAAC,UAAU,SAAS,aAAa;AACrD,YAAM,KAAK,GAAG,YAAY,GAAG,QAAQ;AACrC,YAAM,QAAQ,KAAK,CAAC,UAAU,GAAG,KAAK;AAAA,IACxC;AAAA;AAAA;AAAA,IAIA,YAAY,CAAC,OAAO,QAAQ;AAC1B,UAAI,OAAO,MAAO,QAAO;AAEzB,YAAM,YAAY,UAAU;AAC5B,YAAM,WAAY,MAAM,MAAO;AAC/B,YAAM,UAAU,QAAQ;AACxB,YAAM,SAAU,MAAM,IAAK,aAAa;AAExC,UAAI,cAAc,UAAU;AAC1B,eAAO,SAAS,MAAM,SAAS,IAAI,UAAU,SAAS,KAAK,CAAC;AAAA,MAC9D;AAEA,UAAI,QAAQ,SAAS,MAAM,SAAS,IAAI,UAAU,SAAS,aAAa,CAAC;AACzE,eAAS,IAAI,YAAY,GAAG,IAAI,UAAU,KAAK,EAAG,UAAS,SAAS,MAAM,CAAC,CAAC;AAC5E,eAAS,SAAS,MAAM,QAAQ,IAAI,UAAU,GAAG,KAAK,CAAC;AACvD,aAAO;AAAA,IACT;AAAA,IAEA,OAAO,MAAM;AACX,YAAM,KAAK,CAAC;AACZ,cAAQ;AAAA,IACV;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,UAAU,CAAC,SAAS;AAClB,cAAQ;AAAA,IACV;AAAA,IAEA,IAAI,QAAQ;AACV,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;ACrHA,IAAM,cAAc,CAAC,UACnB,YAAY,OAAO,KAAK,KAAK,OAAO,MAAM,CAAC,MAAM;AAEnD,IAAM,YAAY,CAAC,UAA8B;AAC/C,QAAM,QAAgB,CAAC;AAEvB,MAAI,YAAY,KAAK,GAAG;AACtB,UAAM,YAAY,KAAK,MAAM,MAAM,SAAS,UAAU;AACtD,aAAS,IAAI,GAAG,IAAI,WAAW,KAAK,GAAG;AACrC,YAAM,KAAK,CAAC,MAAM,IAAI,UAAU,GAAG,MAAM,IAAI,aAAa,CAAC,CAAC,CAAC;AAAA,IAC/D;AACA,WAAO;AAAA,EACT;AAEA,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;AACxC,UAAM,QAAQ,MAAM,CAAC;AACrB,UAAM,KAAK,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;AAAA,EACjC;AACA,SAAO;AACT;AASO,IAAM,kBAAkB,CAAC,OAAmB,gBAAqC;AACtF,QAAM,UAAkB,CAAC;AACzB,aAAW,CAAC,UAAU,MAAM,KAAK,UAAU,KAAK,GAAG;AACjD,QAAI,CAAC,OAAO,UAAU,QAAQ,KAAK,CAAC,OAAO,UAAU,MAAM,EAAG;AAC9D,UAAM,QAAQ,KAAK,IAAI,GAAG,QAAQ;AAClC,UAAM,MAAM,KAAK,IAAI,aAAa,MAAM;AACxC,QAAI,MAAM,MAAO,SAAQ,KAAK,CAAC,OAAO,GAAG,CAAC;AAAA,EAC5C;AACA,UAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;AAIlC,QAAM,SAAmB,CAAC;AAC1B,aAAW,CAAC,OAAO,GAAG,KAAK,SAAS;AAClC,UAAM,YAAY,OAAO,SAAS;AAClC,QAAI,OAAO,SAAS,KAAK,SAAS,OAAO,SAAS,GAAG;AACnD,aAAO,SAAS,IAAI,KAAK,IAAI,OAAO,SAAS,GAAG,GAAG;AAAA,IACrD,OAAO;AACL,aAAO,KAAK,OAAO,GAAG;AAAA,IACxB;AAAA,EACF;AACA,SAAO,YAAY,KAAK,MAAM;AAChC;AAGO,IAAM,eAAe,CAAC,SAA8B;AACzD,MAAI,QAAQ;AACZ,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,WAAY,UAAS,KAAK,IAAI,CAAC,IAAI,KAAK,CAAC;AAC/E,SAAO;AACT;;;ACpCA,IAAM,cAAc,IAAI,YAAY,CAAC;AAErC,IAAM,eAAe,CAAC,OAAe,gBACnC,OAAO,UAAU,KAAK,KAAK,SAAS,KAAK,QAAQ;AAE5C,IAAM,mBAAmB,CAAC,gBAAoC;AACnE,MAAI,CAAC,OAAO,UAAU,WAAW,KAAK,cAAc,GAAG;AACrD,UAAM,IAAI,WAAW,+CAA+C,WAAW,EAAE;AAAA,EACnF;AAKA,MAAI,cAAc,YAAY;AAC5B,UAAM,IAAI;AAAA,MACR,0DAA0D,UAAU,UAAU,WAAW;AAAA,IAC3F;AAAA,EACF;AAEA,QAAM,SAAS,aAAa,WAAW;AACvC,MAAI,OAAkB,SAAS,WAAW;AAI1C,MAAI,OAAa,WAAW,MAAM,CAAC;AAEnC,QAAM,SAAS,CAAC,UAA2B,SAAS,KAAK,SAAS,QAAQ,KAAK;AAS/E,QAAM,MAAM,CAAC,YAA4C;AACvD,UAAM,UAAU,KAAK,WAAW;AAChC,UAAM,QAAQ,oBAAI,IAAY;AAE9B,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,GAAG;AAC1C,YAAM,QAAQ,QAAQ,CAAC;AACvB,UAAI,CAAC,aAAa,OAAO,WAAW,KAAK,CAAC,OAAO,IAAI,KAAK,EAAG;AAC7D,UAAI,CAAC,WAAW,CAAC,OAAO,KAAK,EAAG;AAEhC,YAAM,MAAM,KAAK,MAAM,KAAK;AAC5B,WAAK,OAAO,GAAG,KAAK;AACpB,YAAM,IAAI,GAAG;AAAA,IACf;AAEA,WAAO,MAAM,SAAS,IAAI,cAAc,YAAY,KAAK,KAAK,EAAE,KAAK;AAAA,EACvE;AAUA,QAAM,cAAc,CAAC,OAAmB,aAAmC;AACzE,UAAM,OAAO,gBAAgB,OAAO,WAAW;AAC/C,UAAM,QAAQ,oBAAI,IAAY;AAC9B,QAAI,QAAQ;AAEZ,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,YAAY;AAChD,YAAM,QAAQ,KAAK,CAAC;AACpB,YAAM,MAAM,KAAK,IAAI,CAAC;AACtB,YAAM,UAAU,OAAO,WAAW,OAAO,GAAG;AAE5C,UAAI,UAAU;AACZ,eAAO,KAAK,OAAO,GAAG;AACtB,iBAAS,MAAM,QAAQ;AAAA,MACzB,OAAO;AACL,eAAO,WAAW,OAAO,GAAG;AAC5B,iBAAS;AAAA,MACX;AAEA,iBAAW,OAAO,KAAK,aAAa,OAAO,GAAG,EAAG,OAAM,IAAI,GAAG;AAAA,IAChE;AAEA,SAAK,YAAY,OAAO,OAAO,UAAU;AACzC,WAAO,SAAS,OAAO,QAAQ,KAAK;AAEpC,WAAO,MAAM,SAAS,IAAI,cAAc,YAAY,KAAK,KAAK,EAAE,KAAK;AAAA,EACvE;AAEA,SAAO;AAAA,IACL;AAAA,IACA,IAAI,WAAW;AACb,aAAO,KAAK;AAAA,IACd;AAAA;AAAA;AAAA,IAGA,QAAQ,CAAC,aAAa;AACpB,aAAO,WAAW,MAAM,QAAQ;AAChC,WAAK,QAAQ,OAAO,UAAU;AAAA,IAChC;AAAA;AAAA;AAAA;AAAA,IAKA,SAAS,CAAC,SAAS;AACjB,UAAI,SAAS,MAAM,IAAI,EAAG,QAAO;AAEjC,aAAO,EAAE,GAAG,KAAK;AACjB,aAAO,WAAW,MAAM,KAAK,QAAQ;AACrC,WAAK,QAAQ,OAAO,UAAU;AAC9B,aAAO;AAAA,IACT;AAAA;AAAA,IAGA,SAAS,OAAO,EAAE,GAAG,KAAK;AAAA,IAE1B;AAAA,IAEA,SAAS,CAAC,YAAY;AACpB,aAAO,MAAM;AACb,WAAK,OAAO,KAAK,CAAC;AAClB,UAAI,OAAO;AAAA,IACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,eAAe,CAAC,UAAU;AACxB,YAAM,OAAO,gBAAgB,OAAO,WAAW;AAC/C,aAAO,MAAM;AACb,WAAK,OAAO,KAAK,CAAC;AAElB,eAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,YAAY;AAChD,eAAO,KAAK,KAAK,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;AAChC,aAAK,SAAS,KAAK,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;AAAA,MACpC;AAEA,aAAO,SAAS,aAAa,IAAI,CAAC;AAAA,IACpC;AAAA,IAEA,WAAW,CAAC,UAAU,YAAY,OAAO,IAAI;AAAA,IAC7C,cAAc,CAAC,UAAU,YAAY,OAAO,KAAK;AAAA,IAEjD,KAAK,OAAO;AAAA;AAAA,IAGZ,UAAU,CAAC,QAAQ;AACjB,YAAM,OAAO,KAAK,OAAO,GAAG;AAC5B,aAAO,SAAS,IAAI,IAAI,KAAK,OAAO,GAAG,IAAI;AAAA,IAC7C;AAAA,IAEA,SAAS,CAAC,QAAQ,KAAK,OAAO,GAAG;AAAA,IACjC,SAAS,CAAC,QAAQ,KAAK,QAAQ,GAAG;AAAA,IAClC,UAAU,OAAO,EAAE,WAAW,OAAO,OAAO,OAAO,YAAY;AAAA,EACjE;AACF;;;ACnLA,IAAM,SAAS;AACf,IAAM,oBAAoB;AAC1B,IAAM,YAAY;AAClB,IAAM,UAAU;AAEhB,IAAM,cAAc,IAAI,KAAK,aAAa,MAAM;AAIhD,IAAM,mBAAmB,IAAI,gBAAgB;AAC7C,IAAM,eAAe,UAAU,kBAAkB,QAAQ,uBAAuB;AAChF,IAAM,cAAc,iBAAiB,QAAQ,uBAAuB;AACpE,IAAM,YAAY,QAAQ,QAAQ,uBAAuB;AACzD,IAAM,YAAa,GAAG,QAAQ,uBAAuB;AAQrD,IAAM,gBAAgB,CAAC,OAAe,SAAyB;AAC7D,MAAI,UAAU,EAAG,QAAO;AACxB,MAAI,UAAU,KAAM,QAAO;AAE3B,QAAM,QAAS,QAAQ,OAAQ;AAC/B,QAAM,OAAO,MAAM,QAAQ,uBAAuB;AAElD,MAAI,SAAS,WAAW;AACtB,WAAO,IAAI,WAAW;AAAA,EACxB;AACA,MAAI,SAAS,WAAW;AACtB,WAAO,IAAI,WAAW;AAAA,EACxB;AACA,SAAO,GAAG,IAAI;AAChB;AAIO,IAAM,mBAAmB,CAAC,CAAC,OAAO,GAAG,GAAe,UAA0B;AACnF,QAAM,OAAO,MAAM;AACnB,QAAM,QACJ,SAAS,IACL,SAAS,YAAY,OAAO,KAAK,CAAC,KAClC,UAAU,YAAY,OAAO,KAAK,CAAC,GAAG,OAAO,GAAG,YAAY,OAAO,MAAM,CAAC,CAAC;AACjF,SAAO,GAAG,KAAK,GAAG,SAAS,GAAG,YAAY,OAAO,KAAK,CAAC,MAAM,YAAY,OAAO,IAAI,CAAC,KAAK,cAAc,OAAO,IAAI,CAAC;AACtH;AAIO,IAAM,qBAAqB,CAAC,OAAe,aAChD,SAAS,YAAY,OAAO,KAAK,CAAC,GAAG,SAAS,GAAG,WAAW,aAAa,SAAS;AAE7E,IAAM,gBAAgB,CAAC,cAAoC;AAIhE,QAAM,KAAK,SAAS,cAAc,KAAK;AACvC,KAAG,aAAa,mBAAmB,EAAE;AACrC,SAAO,OAAO,GAAG,OAAO;AAAA,IACtB,UAAU;AAAA,IACV,KAAK;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,WAAW;AAAA,EACb,CAAC;AACD,iBAAe,SAAS;AACxB,YAAU,OAAO,EAAE;AAEnB,SAAO;AAAA;AAAA;AAAA,IAGL,MAAM,CAAC,GAAG,SAAS;AACjB,SAAG,cAAc;AACjB,SAAG,MAAM,OAAO,GAAG,IAAI,iBAAiB;AACxC,SAAG,MAAM,UAAU;AAAA,IACrB;AAAA,IAEA,MAAM,MAAM;AACV,SAAG,MAAM,UAAU;AAAA,IACrB;AAAA,IAEA,SAAS,MAAM;AACb,SAAG,OAAO;AAAA,IACZ;AAAA,EACF;AACF;;;ACzBA,IAAMA,QAAO;AACb,IAAM,aAAa;AACnB,IAAMC,YAAW;AACjB,IAAMC,UAAS;AACf,IAAM,uBAAuB;AAE7B,IAAM,eAAe,IAAI,KAAK,aAAaA,OAAM;AAM1C,IAAM,eAAe,CAAC,aAA+B;AAC1D,QAAM,UAAU,KAAK,MAAO,SAAS,YAAY,SAAS,QAAS,OAAO,EAAE;AAAA,IAC1E;AAAA,EACF;AACA,SAAO,sBAAsB,aAAa,OAAO,SAAS,SAAS,CAAC,OAAO,aAAa,OAAO,SAAS,KAAK,CAAC,YAAY,OAAO;AACnI;AAIA,IAAM,WAAW,CAAC,QAAwB,gBAAgB,GAAG;AAEtD,IAAM,iBAAiB,CAAC,WAAwB,YAAuC;AAC5F,QAAM,QAAQ,iBAAiB,QAAQ,WAAW;AAClD,QAAM,cAAc,QAAQ,QAAQ,UAAU,QAAQ,MAAM,MAAM,WAAW;AAC7E,MAAI,YAAa,OAAM,QAAQ,WAAW;AAK1C,QAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,SAAO,OAAO,OAAO,OAAO;AAAA,IAC1B,SAAS;AAAA,IACT,OAAOF;AAAA,IACP,QAAQA;AAAA,IACR,aAAa;AAAA,EACf,CAAC;AACD,SAAO,SAAS;AAChB,SAAO,aAAa,QAAQ,KAAK;AACjC,SAAO,aAAa,wBAAwB,oBAAoB;AAIhE,MAAI,QAAQ,gBAAgB,OAAO;AACjC,WAAO,WAAW;AAClB,WAAO,aAAa,qBAAqB,iBAAiB;AAAA,EAC5D;AACA,YAAU,OAAO,MAAM;AAIvB,QAAM,MAAM,OAAO,WAAW,UAAU;AACxC,MAAI,CAAC,KAAK;AACR,WAAO,OAAO;AACd,UAAM,IAAI,MAAM,uBAAuB,UAAU,iBAAiB;AAAA,EACpE;AAEA,QAAM,WAAqB,eAAe,KAAK,KAAK;AAIpD,MAAI,YAAY;AAChB,MAAI,WAAW;AACf,MAAI,YAAY;AAChB,MAAI,MAAM;AACV,MAAI;AACJ,MAAI,QAAe,UAAU,SAAS;AACtC,MAAI,cAAc;AAClB,MAAI,WAAW;AAGf,MAAI,QAAQ,OAAO;AAMnB,QAAM,UACJ,QAAQ,UAAU,OACd,mBAAmB,WAAW;AAAA,IAC5B,SAAS,MAAM,MAAM,QAAQ;AAAA,IAC7B,UAAU,MAAM;AAAA,IAChB,WAAW,MAAM;AAAA,IACjB,gBAAgB,MAAM,MAAM;AAAA,IAC5B,SAAS,CAAC,cAAc,QAAQ,UAAU,SAAS;AAAA,IACnD,YAAY,CAAC,cAAc,QAAQ,aAAa,SAAS;AAAA,EAC3D,CAAC,IACD;AAEN,QAAM,UAAU,cAAc,SAAS;AAIvC,QAAM,cACJ,QAAQ,SAAS,OACb,CAAC,IACD,QAAQ,SAAS,SAAS,QAAQ,SAAS,SACzC,SACA,QAAQ;AAChB,QAAM,OAAyB,cAAc,WAAW,WAAW,WAAW,IAAI;AAIlF,QAAM,cAAc,WAAW,oBAAoB;AACnD,QAAM,eAAe,MAAe,CAAC,YAAY;AAEjD,QAAM,iBAAiB,MAAY;AACjC,aAAS,aAAa,aAAa,eAAe,KAAK,GAAG,aAAa,CAAC;AAAA,EAC1E;AAKA,QAAM,iBAAiB,MAAY;AACjC,QAAI,YAAa,gBAAe;AAAA,EAClC;AACA,cAAY,iBAAiB,UAAU,cAAc;AAIrD,QAAM,aAAa,MAAY;AAC7B,YAAQ,UAAU,SAAS;AAC3B,aAAS,UAAU,iBAAiB,OAAO,QAAQ,OAAO,CAAC;AAC3D,QAAI,YAAa,UAAS,kBAAkB,eAAe,KAAK,CAAC;AACjE,aAAS,SAAS,MAAM,KAAK;AAAA,EAC/B;AAMA,QAAM,aAAa,MAAY;AAC7B,WAAO,aAAa,uBAAuB,QAAQ,SAAS,cAAc,MAAM,SAAS,CAAC,CAAC;AAAA,EAC7F;AAIA,QAAM,eAAe,MAAY;AAC/B,UAAM,OAAO,MAAM,QAAQ,GAAG,QAAQ;AACtC,aAAS,OAAO;AAAA,EAClB;AAEA,MAAI;AACJ,MAAI,YAAYC;AAIhB,QAAM,UAAU,OAAkB,EAAE,GAAI,eAAe,MAAM,QAAQ,EAAG;AAOxE,QAAM,aAAa,MAAY;AAC7B,gBAAYA;AACZ,UAAM,OAAO;AACb,kBAAc;AACd,QAAI,CAAC,QAAQ,CAAC,MAAM,QAAQ,IAAI,EAAG;AAEnC,aAAS,aAAa;AACtB,iBAAa;AACb,YAAQ,eAAe,MAAM,QAAQ,CAAC;AAAA,EACxC;AAOA,QAAM,UAAU,CAAC,SAA0B;AACzC,UAAM,QAAQ,UAAU,MAAM,MAAM,WAAW;AAC/C,QAAI,CAAC,SAAS,SAAS,OAAO,eAAe,MAAM,QAAQ,CAAC,EAAG;AAE/D,kBAAc;AACd,QAAI,cAAcA,UAAU;AAC5B,gBAAY,sBAAsB,UAAU;AAAA,EAC9C;AAEA,QAAM,YAAY,MAAY,QAAQ,SAAS,MAAM,WAAW,CAAC;AAYjE,QAAM,SAAS,MAAY;AACzB,QAAI,aAAa,EAAG;AAEpB,UAAM,WAAW,KAAK,MAAM,WAAW,GAAG;AAC1C,QAAI,aAAa,MAAM,UAAU;AAC/B,aAAO,QAAQ;AACf,YAAM,OAAO,QAAQ;AACrB,eAAS,aAAa;AAAA,IACxB;AACA,iBAAa;AAAA,EACf;AAEA,QAAM,cAAc,MAAY;AAC9B,UAAM;AACN,WAAO;AACP,aAAS;AAAA,EACX;AAKA,QAAM,WAAW,MAAY;AAC3B,cAAU,oBAAoB,UAAU,WAAW;AACnD,eAAW,WAAW,SAAS,GAAG,CAAC;AACnC,aAAS,iBAAiB,UAAU,aAAa,EAAE,MAAM,KAAK,CAAC;AAAA,EACjE;AAEA,QAAM,iBAAiB,IAAI,eAAe,CAAC,YAAY;AACrD,UAAM,QAAQ,QAAQ,CAAC;AACvB,QAAI,CAAC,MAAO;AACZ,eAAW,MAAM,YAAY;AAC7B,gBAAY,MAAM,YAAY;AAC9B,WAAO;AAAA,EACT,CAAC;AACD,iBAAe,QAAQ,SAAS;AAoBhC,QAAM,SAAS,CAAC,MAA+B;AAC7C,UAAM,EAAE,SAAS,IAAI;AACrB,QAAI,aAAa,EAAG,QAAO;AAC3B,QAAI,IAAI,KAAK,KAAK,SAAU,QAAO;AAEnC,UAAM,SAAS,KAAK,IAAI,WAAW,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,GAAG,CAAC,CAAC;AACtE,UAAM,OAAO,MAAM,QAAQ;AAC3B,UAAM,SAAS,WAAW,IAAI;AAE9B,QAAI,SAAS,UAAU;AACrB,YAAM,QAAQ,KAAK,QAAQ,cAAc,QAAQ,QAAQ,QAAQ;AACjE,aAAO;AAAA,QACL;AAAA,QACA,OAAO,CAAC,OAAO,QAAQ,CAAC;AAAA,QACxB,WAAW,MAAM,IAAI,KAAK,IAAI,IAAI;AAAA,QAClC,OAAO;AAAA,MACT;AAAA,IACF;AAEA,UAAM,CAAC,OAAO,GAAG,IAAI,MAAM,QAAQ,MAAM;AACzC,WAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO,CAAC,OAAO,GAAG;AAAA,MAClB,WAAW,MAAM,QAAQ,MAAM;AAAA,MAC/B,OAAO,MAAM;AAAA,IACf;AAAA,EACF;AAMA,QAAM,gBAAgB,CAAC,UAA8B;AACnD,QAAI,SAAU;AAEd,UAAM,IAAI,QAAQ,QAAQ,KAAK;AAC/B,UAAM,MAAM,OAAO,CAAC;AACpB,QAAI,CAAC,KAAK;AACR,cAAQ,KAAK;AACb;AAAA,IACF;AAEA,UAAM,OACJ,WAAW,MAAM,QAAQ,CAAC,IAAI,MAAM,WAChC,mBAAmB,IAAI,OAAO,IAAI,cAAc,CAAC,IACjD,iBAAiB,IAAI,OAAO,IAAI,SAAS;AAC/C,YAAQ,KAAK,GAAG,IAAI;AAAA,EACtB;AAEA,QAAM,iBAAiB,MAAY,QAAQ,KAAK;AAEhD,QAAM,gBAAgB,CAAC,UAA8B;AACnD,YAAQ,MAAM;AAAA,EAChB;AAQA,QAAM,UAAU,CAAC,UAA4B;AAC3C,UAAM,YAAY,KAAK,IAAI,MAAM,UAAU,KAAK;AAChD,YAAQ,OAAO;AACf,QAAI,aAAa,kBAAmB;AACpC,QAAI,CAAC,QAAQ,aAAc;AAE3B,UAAM,MAAM,OAAO,QAAQ,QAAQ,KAAK,CAAC;AACzC,QAAI,CAAC,IAAK;AAEV,YAAQ,aAAa,EAAE,GAAG,KAAK,eAAe,MAAM,CAAC;AAAA,EACvD;AAEA,YAAU,iBAAiB,eAAe,aAAa;AACvD,YAAU,iBAAiB,gBAAgB,cAAc;AACzD,YAAU,iBAAiB,eAAe,aAAa;AACvD,YAAU,iBAAiB,SAAS,OAAO;AAI3C,QAAM,oBACJ,QAAQ,gBAAgB,QACpB,SACA,kBAAkB,WAAW,QAAQ;AAAA,IACnC;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,MAAM,MAAM;AAAA,IACzB,QAAQ,MAAM;AAAA,IACd,gBAAgB,MAAM,MAAM;AAAA,IAC5B,kBAAkB,MAAM,YAAY;AAAA,IACpC,aAAa,MAAM;AACjB,iBAAW;AACX,cAAQ,KAAK;AAAA,IACf;AAAA,IACA,WAAW,MAAM;AACf,iBAAW;AAAA,IACb;AAAA,EACF,CAAC;AAEP,QAAM,eAAe,WAAW,UAAU;AAC1C,WAAS;AACT,aAAW;AAIX,MAAI,QAAQ,cAAe,OAAM,cAAc,QAAQ,aAAa;AACpE,MAAI,QAAQ,cAAe,UAAS,UAAU,MAAM,IAAI,QAAQ,aAAa,CAAC;AAC9E,aAAW;AAKX,QAAM,OACJ,CAAsB,WACtB,IAAI,SAAkB;AACpB,QAAI,CAAC,UAAW,QAAO,GAAG,IAAI;AAAA,EAChC;AAEF,SAAO;AAAA,IACL,WAAW,KAAK,CAAC,YAAY;AAC3B,YAAM,QAAQ,OAAO;AACrB,eAAS,aAAa;AACtB,iBAAW;AAAA,IACb,CAAC;AAAA,IACD,WAAW,KAAK,CAAC,WAAW;AAC1B,YAAM,cAAc,MAAM;AAC1B,eAAS,aAAa;AACtB,iBAAW;AAAA,IACb,CAAC;AAAA;AAAA,IAGD,WAAW,KAAK,CAAC,YAAY;AAC3B,eAAS,UAAU,MAAM,IAAI,OAAO,CAAC;AACrC,iBAAW;AAAA,IACb,CAAC;AAAA,IACD,WAAW,KAAK,CAAC,WAAW;AAC1B,eAAS,UAAU,MAAM,UAAU,MAAM,CAAC;AAC1C,iBAAW;AAAA,IACb,CAAC;AAAA,IACD,cAAc,KAAK,CAAC,WAAW;AAC7B,eAAS,UAAU,MAAM,aAAa,MAAM,CAAC;AAC7C,iBAAW;AAAA,IACb,CAAC;AAAA,IAED,SAAS,KAAK,OAAO;AAAA,IACrB,WAAW,KAAK,SAAS;AAAA,IACzB;AAAA;AAAA;AAAA,IAIA,UAAU,MAAM,YAAY,MAAM,QAAQ,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,IAMrD,UAAU,KAAK,CAAC,cAAc,SAAS,aAAa,SAAS,CAAC;AAAA,IAC9D,UAAU,MAAO,YAAY,OAAQ,SAAS,aAAa,KAAK;AAAA,IAEhE,cAAc,KAAK,CAAC,OAAO;AACzB,oBAAc;AACd,qBAAe;AAAA,IACjB,CAAC;AAAA,IACD,aAAa,MAAO,YAAY,EAAE,WAAW,GAAG,OAAO,MAAM,YAAY,IAAI,MAAM,SAAS;AAAA;AAAA;AAAA,IAI5F,SAAS,KAAK,MAAM;AAClB,kBAAY;AAEZ,UAAI,cAAcA,UAAU,sBAAqB,SAAS;AAC1D,kBAAYA;AACZ,oBAAc;AAEd,qBAAe,WAAW;AAC1B,mBAAa;AACb,gBAAU,oBAAoB,UAAU,WAAW;AACnD,kBAAY,oBAAoB,UAAU,cAAc;AACxD,gBAAU,oBAAoB,eAAe,aAAa;AAC1D,gBAAU,oBAAoB,gBAAgB,cAAc;AAC5D,gBAAU,oBAAoB,eAAe,aAAa;AAC1D,gBAAU,oBAAoB,SAAS,OAAO;AAC9C,0BAAoB;AAEpB,eAAS,QAAQ;AACjB,cAAQ,QAAQ;AAChB,YAAM,QAAQ;AACd,eAAS,QAAQ;AACjB,aAAO,OAAO;AAAA,IAChB,CAAC;AAAA,EACH;AACF;","names":["FULL","NO_FRAME","LOCALE"]}
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
+ /**
2
+ * The visible block window as a half-open [start, end) range, with the
3
+ * clamp/zoom/pan arithmetic that keeps it legal. Pure functions over plain
4
+ * objects: no DOM, no event handling, so the maths is testable on its own.
5
+ */
1
6
  type BlockView = {
2
7
  start: number;
3
8
  end: number;
4
9
  };
5
-
6
- type AxisOptions = {
7
- format?: (block: number, step: number) => string;
8
- tickCount?: number;
9
- };
10
10
  type BlockScale = {
11
11
  domain: readonly [number, number];
12
12
  range: readonly [number, number];
@@ -14,6 +14,23 @@ type BlockScale = {
14
14
  block(x: number): number;
15
15
  };
16
16
 
17
+ /**
18
+ * Block-number axis under the canvas: a linear scale, nice tick positions,
19
+ * and absolutely positioned labels. Hand-rolled rather than pulling in
20
+ * d3-scale/d3-axis, which assume SVG; only the tick-step maths comes from D3.
21
+ */
22
+
23
+ type AxisOptions = {
24
+ format?: (block: number, step: number) => string;
25
+ tickCount?: number;
26
+ };
27
+
28
+ /**
29
+ * Turns the container's CSS custom properties into a 256-entry RGBA lookup
30
+ * table, so painting a bin is an array slice rather than a colour
31
+ * computation. Owns theme reading and colour maths only; it never touches
32
+ * the canvas or the bin counts.
33
+ */
17
34
  type Theme = {
18
35
  missing: string;
19
36
  partialLow: string;
@@ -21,17 +38,38 @@ type Theme = {
21
38
  partial: string;
22
39
  complete: string;
23
40
  highlight: string;
41
+ brush: string;
24
42
  };
25
43
  type PartialStyle = "gradient" | "coverage";
26
44
 
27
- type BlockRangeTuple = readonly [start: number, end: number];
28
- type RangeInput = ArrayLike<BlockRangeTuple> | ArrayLike<number>;
45
+ /**
46
+ * Normalises caller-supplied block ranges into one canonical form: a flat
47
+ * Uint32Array of sorted, non-overlapping [start, end) pairs. Everything
48
+ * downstream can then assume that shape; this module does no state keeping.
49
+ */
50
+ type BlockRange = readonly [start: number, end: number];
51
+ type RangeInput = ArrayLike<BlockRange> | ArrayLike<number>;
52
+
53
+ /**
54
+ * The model: which blocks are complete (bitset), how they fold into the
55
+ * current view's bins, and which bins changed. Deliberately free of DOM, D3
56
+ * and canvas imports so it stays portable - it could run in a worker and
57
+ * post dirty-bin arrays back to the renderer unchanged.
58
+ */
29
59
 
30
60
  type Progress = {
31
61
  completed: number;
32
62
  total: number;
33
63
  };
34
64
 
65
+ /**
66
+ * The façade: creates the canvas, wires state, renderer, tooltip, axis,
67
+ * brush overlay and interaction together, and owns everything with a
68
+ * lifetime - observers, media queries, the pending-view frame. The pieces it
69
+ * composes stay unaware of each other; all cross-cutting policy (when to
70
+ * repaint, what a gesture means for the model) lives here.
71
+ */
72
+
35
73
  type BlockMapOptions = {
36
74
  totalBlocks: number;
37
75
  initialBlocks?: ArrayLike<number>;
@@ -40,7 +78,19 @@ type BlockMapOptions = {
40
78
  view?: BlockView;
41
79
  interactive?: boolean;
42
80
  axis?: boolean | AxisOptions;
81
+ brush?: boolean;
43
82
  onViewChange?: (view: BlockView) => void;
83
+ onBlockClick?: (event: BlockClickEvent) => void;
84
+ onBrush?: (selection: BlockRange | null) => void;
85
+ onBrushEnd?: (selection: BlockRange | null) => void;
86
+ label?: (progress: Progress) => string;
87
+ };
88
+ type BlockClickEvent = {
89
+ block: number;
90
+ range: BlockRange;
91
+ completed: number;
92
+ total: number;
93
+ originalEvent: MouseEvent;
44
94
  };
45
95
  type BlockMap = {
46
96
  setBlocks(indices: ArrayLike<number>): void;
@@ -52,6 +102,8 @@ type BlockMap = {
52
102
  resetView(): void;
53
103
  getView(): BlockView;
54
104
  getScale(): BlockScale;
105
+ setBrush(selection: BlockRange | null): void;
106
+ getBrush(): BlockRange | null;
55
107
  setHighlight(on: boolean): void;
56
108
  getProgress(): Progress;
57
109
  destroy(): void;
@@ -59,4 +111,4 @@ type BlockMap = {
59
111
  declare const createBlockMap: (container: HTMLElement, options: BlockMapOptions) => BlockMap;
60
112
 
61
113
  export { createBlockMap };
62
- export type { AxisOptions, BlockMap, BlockMapOptions, BlockRangeTuple, BlockScale, BlockView, PartialStyle, Progress, RangeInput, Theme };
114
+ export type { AxisOptions, BlockClickEvent, BlockMap, BlockMapOptions, BlockRange, BlockScale, BlockView, PartialStyle, Progress, RangeInput, Theme };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createBlockMap
3
- } from "./chunk-WIDC3EN2.js";
3
+ } from "./chunk-6BBJWE5O.js";
4
4
  export {
5
5
  createBlockMap
6
6
  };
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { CSSProperties, Ref } from 'react';
3
- import { RangeInput, PartialStyle, BlockView, AxisOptions, BlockMap as BlockMap$1 } from '../index.js';
3
+ import { RangeInput, PartialStyle, BlockView, AxisOptions, BlockRange, BlockClickEvent, Progress, BlockMap as BlockMap$1 } from '../index.js';
4
4
 
5
5
  type BlockMapProps = {
6
6
  totalBlocks: number;
@@ -10,14 +10,19 @@ type BlockMapProps = {
10
10
  view?: BlockView;
11
11
  interactive?: boolean;
12
12
  axis?: boolean | AxisOptions;
13
+ brush?: boolean;
13
14
  onViewChange?: (view: BlockView) => void;
15
+ onBrush?: (selection: BlockRange | null) => void;
16
+ onBrushEnd?: (selection: BlockRange | null) => void;
17
+ onBlockClick?: (event: BlockClickEvent) => void;
18
+ label?: (progress: Progress) => string;
14
19
  highlight?: boolean;
15
20
  className?: string;
16
21
  style?: CSSProperties;
17
22
  ref?: Ref<BlockMapHandle>;
18
23
  };
19
- type BlockMapHandle = Pick<BlockMap$1, "addBlocks" | "setRanges" | "addRanges" | "removeRanges" | "setView" | "resetView" | "getView" | "getScale" | "setHighlight" | "getProgress">;
20
- declare const BlockMap: ({ totalBlocks, blocks, ranges, partial, view, interactive, axis, onViewChange, highlight, className, style, ref, }: BlockMapProps) => react.JSX.Element;
24
+ type BlockMapHandle = Omit<BlockMap$1, "destroy">;
25
+ declare const BlockMap: ({ totalBlocks, blocks, ranges, partial, view, interactive, axis, brush, onViewChange, onBrush, onBrushEnd, onBlockClick, label, highlight, className, style, ref, }: BlockMapProps) => react.JSX.Element;
21
26
 
22
27
  export { BlockMap };
23
28
  export type { BlockMapHandle, BlockMapProps };
@@ -1,12 +1,16 @@
1
1
  import {
2
- createBlockMap
3
- } from "../chunk-WIDC3EN2.js";
2
+ createBlockMap,
3
+ createScale,
4
+ defaultLabel,
5
+ fullView
6
+ } from "../chunk-6BBJWE5O.js";
4
7
 
5
8
  // src/react/BlockMap.tsx
6
- import { useEffect, useImperativeHandle, useRef } from "react";
9
+ import { useLayoutEffect, useImperativeHandle, useRef, useState } from "react";
7
10
  import { jsx } from "react/jsx-runtime";
8
11
  var EMPTY_BLOCKS = [];
9
12
  var EMPTY_PROGRESS = { completed: 0, total: 0 };
13
+ var UNMEASURED_WIDTH = 0;
10
14
  var BlockMap = ({
11
15
  totalBlocks,
12
16
  blocks,
@@ -15,36 +19,45 @@ var BlockMap = ({
15
19
  view,
16
20
  interactive,
17
21
  axis,
22
+ brush,
18
23
  onViewChange,
24
+ onBrush,
25
+ onBrushEnd,
26
+ onBlockClick,
27
+ label,
19
28
  highlight,
20
29
  className,
21
30
  style,
22
31
  ref
23
32
  }) => {
24
33
  const containerRef = useRef(null);
25
- const instanceRef = useRef(null);
26
- const onViewChangeRef = useRef(onViewChange);
27
- onViewChangeRef.current = onViewChange;
28
- useEffect(() => {
34
+ const [instance, setInstance] = useState(null);
35
+ const latest = useRef({});
36
+ latest.current = { onViewChange, onBrush, onBrushEnd, onBlockClick, label };
37
+ useLayoutEffect(() => {
29
38
  const container = containerRef.current;
30
39
  if (!container) return void 0;
31
40
  const options = {
32
41
  totalBlocks,
33
- onViewChange: (next) => onViewChangeRef.current?.(next)
42
+ onViewChange: (next) => latest.current.onViewChange?.(next),
43
+ onBrush: (selection) => latest.current.onBrush?.(selection),
44
+ onBrushEnd: (selection) => latest.current.onBrushEnd?.(selection),
45
+ onBlockClick: (event) => latest.current.onBlockClick?.(event),
46
+ label: (progress) => (latest.current.label ?? defaultLabel)(progress)
34
47
  };
35
48
  if (partial) options.partial = partial;
36
49
  if (interactive !== void 0) options.interactive = interactive;
37
50
  if (view) options.view = view;
38
51
  if (axis !== void 0) options.axis = axis;
39
- const instance = createBlockMap(container, options);
40
- instanceRef.current = instance;
52
+ if (brush !== void 0) options.brush = brush;
53
+ const created = createBlockMap(container, options);
54
+ setInstance(created);
41
55
  return () => {
42
- instance.destroy();
43
- instanceRef.current = null;
56
+ created.destroy();
57
+ setInstance(null);
44
58
  };
45
- }, [totalBlocks, partial, interactive, axis]);
46
- useEffect(() => {
47
- const instance = instanceRef.current;
59
+ }, [totalBlocks, partial, interactive, axis, brush]);
60
+ useLayoutEffect(() => {
48
61
  if (!instance) return;
49
62
  if (ranges !== void 0) {
50
63
  instance.setRanges(ranges);
@@ -52,33 +65,31 @@ var BlockMap = ({
52
65
  return;
53
66
  }
54
67
  instance.setBlocks(blocks ?? EMPTY_BLOCKS);
55
- }, [totalBlocks, partial, interactive, axis, ranges, blocks]);
56
- useEffect(() => {
57
- if (view !== void 0) instanceRef.current?.setView(view);
58
- }, [totalBlocks, partial, interactive, axis, view]);
59
- useEffect(() => {
60
- instanceRef.current?.setHighlight(highlight ?? false);
61
- }, [totalBlocks, partial, interactive, axis, highlight]);
68
+ }, [instance, ranges, blocks]);
69
+ useLayoutEffect(() => {
70
+ if (view !== void 0) instance?.setView(view);
71
+ }, [instance, view]);
72
+ useLayoutEffect(() => {
73
+ instance?.setHighlight(highlight ?? false);
74
+ }, [instance, highlight]);
62
75
  useImperativeHandle(
63
76
  ref,
64
77
  () => ({
65
- addBlocks: (indices) => instanceRef.current?.addBlocks(indices),
66
- setRanges: (next) => instanceRef.current?.setRanges(next),
67
- addRanges: (next) => instanceRef.current?.addRanges(next),
68
- removeRanges: (next) => instanceRef.current?.removeRanges(next),
69
- setView: (next) => instanceRef.current?.setView(next),
70
- resetView: () => instanceRef.current?.resetView(),
71
- getView: () => instanceRef.current?.getView() ?? { start: 0, end: totalBlocks },
72
- getScale: () => instanceRef.current?.getScale() ?? {
73
- domain: [0, totalBlocks],
74
- range: [0, 0],
75
- x: () => 0,
76
- block: () => 0
77
- },
78
- setHighlight: (on) => instanceRef.current?.setHighlight(on),
79
- getProgress: () => instanceRef.current?.getProgress() ?? { ...EMPTY_PROGRESS, total: totalBlocks }
78
+ setBlocks: (indices) => instance?.setBlocks(indices),
79
+ addBlocks: (indices) => instance?.addBlocks(indices),
80
+ setRanges: (next) => instance?.setRanges(next),
81
+ addRanges: (next) => instance?.addRanges(next),
82
+ removeRanges: (next) => instance?.removeRanges(next),
83
+ setView: (next) => instance?.setView(next),
84
+ resetView: () => instance?.resetView(),
85
+ getView: () => instance?.getView() ?? fullView(totalBlocks),
86
+ getScale: () => instance?.getScale() ?? createScale(fullView(totalBlocks), UNMEASURED_WIDTH),
87
+ setBrush: (selection) => instance?.setBrush(selection),
88
+ getBrush: () => instance?.getBrush() ?? null,
89
+ setHighlight: (on) => instance?.setHighlight(on),
90
+ getProgress: () => instance?.getProgress() ?? { ...EMPTY_PROGRESS, total: totalBlocks }
80
91
  }),
81
- [totalBlocks]
92
+ [instance, totalBlocks]
82
93
  );
83
94
  return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style });
84
95
  };