@jimka/typescript-ui 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/{AbstractInput-B10DfLOf.js → AbstractInput-sRNPzWel.js} +2 -2
- package/dist/lib/{AbstractInput-B10DfLOf.js.map → AbstractInput-sRNPzWel.js.map} +1 -1
- package/dist/lib/Anchor-CwRhdP2J.js +2 -0
- package/dist/lib/Anchor-CwRhdP2J.js.map +1 -0
- package/dist/lib/AnimatedDropdown-H3Rh-YMZ.js +2 -0
- package/dist/lib/AnimatedDropdown-H3Rh-YMZ.js.map +1 -0
- package/dist/lib/Border-0DJFDiij.js +2 -0
- package/dist/lib/Border-0DJFDiij.js.map +1 -0
- package/dist/lib/Button-MpMIYBYJ.js +2 -0
- package/dist/lib/Button-MpMIYBYJ.js.map +1 -0
- package/dist/lib/{Card-CD3jxshU.js → Card-QYo27r5b.js} +2 -2
- package/dist/lib/{Card-CD3jxshU.js.map → Card-QYo27r5b.js.map} +1 -1
- package/dist/lib/Checkbox-D0JYK-wc.js +2 -0
- package/dist/lib/Checkbox-D0JYK-wc.js.map +1 -0
- package/dist/lib/CheckboxMenuRow-CesLhSnp.js +2 -0
- package/dist/lib/CheckboxMenuRow-CesLhSnp.js.map +1 -0
- package/dist/lib/{CodeEditor-BGlB232h.js → CodeEditor-N1FQodcF.js} +2 -2
- package/dist/lib/CodeEditor-N1FQodcF.js.map +1 -0
- package/dist/lib/ComboBox-Dx8Tk9yg.js +2 -0
- package/dist/lib/ComboBox-Dx8Tk9yg.js.map +1 -0
- package/dist/lib/Component-B0ShfPVi.js +2 -0
- package/dist/lib/Component-B0ShfPVi.js.map +1 -0
- package/dist/lib/Container-abY5Ww3W.js +2 -0
- package/dist/lib/{Container-C7RPLQEc.js.map → Container-abY5Ww3W.js.map} +1 -1
- package/dist/lib/{DOM-Ba8YRVel.js → DOM-C-Edb3tA.js} +2 -2
- package/dist/lib/DOM-C-Edb3tA.js.map +1 -0
- package/dist/lib/Dialog-cbA9V17e.js +2 -0
- package/dist/lib/Dialog-cbA9V17e.js.map +1 -0
- package/dist/lib/{FieldDecorator-HG65hEtk.js → FieldDecorator-BlYMfZ9L.js} +2 -2
- package/dist/lib/{FieldDecorator-HG65hEtk.js.map → FieldDecorator-BlYMfZ9L.js.map} +1 -1
- package/dist/lib/{Fit-Cg3dPyZg.js → Fit-BhlB-HAo.js} +2 -2
- package/dist/lib/{Fit-Cg3dPyZg.js.map → Fit-BhlB-HAo.js.map} +1 -1
- package/dist/lib/{FloatingPanel-D5buuVrQ.js → FloatingPanel-DkJXsZ2D.js} +2 -2
- package/dist/lib/{FloatingPanel-D5buuVrQ.js.map → FloatingPanel-DkJXsZ2D.js.map} +1 -1
- package/dist/lib/{Glyph-DeJF_zsR.js → Glyph-_VeBacS2.js} +2 -2
- package/dist/lib/{Glyph-DeJF_zsR.js.map → Glyph-_VeBacS2.js.map} +1 -1
- package/dist/lib/{Grid-BYBohJIA.js → Grid-Daiz7F5d.js} +2 -2
- package/dist/lib/Grid-Daiz7F5d.js.map +1 -0
- package/dist/lib/GridConstraints-C58LuTqM.js +2 -0
- package/dist/lib/{GridConstraints-Bts284FI.js.map → GridConstraints-C58LuTqM.js.map} +1 -1
- package/dist/lib/{Header-BIFJK9nR.js → Header-Dql3Zxc6.js} +2 -2
- package/dist/lib/{Header-BIFJK9nR.js.map → Header-Dql3Zxc6.js.map} +1 -1
- package/dist/lib/{IconText-Dsb1xS-E.js → IconText-PUjhTLlp.js} +2 -2
- package/dist/lib/{IconText-Dsb1xS-E.js.map → IconText-PUjhTLlp.js.map} +1 -1
- package/dist/lib/LayoutSerialization-CMxunLfV.js +2 -0
- package/dist/lib/LayoutSerialization-CMxunLfV.js.map +1 -0
- package/dist/lib/List-nx8220Fi.js +2 -0
- package/dist/lib/{List-BPd-IXZR.js.map → List-nx8220Fi.js.map} +1 -1
- package/dist/lib/MemoryStore-DhPoU-7X.js +2 -0
- package/dist/lib/MemoryStore-DhPoU-7X.js.map +1 -0
- package/dist/lib/Menu-Q0bpXXze.js +2 -0
- package/dist/lib/Menu-Q0bpXXze.js.map +1 -0
- package/dist/lib/{MenuButton-BR4KOFld.js → MenuButton-DCXi0tsP.js} +2 -2
- package/dist/lib/{MenuButton-BR4KOFld.js.map → MenuButton-DCXi0tsP.js.map} +1 -1
- package/dist/lib/OverlayFade-3pof2AJH.js +2 -0
- package/dist/lib/OverlayFade-3pof2AJH.js.map +1 -0
- package/dist/lib/{Panel-CpNQTLNF.js → Panel-Cy1pdlWO.js} +2 -2
- package/dist/lib/{Panel-CpNQTLNF.js.map → Panel-Cy1pdlWO.js.map} +1 -1
- package/dist/lib/{Position-PBVBW1JS.js → Position-BwtxouxK.js} +2 -2
- package/dist/lib/{Position-PBVBW1JS.js.map → Position-BwtxouxK.js.map} +1 -1
- package/dist/lib/{ProgressSpinner-lAsK26C-.js → ProgressSpinner-DK8xEQ9y.js} +2 -2
- package/dist/lib/{ProgressSpinner-lAsK26C-.js.map → ProgressSpinner-DK8xEQ9y.js.map} +1 -1
- package/dist/lib/RadioButton-BEwpqg1k.js +2 -0
- package/dist/lib/{RadioButton-CCcIaL54.js.map → RadioButton-BEwpqg1k.js.map} +1 -1
- package/dist/lib/RadioMenuRow-DP5qPyFR.js +2 -0
- package/dist/lib/RadioMenuRow-DP5qPyFR.js.map +1 -0
- package/dist/lib/{RovingTabIndex-C2Vzq0IX.js → RovingTabIndex-pHEv5oLD.js} +2 -2
- package/dist/lib/{RovingTabIndex-C2Vzq0IX.js.map → RovingTabIndex-pHEv5oLD.js.map} +1 -1
- package/dist/lib/Scrollbar-t8fFrUVQ.js +2 -0
- package/dist/lib/Scrollbar-t8fFrUVQ.js.map +1 -0
- package/dist/lib/{Slider-DZ9drUxn.js → Slider-CTbwR6HR.js} +2 -2
- package/dist/lib/{Slider-DZ9drUxn.js.map → Slider-CTbwR6HR.js.map} +1 -1
- package/dist/lib/{Spacer-SDhOMp2g.js → Spacer-srdN2d2b.js} +2 -2
- package/dist/lib/{Spacer-SDhOMp2g.js.map → Spacer-srdN2d2b.js.map} +1 -1
- package/dist/lib/TabButton-DLoMQzky.js +2 -0
- package/dist/lib/TabButton-DLoMQzky.js.map +1 -0
- package/dist/lib/TabCloseButton-CY7BGitY.js +2 -0
- package/dist/lib/TabCloseButton-CY7BGitY.js.map +1 -0
- package/dist/lib/{Text-CdHf3B-1.js → Text-CMKnK9uB.js} +2 -2
- package/dist/lib/{Text-CdHf3B-1.js.map → Text-CMKnK9uB.js.map} +1 -1
- package/dist/lib/{Tooltip-B_UF5bXD.js → Tooltip-CUOJOqQi.js} +3 -3
- package/dist/lib/Tooltip-CUOJOqQi.js.map +1 -0
- package/dist/lib/Tree-C64JNPWJ.js +2 -0
- package/dist/lib/Tree-C64JNPWJ.js.map +1 -0
- package/dist/lib/VBox-DfJPWXqc.js +2 -0
- package/dist/lib/VBox-DfJPWXqc.js.map +1 -0
- package/dist/lib/VirtualScroller-DTIn19o1.js +2 -0
- package/dist/lib/VirtualScroller-DTIn19o1.js.map +1 -0
- package/dist/lib/assets/StoreWorker-B5NwRM2I.js +2 -0
- package/dist/lib/assets/StoreWorker-B5NwRM2I.js.map +1 -0
- package/dist/lib/component/button.es.js +1 -1
- package/dist/lib/component/button.es.js.map +1 -1
- package/dist/lib/component/chart.es.js +1 -1
- package/dist/lib/component/container.es.js +1 -1
- package/dist/lib/component/container.es.js.map +1 -1
- package/dist/lib/component/diagram.es.js +1 -1
- package/dist/lib/component/diagram.es.js.map +1 -1
- package/dist/lib/component/display.es.js +1 -1
- package/dist/lib/component/display.es.js.map +1 -1
- package/dist/lib/component/editor.es.js +2 -2
- package/dist/lib/component/editor.es.js.map +1 -1
- package/dist/lib/component/input.es.js +1 -1
- package/dist/lib/component/input.es.js.map +1 -1
- package/dist/lib/component/list.es.js +1 -1
- package/dist/lib/component/list.es.js.map +1 -1
- package/dist/lib/component/menubar.es.js +1 -1
- package/dist/lib/component/menubar.es.js.map +1 -1
- package/dist/lib/component/table.es.js +6 -3
- package/dist/lib/component/table.es.js.map +1 -1
- package/dist/lib/component/tree.es.js +1 -1
- package/dist/lib/core.es.js +1 -1
- package/dist/lib/core.es.js.map +1 -1
- package/dist/lib/data.es.js +1 -1
- package/dist/lib/glyphs/solid/index.es.js +1 -1
- package/dist/lib/layout.es.js +1 -1
- package/dist/lib/layout.es.js.map +1 -1
- package/dist/lib/overlay.es.js +1 -1
- package/dist/lib/overlay.es.js.map +1 -1
- package/dist/lib/primitive.es.js +1 -1
- package/dist/lib/router.es.js +1 -1
- package/dist/lib/router.es.js.map +1 -1
- package/dist/lib/selectionsEqual-gMG7gCVI.js +2 -0
- package/dist/lib/selectionsEqual-gMG7gCVI.js.map +1 -0
- package/dist/lib/types/component/button/Button.d.ts +13 -0
- package/dist/lib/types/component/button/MenuButton.d.ts +1 -0
- package/dist/lib/types/component/button/PopupButton.d.ts +22 -0
- package/dist/lib/types/component/button/TabButton.d.ts +4 -0
- package/dist/lib/types/component/button/index.d.ts +2 -0
- package/dist/lib/types/component/container/CheckboxMenuRow.d.ts +33 -0
- package/dist/lib/types/component/container/CollapseButton.d.ts +11 -3
- package/dist/lib/types/component/container/MenuItem.d.ts +15 -5
- package/dist/lib/types/component/container/MenuRow.d.ts +24 -0
- package/dist/lib/types/component/container/MenuSeparator.d.ts +4 -2
- package/dist/lib/types/component/container/RadioMenuRow.d.ts +33 -0
- package/dist/lib/types/component/container/Scrollbar.d.ts +5 -0
- package/dist/lib/types/component/container/SplitGutter.d.ts +7 -2
- package/dist/lib/types/component/container/TabBar.d.ts +2 -0
- package/dist/lib/types/component/container/WindowBorder.d.ts +1 -1
- package/dist/lib/types/component/container/index.d.ts +6 -1
- package/dist/lib/types/component/display/WebGLCanvas.d.ts +1 -0
- package/dist/lib/types/component/input/AbstractCalendarDropdown.d.ts +2 -0
- package/dist/lib/types/component/input/AbstractPickerField.d.ts +3 -2
- package/dist/lib/types/component/list/AbstractSelectableList.d.ts +1 -1
- package/dist/lib/types/component/list/MultiSelectList.d.ts +2 -1
- package/dist/lib/types/component/shared/VirtualRowView.d.ts +2 -0
- package/dist/lib/types/component/table/Body.d.ts +22 -1
- package/dist/lib/types/component/table/Column.d.ts +3 -1
- package/dist/lib/types/component/table/ColumnConfig.d.ts +2 -0
- package/dist/lib/types/component/table/ColumnFilter.d.ts +27 -0
- package/dist/lib/types/component/table/Header.d.ts +30 -3
- package/dist/lib/types/component/table/Row.d.ts +4 -0
- package/dist/lib/types/component/table/Table.d.ts +29 -0
- package/dist/lib/types/component/table/TableExporter.d.ts +4 -3
- package/dist/lib/types/component/table/TreeBody.d.ts +2 -1
- package/dist/lib/types/component/table/cell/CellText.d.ts +9 -0
- package/dist/lib/types/component/table/cell/Filter.d.ts +54 -0
- package/dist/lib/types/component/table/cell/FilterClauseBadge.d.ts +21 -0
- package/dist/lib/types/component/table/cell/GroupSeparator.d.ts +9 -0
- package/dist/lib/types/component/table/cell/Header.d.ts +1 -0
- package/dist/lib/types/component/table/cell/editor/CellEditor.d.ts +1 -0
- package/dist/lib/types/component/table/cell/renderer/CellRenderer.d.ts +1 -0
- package/dist/lib/types/component/table/cell/renderer/Combo.d.ts +2 -0
- package/dist/lib/types/component/table/cell/renderer/Date.d.ts +2 -0
- package/dist/lib/types/component/table/cell/renderer/DateTime.d.ts +2 -0
- package/dist/lib/types/component/table/cell/renderer/Filter.d.ts +16 -0
- package/dist/lib/types/component/table/cell/renderer/Glyph.d.ts +1 -0
- package/dist/lib/types/component/table/cell/renderer/Link.d.ts +2 -0
- package/dist/lib/types/component/table/cell/renderer/Number.d.ts +3 -1
- package/dist/lib/types/component/table/cell/renderer/String.d.ts +2 -0
- package/dist/lib/types/component/table/cell/renderer/Time.d.ts +2 -0
- package/dist/lib/types/component/table/cell/renderer/TreeCell.d.ts +1 -0
- package/dist/lib/types/component/table/index.d.ts +6 -0
- package/dist/lib/types/component/tree/Tree.d.ts +17 -1
- package/dist/lib/types/component/tree/index.d.ts +1 -1
- package/dist/lib/types/core/AnimatedDropdown.d.ts +0 -9
- package/dist/lib/types/core/Body.d.ts +4 -0
- package/dist/lib/types/core/ClassStyleRules.d.ts +1 -0
- package/dist/lib/types/core/DOM.d.ts +10 -0
- package/dist/lib/types/core/Event.d.ts +11 -2
- package/dist/lib/types/core/OverlayFade.d.ts +12 -0
- package/dist/lib/types/core/OverlayPosition.d.ts +6 -0
- package/dist/lib/types/core/Theme.d.ts +8 -0
- package/dist/lib/types/core/index.d.ts +5 -3
- package/dist/lib/types/data/AbstractStore.d.ts +3 -0
- package/dist/lib/types/data/FilterDescriptor.d.ts +13 -0
- package/dist/lib/types/data/index.d.ts +2 -1
- package/dist/lib/types/data/temporalText.d.ts +2 -0
- package/dist/lib/types/layout/BoxLayout.d.ts +6 -1
- package/dist/lib/types/layout/Grid.d.ts +1 -0
- package/dist/lib/types/layout/HFlow.d.ts +1 -1
- package/dist/lib/types/layout/Split.d.ts +10 -1
- package/dist/lib/types/layout/Tab.d.ts +7 -1
- package/dist/lib/types/layout/VFlow.d.ts +1 -1
- package/dist/lib/types/layout/index.d.ts +1 -1
- package/dist/lib/types/overlay/Dock.d.ts +1 -0
- package/dist/lib/types/overlay/Menu.d.ts +1 -1
- package/dist/lib/types/overlay/Popover.d.ts +3 -0
- package/dist/lib/types/overlay/PopupPanel.d.ts +17 -0
- package/dist/lib/types/overlay/Window.d.ts +2 -0
- package/dist/lib/types/overlay/index.d.ts +2 -0
- package/dist/lib/types/router/RoutePattern.d.ts +7 -0
- package/dist/lib/types/router/Router.d.ts +6 -2
- package/dist/lib/types/router/index.d.ts +1 -1
- package/dist/lib/validation.es.js +1 -1
- package/llms.txt +4 -0
- package/package.json +1 -1
- package/dist/lib/AbstractBooleanInput-CbW7ZrDT.js +0 -2
- package/dist/lib/AbstractBooleanInput-CbW7ZrDT.js.map +0 -1
- package/dist/lib/Anchor-BsblS5it.js +0 -2
- package/dist/lib/Anchor-BsblS5it.js.map +0 -1
- package/dist/lib/AnimatedDropdown-kL47HPGP.js +0 -2
- package/dist/lib/AnimatedDropdown-kL47HPGP.js.map +0 -1
- package/dist/lib/Border-CNS6aEXB.js +0 -2
- package/dist/lib/Border-CNS6aEXB.js.map +0 -1
- package/dist/lib/Button-T16zSZip.js +0 -2
- package/dist/lib/Button-T16zSZip.js.map +0 -1
- package/dist/lib/CodeEditor-BGlB232h.js.map +0 -1
- package/dist/lib/ComboBox-C9FYMuLv.js +0 -2
- package/dist/lib/ComboBox-C9FYMuLv.js.map +0 -1
- package/dist/lib/Component-w65rPK75.js +0 -2
- package/dist/lib/Component-w65rPK75.js.map +0 -1
- package/dist/lib/Container-C7RPLQEc.js +0 -2
- package/dist/lib/DOM-Ba8YRVel.js.map +0 -1
- package/dist/lib/DragManager-UvPHSoRm.js +0 -2
- package/dist/lib/DragManager-UvPHSoRm.js.map +0 -1
- package/dist/lib/Grid-BYBohJIA.js.map +0 -1
- package/dist/lib/GridConstraints-Bts284FI.js +0 -2
- package/dist/lib/LayoutSerialization-ySzY1h2l.js +0 -2
- package/dist/lib/LayoutSerialization-ySzY1h2l.js.map +0 -1
- package/dist/lib/LayoutSizes-DKDK1Cle.js +0 -2
- package/dist/lib/LayoutSizes-DKDK1Cle.js.map +0 -1
- package/dist/lib/List-BPd-IXZR.js +0 -2
- package/dist/lib/MemoryStore-BgP7-Uzn.js +0 -2
- package/dist/lib/MemoryStore-BgP7-Uzn.js.map +0 -1
- package/dist/lib/Menu-C0roOCY7.js +0 -2
- package/dist/lib/Menu-C0roOCY7.js.map +0 -1
- package/dist/lib/RadioButton-CCcIaL54.js +0 -2
- package/dist/lib/Scrollbar-C3ZZyZ1e.js +0 -2
- package/dist/lib/Scrollbar-C3ZZyZ1e.js.map +0 -1
- package/dist/lib/TabButton-DNo4ZgV8.js +0 -2
- package/dist/lib/TabButton-DNo4ZgV8.js.map +0 -1
- package/dist/lib/Tooltip-B_UF5bXD.js.map +0 -1
- package/dist/lib/Tree-C7qLbIYH.js +0 -2
- package/dist/lib/Tree-C7qLbIYH.js.map +0 -1
- package/dist/lib/VBox-32fxpkDl.js +0 -2
- package/dist/lib/VBox-32fxpkDl.js.map +0 -1
- package/dist/lib/VirtualScroller-cvFNMS7U.js +0 -2
- package/dist/lib/VirtualScroller-cvFNMS7U.js.map +0 -1
- package/dist/lib/assets/StoreWorker-CgOBYoai.js +0 -2
- package/dist/lib/assets/StoreWorker-CgOBYoai.js.map +0 -1
- package/dist/lib/selectionsEqual-BrTxPMmK.js +0 -2
- package/dist/lib/selectionsEqual-BrTxPMmK.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"display.es.js","names":[],"sources":["../../../src/typescript/lib/component/display/Canvas.ts","../../../src/typescript/lib/component/display/WebGLCanvas.ts","../../../src/typescript/lib/component/display/Image.ts","../../../src/typescript/lib/component/display/Markdown.ts","../../../src/typescript/lib/component/display/MarkdownMinimap.ts","../../../src/typescript/lib/component/display/MarkdownViewer.ts","../../../src/typescript/lib/component/display/IconLabel.ts","../../../src/typescript/lib/component/display/ProgressBar.ts","../../../src/typescript/lib/component/display/PaginationBar.ts","../../../src/typescript/lib/component/display/Video.ts","../../../src/typescript/lib/component/display/PlaybackEngine.ts","../../../src/typescript/lib/component/display/VideoPlayer.ts"],"sourcesContent":["// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * Draw callback: receives the live 2D context and the logical (CSS-px) size.\n *\n * @param ctx - The canvas 2D rendering context, pre-scaled so one unit is one\n * CSS pixel (the device-pixel-ratio transform is already applied).\n * @param width - The drawing surface width in CSS pixels.\n * @param height - The drawing surface height in CSS pixels.\n * @param elapsedMs - Milliseconds since the current animation run started, or\n * `0` when the canvas is not animating. Derive motion from this rather than\n * advancing a counter once per call: frames arrive at the display's refresh\n * rate, so a per-call increment runs three times faster on a 180Hz monitor\n * than on a 60Hz one. Outside the animation loop (a resize, a DPR change, an\n * explicit {@link Canvas.redraw}) this repeats the most recent frame's value,\n * so a redraw re-renders the same moment rather than jumping.\n *\n * @category Components\n */\nexport type CanvasDrawCallback = (\n ctx: CanvasRenderingContext2D,\n width: number,\n height: number,\n elapsedMs: number,\n) => void;\n\n/**\n * Construction-time options for {@link Canvas}.\n *\n * @category Components\n */\nexport interface CanvasOptions extends ComponentOptions {\n\n /** Draw hook, re-invoked on demand and after every resize / DPR change. */\n onDraw?: CanvasDrawCallback;\n\n /**\n * Keeps the animation loop running while the canvas is not effectively\n * on-screen (e.g. on an inactive `Tab` panel). Default `false`: the loop\n * pauses automatically when hidden and resumes when shown again.\n */\n animateWhenHidden?: boolean;\n\n /**\n * Upper bound, in frames per second, on how often the animation loop\n * redraws. Defaults to 30 — enough for smooth motion at a predictable cost,\n * and independent of the display's refresh rate, so the same animation\n * costs the same on a 60Hz and a 180Hz monitor. Pass `0` to opt out and\n * redraw on every animation frame the browser delivers. A positive value\n * skips frames that arrive sooner than `1000 / maxFps` after the last\n * one; the loop itself keeps running, so the cap trades smoothness for\n * CPU rather than pausing anything.\n */\n maxFps?: number;\n}\n\n/**\n * Class-level defaults forwarded to `super` so the cascade hits Component's\n * applyOptions with `{ tag: \"canvas\" }` already merged into `_defaultOptions`.\n */\nconst _defaultCanvasOptions: Partial<CanvasOptions> = {\n tag: \"canvas\",\n maxFps: 30,\n};\n\n// Sentinel for the last-synced width/height/dpr guard. A real size is always\n// >= 0 and a real dpr always >= 1, so -1 can never equal a genuine value and\n// therefore forces the first `syncBackingStore` to run rather than short-circuit.\nconst NOT_YET_SYNCED = -1;\n\n/**\n * A raster drawing surface backed by a `<canvas>` element and a live\n * `CanvasRenderingContext2D`.\n *\n * The component keeps two sizes in lockstep: the CSS size the framework commits\n * (`setWidth` / `setHeight`) and the backing store (the element's `width` /\n * `height` attributes, sized CSS × device-pixel-ratio for a crisp HiDPI\n * result). Callers draw in CSS pixels via the {@link CanvasDrawCallback} `onDraw`\n * hook — the dpr transform is applied for them. Because reassigning the backing\n * store wipes it, `onDraw` is re-invoked after every resize / DPR change; content\n * that must survive a resize belongs there rather than in a one-off\n * {@link Canvas.getContext} draw.\n *\n * `Canvas` is **live-only**: a rendering context cannot be modelled offline or\n * forwarded across a worker, so under a modelled sink `getContext` returns `null`\n * and every draw path no-ops.\n *\n * @category Components\n */\nclass Canvas extends Component<CanvasOptions> {\n\n /** Cached 2D context; `null` offline or before the element renders. */\n private _ctx: CanvasRenderingContext2D | null = null;\n\n /** Active animation-frame handle; `null` when the loop is idle. */\n private _rafId: number | null = null;\n\n // Frame clock for the animation loop. `_animationStartMs` is null until the\n // first frame of a run anchors it, so elapsed time starts at 0 for that\n // frame however long the run waited to be scheduled. `_lastDrawMs` gates\n // the maxFps cap and `_elapsedMs` is what the draw hook is handed, kept as\n // a field so redraws from outside the loop repeat the last frame's value.\n private _animationStartMs: number | null = null;\n private _lastDrawMs : number | null = null;\n private _elapsedMs = 0;\n\n /** Consumer/auto intent to animate; the loop actually runs only while also\n * effectively visible (or animateWhenHidden is set). Plain initializer:\n * never written during the super() cascade (only startAnimation /\n * stopAnimation, which run post-render, write it), so it needs no\n * `declare`. */\n private _animationRequested = false;\n\n /** Last-synced CSS width; guards against a redundant backing-store wipe. */\n private _syncedWidth: number = NOT_YET_SYNCED;\n\n /** Last-synced CSS height; guards against a redundant backing-store wipe. */\n private _syncedHeight: number = NOT_YET_SYNCED;\n\n /** Last-synced device-pixel ratio; guards against a redundant wipe. */\n private _syncedDpr: number = NOT_YET_SYNCED;\n\n /** Generation counter so only the newest DPR-watch arm acts on a change. */\n private _dprToken: number = 0;\n\n /**\n * Constructs a raster canvas.\n *\n * @param options - Optional construction options.\n * @param subclassDefaults - Per-subclass default bag layered over this\n * class's defaults; subclasses forward their `_defaultXxxOptions`\n * constant here.\n */\n constructor(options?: CanvasOptions, subclassDefaults?: Partial<CanvasOptions>) {\n super(options, { ..._defaultCanvasOptions, ...(subclassDefaults ?? {}) });\n\n this.clearInsets();\n }\n\n /**\n * Forwards the consumer-configurable `onDraw` hook to its setter.\n *\n * @param options - The construction options.\n * @returns This component, for method chaining.\n */\n protected applyOptions(options: CanvasOptions): this {\n super.applyOptions(options);\n\n if (options.onDraw !== undefined) {\n this.setOnDraw(options.onDraw);\n }\n\n if (options.animateWhenHidden !== undefined) {\n this.setAnimateWhenHidden(options.animateWhenHidden);\n }\n\n if (options.maxFps !== undefined) {\n this.setMaxFps(options.maxFps);\n }\n\n return this;\n }\n\n /**\n * Returns the 2D rendering context, lazily obtaining it from the seam on\n * first access once the element exists.\n *\n * @returns The context, or `null` offline / before the element renders.\n */\n getContext(): CanvasRenderingContext2D | null {\n if (this._ctx) {\n return this._ctx;\n }\n\n const element = this.getElement();\n if (!element) {\n return null;\n }\n\n this._ctx = DOM.sink.getContext(element, \"2d\") as CanvasRenderingContext2D | null;\n\n return this._ctx;\n }\n\n /**\n * Sets (or clears) the draw hook and triggers an immediate redraw.\n *\n * @param handler - The draw callback, or `null` to clear it.\n * @returns This component, for method chaining.\n */\n setOnDraw(handler: CanvasDrawCallback | null): this {\n this._options.onDraw = handler ?? undefined;\n this.redraw();\n\n return this;\n }\n\n /**\n * Returns the current draw hook.\n *\n * @returns The draw callback, or `null` when none is set.\n */\n getOnDraw(): CanvasDrawCallback | null {\n return this._options.onDraw ?? this._defaultOptions.onDraw ?? null;\n }\n\n /**\n * Clears the surface (in CSS pixels) and re-invokes `onDraw` against the\n * current context. Public so a consumer can force a repaint after mutating\n * its own model without a resize. No-ops when the context is unavailable.\n *\n * @returns This component, for method chaining.\n */\n redraw(): this {\n const ctx = this.getContext();\n if (!ctx) {\n return this;\n }\n\n const width = this.getWidth();\n const height = this.getHeight();\n\n ctx.clearRect(0, 0, width, height);\n this.getOnDraw()?.(ctx, width, height, this._elapsedMs);\n\n return this;\n }\n\n /**\n * Starts a per-frame redraw loop. Idempotent: a second call while already\n * animating does not schedule a second frame. Records the intent to\n * animate; the loop only actually runs while the canvas is also\n * effectively on-screen (or {@link setAnimateWhenHidden} opts out).\n *\n * @returns This component, for method chaining.\n */\n startAnimation(): this {\n this._animationRequested = true;\n this._animationStartMs = null;\n this._lastDrawMs = null;\n this._elapsedMs = 0;\n this.reconcileAnimation();\n\n return this;\n }\n\n /**\n * Stops the per-frame redraw loop, cancelling any pending frame. Clears\n * the intent to animate, so a later resize/show cannot resurrect it.\n *\n * @returns This component, for method chaining.\n */\n stopAnimation(): this {\n this._animationRequested = false;\n this.reconcileAnimation();\n\n return this;\n }\n\n /**\n * Whether the per-frame redraw loop is currently running. `false` while\n * paused for being effectively hidden, even if animation was requested.\n *\n * @returns `true` while animating.\n */\n isAnimating(): boolean {\n return this._rafId !== null;\n }\n\n /**\n * Keeps the animation loop running while the canvas is not effectively\n * on-screen. Reconciles immediately, so toggling it can start or pause\n * the loop right away.\n *\n * @param value - `true` to animate regardless of visibility.\n * @returns This component, for method chaining.\n */\n setAnimateWhenHidden(value: boolean): this {\n this._options.animateWhenHidden = value;\n this.reconcileAnimation();\n\n return this;\n }\n\n /**\n * Caps how often the animation loop redraws, in frames per second. Takes\n * effect on the next frame; the loop keeps running either way, so this\n * thins redraws rather than pausing anything. Because frames are delivered\n * at the display's refresh rate, an uncapped loop costs proportionally more\n * on a high-refresh monitor — a cap makes that cost predictable.\n *\n * @param fps - Maximum redraws per second, or `0` to remove the cap entirely.\n * Negative values are treated as `0`. The class default is 30.\n * @returns This component, for method chaining.\n */\n setMaxFps(fps: number): this {\n // On the options bag rather than a private field, matching\n // `animateWhenHidden` above: `applyOptions` runs inside the `super()`\n // cascade, before this class's field initializers, so a plain\n // `_maxFps = 0` initializer would overwrite a construction-time value.\n this._options.maxFps = Math.max(0, fps);\n\n return this;\n }\n\n /**\n * Returns the current redraw cap in frames per second.\n *\n * @returns The cap, or `0` when the loop is uncapped. Resolves the class\n * default (30) when no explicit value was set.\n */\n getMaxFps(): number {\n // Consults `_defaultOptions` as well as `_options`, matching the\n // framework's getter convention (`getZIndex` is\n // `_options.zIndex ?? _defaultOptions.zIndex ?? 0`). A class-level\n // default bag lands in `_defaultOptions`, never in `_options`, so\n // reading only the latter would silently ignore a default-supplied cap\n // and leave the loop uncapped from the first frame.\n return this._options.maxFps ?? this._defaultOptions.maxFps ?? 0;\n }\n\n /**\n * Returns whether the animation loop keeps running while hidden.\n *\n * @returns `true` if the loop ignores effective visibility.\n */\n getAnimateWhenHidden(): boolean {\n return this._options.animateWhenHidden ?? this._defaultOptions.animateWhenHidden ?? false;\n }\n\n /**\n * Reusable seam shared with the WebGL sibling: resizes the backing store to\n * CSS × dpr, re-applies the dpr transform (reassigning the attributes resets\n * all context state), and redraws. Called from `doLayout` on every size\n * change. Reads only cached CSS sizes — never DOM geometry, which inside\n * `doLayout` is still buffered — and short-circuits when width/height/dpr are\n * unchanged so idle layout passes never wipe the buffer.\n */\n protected syncBackingStore(): void {\n const ctx = this.getContext();\n if (!ctx) {\n return;\n }\n\n const width = this.getWidth();\n const height = this.getHeight();\n const dpr = DOM.source.getDevicePixelRatio();\n\n if (width === this._syncedWidth && height === this._syncedHeight && dpr === this._syncedDpr) {\n return;\n }\n\n const element = this.getElement()!;\n\n DOM.sink.apply(element, { setAttr: {\n width: String(Math.round(width * dpr)),\n height: String(Math.round(height * dpr)),\n }});\n\n // Reassigning the backing-store attributes reset the context, so re-apply\n // the dpr scale (identity skew/translate, dpr on both axes) — one context\n // unit is then one CSS pixel and callers draw in logical coordinates.\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n this._syncedWidth = width;\n this._syncedHeight = height;\n this._syncedDpr = dpr;\n\n this.redraw();\n }\n\n /**\n * Delegates to the base layout then syncs the backing store — the single\n * settled hook that fires once after both axes are committed, on initial\n * mount and every subsequent resize.\n *\n * @returns This component, for method chaining.\n */\n doLayout(): this {\n super.doLayout();\n this.syncBackingStore();\n\n return this;\n }\n\n /**\n * Renders the element and arms the DPR-change watcher.\n *\n * @returns The created element handle.\n */\n protected render(): Handle {\n const element = super.render();\n\n this.watchDevicePixelRatio();\n\n return element;\n }\n\n /**\n * Stops the animation loop before the inherited destructor detaches the\n * element, so no stray frame survives teardown.\n */\n protected destructor(): void {\n this.stopAnimation();\n super.destructor();\n }\n\n /**\n * Whether the loop should be scheduled right now: animation was\n * requested, and either the canvas is effectively on-screen or the\n * consumer opted out of pausing via `animateWhenHidden`.\n */\n private shouldAnimate(): boolean {\n return this._animationRequested\n && (this.getAnimateWhenHidden() || this.isEffectivelyVisible());\n }\n\n /**\n * Brings the raw rAF loop into agreement with `shouldAnimate()`.\n * Idempotent — safe to call every `doLayout` and from the option setter.\n * A no-op during the construction cascade because `_animationRequested`\n * is still false.\n */\n private reconcileAnimation(): void {\n if (this.shouldAnimate()) {\n if (this._rafId === null) {\n this._rafId = DOM.sink.requestAnimationFrame(this.animationStep);\n }\n } else if (this._rafId != null) {\n // Loose comparison: during the super() cascade (a construction-time\n // animateWhenHidden option), this class's own field initializers\n // haven't run yet, so _rafId briefly reads as `undefined` rather than\n // its declared `null` default — treat both as \"nothing scheduled\".\n DOM.sink.cancelAnimationFrame(this._rafId);\n this._rafId = null;\n }\n }\n\n /**\n * One animation-frame step: self-pauses when animation is no longer\n * requested, otherwise redraws and reschedules. Effective-visibility\n * pausing is handled by {@link onEffectiveVisibilityChange} reconciling the\n * loop on the change event, not by this step re-checking visibility every\n * frame. Arrow field so the rAF callback keeps a stable bound ref.\n */\n private readonly animationStep = (timestamp: number): void => {\n if (!this._animationRequested) {\n this._rafId = null;\n return;\n }\n\n if (this._animationStartMs === null) {\n this._animationStartMs = timestamp;\n }\n\n // A skipped frame still reschedules: the cap thins out redraws, it does\n // not stop the loop, so raising maxFps again takes effect immediately.\n const maxFps = this.getMaxFps();\n const minIntervalMs = maxFps > 0 ? 1000 / maxFps : 0;\n const dueForRedraw = this._lastDrawMs === null\n || timestamp - this._lastDrawMs >= minIntervalMs;\n\n if (dueForRedraw) {\n this._lastDrawMs = timestamp;\n this._elapsedMs = timestamp - this._animationStartMs;\n this.redraw();\n }\n\n this._rafId = DOM.sink.requestAnimationFrame(this.animationStep);\n };\n\n /**\n * Reacts to an effective-visibility change by reconciling the animation\n * loop — the replacement for the old per-frame `isEffectivelyVisible()`\n * poll inside `doLayout`.\n *\n * @param effective - The component's new effective-visibility state.\n */\n protected onEffectiveVisibilityChange(effective: boolean): void {\n super.onEffectiveVisibilityChange(effective);\n this.reconcileAnimation();\n }\n\n /**\n * Arms a one-shot `matchMedia` watch for the current device-pixel ratio. A\n * window dragged to a different-DPI monitor changes the ratio without a\n * resize/relayout, which `doLayout` would miss; on change this re-syncs the\n * backing store and re-arms for the new ratio. A generation token keeps only\n * the newest arm active, so re-arming cannot fan out — and the seam has no\n * unsubscribe, so each change leaves one inert native listener behind\n * (bounded by the number of DPR changes in a session).\n */\n private watchDevicePixelRatio(): void {\n const token = ++this._dprToken;\n const dpr = DOM.source.getDevicePixelRatio();\n\n DOM.source\n .matchMedia(`(resolution: ${dpr}dppx)`)\n .addChangeListener(() => this.onDevicePixelRatioChange(token));\n }\n\n /**\n * Handles a device-pixel-ratio change from the newest armed watch: re-syncs\n * the backing store at the fresh ratio and re-arms. Superseded arms (a stale\n * token) and post-teardown fires (no element) are inert.\n *\n * @param token - The generation token this listener was armed with.\n */\n private onDevicePixelRatioChange(token: number): void {\n if (token !== this._dprToken) {\n return;\n }\n\n if (!this.getElement()) {\n return;\n }\n\n this.syncBackingStore();\n this.watchDevicePixelRatio();\n }\n}\n\nconst CanvasCallable = callable(Canvas);\ntype CanvasCallable = Canvas;\nexport {\n Canvas as _Canvas,\n CanvasCallable as Canvas\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport { Event } from \"~/core/Event.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * GL-resource (re)build hook: receives the live WebGL2 context. Called once on\n * first context acquisition and again after every context restore — build (or\n * rebuild) shaders, programs, buffers, VAOs, and textures here.\n *\n * @param gl - The live `WebGL2RenderingContext`.\n *\n * @category Components\n */\nexport type WebGLContextInitCallback = (gl: WebGL2RenderingContext) => void;\n\n/**\n * Per-frame draw hook: receives the live WebGL2 context and the logical\n * (CSS-px) size. Issue draw calls here; the component has already set the\n * drawing-buffer viewport in device pixels.\n *\n * @param gl - The live `WebGL2RenderingContext`.\n * @param width - The drawing surface width in CSS pixels.\n * @param height - The drawing surface height in CSS pixels.\n *\n * @category Components\n */\nexport type WebGLFrameCallback = (\n gl: WebGL2RenderingContext,\n width: number,\n height: number,\n elapsedMs: number,\n) => void;\n\n/**\n * Construction-time options for {@link WebGLCanvas}.\n *\n * @category Components\n */\nexport interface WebGLCanvasOptions extends ComponentOptions {\n\n /** GL-resource (re)build hook; runs on init and after each context restore. */\n onContextInit?: WebGLContextInitCallback;\n\n /**\n * Per-frame draw hook. Its fourth argument is the milliseconds elapsed\n * since the current animation run started — derive motion from that rather\n * than advancing a counter once per call, since frames arrive at the\n * display's refresh rate and a per-call increment runs three times faster\n * on a 180Hz monitor than on a 60Hz one.\n */\n onFrame?: WebGLFrameCallback;\n\n /**\n * Keeps the animation loop running while the canvas is not effectively\n * on-screen (e.g. on an inactive `Tab` panel). Default `false`: the loop\n * pauses automatically when hidden and resumes when shown again.\n */\n animateWhenHidden?: boolean;\n\n /**\n * Upper bound, in frames per second, on how often the animation loop\n * renders. Defaults to 30 — enough for smooth motion at a predictable cost,\n * and independent of the display's refresh rate, so the same animation\n * costs the same on a 60Hz and a 180Hz monitor. Pass `0` to opt out and\n * render on every animation frame the browser delivers. A positive value\n * skips frames that arrive sooner than `1000 / maxFps` after the last\n * one; the loop itself keeps running, so the cap trades smoothness for\n * CPU rather than pausing anything.\n */\n maxFps?: number;\n}\n\n/**\n * Class-level defaults forwarded to `super` so the cascade hits Component's\n * applyOptions with `{ tag: \"canvas\" }` already merged into `_defaultOptions`.\n */\nconst _defaultWebGLCanvasOptions: Partial<WebGLCanvasOptions> = {\n tag: \"canvas\",\n maxFps: 30,\n};\n\n// Sentinel for the last-synced width/height/dpr guard. A real size is always\n// >= 0 and a real dpr always >= 1, so -1 can never equal a genuine value and\n// therefore forces the first `syncBackingStore` to run rather than short-circuit.\nconst NOT_YET_SYNCED = -1;\n\n/**\n * A GPU drawing surface backed by a `<canvas>` element and a live\n * `WebGL2RenderingContext`.\n *\n * The component owns the canvas element, the GL context, the animation loop,\n * and context-loss recovery; the consumer owns shaders, buffers, and draw calls\n * through two hooks. {@link WebGLCanvasOptions.onContextInit | onContextInit}\n * (re)builds GPU resources — it runs once on first acquisition and again after\n * every context restore. {@link WebGLCanvasOptions.onFrame | onFrame} draws each\n * frame; the drawing-buffer viewport is already set in device pixels, and the\n * hook receives the logical (CSS-px) size for projection math.\n *\n * Backing-store sizing mirrors the 2D sibling: the element's `width` / `height`\n * attributes are kept at CSS × device-pixel-ratio for a crisp HiDPI result, and\n * every resize re-emits `gl.viewport(0, 0, backingW, backingH)` in device pixels\n * (reassigning the attributes resizes the drawing buffer but leaves GL resources\n * intact). The render loop starts automatically on the first connected layout,\n * pauses automatically while the surface is not effectively on-screen (e.g. on\n * an inactive `Tab` panel) — resuming once it's shown again — and stops on\n * teardown; call {@link WebGLCanvas.startAnimation | startAnimation} /\n * {@link WebGLCanvas.stopAnimation | stopAnimation} to drive a static or on-demand\n * surface explicitly.\n *\n * `WebGLCanvas` is **live-only**: a GL context cannot be modelled offline or\n * forwarded across a worker, so under a modelled sink `getContext` returns `null`\n * and every render path no-ops. It is WebGL2 only.\n *\n * @category Components\n */\nclass WebGLCanvas extends Component<WebGLCanvasOptions> {\n\n /** Cached WebGL2 context; `null` offline or before the element renders. */\n private _gl: WebGL2RenderingContext | null = null;\n\n /** Active animation-frame handle; `null` when the loop is idle. */\n private _rafId: number | null = null;\n\n // Frame clock for the animation loop. `_animationStartMs` is null until the\n // first frame of a run anchors it, so elapsed time starts at 0 for that\n // frame however long the run waited to be scheduled. `_lastDrawMs` gates\n // the maxFps cap and `_elapsedMs` is what the frame hook is handed, kept as\n // a field so renders from outside the loop repeat the last frame's value.\n private _animationStartMs: number | null = null;\n private _lastDrawMs : number | null = null;\n private _elapsedMs = 0;\n\n /** Consumer/auto intent to animate; the loop actually runs only while also\n * effectively visible (or animateWhenHidden is set). Plain initializer:\n * never written during the super() cascade (only startAnimation /\n * stopAnimation, which run post-render, write it), so it needs no\n * `declare`. */\n private _animationRequested = false;\n\n /** True between `webglcontextlost` and `webglcontextrestored`; frames skip. */\n private _contextLost: boolean = false;\n\n /**\n * False until `onContextInit` has run for the current context; reset on\n * restore (and by `setOnContextInit`) so the next frame re-runs the hook.\n * Written by `setOnContextInit`, which `applyOptions` can dispatch during the\n * `super()` cascade, so it is `declare`d and assigned in the constructor body.\n */\n declare private _contextInitialised: boolean;\n\n /** Last-synced CSS width; guards against a redundant buffer resize. */\n private _syncedWidth: number = NOT_YET_SYNCED;\n\n /** Last-synced CSS height; guards against a redundant buffer resize. */\n private _syncedHeight: number = NOT_YET_SYNCED;\n\n /** Last-synced device-pixel ratio; guards against a redundant buffer resize. */\n private _syncedDpr: number = NOT_YET_SYNCED;\n\n /** Generation counter so only the newest DPR-watch arm acts on a change. */\n private _dprToken: number = 0;\n\n /**\n * Constructs a WebGL2 canvas.\n *\n * @param options - Optional construction options.\n * @param subclassDefaults - Per-subclass default bag layered over this\n * class's defaults; subclasses forward their `_defaultXxxOptions`\n * constant here.\n */\n constructor(options?: WebGLCanvasOptions, subclassDefaults?: Partial<WebGLCanvasOptions>) {\n super(options, { ..._defaultWebGLCanvasOptions, ...(subclassDefaults ?? {}) });\n\n this._contextInitialised = false;\n\n this.clearInsets();\n }\n\n /**\n * Forwards the consumer-configurable hooks to their setters.\n *\n * @param options - The construction options.\n * @returns This component, for method chaining.\n */\n protected applyOptions(options: WebGLCanvasOptions): this {\n super.applyOptions(options);\n\n if (options.onContextInit !== undefined) {\n this.setOnContextInit(options.onContextInit);\n }\n\n if (options.onFrame !== undefined) {\n this.setOnFrame(options.onFrame);\n }\n\n if (options.animateWhenHidden !== undefined) {\n this.setAnimateWhenHidden(options.animateWhenHidden);\n }\n\n if (options.maxFps !== undefined) {\n this.setMaxFps(options.maxFps);\n }\n\n return this;\n }\n\n /**\n * Returns the WebGL2 rendering context, lazily obtaining it from the seam on\n * first access once the element exists and narrowing the generic seam result\n * to `WebGL2RenderingContext`.\n *\n * @returns The context, or `null` offline / before the element renders.\n */\n getContext(): WebGL2RenderingContext | null {\n if (this._gl) {\n return this._gl;\n }\n\n const element = this.getElement();\n if (!element) {\n return null;\n }\n\n this._gl = DOM.sink.getContext(element, \"webgl2\") as WebGL2RenderingContext | null;\n\n return this._gl;\n }\n\n /**\n * Sets (or clears) the GL-resource build hook. Marks the context\n * uninitialised so the new hook runs on the next frame.\n *\n * @param handler - The context-init callback, or `null` to clear it.\n * @returns This component, for method chaining.\n */\n setOnContextInit(handler: WebGLContextInitCallback | null): this {\n this._options.onContextInit = handler ?? undefined;\n this._contextInitialised = false;\n\n return this;\n }\n\n /**\n * Returns the current GL-resource build hook.\n *\n * @returns The context-init callback, or `null` when none is set.\n */\n getOnContextInit(): WebGLContextInitCallback | null {\n return this._options.onContextInit ?? this._defaultOptions.onContextInit ?? null;\n }\n\n /**\n * Sets (or clears) the per-frame draw hook.\n *\n * @param handler - The frame callback, or `null` to clear it.\n * @returns This component, for method chaining.\n */\n setOnFrame(handler: WebGLFrameCallback | null): this {\n this._options.onFrame = handler ?? undefined;\n\n return this;\n }\n\n /**\n * Returns the current per-frame draw hook.\n *\n * @returns The frame callback, or `null` when none is set.\n */\n getOnFrame(): WebGLFrameCallback | null {\n return this._options.onFrame ?? this._defaultOptions.onFrame ?? null;\n }\n\n /**\n * Starts the per-frame render loop. Idempotent: a second call while already\n * animating does not schedule a second frame. Records the intent to\n * animate; the loop only actually runs while the canvas is also\n * effectively on-screen (or {@link setAnimateWhenHidden} opts out).\n *\n * @returns This component, for method chaining.\n */\n startAnimation(): this {\n this._animationRequested = true;\n this._animationStartMs = null;\n this._lastDrawMs = null;\n this._elapsedMs = 0;\n this.reconcileAnimation();\n\n return this;\n }\n\n /**\n * Stops the per-frame render loop, cancelling any pending frame. Clears\n * the intent to animate, so a later resize/show cannot resurrect it.\n *\n * @returns This component, for method chaining.\n */\n stopAnimation(): this {\n this._animationRequested = false;\n this.reconcileAnimation();\n\n return this;\n }\n\n /**\n * Whether the per-frame render loop is currently running. `false` while\n * paused for being effectively hidden, even if animation was requested.\n *\n * @returns `true` while animating.\n */\n isAnimating(): boolean {\n return this._rafId !== null;\n }\n\n /**\n * Keeps the animation loop running while the canvas is not effectively\n * on-screen. Reconciles immediately, so toggling it can start or pause\n * the loop right away.\n *\n * @param value - `true` to animate regardless of visibility.\n * @returns This component, for method chaining.\n */\n setAnimateWhenHidden(value: boolean): this {\n this._options.animateWhenHidden = value;\n this.reconcileAnimation();\n\n return this;\n }\n\n /**\n * Returns whether the animation loop keeps running while hidden.\n *\n * @returns `true` if the loop ignores effective visibility.\n */\n getAnimateWhenHidden(): boolean {\n return this._options.animateWhenHidden ?? this._defaultOptions.animateWhenHidden ?? false;\n }\n\n /**\n * Caps how often the animation loop renders, in frames per second. Takes\n * effect on the next frame; the loop keeps running either way, so this\n * thins renders rather than pausing anything. Because frames are delivered\n * at the display's refresh rate, an uncapped loop costs proportionally more\n * on a high-refresh monitor — a cap makes that cost predictable.\n *\n * @param fps - Maximum renders per second, or `0` to remove the cap entirely.\n * Negative values are treated as `0`. The class default is 30.\n * @returns This component, for method chaining.\n */\n setMaxFps(fps: number): this {\n // On the options bag rather than a private field, matching\n // `animateWhenHidden`: `applyOptions` runs inside the `super()`\n // cascade, before this class's field initializers, so a plain\n // `_maxFps = 0` initializer would overwrite a construction-time value.\n this._options.maxFps = Math.max(0, fps);\n\n return this;\n }\n\n /**\n * Returns the current render cap in frames per second.\n *\n * @returns The cap, or `0` when the loop is uncapped. Resolves the class\n * default (30) when no explicit value was set.\n */\n getMaxFps(): number {\n // Consults `_defaultOptions` as well as `_options`, matching the\n // framework's getter convention (`getZIndex` is\n // `_options.zIndex ?? _defaultOptions.zIndex ?? 0`). A class-level\n // default bag lands in `_defaultOptions`, never in `_options`, so\n // reading only the latter would silently ignore a default-supplied cap\n // and leave the loop uncapped from the first frame.\n return this._options.maxFps ?? this._defaultOptions.maxFps ?? 0;\n }\n\n /**\n * Resizes the backing store to CSS × dpr, sets `gl.viewport` in device\n * pixels (WebGL's replacement for the 2D transform), and re-emits one frame.\n * Called from `doLayout` on every size change. Reads only cached CSS sizes —\n * never DOM geometry, which inside `doLayout` is still buffered — and\n * short-circuits when width/height/dpr are unchanged so idle layout passes\n * never resize the drawing buffer. Reassigning the attributes resizes the\n * buffer but leaves GL resources intact, so only the viewport is refreshed.\n */\n protected syncBackingStore(): void {\n const gl = this.getContext();\n if (!gl) {\n return;\n }\n\n const width = this.getWidth();\n const height = this.getHeight();\n const dpr = DOM.source.getDevicePixelRatio();\n\n if (width === this._syncedWidth && height === this._syncedHeight && dpr === this._syncedDpr) {\n return;\n }\n\n const backingW = Math.round(width * dpr);\n const backingH = Math.round(height * dpr);\n\n DOM.sink.apply(this.getElement()!, { setAttr: {\n width: String(backingW),\n height: String(backingH),\n }});\n\n // Device pixels, not CSS px — the drawing buffer is CSS × dpr and the\n // viewport must cover it fully for a crisp, unstretched result.\n gl.viewport(0, 0, backingW, backingH);\n\n this._syncedWidth = width;\n this._syncedHeight = height;\n this._syncedDpr = dpr;\n\n this.renderFrame();\n }\n\n /**\n * Delegates to the base layout then syncs the backing store — the single\n * settled hook that fires once after both axes are committed, on initial\n * mount and every subsequent resize.\n *\n * @returns This component, for method chaining.\n */\n doLayout(): this {\n super.doLayout();\n this.syncBackingStore();\n\n return this;\n }\n\n /**\n * Renders the element, wires context loss / restore, arms the DPR-change\n * watcher, and starts the render loop on the first connected layout.\n *\n * @returns The created element handle.\n */\n protected render(): Handle {\n const element = super.render();\n\n Event.addListener(this, \"webglcontextlost\", (e: WebGLContextEvent) => {\n // REQUIRED — without it the browser never fires `webglcontextrestored`.\n e.preventDefault();\n this._contextLost = true;\n });\n Event.addListener(this, \"webglcontextrestored\", () => {\n this._contextLost = false;\n this._contextInitialised = false;\n this.syncBackingStore();\n });\n\n this.watchDevicePixelRatio();\n this.onFirstLayout(() => this.startAnimation());\n\n return element;\n }\n\n /**\n * Stops the render loop before the inherited destructor detaches the\n * element, so no stray frame survives teardown.\n */\n protected destructor(): void {\n this.stopAnimation();\n super.destructor();\n }\n\n /**\n * Whether the loop should be scheduled right now: animation was\n * requested, and either the canvas is effectively on-screen or the\n * consumer opted out of pausing via `animateWhenHidden`.\n */\n private shouldAnimate(): boolean {\n return this._animationRequested\n && (this.getAnimateWhenHidden() || this.isEffectivelyVisible());\n }\n\n /**\n * Brings the raw rAF loop into agreement with `shouldAnimate()`.\n * Idempotent — safe to call every `doLayout` and from the option setter.\n * A no-op during the construction cascade because `_animationRequested`\n * is still false.\n */\n private reconcileAnimation(): void {\n if (this.shouldAnimate()) {\n if (this._rafId === null) {\n this._rafId = DOM.sink.requestAnimationFrame(this.animationStep);\n }\n } else if (this._rafId != null) {\n // Loose comparison: during the super() cascade (a construction-time\n // animateWhenHidden option), this class's own field initializers\n // haven't run yet, so _rafId briefly reads as `undefined` rather than\n // its declared `null` default — treat both as \"nothing scheduled\".\n DOM.sink.cancelAnimationFrame(this._rafId);\n this._rafId = null;\n }\n }\n\n /**\n * One animation-frame step: self-pauses when animation is no longer\n * requested, otherwise renders a frame and reschedules. Effective-visibility\n * pausing is handled by {@link onEffectiveVisibilityChange} reconciling the\n * loop on the change event, not by this step re-checking visibility every\n * frame. Arrow field so the rAF callback keeps a stable bound ref.\n */\n private readonly animationStep = (timestamp: number): void => {\n if (!this._animationRequested) {\n this._rafId = null;\n return;\n }\n\n if (this._animationStartMs === null) {\n this._animationStartMs = timestamp;\n }\n\n // A skipped frame still reschedules: the cap thins out renders, it does\n // not stop the loop, so raising maxFps again takes effect immediately.\n const maxFps = this.getMaxFps();\n const minIntervalMs = maxFps > 0 ? 1000 / maxFps : 0;\n const dueForRender = this._lastDrawMs === null\n || timestamp - this._lastDrawMs >= minIntervalMs;\n\n if (dueForRender) {\n this._lastDrawMs = timestamp;\n this._elapsedMs = timestamp - this._animationStartMs;\n this.renderFrame();\n }\n\n this._rafId = DOM.sink.requestAnimationFrame(this.animationStep);\n };\n\n /**\n * Reacts to an effective-visibility change by reconciling the animation\n * loop — the replacement for the old per-frame `isEffectivelyVisible()`\n * poll inside `doLayout`.\n *\n * @param effective - The component's new effective-visibility state.\n */\n protected onEffectiveVisibilityChange(effective: boolean): void {\n super.onEffectiveVisibilityChange(effective);\n this.reconcileAnimation();\n }\n\n /**\n * One frame: lazily runs `onContextInit` the first time (and after a\n * restore), then invokes `onFrame` with the logical CSS-px size. Skips while\n * the context is lost or unavailable.\n */\n private renderFrame(): void {\n const gl = this.getContext();\n if (!gl || this._contextLost) {\n return;\n }\n\n if (!this._contextInitialised) {\n this.getOnContextInit()?.(gl);\n this._contextInitialised = true;\n }\n\n this.getOnFrame()?.(gl, this.getWidth(), this.getHeight(), this._elapsedMs);\n }\n\n /**\n * Arms a one-shot `matchMedia` watch for the current device-pixel ratio. A\n * window dragged to a different-DPI monitor changes the ratio without a\n * resize/relayout, which `doLayout` would miss; on change this re-syncs the\n * backing store and re-arms for the new ratio. A generation token keeps only\n * the newest arm active, so re-arming cannot fan out — and the seam has no\n * unsubscribe, so each change leaves one inert native listener behind\n * (bounded by the number of DPR changes in a session).\n */\n private watchDevicePixelRatio(): void {\n const token = ++this._dprToken;\n const dpr = DOM.source.getDevicePixelRatio();\n\n DOM.source\n .matchMedia(`(resolution: ${dpr}dppx)`)\n .addChangeListener(() => this.onDevicePixelRatioChange(token));\n }\n\n /**\n * Handles a device-pixel-ratio change from the newest armed watch: re-syncs\n * the backing store at the fresh ratio and re-arms. Superseded arms (a stale\n * token) and post-teardown fires (no element) are inert.\n *\n * @param token - The generation token this listener was armed with.\n */\n private onDevicePixelRatioChange(token: number): void {\n if (token !== this._dprToken) {\n return;\n }\n\n if (!this.getElement()) {\n return;\n }\n\n this.syncBackingStore();\n this.watchDevicePixelRatio();\n }\n}\n\nconst WebGLCanvasCallable = callable(WebGLCanvas);\ntype WebGLCanvasCallable = WebGLCanvas;\nexport {\n WebGLCanvas as _WebGLCanvas,\n WebGLCanvasCallable as WebGLCanvas\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport { Size } from \"~/primitive/Size.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * Construction-time options for {@link Image}.\n *\n * @remarks Supplying `preferredSize` locks the rendered size and disables the\n * default natural-dimension auto-fit behaviour.\n *\n * @category Components\n */\nexport interface ImageOptions extends ComponentOptions {\n}\n\n// Upper bound for the auto-derived `minSize` per axis. Small images report\n// their intrinsic size (so a 16×16 favicon stays sharp at full natural size);\n// larger images cap here so their parent layout can always shrink them down.\nconst IMAGE_AUTO_MIN_CAP_PX = 100;\n\n/**\n * Class-level defaults forwarded to `super` so the cascade hits Component's\n * applyOptions with `{ tag: \"img\" }` already merged into `_defaultOptions`.\n */\nconst _defaultImageOptions: Partial<ImageOptions> = {\n tag: \"img\",\n};\n\n/**\n * An image component backed by an `<img>` element.\n *\n * Reports its preferred size from the image's natural intrinsic dimensions once loaded.\n *\n * @category Components\n */\nclass Image extends Component<ImageOptions> {\n\n private _src: String;\n\n /**\n * @param src - Image source URL.\n * @param options - Optional construction options.\n * @param subclassDefaults - Per-subclass default bag layered over this\n * class's defaults; subclasses forward their `_defaultXxxOptions`\n * constant here.\n */\n constructor(src: String, options?: ImageOptions, subclassDefaults?: Partial<ImageOptions>) {\n super(options, { ..._defaultImageOptions, ...(subclassDefaults ?? {}) });\n\n this._src = src;\n this.clearInsets();\n }\n\n /**\n * Returns the component's element handle.\n *\n * @param createIfMissing - Optional. When true, renders the element if it does not yet exist.\n *\n * @returns The component's element handle.\n */\n getElement(createIfMissing: boolean = false): Handle | undefined {\n return super.getElement(createIfMissing);\n }\n\n /**\n * Returns the natural intrinsic size of the image from the DOM element.\n *\n * @returns A Size with the image's naturalWidth and naturalHeight.\n */\n getPreferredSize(): Size | null {\n let element = this.getElement();\n\n const natural = DOM.source.getNaturalSize(element!);\n return {\n width: natural.width,\n height: natural.height\n };\n }\n\n /**\n * Returns a minimum size derived from the image's intrinsic dimensions\n * (mirrors the `Math.min(natural, 100)` cap that `Text` applies), so\n * small images keep their full size while large images stay shrinkable\n * by their parent layout. An explicit `setMinSize` from the caller\n * wins via `Component.getMinSize`'s `_options.minSize` priority.\n *\n * @returns The minimum `{width, height}` from intrinsic dims, or a\n * `20×20` pre-load fallback before the image has decoded.\n */\n getMinSize(): Size | null {\n const explicit = super.getMinSize();\n if (this._options.minSize) {\n return explicit;\n }\n\n const element = this.getElement();\n if (!element || !DOM.source.getNaturalSize(element).width) {\n return { width: 20, height: 20 };\n }\n\n const natural = DOM.source.getNaturalSize(element);\n return {\n width: Math.min(natural.width, IMAGE_AUTO_MIN_CAP_PX),\n height: Math.min(natural.height, IMAGE_AUTO_MIN_CAP_PX),\n };\n }\n\n /**\n * Renders the img element and sets its src attribute.\n *\n * @returns The created HTMLImageElement with its src initialised.\n */\n render(): Handle {\n let element = super.render();\n\n DOM.sink.apply(element, { setAttr: { src: this._src.valueOf() } });\n\n return element;\n }\n}\n\nconst ImageCallable = callable(Image);\ntype ImageCallable = Image;\nexport {\n Image as _Image,\n ImageCallable as Image\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport { Event } from \"~/core/Event.js\";\nimport { StyleRule } from \"~/core/StyleTarget.js\";\nimport { ThemeManager } from \"~/core/Theme.js\";\nimport { callable } from \"~/core/Callable.js\";\nimport { Size } from \"~/primitive/Size.js\";\nimport { lexer } from \"marked\";\nimport type { Token, Tokens } from \"marked\";\n// Type-only: erased at compile time. `CodeEditor` itself is loaded through a\n// narrow dynamic import (see `loadCodeEditorUpgrade`) so a static top-level\n// value import here would force every `Markdown` consumer's bundler to\n// resolve CodeMirror the moment it imports `Markdown` at all.\nimport type { CodeEditor } from \"~/component/editor/CodeEditor.js\";\n\n/**\n * Shared class names for the prose elements built by {@link Markdown}. Kept as\n * constants so the render helpers and the module-level rule builder reference\n * the same strings.\n */\nconst CODE_CLASS = \"ts-ui-md-code\";\nconst PRE_CLASS = \"ts-ui-md-pre\";\nconst QUOTE_CLASS = \"ts-ui-md-quote\";\nconst LIST_CLASS = \"ts-ui-md-list\";\nconst LINK_CLASS = \"ts-ui-md-link\";\nconst HEADING_CLASS = \"ts-ui-md-heading\";\nconst TABLE_WRAP_CLASS = \"ts-ui-md-table-wrap\";\nconst TABLE_CLASS = \"ts-ui-md-table\";\nconst TH_CLASS = \"ts-ui-md-th\";\nconst TD_CLASS = \"ts-ui-md-td\";\nconst ALIGN_LEFT_CLASS = \"ts-ui-md-align-left\";\nconst ALIGN_CENTER_CLASS = \"ts-ui-md-align-center\";\nconst ALIGN_RIGHT_CLASS = \"ts-ui-md-align-right\";\n/**\n * Wraps a fenced block that upgrades to a live `CodeEditor`. `position:\n * relative` gives the absolutely-positioned `CodeEditor` child a local\n * positioning context wherever the block sits in the token tree (top-level,\n * inside a blockquote, inside a list item) — without it, the editor would\n * resolve `top`/`left` against `Markdown`'s own root instead.\n */\nconst CODE_HOST_CLASS = \"ts-ui-md-code-host\";\n\n// Markdown heading levels span h1..h6; a deeper `#######` run is not a heading\n// in CommonMark, but clamp anyway so a stray `depth` never mints an invalid tag.\nconst HEADING_MIN_DEPTH = 1;\nconst HEADING_MAX_DEPTH = 6;\n\n/**\n * Maps a fenced code block's info-string language (lowercased, first word) to\n * the [`CodeEditor`](/components/CodeEditor) registry id it upgrades to. Only\n * the five ids `languages.ts` registers are\n * reachable; every other alias, and any language with no registered editor\n * support, is intentionally absent — {@link mapFenceLangToEditorId} returns\n * `null` for those rather than guessing, since passing an unregistered id to\n * `CodeEditor` would silently render unhighlighted text.\n */\nconst FENCE_LANG_ALIASES: Record<string, string> = {\n js: \"javascript\", javascript: \"javascript\", jsx: \"javascript\", mjs: \"javascript\", cjs: \"javascript\",\n ts: \"javascript\", typescript: \"javascript\", tsx: \"javascript\",\n json: \"json\",\n html: \"html\", htm: \"html\",\n sql: \"sql\",\n md: \"markdown\", markdown: \"markdown\",\n};\n\n/**\n * Cap on how many rows a fenced block's upgraded CodeEditor grows to before\n * its own vertical scrollbar takes over, rather than the wrapper continuing\n * to grow — keeps one long fenced block from pushing the rest of the prose\n * far down the page. Not a theme token: this is the only call site that\n * needs it, and BaseTheme.ts has no existing \"row count\" token shape to\n * extend (see Architecture Decisions).\n */\nconst CODE_BLOCK_MAX_AUTO_ROWS = 20;\n\n/**\n * Threshold, in pixels, for warning when a fenced block's `CodeEditor`\n * corrects its mount-time guessed height (measured from the plain-text\n * placeholder `<pre>`) to CodeMirror's real, measured content height. Two\n * live reproductions of a \"spasms and never settles\" report measured this\n * correction at 20px and 29px on an ordinary page — this sits above that\n * noise floor's sub-pixel/fractional rounding (CodeMirror's own line\n * metrics can report fractional heights like 19.5938px) while still\n * catching either measured case, so a future report of the same shape\n * comes with hard numbers instead of a cold trail.\n */\nconst GUESS_HEIGHT_CORRECTION_WARN_PX = 8;\n\n/**\n * How many viewport-heights below the fold a fenced block's `CodeEditor`\n * upgrade starts before scrolling actually reaches it (see\n * {@link Markdown.isBlockNearViewport}) — applied below the fold only, never\n * above. One viewport-height is roughly 1.5 seconds of runway at a typical\n * 900px pane and 60fps scroll speed: enough that a reader scrolling down\n * never catches an unhighlighted block, while costing at most a screenful of\n * extra upgrades over the strict minimum. A fixed pixel margin was rejected\n * because it would make a tall monitor prefetch proportionally less.\n */\nconst CODE_UPGRADE_LOOKAHEAD_VIEWPORTS = 1;\n\n/**\n * Resolves a fenced code block's info-string language to the `CodeEditor`\n * registry id it should upgrade to, per {@link FENCE_LANG_ALIASES}.\n *\n * @param lang - The fence's info string as reported by `marked` (e.g. `\"js\"`,\n * `\"ts {1,3}\"`), or `undefined` when the fence carries none.\n * @returns The mapped `CodeEditor` language id, or `null` when `lang` is\n * unset or names a language with no registered editor support — the caller\n * treats both identically (render the plain `<pre>`).\n */\nfunction mapFenceLangToEditorId(lang: string | undefined): string | null {\n if (!lang) {\n return null;\n }\n\n // Only the first whitespace-delimited word is the language token per\n // CommonMark; a shebang-style modifier after it (`js {1,3}`) must not\n // defeat the match.\n const word = lang.trim().split(/\\s+/, 1)[0]?.toLowerCase();\n\n return word ? (FENCE_LANG_ALIASES[word] ?? null) : null;\n}\n\nlet _classRulesEnsured = false;\n\n/**\n * Injects the shared prose class rules on first use. Idempotent — guarded by the\n * module-level `_classRulesEnsured` flag, mirroring `Glyph`'s keyframe singleton.\n *\n * @remarks Presentation references the framework theme tokens actually used by\n * the rules — `--ts-ui-font-mono` (code/pre font), `--ts-ui-border-radius`\n * (code/pre corners), `--ts-ui-border-color` (the blockquote bar), and the\n * accent `--ts-ui-indicator-focus` (link colour) — each with a fallback that\n * works in both light and dark themes; the code/pre background is a theme-neutral\n * translucent grey wash rather than a token, since no surface token exists. Every\n * spacing constant is genuine structural spacing (a code padding, a list marker\n * gutter, a blockquote bar), expressed in `em` so it scales with the surrounding\n * font — not a cosmetic inset.\n */\nfunction ensureMarkdownClassRules(): void {\n if (_classRulesEnsured) {\n return;\n }\n\n _classRulesEnsured = true;\n\n new StyleRule({\n scope: \"class\",\n name: CODE_CLASS,\n styles: {\n fontFamily: \"var(--ts-ui-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)\",\n // Translucent grey wash reads as \"code\" on both light and dark\n // backgrounds without needing a dedicated surface token.\n background: \"rgba(127, 127, 127, 0.16)\",\n borderRadius: \"var(--ts-ui-border-radius, 3px)\",\n // Snug em-relative padding so the wash hugs inline code glyphs.\n padding: \"0.1em 0.3em\",\n // Reset the prose line-height the root sets for reading — code reads\n // as fixed-width text, not continuous prose.\n lineHeight: \"normal\",\n },\n });\n\n new StyleRule({\n scope: \"class\",\n name: PRE_CLASS,\n styles: {\n fontFamily: \"var(--ts-ui-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)\",\n background: \"rgba(127, 127, 127, 0.16)\",\n borderRadius: \"var(--ts-ui-border-radius, 3px)\",\n // Block padding gives the fenced code room; it is the code frame's\n // structural inset, not a visual nudge.\n padding: \"0.6em 0.8em\",\n // Preserve authored whitespace/newlines and scroll long lines rather\n // than reflow them.\n whiteSpace: \"pre\",\n overflow: \"auto\",\n // Reset the prose line-height the root sets for reading — fenced\n // code reads as fixed-width text, not continuous prose.\n lineHeight: \"normal\",\n },\n });\n\n new StyleRule({\n scope: \"class\",\n name: QUOTE_CLASS,\n styles: {\n // 3px quote bar — the framework's thin-border weight — plus an\n // em-relative gutter that indents the quoted prose off the bar.\n borderLeft: \"3px solid var(--ts-ui-border-color, rgba(127, 127, 127, 0.4))\",\n paddingLeft: \"1em\",\n marginLeft: \"0\",\n },\n });\n\n new StyleRule({\n scope: \"class\",\n name: LIST_CLASS,\n // Room for the list marker; structural indentation, em-relative so it\n // tracks the font size.\n styles: { paddingLeft: \"1.5em\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: LINK_CLASS,\n // The framework's single accent hue (shared with focus/selection).\n styles: { color: \"var(--ts-ui-indicator-focus, #2563eb)\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: HEADING_CLASS,\n // Semibold so headings read as headings independent of any UA-style\n // reset the host page may apply.\n styles: { fontWeight: \"600\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: TABLE_WRAP_CLASS,\n // A table's columns cannot reflow below their content width, so the\n // wrapper scrolls horizontally instead of letting the table spill\n // past the component's assigned width — the same story fenced code\n // already tells with its own frame.\n styles: { maxWidth: \"100%\", overflowX: \"auto\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: TABLE_CLASS,\n styles: { borderCollapse: \"collapse\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: TH_CLASS,\n styles: {\n border: \"1px solid var(--ts-ui-border-color, rgba(127, 127, 127, 0.4))\",\n // Structural cell padding, em-relative so it tracks the font size.\n padding: \"0.3em 0.6em\",\n fontWeight: \"600\",\n // Overrides the browser's centred <th> default so an unaligned\n // header cell reads left, matching its unaligned body cells.\n textAlign: \"left\",\n },\n });\n\n new StyleRule({\n scope: \"class\",\n name: TD_CLASS,\n styles: {\n border: \"1px solid var(--ts-ui-border-color, rgba(127, 127, 127, 0.4))\",\n padding: \"0.3em 0.6em\",\n },\n });\n\n new StyleRule({\n scope: \"class\",\n name: ALIGN_LEFT_CLASS,\n styles: { textAlign: \"left\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: ALIGN_CENTER_CLASS,\n styles: { textAlign: \"center\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: ALIGN_RIGHT_CLASS,\n styles: { textAlign: \"right\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: CODE_HOST_CLASS,\n styles: { position: \"relative\" },\n });\n}\n\n/**\n * Maps marked's per-column alignment to the class that applies it.\n *\n * @param align - The column's alignment, as reported per-cell by marked's\n * table token.\n * @returns The alignment class, or `null` when the column carries no\n * alignment marker.\n */\nfunction alignmentClass(align: \"center\" | \"left\" | \"right\" | null): string | null {\n switch (align) {\n case \"left\": return ALIGN_LEFT_CLASS;\n case \"center\": return ALIGN_CENTER_CLASS;\n case \"right\": return ALIGN_RIGHT_CLASS;\n default: return null;\n }\n}\n\n/**\n * GitHub/VitePress-compatible slug: lowercase, non-alphanumerics collapsed to\n * single hyphens, ends trimmed. Does not dedupe — the caller folds in a `-N`\n * suffix via a per-render counter so every id on the page is unique.\n *\n * @param text - The heading's plain text.\n * @returns The slug, with no leading, trailing, or doubled hyphens.\n */\nfunction slugify(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\n/**\n * Shared by {@link Markdown.appendHeading} and {@link extractMarkdownHeadings}\n * — the single place a heading's slug is deduped against ids already used\n * earlier in the same render/extraction pass, so both produce identical ids\n * from identical inputs.\n *\n * @param text - The heading's plain text.\n * @param headingIds - The current pass's dedupe counter, keyed by slug.\n * @returns The slug, suffixed with `-N` when it was already seen this pass.\n */\nfunction nextHeadingId(text: string, headingIds: Map<string, number>): string {\n const slug = slugify(text);\n const seen = headingIds.get(slug) ?? 0;\n\n headingIds.set(slug, seen + 1);\n\n return seen === 0 ? slug : `${slug}-${seen}`;\n}\n\n/**\n * How a link href should be rendered: the final href, and whether it leaves\n * the site the {@link Markdown} instance is embedded in.\n *\n * @category Components\n */\nexport interface MarkdownLinkResolution {\n href: string;\n external: boolean;\n}\n\n/**\n * Maps an authored Markdown href to its rendered form. See\n * {@link MarkdownOptions.linkResolver}.\n *\n * @category Components\n */\nexport type MarkdownLinkResolver = (href: string) => MarkdownLinkResolution;\n\n/**\n * The default {@link MarkdownLinkResolver}: every href is rendered exactly as\n * authored and marked external, reproducing the component's pre-`linkResolver`\n * behaviour (`target=\"_blank\" rel=\"noopener noreferrer\"` on every link).\n *\n * @param href - The authored link href.\n * @returns The href unchanged, marked external.\n */\nfunction defaultLinkResolver(href: string): MarkdownLinkResolution {\n return { href, external: true };\n}\n\n/**\n * Construction-time options for {@link Markdown}.\n *\n * @category Components\n */\nexport interface MarkdownOptions extends ComponentOptions {\n /** The Markdown source string to render. */\n markdown?: string;\n\n /**\n * Maps an authored link href to its rendered form. Defaults to a resolver\n * that renders every href unchanged and marks it external, preserving\n * today's behaviour (every link opens in a new tab). A consumer embedding\n * `Markdown` in an app with its own routing (e.g. a docs site) can rewrite\n * in-site hrefs and suppress the new tab for them, while leaving external\n * links external.\n */\n linkResolver?: MarkdownLinkResolver;\n\n /**\n * Per-instance override of the prose column's max width (e.g. `\"60ch\"`,\n * `60`). `null` or omitted uses the theme's `--ts-ui-md-max-measure`\n * default.\n */\n maxMeasure?: string | number | null;\n\n /**\n * Multiplies the prose's base font size; headings scale with it via their\n * own relative sizing. Default `1`.\n */\n fontScale?: number;\n}\n\n/**\n * The fixed identity of one fenced block's pending upgrade — the placeholder\n * handles it will replace, its literal text and mapped language, and the\n * render generation it belongs to (see {@link Markdown._renderGeneration}).\n * Shared shape between {@link QueuedCodeUpgrade} (waiting on visibility,\n * import not yet started) and {@link PendingCodeUpgrade} (import already\n * resolved, still waiting on visibility to apply).\n */\ninterface CodeUpgradeIdentity {\n wrapper: Handle;\n pre: Handle;\n code: Handle;\n text: string;\n languageId: string;\n generation: number;\n}\n\n/**\n * A supported-language fenced block whose `CodeEditor` dynamic import has not\n * started yet because `Markdown` was not effectively visible when its\n * deferred kickoff (queued by {@link Markdown.appendCode} through\n * `onFirstLayout`) ran. Flushed by {@link Markdown.onEffectiveVisibilityChange}\n * once visibility flips to `true` — edge-triggered, not polled, mirroring\n * `Canvas.onEffectiveVisibilityChange`'s animation-loop reconcile.\n */\ntype QueuedCodeUpgrade = CodeUpgradeIdentity;\n\n/**\n * A supported-language fenced block still waiting for `CodeEditor` to\n * become visible after its dynamic import already resolved (queued by\n * {@link Markdown.loadCodeEditorUpgrade} when the import settles while\n * `Markdown` is not effectively visible).\n */\ninterface PendingCodeUpgrade extends CodeUpgradeIdentity {\n // Constructor type built from the type-only `CodeEditor` import — `typeof\n // CodeEditor` is not available here because the value itself is never\n // statically imported.\n CodeEditorClass: new (\n value: string,\n options: { readOnly: true; language: string; autoHeightMaxRows?: number },\n ) => CodeEditor;\n}\n\n/**\n * A display component that renders a Markdown source string as a live DOM\n * subtree.\n *\n * @remarks\n * Parsing uses the `marked` library's lexer only (`marked.lexer(src)`): the\n * component walks the returned token AST and builds every prose element\n * (`<h1>`–`<h6>`, `<p>`, `<ul>`/`<ol>`/`<li>`, `<blockquote>`, `<pre>`/`<code>`,\n * `<strong>`, `<em>`, `<a>`, `<table>`) through the DOM sink. There is no\n * HTML-string assignment path, so untrusted Markdown can never inject markup,\n * and the render runs against the modelled DOM source in tests.\n *\n * The v1 token set covers headings, paragraphs, ordered/unordered lists,\n * blockquotes, fenced/inline code, bold, italic, links, and GFM pipe tables\n * (including per-column alignment). Any other token type (images, raw HTML,\n * the remaining GFM extensions) falls through to a defined fallback that\n * renders the token's plain text — never a crash, never markup.\n *\n * A fenced code block whose info string names a language\n * [`CodeEditor`](/components/CodeEditor) has a registered grammar for\n * (`js`/`ts`/`json`/`html`/`sql`/`markdown`, plus aliases) upgrades from the\n * plain `<pre>` to a live, read-only, syntax-highlighted `CodeEditor` once it\n * loads; an unrecognised language, or no info string, keeps the plain\n * `<pre>`. The upgrade is lazy in two ways —\n * `CodeEditor`'s CodeMirror dependency loads through a dynamic import that\n * fires only when a fenced block actually needs it, deferred until this\n * component's first connected, displayed layout, and further deferred per\n * block until its wrapper comes within one viewport-height of the visible\n * area — so a `Markdown` with no fenced code (or only unsupported languages)\n * pays no extra bundle cost, and a long document upgrades only the blocks\n * the reader actually scrolls to.\n *\n * Links render as plain `<a href target=\"_blank\" rel=\"noopener noreferrer\">`\n * with native navigation; the component exposes no event surface in v1.\n *\n * Wrapping and scrolling. The prose wraps to the width it is assigned —\n * paragraphs reflow at word boundaries and overlong unbreakable tokens (URLs)\n * break, so content never overflows horizontally; fenced code blocks instead\n * preserve their lines and scroll inside their own frame. Because prose reflows,\n * the component measures its rendered content height at the assigned width and\n * reports it through {@link Markdown.getMinSize} / {@link Markdown.getPreferredSize},\n * so it grows a size-negotiating scroll host to the full prose height — drop one\n * in a vertically-scrolling [`Panel`](/api/component/container/classes/Panel)\n * (`setAutoScroll(\"y\")`) and it scrolls. The height is re-measured on content,\n * width, and theme change; only the height axis is derived (the width stays\n * freely assignable). The measured height is reported as a *minimum*, so an\n * explicit `preferredSize`/`setMinSize` taller than the content still wins; to\n * cap the component below its content, give it a bounded scroll host.\n *\n * @example\n * ```typescript\n * import { Markdown } from '@jimka/typescript-ui/component/display';\n *\n * panel.addComponent(new Markdown('# Title\\n\\nSome **bold** text.'));\n * ```\n *\n * @category Components\n */\nclass Markdown extends Component<MarkdownOptions> {\n\n /**\n * Raw child nodes built into the root through the sink. Tracked so they are\n * released with the component and torn down / rebuilt on {@link setMarkdown}.\n */\n private _contentHandles: Handle[] = [];\n\n /**\n * Measured content height in px (outer/border-box), cached as per-instance\n * derived state — intrinsic runtime bookkeeping, so it lives here rather than\n * in {@link MarkdownOptions}. `null` until the first measure records it; folded\n * into {@link getMinSize} / {@link getPreferredSize} to drive a scroll host.\n */\n private _measuredHeight: number | null = null;\n\n /** Handle to detach the {@link ThemeManager.onThemeChange} listener on {@link dispose}. */\n private readonly _unsubscribeTheme: () => void;\n\n /** Live `CodeEditor` upgrades, raw-DOM-appended per fenced block; see {@link applyCodeEditorUpgrade}. */\n private _codeEditors: Array<{ editor: CodeEditor; wrapper: Handle }> = [];\n\n /** Supported-language fenced blocks still waiting to upgrade; see {@link PendingCodeUpgrade}. */\n private _pendingCodeUpgrades: PendingCodeUpgrade[] = [];\n\n /**\n * Supported-language fenced blocks whose dynamic import hasn't started\n * because `Markdown` wasn't effectively visible when their deferred\n * kickoff ran; see {@link QueuedCodeUpgrade} and\n * {@link onEffectiveVisibilityChange}.\n */\n private _awaitingVisibilityKickoffs: QueuedCodeUpgrade[] = [];\n\n /**\n * Supported-language fenced blocks whose dynamic import hasn't started\n * because their wrapper is not yet within {@link CODE_UPGRADE_LOOKAHEAD_VIEWPORTS}\n * of the viewport; see {@link isBlockNearViewport} and {@link onViewportPass}.\n */\n private _awaitingViewportKickoffs: QueuedCodeUpgrade[] = [];\n\n /** Whether the scroll/resize viewport listeners are currently registered. */\n private _viewportWatchArmed = false;\n\n /** Whether a viewport pass is already queued on the next layout flush. */\n private _viewportPassScheduled = false;\n\n /** Whether a coalesced content-height measure is already queued. */\n private _measureScheduled = false;\n\n /**\n * Arrow field, not a prototype method: {@link Component.afterNextLayout}\n * calls its callback bare with no receiver.\n */\n private readonly handleViewportPass: () => void = () => this.onViewportPass();\n\n /**\n * Arrow field, not a prototype method: {@link Component.afterNextLayout}\n * calls its callback bare with no receiver.\n */\n private readonly handleScheduledMeasure: () => void = () => this.onScheduledMeasure();\n\n /**\n * Bumped by {@link clearContent}; a resolved dynamic import compares its\n * captured generation against this to detect a render it no longer\n * belongs to (a later {@link setMarkdown}, or disposal), mirroring\n * `DiagramView.relayout`'s generation token.\n */\n private _renderGeneration = 0;\n\n /**\n * Normalised form of {@link MarkdownOptions.maxMeasure} — a bare `number`\n * is stored here with its `\"ch\"` suffix appended, so the canonical string\n * form is computed once and reused by every {@link setMaxMeasure} write.\n * `null` (the default) means \"use the theme's `--ts-ui-md-max-measure`\".\n */\n private _maxMeasure: string | null = null;\n\n /**\n * Constructs a Markdown component for the given source string.\n *\n * @param markdown - The Markdown source to render (optional; defaults to \"\").\n * @param options - Optional component options bag.\n */\n constructor(markdown?: string, options?: MarkdownOptions) {\n super(options);\n\n // Positional argument: cache it only when the caller didn't also pass\n // `options.markdown` (which the super-time cascade already stored).\n if (markdown !== undefined && this._options.markdown === undefined) {\n this._options.markdown = markdown;\n }\n\n // Flowed prose must wrap: Component defaults `white-space` to \"nowrap\",\n // which would lay the document out as unwrapping single lines that\n // overflow horizontally. Reflow at word boundaries and break overlong\n // unbreakable tokens (URLs) so nothing spills sideways; fenced code keeps\n // its own `white-space: pre` + self-scroll from the `pre` class rule.\n this.setWhiteSpace(\"normal\");\n this.setElementCSSRule(\"overflowWrap\", \"break-word\");\n\n // Prose reads continuously, unlike the framework's UI controls (tuned\n // for scanned single-line text), so it wants looser leading. Inherits\n // to every prose descendant; code/pre reset it back to \"normal\" via\n // their own class rules.\n this.setElementCSSRule(\"lineHeight\", \"var(--ts-ui-md-line-height, 1.6)\");\n\n // Caps the prose column to a comfortable reading measure regardless of\n // how wide the assigned layout box is; oversized tables/code fall back\n // to their own class-rule horizontal scroll within the capped column.\n // Dispatched through the setter (even for the unset default) so a\n // later `setMaxMeasure(null)` reverts to the same theme-var default\n // this constructor seeds.\n this.setMaxMeasure(this._options.maxMeasure ?? null);\n\n // Scales the prose's base font size; headings scale with it via their\n // own relative sizing (see setFontScale).\n this.setFontScale(this._options.fontScale ?? 1);\n\n // Prose metrics (font, spacing) are theme-bound, so a theme swap can\n // change the rendered height, and `--ts-ui-md-max-measure`'s `ch` unit\n // means it can change a code block's width too — react to both when\n // it fires (mirrors Text).\n this._unsubscribeTheme = ThemeManager.onThemeChange(() => this.onThemeChanged());\n\n // First measurement rides the first connected layout: only then is the\n // element attached and width-assigned, so the `scrollHeight` read is\n // meaningful. Subsequent re-measures come from setWidth / setMarkdown / theme.\n this.onFirstLayout(() => this.measureContentHeight());\n }\n\n /**\n * Applies a {@link MarkdownOptions} bag. Inherited Component fields cascade\n * through `super.applyOptions`; the `markdown` source is cached pure to\n * `_options` and picked up by `render()` (or a later `setMarkdown`).\n *\n * @param options - The options bag carrying the values to apply.\n * @returns This component, for method chaining.\n */\n protected applyOptions(options: MarkdownOptions): this {\n super.applyOptions(options);\n\n if (options.markdown !== undefined) {\n this._options.markdown = options.markdown;\n }\n\n if (options.linkResolver !== undefined) {\n this._options.linkResolver = options.linkResolver;\n }\n\n if (options.maxMeasure !== undefined) {\n this._options.maxMeasure = options.maxMeasure;\n }\n\n if (options.fontScale !== undefined) {\n this._options.fontScale = options.fontScale;\n }\n\n return this;\n }\n\n /**\n * Returns the current Markdown source, or \"\" when unset.\n *\n * @returns The cached Markdown source string.\n */\n getMarkdown(): string {\n return this._options.markdown ?? \"\";\n }\n\n /**\n * Sets the resolver used to render every link's href. Does not re-render\n * already-built content; call {@link setMarkdown} (with the same source, if\n * needed) to re-render links with the new resolver.\n *\n * @param resolver - The new link resolver.\n * @returns This component, for method chaining.\n */\n setLinkResolver(resolver: MarkdownLinkResolver): this {\n this._options.linkResolver = resolver;\n\n return this;\n }\n\n /**\n * Returns the current link resolver, or the default resolver (every href\n * unchanged, marked external) when none was set — never `null`.\n *\n * @returns The active link resolver.\n */\n getLinkResolver(): MarkdownLinkResolver {\n return this._options.linkResolver ?? defaultLinkResolver;\n }\n\n /**\n * Overrides the prose column's max width. Pass `null` to revert to the\n * theme's `--ts-ui-md-max-measure` default.\n *\n * @param value - A CSS width string (e.g. `\"60ch\"`), a bare number of\n * `ch` units, or `null` to revert to the theme default.\n * @returns This component, for method chaining.\n */\n setMaxMeasure(value: string | number | null): this {\n this._options.maxMeasure = value;\n this._maxMeasure = typeof value === \"number\" ? `${value}ch` : value;\n this.setElementCSSRule(\"maxWidth\", this._maxMeasure ?? \"var(--ts-ui-md-max-measure, 70ch)\");\n\n return this;\n }\n\n /**\n * Returns the current max-measure override, or `null` when unset (the\n * theme default applies).\n *\n * @returns The cached {@link MarkdownOptions.maxMeasure} value, or `null`.\n */\n getMaxMeasure(): string | number | null {\n return this._options.maxMeasure ?? null;\n }\n\n /**\n * Scales the prose's base font size; headings and other relatively-sized\n * elements scale with it. Pass `1` to clear the override.\n *\n * @param value - The multiplier applied to the base font size.\n * @returns This component, for method chaining.\n */\n setFontScale(value: number): this {\n this._options.fontScale = value;\n this.setElementCSSRule(\"fontSize\", value === 1 ? null : (value * 100) + \"%\");\n\n return this;\n }\n\n /**\n * Returns the current font-scale multiplier, or `1` (no scaling) when unset.\n *\n * @returns The cached {@link MarkdownOptions.fontScale} value, or `1`.\n */\n getFontScale(): number {\n return this._options.fontScale ?? 1;\n }\n\n /**\n * Sets the Markdown source, re-lexing and rebuilding the rendered subtree\n * when the element already exists. When it does not (pre-render), the source\n * is cached and picked up by `render()`.\n *\n * @param markdown - The new Markdown source string.\n * @returns This component, for method chaining.\n */\n setMarkdown(markdown: string): this {\n this._options.markdown = markdown;\n\n const element = this.getElement();\n\n if (!element) {\n return this;\n }\n\n this.clearContent();\n ensureMarkdownClassRules();\n this.appendBlockTokens(element, lexer(markdown), new Map<string, number>());\n\n // Content changed — the flowed height did too; re-measure and let a host grow.\n this.measureContentHeight();\n\n return this;\n }\n\n /**\n * Folds the measured content height into the inherited minimum as a height\n * floor, so a scroll host grows to the full prose extent (via\n * `Fit.inflateForOverflow`, which reads the child's `getMinSize`). Only the\n * height axis is folded; the width minimum stays `0` so the prose can reflow\n * at any assigned width. An explicit {@link setMinSize} still wins when larger.\n *\n * @returns The min size with the measured height folded in, or the inherited\n * minimum when nothing has been measured yet.\n */\n getMinSize(): Size | null {\n const base = super.getMinSize();\n\n if (this._measuredHeight === null) {\n return base;\n }\n\n if (!base) {\n return { width: 0, height: this._measuredHeight };\n }\n\n return { width: base.width, height: Math.max(base.height, this._measuredHeight) };\n }\n\n /**\n * Reports the measured content height as the preferred height when the caller\n * has set no explicit `preferredSize`, keeping the component's preferred extent\n * honest inside a sizing parent. An explicit `preferredSize` constraint wins.\n *\n * @returns The preferred size with the measured height applied, or the\n * inherited preferred size when a constraint is set or nothing is measured.\n */\n getPreferredSize(): Size | null {\n const base = super.getPreferredSize();\n\n if (this._measuredHeight === null || this.getPreferredSizeConstraint() !== null) {\n return base;\n }\n\n if (!base) {\n return { width: 0, height: this._measuredHeight };\n }\n\n return { width: base.width, height: this._measuredHeight };\n }\n\n /**\n * Re-measures the flowed content height when the assigned width changes: prose\n * height is width-dependent, so a narrower box reflows taller. The re-measure\n * reads the just-committed width back from the DOM before reading the height.\n *\n * @param width - The new width in pixels.\n * @returns This component, for method chaining.\n */\n setWidth(width: number): this {\n const changed = width !== this.getWidth();\n\n super.setWidth(width);\n\n if (changed) {\n this.resyncCodeEditorWidths();\n this.measureContentHeight();\n }\n\n return this;\n }\n\n /**\n * Reacts to a theme swap: re-syncs live editors' widths (a `ch`-unit\n * `--ts-ui-md-max-measure` moves with the font) and re-measures the\n * content height (prose metrics are theme-bound).\n */\n private onThemeChanged(): void {\n this.resyncCodeEditorWidths();\n this.measureContentHeight();\n }\n\n /**\n * Detaches the theme-change listener, then defers to the base class for\n * the rest of teardown. Call when a dynamically-built Markdown is\n * permanently removed from the page, mirroring `CodeEditor.destructor`.\n */\n protected destructor(): void {\n // Disposes every live CodeEditor: raw-DOM-appended (never through\n // addComponent), so the base class's own child-recursion below never\n // reaches them — Markdown must dispose them explicitly.\n this.clearContent();\n this._unsubscribeTheme();\n super.destructor();\n }\n\n /**\n * Measures the rendered subtree's content height at the element's assigned\n * width and folds it into the component's reported size, then schedules a\n * re-layout so a scroll host can grow to fit. This is the component's only\n * live seam read (`scrollHeight`) — isolated here as the single forced-layout\n * point — and no-ops before the element exists (the first connected layout\n * retries via {@link onFirstLayout}). Idempotent: an unchanged height suppresses\n * the re-layout so repeated measures cannot loop.\n */\n private measureContentHeight(): void {\n const element = this.getElement();\n if (!element) {\n return;\n }\n\n // Flush any pending style writes — in particular a `width` queued by\n // LayoutManager.commitBounds, which disables auto-commit\n // (setAutoCommitStyle(false)) for the duration of a layout pass.\n // Markdown.setWidth calls measureContentHeight synchronously from\n // inside that window, so without this flush the scrollHeight read\n // below would measure against the previous frame's width. Same fix\n // as Panel.doLayout's pre-measureScrollbarGutter flush.\n this.commitElementStyle();\n\n this.flushPendingCodeUpgrades();\n\n // Read the true content height, not the committed box. `scrollHeight` is\n // floored at the element's own `clientHeight`, so measuring the live\n // (already height-committed) box would only ever report *growth* — a\n // document that reflows wider or is edited shorter could never shrink its\n // extent, leaving stale dead space. Collapse the box to its content\n // first — the width was already flushed above, so this second flush only\n // needs to commit the `height: auto` write below. The raw style write is\n // a transient probe restored below, not persistent state, so it\n // deliberately bypasses the typed `setHeight` (which takes only a number).\n const restoreHeight = this.getHeight();\n this.setElementStyle(\"height\", \"auto\");\n this.commitElementStyle();\n\n // `scrollHeight` is content + padding (border-box excludes the border),\n // so reach the outer height by adding only the border.\n const border = this.getBorderSize();\n const measured = DOM.source.getScrollMetrics(element).scrollHeight + border.top + border.bottom;\n\n // Restore the laid-out height so the box isn't left content-collapsed\n // between now and the next layout pass (which re-commits it anyway).\n this.setElementStyle(\"height\", restoreHeight + \"px\");\n this.commitElementStyle();\n\n if (measured === this._measuredHeight) {\n return;\n }\n\n this._measuredHeight = measured;\n (this.getParentComponent() ?? this).scheduleLayout();\n }\n\n /**\n * Renders the root element, then lexes the current source and builds the\n * prose subtree into it. Empty/blank source lexes to an empty token list, so\n * the root renders with no prose children.\n *\n * @returns The rendered root element handle.\n */\n protected render(): Handle {\n const element = super.render();\n\n ensureMarkdownClassRules();\n this.appendBlockTokens(element, lexer(this.getMarkdown()), new Map<string, number>());\n\n return element;\n }\n\n /**\n * Creates a tracked child element: minted through the sink, registered for\n * release with the component, and recorded in `_contentHandles` so it is\n * torn down on the next {@link setMarkdown} rebuild.\n *\n * @param tag - The HTML tag to create.\n * @returns The new element handle.\n */\n private create(tag: string): Handle {\n const handle = DOM.sink.createElement(tag);\n\n this.trackHandle(handle);\n this._contentHandles.push(handle);\n\n return handle;\n }\n\n /**\n * Removes every content node from the DOM, releases its handle, and empties\n * the tracked set, so a rebuild neither leaks registry entries nor lets\n * `_ownedHandles` accumulate stale references across edits.\n */\n private clearContent(): void {\n for (const handle of this._contentHandles) {\n DOM.sink.removeElement(handle);\n this.untrackHandle(handle);\n DOM.sink.release(handle);\n }\n\n this._contentHandles.length = 0;\n\n // Bumped so any dynamic import already in flight for this render\n // (see loadCodeEditorUpgrade) recognises itself as stale and no-ops\n // instead of applying against handles this rebuild just tore down.\n this._renderGeneration += 1;\n\n for (const { editor } of this._codeEditors) {\n editor.dispose();\n }\n\n this._codeEditors.length = 0;\n this._pendingCodeUpgrades.length = 0;\n this._awaitingVisibilityKickoffs.length = 0;\n this._awaitingViewportKickoffs.length = 0;\n this.disarmViewportWatch();\n }\n\n /**\n * Swaps a fenced block's placeholder `<pre>`/`<code>` for a live,\n * syntax-highlighted `CodeEditor`, sized to exactly fill the `wrapper`\n * (see the \"position: relative wrapper\" architecture decision — an\n * absolutely positioned child does not contribute to its ancestor's auto\n * height, so the wrapper's own height is pinned explicitly below).\n *\n * @param CodeEditorClass - The dynamically-imported `CodeEditor` constructor.\n * @param wrapper - The `ts-ui-md-code-host` wrapper the placeholder `<pre>` sits in.\n * @param pre - The placeholder `<pre>` handle being replaced.\n * @param code - The placeholder `<code>` handle being replaced.\n * @param text - The fenced block's literal source text.\n * @param languageId - The mapped `CodeEditor` registry id.\n */\n private applyCodeEditorUpgrade(\n CodeEditorClass: PendingCodeUpgrade[\"CodeEditorClass\"],\n wrapper: Handle,\n pre: Handle,\n code: Handle,\n text: string,\n languageId: string,\n ): void {\n const metrics = DOM.source.getScrollMetrics(pre);\n const width = metrics.clientWidth;\n const height = metrics.scrollHeight;\n\n for (const handle of [pre, code]) {\n DOM.sink.removeElement(handle);\n this.untrackHandle(handle);\n DOM.sink.release(handle);\n\n const index = this._contentHandles.indexOf(handle);\n if (index !== -1) {\n this._contentHandles.splice(index, 1);\n }\n }\n\n const editor = new CodeEditorClass(text, {\n readOnly: true,\n language: languageId,\n autoHeightMaxRows: CODE_BLOCK_MAX_AUTO_ROWS,\n });\n\n editor.setX(0).setY(0).setWidth(width).setHeight(height);\n\n let correctionWarned = false;\n\n editor.on(\"heightchange\", (payload) => {\n if (!correctionWarned) {\n correctionWarned = true;\n\n const delta = Math.abs(payload.height - height);\n\n if (delta > GUESS_HEIGHT_CORRECTION_WARN_PX) {\n console.warn(\n `Markdown: fenced \"${languageId}\" code block's CodeEditor corrected its guessed ` +\n `height by ${Math.round(delta)}px (${height}px → ${payload.height}px) on mount.`,\n );\n }\n }\n\n this.handleCodeEditorHeightChange(wrapper, payload.height);\n });\n DOM.sink.appendChild(wrapper, editor.getElement(true)!);\n DOM.sink.apply(wrapper, { style: { height: height + \"px\" } });\n\n this._codeEditors.push({ editor, wrapper });\n }\n\n /**\n * Re-pins the `ts-ui-md-code-host` wrapper's height to a live CodeEditor's\n * own auto-grown height synchronously, then schedules a coalesced\n * re-measure of Markdown's own content height (see {@link scheduleContentMeasure})\n * so the taller/shorter block folds into the reported size. Wired in\n * {@link applyCodeEditorUpgrade} onto each editor's `\"heightchange\"` event.\n *\n * @param wrapper - The wrapper the fired editor sits in.\n * @param height - The editor's new height in pixels.\n */\n private handleCodeEditorHeightChange(wrapper: Handle, height: number): void {\n DOM.sink.apply(wrapper, { style: { height: height + \"px\" } });\n this.scheduleContentMeasure();\n }\n\n /**\n * Applies any {@link PendingCodeUpgrade} that has become visible since it\n * was queued. Called as the first line of {@link measureContentHeight}, so\n * a freshly-applied wrapper's height is committed before `Markdown`'s own\n * content height is measured.\n */\n private flushPendingCodeUpgrades(): void {\n this._pendingCodeUpgrades = this._pendingCodeUpgrades.filter((pending) => {\n if (!this.isEffectivelyVisible()) {\n return true;\n }\n\n this.applyCodeEditorUpgrade(\n pending.CodeEditorClass, pending.wrapper, pending.pre, pending.code,\n pending.text, pending.languageId,\n );\n\n return false;\n });\n }\n\n /**\n * Re-syncs every already-applied editor's width from its wrapper's\n * current `clientWidth` — what keeps a code block's width tracking\n * `Markdown`'s own width. Called only from the two things that can\n * actually change a wrapper's width: {@link setWidth} and the\n * theme-change handler (a font swap moves the `ch`-unit max-measure).\n *\n * @remarks Skipped entirely while not effectively visible: a hidden\n * subtree's `clientWidth` reads `0`, and writing that through would\n * collapse a previously-applied editor with nothing to correct it on\n * re-show (no width actually changes on re-show, so `setWidth`'s \"only\n * re-measure when changed\" guard would never re-run this method).\n * Skipping leaves the last-good width intact instead.\n */\n private resyncCodeEditorWidths(): void {\n this.commitElementStyle();\n\n if (!this.isEffectivelyVisible()) {\n return;\n }\n\n for (const { editor, wrapper } of this._codeEditors) {\n editor.setWidth(DOM.source.getScrollMetrics(wrapper).clientWidth);\n }\n }\n\n /**\n * Starts a fenced block's `CodeEditor` upgrade once it clears two gates in\n * series. First, effective visibility: if `Markdown` is not currently\n * effectively visible, the entry queues in {@link _awaitingVisibilityKickoffs},\n * flushed later by {@link onEffectiveVisibilityChange} once visibility\n * flips to `true`, edge-triggered rather than polled. This is the single\n * check-once gate the deferred kickoff registered in {@link appendCode}\n * runs through: `onFirstLayout` alone only guarantees \"connected +\n * displayed\" for a component's very first render — once the element is\n * already connected (e.g. a second `setMarkdown()` on a previously-shown,\n * now-hidden instance), it takes `Component.afterNextLayout`'s fast path,\n * which fires unconditionally on the next flush with no displayed-gating\n * — so the visibility check has to happen here, not be assumed from\n * having been called at all. Second, proximity to the viewport: once\n * effectively visible, an entry whose wrapper is not yet within\n * {@link CODE_UPGRADE_LOOKAHEAD_VIEWPORTS} of the viewport queues in\n * {@link _awaitingViewportKickoffs} instead, flushed by {@link onViewportPass}\n * as scrolling or resizing brings it into range.\n *\n * @param entry - The fenced block's placeholder handles, text, mapped\n * language, and the render generation it belongs to.\n */\n private startCodeEditorImport(entry: QueuedCodeUpgrade): void {\n if (!this.isEffectivelyVisible()) {\n this._awaitingVisibilityKickoffs.push(entry);\n\n return;\n }\n\n if (!this.isBlockNearViewport(entry.wrapper)) {\n this._awaitingViewportKickoffs.push(entry);\n this.armViewportWatch();\n\n return;\n }\n\n void this.loadCodeEditorUpgrade(entry.wrapper, entry.pre, entry.code, entry.text, entry.languageId, entry.generation);\n }\n\n /**\n * Tests whether a fenced block's wrapper is close enough to the visible\n * window to start its `CodeEditor` upgrade now — not entirely above the\n * top of the window, and not further than {@link CODE_UPGRADE_LOOKAHEAD_VIEWPORTS}\n * viewport-heights below the fold. The lookahead applies below the fold\n * only: swapping the placeholder for a live editor changes the block's\n * height, and giving the margin no upward component means that in\n * ordinary downward reading every upgrade happens at or below the\n * reader's position, so the movement lands on off-screen content.\n *\n * @param wrapper - The fenced block's `ts-ui-md-code-host` wrapper.\n * @returns `true` when the wrapper is within range of the viewport.\n */\n private isBlockNearViewport(wrapper: Handle): boolean {\n const rect = DOM.source.getElementRect(wrapper);\n const viewportHeight = DOM.source.getViewportSize().height;\n const cutoff = viewportHeight * (1 + CODE_UPGRADE_LOOKAHEAD_VIEWPORTS);\n\n return rect.bottom >= 0 && rect.top <= cutoff;\n }\n\n /**\n * Registers the scroll/resize viewport listeners that drive {@link onViewportPass},\n * if not already armed. Idempotent — safe to call from every enqueue.\n */\n private armViewportWatch(): void {\n if (this._viewportWatchArmed) {\n return;\n }\n\n this._viewportWatchArmed = true;\n Event.addViewportListener(this, \"scroll\", this.handleViewportChange);\n Event.addViewportListener(this, \"resize\", this.handleViewportChange);\n }\n\n /**\n * Removes the scroll/resize viewport listeners registered by\n * {@link armViewportWatch}, once nothing remains queued to watch for.\n */\n private disarmViewportWatch(): void {\n if (!this._viewportWatchArmed) {\n return;\n }\n\n this._viewportWatchArmed = false;\n Event.removeViewportListener(this, \"scroll\", this.handleViewportChange);\n Event.removeViewportListener(this, \"resize\", this.handleViewportChange);\n }\n\n /**\n * Plain prototype-method reference passed to {@link Event.addViewportListener} /\n * {@link Event.removeViewportListener}, which invoke it with this\n * component bound as `this` — unlike {@link handleViewportPass}, this\n * cannot be an arrow field (see the class-level remark on the two\n * callback shapes).\n */\n private handleViewportChange(): void {\n this.scheduleViewportPass();\n }\n\n /**\n * Coalesces a burst of scroll/resize events into one {@link onViewportPass}\n * per layout flush.\n */\n private scheduleViewportPass(): void {\n if (this._viewportPassScheduled || this._awaitingViewportKickoffs.length === 0) {\n return;\n }\n\n this._viewportPassScheduled = true;\n Component.afterNextLayout(this.handleViewportPass);\n }\n\n /**\n * Walks {@link _awaitingViewportKickoffs} in document order, starting the\n * upgrade for every entry within range of the viewport and breaking at\n * the first one past the lookahead cutoff (later entries are further\n * down, since fenced blocks are appended in document order). Reads every\n * entry's rect before starting any upgrade, so the pass costs at most one\n * forced reflow rather than interleaving reads with the layout-affecting\n * writes an upgrade triggers.\n */\n private onViewportPass(): void {\n this._viewportPassScheduled = false;\n\n if (this._awaitingViewportKickoffs.length === 0) {\n this.disarmViewportWatch();\n\n return;\n }\n\n if (!this.isEffectivelyVisible()) {\n return;\n }\n\n this.commitElementStyle();\n\n const viewportHeight = DOM.source.getViewportSize().height;\n const cutoff = viewportHeight * (1 + CODE_UPGRADE_LOOKAHEAD_VIEWPORTS);\n const queue = this._awaitingViewportKickoffs;\n const due: QueuedCodeUpgrade[] = [];\n const remaining: QueuedCodeUpgrade[] = [];\n\n for (let i = 0; i < queue.length; i++) {\n const entry = queue[i]!;\n const rect = DOM.source.getElementRect(entry.wrapper);\n\n if (rect.top > cutoff) {\n remaining.push(...queue.slice(i));\n\n break;\n }\n\n (rect.bottom >= 0 ? due : remaining).push(entry);\n }\n\n this._awaitingViewportKickoffs = remaining;\n\n if (remaining.length === 0) {\n this.disarmViewportWatch();\n }\n\n for (const entry of due) {\n void this.loadCodeEditorUpgrade(entry.wrapper, entry.pre, entry.code, entry.text, entry.languageId, entry.generation);\n }\n }\n\n /**\n * Flushes any {@link QueuedCodeUpgrade} once this component becomes\n * effectively visible, starting the dynamic import for each — the\n * edge-triggered replacement for a per-frame visibility poll, mirroring\n * `Canvas.onEffectiveVisibilityChange`'s animation-loop reconcile.\n *\n * @param effective - The component's new effective-visibility state.\n */\n protected onEffectiveVisibilityChange(effective: boolean): void {\n super.onEffectiveVisibilityChange(effective);\n\n if (!effective || this._awaitingVisibilityKickoffs.length === 0) {\n return;\n }\n\n const queued = this._awaitingVisibilityKickoffs;\n\n this._awaitingVisibilityKickoffs = [];\n\n for (const entry of queued) {\n this.startCodeEditorImport(entry);\n }\n }\n\n /**\n * Loads `CodeEditor` through a narrow dynamic import — the two specific\n * modules it needs (`CodeEditor.js` itself, and `languages.js` for its\n * side-effect language registration), never the `component/editor`\n * barrel, which would also pull in the unrelated Lexical-based\n * `MarkdownEditor` stack. Once the import resolves, applies the upgrade\n * immediately if `Markdown` is still showing the render that queued this\n * call and is effectively visible, or queues it in {@link _pendingCodeUpgrades}\n * otherwise.\n *\n * @param wrapper - The `ts-ui-md-code-host` wrapper the placeholder `<pre>` sits in.\n * @param pre - The placeholder `<pre>` handle.\n * @param code - The placeholder `<code>` handle.\n * @param text - The fenced block's literal source text.\n * @param languageId - The mapped `CodeEditor` registry id.\n * @param generation - The {@link _renderGeneration} captured when this\n * fenced block was queued in {@link appendCode}.\n */\n private async loadCodeEditorUpgrade(\n wrapper: Handle,\n pre: Handle,\n code: Handle,\n text: string,\n languageId: string,\n generation: number,\n ): Promise<void> {\n const [{ CodeEditor: CodeEditorClass }] = await Promise.all([\n import(\"~/component/editor/CodeEditor.js\"),\n import(\"~/component/editor/languages.js\"),\n ]);\n\n if (generation !== this._renderGeneration) {\n // A later setMarkdown() (or disposal, which also bumps the\n // generation) rebuilt since this block was queued — the wrapper/\n // pre/code handles this call closed over no longer belong to a\n // live render.\n return;\n }\n\n if (this.isEffectivelyVisible()) {\n this.applyCodeEditorUpgrade(CodeEditorClass, wrapper, pre, code, text, languageId);\n this.scheduleContentMeasure();\n } else {\n this._pendingCodeUpgrades.push({ CodeEditorClass, wrapper, pre, code, text, languageId, generation });\n }\n }\n\n /**\n * Coalesces a burst of upgrade-driven re-measures — {@link loadCodeEditorUpgrade}\n * and {@link handleCodeEditorHeightChange} each fire once per upgraded\n * block — into one {@link measureContentHeight} call per layout flush,\n * rather than one full-document reflow per block.\n */\n private scheduleContentMeasure(): void {\n if (this._measureScheduled) {\n return;\n }\n\n this._measureScheduled = true;\n Component.afterNextLayout(this.handleScheduledMeasure);\n }\n\n /**\n * Runs the coalesced {@link measureContentHeight} queued by\n * {@link scheduleContentMeasure}, then re-evaluates the viewport queue:\n * the measure's reflow can move a still-queued block relative to the\n * fold without any scroll or resize event firing to trigger the pass\n * itself.\n */\n private onScheduledMeasure(): void {\n this._measureScheduled = false;\n this.measureContentHeight();\n this.scheduleViewportPass();\n }\n\n /**\n * Walks a list of block-level tokens, appending each into `parent`.\n *\n * @param parent - The element handle to append the built blocks into.\n * @param tokens - The block-level tokens to render.\n * @param headingIds - The current render pass's heading-id dedupe counter,\n * keyed by slug — see {@link appendHeading}. Threaded as a parameter\n * (never a field) so it cannot survive past the render pass that created it.\n */\n private appendBlockTokens(parent: Handle, tokens: Token[], headingIds: Map<string, number>): void {\n for (const token of tokens) {\n this.appendBlockToken(parent, token, headingIds);\n }\n }\n\n /**\n * Dispatches a single block-level token to its builder. Unknown/unsupported\n * types fall through to a plain-text node — never markup, never a crash.\n *\n * @param parent - The element handle to append into.\n * @param token - The block-level token.\n * @param headingIds - The current render pass's heading-id dedupe counter.\n */\n private appendBlockToken(parent: Handle, token: Token, headingIds: Map<string, number>): void {\n switch (token.type) {\n case \"heading\": this.appendHeading(parent, token as Tokens.Heading, headingIds); break;\n case \"paragraph\": this.appendParagraph(parent, token as Tokens.Paragraph); break;\n case \"list\": this.appendList(parent, token as Tokens.List, headingIds); break;\n case \"blockquote\": this.appendBlockquote(parent, token as Tokens.Blockquote, headingIds); break;\n case \"code\": this.appendCode(parent, token as Tokens.Code); break;\n case \"table\": this.appendTable(parent, token as Tokens.Table); break;\n\n // Blank line between blocks — nothing to render.\n case \"space\": break;\n\n default: this.appendTextNode(parent, token.raw ?? \"\"); break;\n }\n }\n\n /**\n * Builds an `<h1>`..`<h6>` element (tag from the token depth) carrying the\n * heading's inline content and a slugified `id`, so an in-page `#fragment`\n * link can target it.\n *\n * @param parent - The element handle to append into.\n * @param token - The heading token.\n * @param headingIds - The current render pass's dedupe counter: a slug seen\n * before gets a `-N` suffix so every id on the page is unique. Local to\n * one render pass — {@link render} and {@link setMarkdown} each start a\n * fresh `Map`, so a re-render does not accumulate suffixes.\n */\n private appendHeading(parent: Handle, token: Tokens.Heading, headingIds: Map<string, number>): void {\n const depth = Math.min(Math.max(token.depth, HEADING_MIN_DEPTH), HEADING_MAX_DEPTH);\n const heading = this.create(\"h\" + depth);\n const id = nextHeadingId(token.text, headingIds);\n\n DOM.sink.apply(heading, { addClass: [HEADING_CLASS], setAttr: { id } });\n this.appendInlineTokens(heading, token.tokens);\n DOM.sink.appendChild(parent, heading);\n }\n\n /**\n * Builds a `<p>` carrying the paragraph's inline content.\n *\n * @param parent - The element handle to append into.\n * @param token - The paragraph token.\n */\n private appendParagraph(parent: Handle, token: Tokens.Paragraph): void {\n const paragraph = this.create(\"p\");\n\n this.appendInlineTokens(paragraph, token.tokens);\n DOM.sink.appendChild(parent, paragraph);\n }\n\n /**\n * Builds a `<ul>` (or `<ol>` when the token is ordered) with an `<li>` per\n * item.\n *\n * @param parent - The element handle to append into.\n * @param token - The list token.\n * @param headingIds - The current render pass's heading-id dedupe counter.\n */\n private appendList(parent: Handle, token: Tokens.List, headingIds: Map<string, number>): void {\n const list = this.create(token.ordered ? \"ol\" : \"ul\");\n\n DOM.sink.apply(list, { addClass: [LIST_CLASS] });\n\n for (const item of token.items) {\n this.appendListItem(list, item, headingIds);\n }\n\n DOM.sink.appendChild(parent, list);\n }\n\n /**\n * Builds an `<li>` and walks its children, which are block tokens for a\n * loose list and inline-bearing `text` tokens for a tight one — so both\n * render.\n *\n * @param list - The `<ul>`/`<ol>` element handle to append into.\n * @param item - The list-item token.\n * @param headingIds - The current render pass's heading-id dedupe counter.\n */\n private appendListItem(list: Handle, item: Tokens.ListItem, headingIds: Map<string, number>): void {\n const listItem = this.create(\"li\");\n\n for (const token of item.tokens) {\n if (token.type === \"text\") {\n const text = token as Tokens.Text;\n\n if (text.tokens && text.tokens.length > 0) {\n this.appendInlineTokens(listItem, text.tokens);\n } else {\n this.appendInlineToken(listItem, text, item.tokens.length === 1);\n }\n } else {\n this.appendBlockToken(listItem, token, headingIds);\n }\n }\n\n DOM.sink.appendChild(list, listItem);\n }\n\n /**\n * Builds a wrapper `<div>` › `<table>` with a `<thead>` holding the header\n * row and a `<tbody>` holding one row per body entry. The wrapper scrolls\n * horizontally so an overlong table cannot spill sideways.\n *\n * @param parent - The element handle to append into.\n * @param token - The table token.\n */\n private appendTable(parent: Handle, token: Tokens.Table): void {\n const wrapper = this.create(\"div\");\n\n DOM.sink.apply(wrapper, { addClass: [TABLE_WRAP_CLASS] });\n\n const table = this.create(\"table\");\n\n DOM.sink.apply(table, { addClass: [TABLE_CLASS] });\n\n const thead = this.create(\"thead\");\n\n this.appendTableRow(thead, token.header, true);\n DOM.sink.appendChild(table, thead);\n\n const tbody = this.create(\"tbody\");\n\n for (const row of token.rows) {\n this.appendTableRow(tbody, row, false);\n }\n\n DOM.sink.appendChild(table, tbody);\n DOM.sink.appendChild(wrapper, table);\n DOM.sink.appendChild(parent, wrapper);\n }\n\n /**\n * Builds a `<tr>` with one `<th>` (header) or `<td>` (body) per cell,\n * carrying the cell's alignment class (when the column is aligned) and\n * inline content.\n *\n * @param section - The `<thead>`/`<tbody>` element handle to append into.\n * @param cells - The row's cells.\n * @param header - Whether this is the header row (`<th>` cells) or a body\n * row (`<td>` cells).\n */\n private appendTableRow(section: Handle, cells: Tokens.TableCell[], header: boolean): void {\n const row = this.create(\"tr\");\n\n for (const cell of cells) {\n const cellElement = this.create(header ? \"th\" : \"td\");\n const classes = [header ? TH_CLASS : TD_CLASS];\n const align = alignmentClass(cell.align);\n\n if (align) {\n classes.push(align);\n }\n\n DOM.sink.apply(cellElement, { addClass: classes });\n this.appendInlineTokens(cellElement, cell.tokens);\n DOM.sink.appendChild(row, cellElement);\n }\n\n DOM.sink.appendChild(section, row);\n }\n\n /**\n * Builds a `<blockquote>` and recurses into its block-level children.\n *\n * @param parent - The element handle to append into.\n * @param token - The blockquote token.\n * @param headingIds - The current render pass's heading-id dedupe counter.\n */\n private appendBlockquote(parent: Handle, token: Tokens.Blockquote, headingIds: Map<string, number>): void {\n const quote = this.create(\"blockquote\");\n\n DOM.sink.apply(quote, { addClass: [QUOTE_CLASS] });\n this.appendBlockTokens(quote, token.tokens, headingIds);\n DOM.sink.appendChild(parent, quote);\n }\n\n /**\n * Builds a `<pre>` › `<code>` carrying the fenced block's literal text\n * verbatim (newlines preserved). When the fence's info string maps to a\n * registered `CodeEditor` language (see {@link mapFenceLangToEditorId}),\n * the `<pre>` is additionally wrapped in a `ts-ui-md-code-host` div and a\n * deferred upgrade to a live, syntax-highlighted `CodeEditor` is queued —\n * see {@link loadCodeEditorUpgrade}. An unmapped language, or no info\n * string, renders exactly as before: a bare `<pre>` with no wrapper and\n * no dynamic import triggered.\n *\n * @param parent - The element handle to append into.\n * @param token - The code token.\n */\n private appendCode(parent: Handle, token: Tokens.Code): void {\n const pre = this.create(\"pre\");\n\n DOM.sink.apply(pre, { addClass: [PRE_CLASS] });\n\n const code = this.create(\"code\");\n\n DOM.sink.apply(code, { text: token.text });\n DOM.sink.appendChild(pre, code);\n\n const languageId = mapFenceLangToEditorId(token.lang);\n\n if (languageId === null) {\n DOM.sink.appendChild(parent, pre);\n\n return;\n }\n\n const wrapper = this.create(\"div\");\n\n DOM.sink.apply(wrapper, { addClass: [CODE_HOST_CLASS] });\n DOM.sink.appendChild(wrapper, pre);\n DOM.sink.appendChild(parent, wrapper);\n\n // Not a direct call: the kickoff itself, not just the DOM swap, waits\n // for Markdown's first connected layout. `startCodeEditorImport`\n // re-checks visibility itself rather than trusting `onFirstLayout`\n // alone to guarantee \"connected + displayed\" — see its own docblock\n // for why that guarantee doesn't hold for every registration.\n const entry: QueuedCodeUpgrade = { wrapper, pre, code, text: token.text, languageId, generation: this._renderGeneration };\n\n this.onFirstLayout(() => this.startCodeEditorImport(entry));\n }\n\n /**\n * Walks a list of inline-level tokens, appending each into `parent`.\n *\n * @param parent - The element handle to append into.\n * @param tokens - The inline-level tokens to render.\n */\n private appendInlineTokens(parent: Handle, tokens: Token[]): void {\n const sole = tokens.length === 1;\n\n for (const token of tokens) {\n this.appendInlineToken(parent, token, sole);\n }\n }\n\n /**\n * Dispatches a single inline-level token. A lone plain-text run writes\n * straight onto `parent`; interleaved runs are wrapped in a `<span>` (the\n * sink has no raw text-node primitive, so a text sibling of an element needs\n * its own element). Unknown types fall through to a text node.\n *\n * @param parent - The element handle to append into.\n * @param token - The inline-level token.\n * @param sole - Whether this is the only token being appended into `parent`,\n * allowing a direct text write instead of a wrapping span.\n */\n private appendInlineToken(parent: Handle, token: Token, sole: boolean): void {\n switch (token.type) {\n case \"text\": {\n const text = token as Tokens.Text;\n\n if (text.tokens && text.tokens.length > 0) {\n this.appendInlineTokens(parent, text.tokens);\n } else if (sole) {\n DOM.sink.apply(parent, { text: text.text });\n } else {\n this.appendTextNode(parent, text.text);\n }\n\n break;\n }\n\n case \"strong\": this.appendInlineWrapper(parent, \"strong\", (token as Tokens.Strong).tokens); break;\n case \"em\": this.appendInlineWrapper(parent, \"em\", (token as Tokens.Em).tokens); break;\n\n case \"codespan\": {\n const code = this.create(\"code\");\n\n DOM.sink.apply(code, { addClass: [CODE_CLASS], text: (token as Tokens.Codespan).text });\n DOM.sink.appendChild(parent, code);\n\n break;\n }\n\n case \"link\": this.appendLink(parent, token as Tokens.Link); break;\n\n default: this.appendTextNode(parent, (token as Tokens.Text).text ?? token.raw ?? \"\"); break;\n }\n }\n\n /**\n * Builds an inline emphasis wrapper (`<strong>` / `<em>`) around its inline\n * children.\n *\n * @param parent - The element handle to append into.\n * @param tag - The wrapper tag.\n * @param tokens - The inline children.\n */\n private appendInlineWrapper(parent: Handle, tag: string, tokens: Token[]): void {\n const wrapper = this.create(tag);\n\n this.appendInlineTokens(wrapper, tokens);\n DOM.sink.appendChild(parent, wrapper);\n }\n\n /**\n * Builds an `<a>` from the link token's href, passed through\n * {@link getLinkResolver}, and the link's inline text. `target`/`rel` are\n * set only when the resolution is external — the default resolver marks\n * everything external, reproducing the pre-`linkResolver` behaviour.\n *\n * @param parent - The element handle to append into.\n * @param token - The link token.\n */\n private appendLink(parent: Handle, token: Tokens.Link): void {\n const anchor = this.create(\"a\");\n const resolution = this.getLinkResolver()(token.href);\n const setAttr: Record<string, string> = { href: resolution.href };\n\n if (resolution.external) {\n setAttr.target = \"_blank\";\n setAttr.rel = \"noopener noreferrer\";\n }\n\n DOM.sink.apply(anchor, { addClass: [LINK_CLASS], setAttr });\n this.appendInlineTokens(anchor, token.tokens);\n DOM.sink.appendChild(parent, anchor);\n }\n\n /**\n * Appends a plain-text run wrapped in a `<span>`. Used for interleaved text\n * runs and the unsupported-token fallback, where a bare text node cannot be\n * a sibling of element children through the sink.\n *\n * @param parent - The element handle to append into.\n * @param text - The text content.\n */\n private appendTextNode(parent: Handle, text: string): void {\n const span = this.create(\"span\");\n\n DOM.sink.apply(span, { text });\n DOM.sink.appendChild(parent, span);\n }\n}\n\n/**\n * One heading extracted from Markdown source by {@link extractMarkdownHeadings}.\n *\n * @category Components\n */\nexport interface MarkdownHeading {\n /** The slugified id — byte-identical to the `id` {@link Markdown} renders onto the corresponding heading element. */\n id: string;\n /** The heading's plain text. */\n text: string;\n /** The heading's level, clamped to `[1, 6]`. */\n depth: number;\n}\n\n/**\n * Flattens a heading's inline token tree to plain text — the same content\n * {@link Markdown.appendInlineTokens} would render, minus the markup: a\n * `strong`/`em` wrapper contributes its inner text with no `**`/`_` marks, a\n * `codespan` contributes its code text with no backticks, and a `link`\n * contributes its label text with no `[]()` syntax. `heading.text` (the raw\n * source substring) is deliberately not used here — it still carries that\n * markup, which is correct for {@link nextHeadingId} (matching `appendHeading`'s\n * own slug input) but wrong for display.\n *\n * @param tokens - A heading's inline-level tokens (`Tokens.Heading.tokens`).\n * @returns The heading's rendered plain text.\n */\nfunction inlineText(tokens: Token[]): string {\n return tokens.map((token) => {\n switch (token.type) {\n case \"text\": return (token as Tokens.Text).tokens ? inlineText((token as Tokens.Text).tokens!) : (token as Tokens.Text).text;\n case \"strong\": return inlineText((token as Tokens.Strong).tokens);\n case \"em\": return inlineText((token as Tokens.Em).tokens);\n case \"codespan\": return (token as Tokens.Codespan).text;\n case \"link\": return inlineText((token as Tokens.Link).tokens);\n default: return (token as Tokens.Text).text ?? token.raw ?? \"\";\n }\n }).join(\"\");\n}\n\n/**\n * Recursively walks `tokens` for heading tokens, the same block-token shapes\n * {@link Markdown.appendBlockToken} recurses into for headings: top-level, and\n * nested inside a blockquote or a (loose) list item.\n *\n * @param tokens - The block tokens to walk.\n * @param headingIds - The current pass's dedupe counter — see `nextHeadingId`.\n * @param out - The array headings are appended to, in document order.\n */\nfunction collectHeadings(tokens: Token[], headingIds: Map<string, number>, out: MarkdownHeading[]): void {\n for (const token of tokens) {\n if (token.type === \"heading\") {\n const heading = token as Tokens.Heading;\n const depth = Math.min(Math.max(heading.depth, HEADING_MIN_DEPTH), HEADING_MAX_DEPTH);\n\n out.push({ id: nextHeadingId(heading.text, headingIds), text: inlineText(heading.tokens), depth });\n } else if (token.type === \"blockquote\") {\n collectHeadings((token as Tokens.Blockquote).tokens, headingIds, out);\n } else if (token.type === \"list\") {\n for (const item of (token as Tokens.List).items) {\n collectHeadings(item.tokens, headingIds, out);\n }\n }\n }\n}\n\n/**\n * Computes the heading outline of a Markdown source string, without building\n * any DOM — the ids produced are byte-identical to the `id` {@link Markdown}\n * renders onto the corresponding `<h1>`-`<h6>` element for the same source,\n * since both go through `nextHeadingId`.\n *\n * @param source - The Markdown source to extract headings from.\n * @returns The source's headings, in document order.\n *\n * @category Components\n */\nexport function extractMarkdownHeadings(source: string): MarkdownHeading[] {\n const headings: MarkdownHeading[] = [];\n\n collectHeadings(lexer(source), new Map<string, number>(), headings);\n\n return headings;\n}\n\n/**\n * Sub-pixel tolerance for \"at or above the pane's top\". A scroll-to-heading\n * lands its target via a delta computed from sub-pixel-precise\n * `getBoundingClientRect()` reads, but the native `scrollTop` it's applied\n * through can round the requested value — landing the heading a fraction of\n * a pixel past the pane's top, enough to fail a strict `<=` and fall back to\n * the previous heading.\n */\nconst ACTIVE_HEADING_TOP_TOLERANCE_PX = 1;\n\n/**\n * Resolves which heading in `headings` is at or nearest above\n * `scrollElement`'s viewport top — the last heading, in document order,\n * whose top edge is at or above the scroll container's own top (within\n * `ACTIVE_HEADING_TOP_TOLERANCE_PX`). Mirrors\n * `DocsContent.scrollToHeading`'s lookup technique in the read direction.\n *\n * Once `scrollElement` has scrolled to its maximum, the first heading that\n * hasn't yet reached the pane's own top is active outright, instead of\n * whichever heading last crossed it: a heading near the document's end (or\n * several, clustered together) may have less than a full viewport of\n * content left below it, so no amount of scrolling can bring it exactly to\n * the pane's top, and the top-crossing rule alone would otherwise resolve to\n * a much earlier heading than whichever one the scroll actually landed on.\n *\n * @param scrollElement - The scroll-owning element to read the pane's own top from.\n * @param headings - The document's headings, in document order.\n * @returns The active heading's id, or `null` when the pane's top is above every heading.\n *\n * @category Components\n */\nexport function findActiveHeading(scrollElement: Handle, headings: MarkdownHeading[]): string | null {\n const paneTop = DOM.source.getElementRect(scrollElement).top;\n const metrics = DOM.source.getScrollMetrics(scrollElement);\n const atMaxScroll = metrics.scrollHeight > metrics.clientHeight\n && metrics.scrollTop >= metrics.scrollHeight - metrics.clientHeight - ACTIVE_HEADING_TOP_TOLERANCE_PX;\n\n let active: string | null = null;\n\n for (const heading of headings) {\n const el = DOM.source.getElementById(heading.id);\n\n if (!el || !DOM.source.contains(scrollElement, el)) {\n continue;\n }\n\n if (DOM.source.getElementRect(el).top <= paneTop + ACTIVE_HEADING_TOP_TOLERANCE_PX) {\n active = heading.id;\n } else {\n // Headings are in document order; every later one is further\n // below. At max scroll, though, this first not-yet-reached\n // heading is already on screen — nothing can scroll it up any\n // further — so it wins outright rather than leaving whichever\n // earlier heading last crossed the top still active.\n if (atMaxScroll) {\n active = heading.id;\n }\n\n break;\n }\n }\n\n return active;\n}\n\nconst MarkdownCallable = callable(Markdown);\ntype MarkdownCallable = Markdown;\nexport {\n Markdown as _Markdown,\n MarkdownCallable as Markdown,\n // Not re-exported from the package barrel (`component/display/index.ts`):\n // a test-only hook, not part of the public API surface.\n mapFenceLangToEditorId,\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component } from \"~/core/Component.js\";\nimport { FloatingPanel, FloatingPanelOptions } from \"~/component/container/FloatingPanel.js\";\nimport { Tree } from \"~/component/tree/Tree.js\";\nimport type { TreeNode } from \"~/component/tree/TreeNode.js\";\nimport { Text } from \"~/component/input/Text.js\";\nimport { LabelTreeNodeRenderer } from \"~/component/tree/renderer/Label.js\";\nimport { VBox } from \"~/layout/VBox.js\";\nimport { Fit } from \"~/layout/Fit.js\";\nimport { Insets } from \"~/primitive/Insets.js\";\nimport { UNBOUNDED } from \"~/primitive/Size.js\";\nimport type { Size } from \"~/primitive/Size.js\";\nimport { ListenerBag } from \"~/core/ListenerBag.js\";\nimport { callable } from \"~/core/Callable.js\";\nimport type { MarkdownHeading } from \"~/component/display/Markdown.js\";\n\n/**\n * Structural interface a scroll-owning host exposes so a {@link\n * MarkdownMinimap} can highlight the heading currently on screen without\n * depending on `Markdown`, `Panel`, or any other concrete scroll owner.\n */\nexport interface HeadingScrollSource {\n on(event: \"activeheadingchange\", listener: (headingId: string | null) => void): unknown;\n off(event: \"activeheadingchange\", listener: (headingId: string | null) => void): unknown;\n}\n\n/** String-literal union of the events emitted by {@link MarkdownMinimap}. */\nexport type MarkdownMinimapEvent = \"select\";\n\n/**\n * Construction-time options for {@link MarkdownMinimap}.\n *\n * @category Components\n */\nexport interface MarkdownMinimapOptions extends FloatingPanelOptions {\n /** Deepest heading depth shown; deeper headings are dropped entirely. Default `3`. */\n maxHeadingDepth?: number;\n\n /** The scroll-owning source whose active-heading changes drive the highlighted row. */\n scrollSource?: HeadingScrollSource;\n\n listeners?: {\n select?: (headingId: string) => void;\n };\n}\n\n/** Deepest heading depth shown when the caller doesn't specify one. */\nconst DEFAULT_MAX_HEADING_DEPTH = 3;\n\n/** Header row text above the tree. */\nconst HEADER_TEXT = \"On this page\";\n\n/**\n * Height cap: the header plus roughly 20 rows at `Tree`'s fixed 24px row\n * height. `Tree.getPreferredSize` now reports a height derived from the\n * flattened row count with no ceiling of its own (see `Tree.ts`), so an\n * outline longer than this needs to scroll instead of committing an\n * unbounded height that can run past the viewport with nothing to scroll it\n * back into view. Deliberately no `autoScroll` on this outer panel to\n * provide that scrolling: `Tree` is already a virtualized, self-scrolling\n * list (its own row pool plus scrollbar overlays — see `Tree.ts`'s class\n * doc), so marking this panel's own axis as overflowing too would tell\n * `BoxLayout.computeShrink` to skip shrinking `Tree` to fit (it explicitly\n * skips the shrink when the axis is overflowing), leaving `Tree` laid out at\n * its full uncapped content height with both this panel and `Tree` then\n * scrolling the same content independently. Without `autoScroll` here, the\n * VBox shrinks `Tree` down to whatever's left under this cap once it\n * overflows, and `Tree`'s own scrolling takes over from there — one\n * scrollbar, not two.\n */\nconst DEFAULT_MAX_HEIGHT_PX = 500;\n\n/**\n * Width floor: enough for the header text and a reasonably short heading\n * label without wrapping to nothing. `Anchor` bypasses the cell clamp\n * entirely (see `FloatingPanel`'s own docs), so this is a no-op for a\n * floating `MarkdownMinimap`; it only matters for a docked one placed in a\n * shrinking layout (e.g. an `HBox` row) — without it, that layout manager's\n * own shrink-to-min distribution has nothing to stop this panel collapsing\n * to an unreadable sliver on a narrow viewport.\n */\nconst DEFAULT_MIN_WIDTH_PX = 160;\n\n/**\n * Default preferred width. Wider than `Tree`'s own generic content-derived\n * default (200px, `Tree.ts`'s `DEFAULT_PREFERRED_WIDTH`) so a heading label\n * — which can run considerably longer than a typical Tree row elsewhere in\n * this codebase — has more room before truncating.\n */\nconst DEFAULT_WIDTH_PX = 240;\n\n/**\n * Row-label font size in pixels — smaller than `Text`'s own 14px ambient\n * default (the prose this outlines renders at that default), so the outline\n * reads as a secondary navigation aid rather than a second copy of the text\n * at the same visual weight. A plain pixel number, not a relative unit:\n * `Text.setFontSize`'s string overload binds to a named CSS custom property\n * (a theme token) rather than accepting an arbitrary CSS length, so\n * `\"0.85em\"` would silently fail to apply.\n */\nconst ROW_FONT_SIZE = 12;\n\n/** Padding around the header row's text — top/bottom give it room beyond its bare line height. */\nconst HEADER_PADDING = new Insets(8, 12, 4, 12);\n\nconst _defaultMarkdownMinimapOptions: Partial<MarkdownMinimapOptions> = {\n maxHeadingDepth: DEFAULT_MAX_HEADING_DEPTH,\n // Caps the otherwise-unbounded content-derived height (see\n // DEFAULT_MAX_HEIGHT_PX). Class default (not an imperative setter call)\n // so a caller-supplied maxSize still wins.\n maxSize: { width: UNBOUNDED, height: DEFAULT_MAX_HEIGHT_PX },\n minSize: { width: DEFAULT_MIN_WIDTH_PX, height: 0 },\n};\n\n/**\n * A floating card showing a document's heading outline as a `Tree`.\n *\n * Builds a real `TreeNode[]` hierarchy from a flat `MarkdownHeading[]` list\n * (see {@link setHeadings}) and, when constructed with a {@link\n * HeadingScrollSource}, highlights whichever heading is currently on screen.\n * Emits a semantic `\"select\"` event carrying the clicked heading's id rather\n * than navigating itself — the caller decides what \"select\" means (scroll an\n * owned `Markdown`, call a router), the same division of responsibility\n * `Tree` itself keeps between emitting `\"selection\"` and acting on it.\n *\n * @category Components\n */\nclass MarkdownMinimap extends FloatingPanel<MarkdownMinimapOptions> {\n\n private readonly _tree: Tree;\n private readonly _listeners: ListenerBag<MarkdownMinimapEvent> = new ListenerBag<MarkdownMinimapEvent>();\n private readonly _scrollSource: HeadingScrollSource | null;\n\n /** Shown heading id -> its `TreeNode`. */\n private _nodesById: Map<string, TreeNode> = new Map();\n\n /** Every heading id, shown or not, -> the nearest ancestor id that IS shown (or `null`). */\n private _nearestShown: Map<string, string | null> = new Map();\n\n private readonly handleSelection: (nodes: TreeNode[]) => void = (nodes) => this.onTreeSelection(nodes);\n private readonly handleActiveHeadingChange: (headingId: string | null) => void = (id) => this.applyActiveHeading(id);\n\n constructor(options?: MarkdownMinimapOptions, subclassDefaults?: Partial<MarkdownMinimapOptions>) {\n super(options, { ..._defaultMarkdownMinimapOptions, ...(subclassDefaults ?? {}) });\n\n // The opaque card surface lives here, on the outer panel; the inner\n // Tree stays transparent (see class doc) so there is exactly one\n // opaque box, not two stacked ones.\n this.setBackgroundColor(\"var(--ts-ui-input-bg, rgb(255, 255, 255))\");\n this.setShadow(\"var(--ts-ui-popover-shadow, 2px 4px 12px rgba(0, 0, 0, 0.18))\");\n this.setBorderRadius(\"var(--ts-ui-border-radius, 4px)\");\n // stretching: true fills the header and the tree to the panel's own\n // width — the same reason DocsSidebar's own Tree-hosting layouts pass it.\n this.setLayoutManager(new VBox({ spacing: 4, stretching: true }));\n\n const headerText = new Text(HEADER_TEXT);\n headerText.setFontSize(12);\n headerText.setFontWeight(\"700\");\n // Dims the ambient (theme-aware) text colour rather than hardcoding a\n // grey, so this reads correctly in both light and dark themes.\n headerText.setOpacity(0.6);\n\n // `padding` on a bare `Text` is invisible to its own getPreferredSize\n // (which reports only measured font metrics — see Text.ts), so the\n // padding has to live on a wrapping row instead, or VBox allocates\n // the header only its bare line height and the real CSS padding then\n // clips into that too-small box.\n const headerRow = new Component({ layoutManager: new Fit(), padding: HEADER_PADDING });\n headerRow.addComponent(headerText);\n this.addComponent(headerRow);\n\n // \"clip\": a heading label is read, not scrolled sideways to see in\n // full — an outline is a navigation aid, not a place to read a long\n // title one horizontal-scroll-drag at a time.\n this._tree = new Tree({ backgroundColor: \"transparent\", rowOverflow: \"clip\" });\n this._tree.setRendererFactory(() => {\n const renderer = new LabelTreeNodeRenderer();\n renderer.getLabel().setFontSize(ROW_FONT_SIZE);\n\n return renderer;\n });\n this._tree.on(\"selection\", this.handleSelection);\n this.addComponent(this._tree);\n\n this._scrollSource = options?.scrollSource ?? null;\n this._scrollSource?.on(\"activeheadingchange\", this.handleActiveHeadingChange);\n\n this.applyListeners(options?.listeners);\n }\n\n /**\n * Reports `DEFAULT_WIDTH_PX` as the preferred width when the\n * caller has set no explicit `preferredSize` and the computed width\n * (driven by `Tree`'s own generic, content-agnostic default) is\n * narrower — height keeps its normal (content-derived, capped) VBox\n * computation.\n *\n * @returns The preferred `{width, height}`.\n */\n getPreferredSize(): Size | null {\n if (this.getPreferredSizeConstraint() !== null) {\n return super.getPreferredSize();\n }\n\n const computed = super.getPreferredSize();\n\n if (!computed) {\n return computed;\n }\n\n return { width: Math.max(computed.width, DEFAULT_WIDTH_PX), height: computed.height };\n }\n\n /**\n * Dispatches {@link MarkdownMinimapOptions.maxHeadingDepth}; every other\n * option is inherited from {@link FloatingPanel}.\n *\n * @param options - The options bag carrying the values to apply.\n * @returns This panel, for method chaining.\n */\n protected applyOptions(options: MarkdownMinimapOptions): this {\n super.applyOptions(options);\n\n if (options.maxHeadingDepth !== undefined) {\n this._options.maxHeadingDepth = options.maxHeadingDepth;\n }\n\n return this;\n }\n\n /**\n * The deepest heading depth shown; a heading at or past `maxHeadingDepth +\n * 1` has no row.\n *\n * @returns The cached {@link MarkdownMinimapOptions.maxHeadingDepth}, or the class default when never set.\n */\n getMaxHeadingDepth(): number {\n return this._options.maxHeadingDepth ?? this._defaultOptions.maxHeadingDepth ?? DEFAULT_MAX_HEADING_DEPTH;\n }\n\n /**\n * Replaces the shown outline, rebuilding the tree from `headings`. A\n * heading past {@link getMaxHeadingDepth} is dropped entirely — it nests\n * under no other row, and no row of its own survives either — while still\n * anchoring any of its own descendants (that are within depth) to its\n * nearest shown ancestor.\n *\n * @param headings - The document's headings, in document order.\n * @returns This panel, for method chaining.\n */\n setHeadings(headings: MarkdownHeading[]): this {\n const maxDepth = this.getMaxHeadingDepth();\n const roots: TreeNode[] = [];\n const stack: Array<{ depth: number; node: TreeNode }> = [];\n const nodesById = new Map<string, TreeNode>();\n const nearestShown = new Map<string, string | null>();\n\n for (const heading of headings) {\n while (stack.length > 0 && stack[stack.length - 1].depth >= heading.depth) {\n stack.pop();\n }\n\n const ancestor = stack.length > 0 ? stack[stack.length - 1].node : null;\n const shown = heading.depth <= maxDepth;\n\n if (shown) {\n const node: TreeNode = { label: heading.text, data: heading.id, children: [] };\n\n if (ancestor) {\n ancestor.children!.push(node);\n } else {\n roots.push(node);\n }\n\n nodesById.set(heading.id, node);\n nearestShown.set(heading.id, heading.id);\n stack.push({ depth: heading.depth, node });\n } else {\n nearestShown.set(heading.id, ancestor ? (nearestShown.get(ancestor.data as string) ?? null) : null);\n }\n }\n\n this._nodesById = nodesById;\n this._nearestShown = nearestShown;\n\n this._tree.setNodes(roots);\n this._tree.expandAll();\n\n return this;\n }\n\n on(event: \"select\", listener: (headingId: string) => void): this {\n this._listeners.add(event, listener);\n\n return this;\n }\n\n off(event: \"select\", listener: (headingId: string) => void): this {\n this._listeners.remove(event, listener);\n\n return this;\n }\n\n protected emit(event: \"select\", headingId: string): void {\n this._listeners.fire(event, headingId);\n }\n\n /**\n * Maps a `Tree` `\"selection\"` event onto this minimap's own `\"select\"`\n * event, carrying just the clicked heading's id.\n *\n * @param nodes - The tree's newly selected nodes; only the first is used.\n */\n private onTreeSelection(nodes: TreeNode[]): void {\n const node = nodes[0];\n\n if (!node || node.data === undefined) {\n return;\n }\n\n this.emit(\"select\", node.data as string);\n }\n\n /**\n * Highlights the row for `id`'s nearest shown ancestor (or `id` itself\n * when it is shown). A `null` id — no heading is above the scroll pane's\n * top yet — is a no-op, leaving the previously selected row standing.\n *\n * @param id - The scroll source's newly active heading id, or `null`.\n */\n private applyActiveHeading(id: string | null): void {\n if (id === null) {\n return;\n }\n\n const resolvedId = this._nearestShown.get(id) ?? null;\n\n if (resolvedId === null) {\n return;\n }\n\n const node = this._nodesById.get(resolvedId);\n\n if (node) {\n this._tree.selectNode(node);\n }\n }\n\n /**\n * Unwires the `scrollSource` listener before the inherited destructor\n * disposes the tree — a `scrollSource` outliving this minimap must not\n * keep firing into torn-down state.\n */\n protected destructor(): void {\n this._scrollSource?.off(\"activeheadingchange\", this.handleActiveHeadingChange);\n\n super.destructor();\n }\n}\n\nconst MarkdownMinimapCallable = callable(MarkdownMinimap);\ntype MarkdownMinimapCallable = MarkdownMinimap;\nexport {\n MarkdownMinimap as _MarkdownMinimap,\n MarkdownMinimapCallable as MarkdownMinimap,\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Panel, PanelOptions } from \"~/core/Panel.js\";\nimport { Anchor } from \"~/layout/Anchor.js\";\nimport { AnchorConstraints } from \"~/layout/AnchorConstraints.js\";\nimport { VBox } from \"~/layout/VBox.js\";\nimport { Insets } from \"~/primitive/Insets.js\";\nimport { FloatingPanel } from \"~/component/container/FloatingPanel.js\";\nimport { Button } from \"~/component/button/Button.js\";\nimport { Glyph } from \"~/component/display/Glyph.js\";\nimport { compress } from \"~/glyphs/solid/compress.js\";\nimport { expand } from \"~/glyphs/solid/expand.js\";\nimport { magnifying_glass_plus } from \"~/glyphs/solid/magnifying_glass_plus.js\";\nimport { magnifying_glass_minus } from \"~/glyphs/solid/magnifying_glass_minus.js\";\nimport { arrows_rotate } from \"~/glyphs/solid/arrows_rotate.js\";\nimport { Event } from \"~/core/Event.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport { ListenerBag } from \"~/core/ListenerBag.js\";\nimport { Util } from \"~/core/Util.js\";\nimport { callable } from \"~/core/Callable.js\";\nimport { Markdown, extractMarkdownHeadings, findActiveHeading } from \"~/component/display/Markdown.js\";\nimport type { MarkdownLinkResolver, MarkdownHeading } from \"~/component/display/Markdown.js\";\nimport { MarkdownMinimap } from \"~/component/display/MarkdownMinimap.js\";\nimport type { HeadingScrollSource } from \"~/component/display/MarkdownMinimap.js\";\n\nGlyph.register(compress, expand, magnifying_glass_plus, magnifying_glass_minus, arrows_rotate);\n\n/**\n * Prose column width presets, in `ch` units — narrow / default / wide. `70`\n * matches the theme's own `--ts-ui-md-max-measure` default, so stepping back\n * to the middle preset reads the same as never having touched the control.\n */\nconst WIDTH_PRESETS_CH = [60, 70, 90];\n\n/** Font-scale presets the zoom controls step through. */\nconst ZOOM_PRESETS = [0.85, 1.0, 1.15, 1.3];\n\n/** Index into {@link WIDTH_PRESETS_CH} a freshly constructed viewer starts at. */\nconst DEFAULT_WIDTH_INDEX = 1;\n\n/** Index into {@link ZOOM_PRESETS} a freshly constructed viewer starts at. */\nconst DEFAULT_ZOOM_INDEX = 1;\n\n/**\n * Left margin kept on the internal `Markdown`'s own content box, so the\n * prose starts indented from the pane's edge the way text sits on a printed\n * page or in a word processor, rather than flush against it.\n */\nconst PROSE_LEFT_MARGIN_PX = 32;\n\n/**\n * Construction-time options for {@link MarkdownViewer}.\n *\n * @category Components\n */\nexport interface MarkdownViewerOptions extends PanelOptions {\n /** The Markdown source string to render. */\n markdown?: string;\n\n /** Maps an authored link href to its rendered form; forwarded to the internal `Markdown`. */\n linkResolver?: MarkdownLinkResolver;\n\n /** Deepest heading depth the minimap shows. Default `3`, forwarded to the internal `MarkdownMinimap`. */\n maxHeadingDepth?: number;\n\n /** Whether the floating heading outline shows. Default `true`. */\n showMinimap?: boolean;\n\n /** Whether the floating width/zoom controls show. Default `true`. */\n showControls?: boolean;\n\n listeners?: {\n activeheadingchange?: (headingId: string | null) => void;\n };\n}\n\nconst _defaultMarkdownViewerOptions: Partial<MarkdownViewerOptions> = {\n showMinimap: true,\n showControls: true,\n};\n\n/**\n * A single-document Markdown viewer with a floating outline minimap and\n * width/zoom controls, both pinned over the prose.\n *\n * Wraps one `Markdown` instance in an `Anchor` layout, with the minimap\n * top-right and the controls bottom-right. Any consumer embedding one\n * `Markdown` instance gets both for free by using `MarkdownViewer` instead\n * of `Markdown` directly. Exposes the same `\"activeheadingchange\"` event\n * `DocsContent` exposes, computed from its own native scroll the same way —\n * see {@link HeadingScrollSource}. `MarkdownMinimap` consumes that event to\n * highlight whichever heading is currently on screen without depending on\n * this class concretely.\n *\n * @category Components\n */\nclass MarkdownViewer extends Panel<MarkdownViewerOptions> implements HeadingScrollSource {\n\n private readonly _markdown: Markdown;\n private readonly _minimap: MarkdownMinimap;\n private _controls!: FloatingPanel;\n private _narrowerBtn!: Button;\n private _widerBtn!: Button;\n private _zoomOutBtn!: Button;\n private _zoomInBtn!: Button;\n private _resetBtn!: Button;\n\n private _headings: MarkdownHeading[];\n private _widthIndex: number = DEFAULT_WIDTH_INDEX;\n private _zoomIndex: number = DEFAULT_ZOOM_INDEX;\n private _lastActiveHeadingId: string | null = null;\n\n /**\n * The scrollTop {@link scrollToHeading} last landed the pane on, or\n * `null` once a later native scroll has moved past it. Lets\n * `onNativeScroll` recognise \"nothing has organically scrolled since\n * that click\" and skip re-deriving the active heading from geometry —\n * see both methods' own doc comments for why that re-derivation alone\n * cannot be trusted here.\n */\n private _pendingClickScrollTop: number | null = null;\n\n private readonly _listeners: ListenerBag<\"activeheadingchange\"> = new ListenerBag<\"activeheadingchange\">();\n\n private readonly handleNativeScroll: () => void = () => this.onNativeScroll();\n private readonly handleMinimapSelect: (id: string) => void = (id) => this.scrollToHeading(id);\n private readonly _onNarrower: () => void = () => this.stepWidth(-1);\n private readonly _onWider: () => void = () => this.stepWidth(1);\n private readonly _onZoomOut: () => void = () => this.stepZoom(-1);\n private readonly _onZoomIn: () => void = () => this.stepZoom(1);\n private readonly _onReset: () => void = () => this.resetViewerProperties();\n\n constructor(options?: MarkdownViewerOptions, subclassDefaults?: Partial<MarkdownViewerOptions>) {\n super(options, {\n ..._defaultMarkdownViewerOptions,\n ...(subclassDefaults ?? {}),\n // Last, so a subclass default can't silently swap out the\n // Anchor every addComponent call below depends on. (A caller\n // passing its own `options.layoutManager` still wins, per\n // Component's own dispatch — the same pre-existing tradeoff\n // DiagramView's own Anchor-dependent constructor carries.)\n layoutManager: new Anchor(),\n autoScroll: \"y\",\n } as Partial<MarkdownViewerOptions>);\n\n this._markdown = new Markdown(options?.markdown, {\n linkResolver: options?.linkResolver,\n padding: new Insets(0, 0, 0, PROSE_LEFT_MARGIN_PX),\n });\n\n const markdownConstraints = new AnchorConstraints();\n markdownConstraints.left = 0;\n markdownConstraints.right = 0;\n this.addComponent(this._markdown, markdownConstraints);\n\n this._headings = extractMarkdownHeadings(options?.markdown ?? \"\");\n\n this._minimap = new MarkdownMinimap({ scrollSource: this, maxHeadingDepth: options?.maxHeadingDepth, corner: \"top-right\" });\n this._minimap.setHeadings(this._headings);\n this._minimap.on(\"select\", this.handleMinimapSelect);\n this.addComponent(this._minimap, this._minimap.getAnchorConstraints());\n\n this.buildControls();\n this.wireControlListeners();\n this.addComponent(this._controls, this._controls.getAnchorConstraints());\n\n Event.addSubtreeListener(this, \"scroll\", this.handleNativeScroll);\n\n this.setMinimapVisible(this._options.showMinimap ?? this._defaultOptions.showMinimap ?? true);\n this.setControlsVisible(this._options.showControls ?? this._defaultOptions.showControls ?? true);\n\n this.applyListeners(options?.listeners);\n }\n\n /**\n * Lays out `Markdown` / the minimap / the controls as usual, then\n * re-hugs the minimap against `_markdown`'s freshly committed geometry —\n * `MarkdownMinimap.placeNextTo` needs calling after every pass that can\n * move either this viewer's own width or `_markdown`'s rendered width\n * (see its own doc comment for why it's an owner-driven call rather than\n * a `MarkdownMinimap`-internal `doLayout` override).\n *\n * @returns This viewer, for method chaining.\n */\n doLayout(): this {\n super.doLayout();\n\n // Guards against the super() cascade's own options dispatch (e.g.\n // setAutoScroll) triggering a layout pass before this constructor has\n // reached the point of assigning _minimap/_markdown.\n this._minimap?.placeNextTo(this._markdown ?? null);\n\n return this;\n }\n\n /**\n * Dispatches `showMinimap` / `showControls`; every other option either\n * routes to the internal `Markdown` (constructor-only) or is inherited\n * from `Panel`.\n *\n * @param options - The options bag carrying the values to apply.\n * @returns This viewer, for method chaining.\n */\n protected applyOptions(options: MarkdownViewerOptions): this {\n super.applyOptions(options);\n\n // Cached only: the minimap/controls don't exist yet during the\n // super() cascade. The constructor dispatches\n // setMinimapVisible/setControlsVisible itself once they're built —\n // mirrors DiagramView's own `controls` option.\n if (options.showMinimap !== undefined) {\n this._options.showMinimap = options.showMinimap;\n }\n\n if (options.showControls !== undefined) {\n this._options.showControls = options.showControls;\n }\n\n return this;\n }\n\n /**\n * Read-only escape hatch; change content via {@link setMarkdown}, not by\n * calling `getMarkdown().setMarkdown(...)` directly — that would desync\n * the minimap.\n *\n * @returns The internal `Markdown` instance.\n */\n getMarkdown(): Markdown {\n return this._markdown;\n }\n\n /**\n * Replaces the rendered source, recomputes headings, and refreshes the minimap.\n *\n * @param markdown - The new Markdown source string.\n * @returns This viewer, for method chaining.\n */\n setMarkdown(markdown: string): this {\n this._markdown.setMarkdown(markdown);\n this._headings = extractMarkdownHeadings(markdown);\n this._minimap.setHeadings(this._headings);\n\n return this;\n }\n\n /**\n * Whether the floating heading-outline minimap shows.\n *\n * @returns `true` when the minimap is visible.\n */\n isMinimapVisible(): boolean {\n return this._options.showMinimap ?? this._defaultOptions.showMinimap ?? true;\n }\n\n /**\n * Shows or hides the floating heading-outline minimap.\n *\n * @param value - Whether the minimap is visible.\n * @returns This viewer, for method chaining.\n */\n setMinimapVisible(value: boolean): this {\n this._options.showMinimap = value;\n this._minimap.setVisible(value);\n\n return this;\n }\n\n /**\n * Whether the floating width/zoom control cluster shows.\n *\n * @returns `true` when the control cluster is visible.\n */\n isControlsVisible(): boolean {\n return this._options.showControls ?? this._defaultOptions.showControls ?? true;\n }\n\n /**\n * Shows or hides the floating width/zoom control cluster.\n *\n * @param value - Whether the control cluster is visible.\n * @returns This viewer, for method chaining.\n */\n setControlsVisible(value: boolean): this {\n this._options.showControls = value;\n this._controls.setVisible(value);\n\n return this;\n }\n\n on(event: \"activeheadingchange\", listener: (headingId: string | null) => void): this {\n this._listeners.add(event, listener);\n\n return this;\n }\n\n off(event: \"activeheadingchange\", listener: (headingId: string | null) => void): this {\n this._listeners.remove(event, listener);\n\n return this;\n }\n\n protected emit(event: \"activeheadingchange\", headingId: string | null): void {\n this._listeners.fire(event, headingId);\n }\n\n /** Builds the corner-pinned width-narrower/wider, zoom-out/in, and reset control cluster. */\n private buildControls(): void {\n this._narrowerBtn = this.makeControlButton(\"compress\", \"Narrower\");\n this._widerBtn = this.makeControlButton(\"expand\", \"Wider\");\n this._zoomOutBtn = this.makeControlButton(\"magnifying-glass-minus\", \"Zoom out\");\n this._zoomInBtn = this.makeControlButton(\"magnifying-glass-plus\", \"Zoom in\");\n this._resetBtn = this.makeControlButton(\"arrows-rotate\", \"Reset\");\n\n this._controls = new FloatingPanel({ corner: \"bottom-right\", layoutManager: new VBox({ spacing: 4 }) });\n this._controls.addComponent(this._narrowerBtn);\n this._controls.addComponent(this._widerBtn);\n this._controls.addComponent(this._zoomOutBtn);\n this._controls.addComponent(this._zoomInBtn);\n this._controls.addComponent(this._resetBtn);\n }\n\n /**\n * Builds a glyph-only control button with an accessible label, mirroring\n * `DiagramView`'s own control-cluster buttons.\n *\n * @param glyph - The glyph name to show.\n * @param label - The accessible name (drives `aria-label` and the tooltip).\n * @returns The configured button.\n */\n private makeControlButton(glyph: string, label: string): Button {\n return new Button({ glyph, text: label, showText: false });\n }\n\n /** Wires the control cluster's buttons to their viewer-property step methods. */\n private wireControlListeners(): void {\n this._narrowerBtn.on(\"action\", this._onNarrower);\n this._widerBtn.on(\"action\", this._onWider);\n this._zoomOutBtn.on(\"action\", this._onZoomOut);\n this._zoomInBtn.on(\"action\", this._onZoomIn);\n this._resetBtn.on(\"action\", this._onReset);\n }\n\n /**\n * Moves `_widthIndex` by one step, clamped to `WIDTH_PRESETS_CH`'s bounds,\n * and applies the resulting preset to the internal `Markdown`.\n *\n * @param direction - `1` for wider, `-1` for narrower.\n */\n private stepWidth(direction: 1 | -1): void {\n this._widthIndex = Util.clamp(this._widthIndex + direction, 0, WIDTH_PRESETS_CH.length - 1);\n this._markdown.setMaxMeasure(WIDTH_PRESETS_CH[this._widthIndex] + \"ch\");\n // setMaxMeasure writes a CSS rule directly and schedules no layout of\n // its own, so the minimap's hug would otherwise go stale against the\n // prose's new rendered width — see MarkdownMinimap.placeNextTo.\n this._minimap.placeNextTo(this._markdown);\n }\n\n /**\n * Moves `_zoomIndex` by one step, clamped to `ZOOM_PRESETS`'s bounds, and\n * applies the resulting preset to the internal `Markdown`.\n *\n * @param direction - `1` for zoom in, `-1` for zoom out.\n */\n private stepZoom(direction: 1 | -1): void {\n this._zoomIndex = Util.clamp(this._zoomIndex + direction, 0, ZOOM_PRESETS.length - 1);\n this._markdown.setFontScale(ZOOM_PRESETS[this._zoomIndex]);\n // setFontScale can also change the prose's rendered width (ch-based\n // maxMeasure scales with font size) — see stepWidth's own comment.\n this._minimap.placeNextTo(this._markdown);\n }\n\n /**\n * Resets both viewer properties to their default index and clears their\n * overrides entirely (`setMaxMeasure(null)` / `setFontScale(1)`) rather\n * than re-applying the default preset, so a live theme change afterward\n * still takes effect.\n */\n private resetViewerProperties(): void {\n this._widthIndex = DEFAULT_WIDTH_INDEX;\n this._zoomIndex = DEFAULT_ZOOM_INDEX;\n this._markdown.setMaxMeasure(null);\n this._markdown.setFontScale(1);\n this._minimap.placeNextTo(this._markdown);\n }\n\n /**\n * Computes the active heading from the current native scroll position and\n * emits `\"activeheadingchange\"` only when it differs from the previous\n * tick. A no-op while the pane is still sitting exactly where {@link\n * scrollToHeading} last left it (see `_pendingClickScrollTop`'s own doc\n * comment) — geometry alone can't be trusted to reproduce that click's\n * own target there, so this defers to whatever it already set.\n */\n private onNativeScroll(): void {\n const scrollElement = this.getScrollElement();\n\n if (!scrollElement) {\n return;\n }\n\n if (this._pendingClickScrollTop !== null) {\n // Reads the live DOM value, not the cached getScrollTop(): an\n // organic scroll (wheel, scrollbar drag) updates the pane's real\n // scrollTop without ever going through setScrollTop, so the cache\n // would otherwise still read the click's own landing spot forever.\n if (DOM.source.getScrollTop(scrollElement) === this._pendingClickScrollTop) {\n return;\n }\n\n this._pendingClickScrollTop = null;\n }\n\n const id = findActiveHeading(scrollElement, this._headings);\n\n if (id === this._lastActiveHeadingId) {\n return;\n }\n\n this._lastActiveHeadingId = id;\n this.emit(\"activeheadingchange\", id);\n }\n\n /**\n * Scrolls this viewer so `id`'s heading sits at the pane's own top edge —\n * the geometry technique `DocsContent.scrollToHeading` uses, kept local to\n * this class rather than shared (see the plan's Non-Goals). Marks `id`\n * active immediately rather than waiting for the resulting native scroll\n * event to drive that through `findActiveHeading`: a heading close to the\n * document's end can share its clamped landing scrollTop with a\n * neighbouring heading, and geometry alone then can't tell which of them\n * this click actually targeted (`findActiveHeading`'s own doc comment).\n *\n * @param id - The heading id to scroll to.\n */\n private scrollToHeading(id: string): void {\n const scrollElement = this.getScrollElement();\n\n if (!scrollElement) {\n return;\n }\n\n const heading = DOM.source.getElementById(id);\n\n if (!heading || !DOM.source.contains(scrollElement, heading)) {\n return;\n }\n\n const headingTop = DOM.source.getElementRect(heading).top;\n const paneTop = DOM.source.getElementRect(scrollElement).top;\n\n this.setScrollTop(this.getScrollTop() + (headingTop - paneTop));\n this._pendingClickScrollTop = this.getScrollTop();\n\n if (id !== this._lastActiveHeadingId) {\n this._lastActiveHeadingId = id;\n this.emit(\"activeheadingchange\", id);\n }\n }\n}\n\nconst MarkdownViewerCallable = callable(MarkdownViewer);\ntype MarkdownViewerCallable = MarkdownViewer;\nexport {\n MarkdownViewer as _MarkdownViewer,\n MarkdownViewerCallable as MarkdownViewer,\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { HBox } from \"~/layout/HBox.js\";\nimport { Insets } from \"~/primitive/Insets.js\";\nimport { Label } from \"~/component/input/Label.js\";\nimport { Glyph } from \"~/component/display/Glyph.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * Construction-time options for {@link IconLabel}.\n *\n * @category Components\n */\nexport interface IconLabelOptions extends ComponentOptions {\n glyph?: string;\n text?: string;\n forId?: string;\n gap?: number;\n}\n\n/**\n * User-overridable defaults forwarded to `super` via the options bag. The\n * cascade dispatches each present setter once with the final value.\n * `layoutManager` is *not* listed — each instance needs its own fresh HBox.\n * `gap`/`glyph`/`text`/`forId` are late-built state (their setters reach into\n * children) and are written pure by `applyOptions`, then dispatched from the\n * constructor body once the row exists.\n */\nconst _defaultIconLabelOptions: Partial<IconLabelOptions> = {\n gap: 2,\n insets: new Insets(0, 0, 0, 0),\n};\n\n/**\n * A small composite pairing a leading [`Glyph`](/api/component/display/classes/Glyph)\n * with a trailing [`Label`](/api/component/input/classes/Label), laid out\n * horizontally with a configurable gap (default 2).\n *\n * Use this when the icon belongs to a form control: the inner element is a\n * real `<label for=\"…\">`, so the browser still focuses the associated input\n * when the label text is clicked. For icon-with-text that has no form-control\n * association, use [`IconText`](/api/component/display/classes/IconText) instead.\n *\n * @example\n * ```typescript\n * import { IconLabel } from '@jimka/typescript-ui/component/display';\n * import { TextField } from '@jimka/typescript-ui/component/input';\n *\n * const field = new TextField();\n * panel.addComponent(new IconLabel('times', 'Email:', field.getId()));\n * panel.addComponent(field);\n * ```\n *\n * @category Components\n */\nclass IconLabel extends Component<IconLabelOptions> {\n\n private _glyph!: Glyph;\n private _label!: Label;\n\n /**\n * Constructs an `IconLabel` pairing the named glyph with a `<label for=\"…\">`.\n *\n * @param glyph - Registry glyph name. Must be present in the internal `Glyphs` registry.\n * @param text - Label text shown to the right of the glyph.\n * @param forId - Element id of the form control this label is associated with.\n * Must be non-empty; mirrors [`Label`](/api/component/input/classes/Label)'s constructor contract.\n * @param options - Optional configuration bag (gap override, common Component fields).\n * @param subclassDefaults - Per-subclass default bag layered over this\n * class's defaults; subclasses forward their `_defaultXxxOptions`\n * constant here.\n */\n constructor(\n glyph: string,\n text: string,\n forId: string,\n options?: IconLabelOptions,\n subclassDefaults?: Partial<IconLabelOptions>,\n ) {\n super(options, { ..._defaultIconLabelOptions, ...(subclassDefaults ?? {}) });\n\n // Per-instance layout manager seeded with the effective gap (caller\n // override, else the class default) so it flows into the HBox spacing.\n this.setLayoutManager(new HBox({ spacing: this.getGap() }));\n\n // Build children with the effective values up front so the late-built\n // dispatch below has nothing to overwrite. The bag-written values\n // from the cascade take precedence over the positional arguments.\n // `setGlyph` would rebuild the inner Glyph, and `setText`/`setForId`\n // would push the same value into the Label a second time — so we\n // resolve the effective value here once.\n const effectiveGlyph = this._options.glyph ?? glyph;\n const effectiveText = this._options.text ?? text;\n const effectiveForId = this._options.forId ?? forId;\n\n this._glyph = new Glyph(effectiveGlyph);\n this._label = new Label(effectiveText, effectiveForId);\n\n this.addComponent(this._glyph);\n this.addComponent(this._label);\n\n // Late-built state: bag-written by `applyOptions`. Only `gap` needs\n // post-construction dispatch — the HBox's spacing was seeded from\n // the bag at construction, but a later cascade-time write to\n // `_options.gap` (consumer override) needs to push into the HBox.\n if (this._options.gap !== undefined) {\n (this.getLayoutManager() as HBox).setComponentSpacing(this._options.gap);\n }\n }\n\n /**\n * Applies an {@link IconLabelOptions} bag. Inherited Component fields\n * cascade through `super.applyOptions`; the gap/glyph/text/forId fields\n * are written pure to `_options` here and dispatched from the constructor\n * body once children exist.\n *\n * @param options - The options bag carrying the values to apply.\n */\n protected applyOptions(options: IconLabelOptions): this {\n super.applyOptions(options);\n\n if (options.gap !== undefined) this._options.gap = options.gap;\n if (options.glyph !== undefined) this._options.glyph = options.glyph;\n if (options.text !== undefined) this._options.text = options.text;\n if (options.forId !== undefined) this._options.forId = options.forId;\n\n return this;\n }\n\n /**\n * Replaces the leading glyph with a fresh instance for the given registry name.\n *\n * @param name - Registry glyph name. Must be present in the internal registry.\n *\n * @returns This component, for method chaining.\n */\n setGlyph(name: string): this {\n this.removeComponent(this._glyph);\n\n this._glyph = new Glyph(name);\n this.insertComponent(this._glyph, 0);\n\n return this;\n }\n\n /**\n * Updates the trailing label text.\n *\n * @param text - The new label string.\n *\n * @returns This component, for method chaining.\n */\n setText(text: string): this {\n this._label.setText(text);\n\n return this;\n }\n\n /**\n * Updates the trailing label's `for` association.\n *\n * @param id - Element id of the form control this label should be associated with.\n *\n * @returns This component, for method chaining.\n */\n setForId(id: string): this {\n this._label.setForId(id);\n\n return this;\n }\n\n /**\n * Sets the pixel gap between the glyph and the label.\n *\n * @param px - Gap in pixels.\n *\n * @returns This component, for method chaining.\n */\n setGap(px: number): this {\n this._options.gap = px;\n (this.getLayoutManager() as HBox).setComponentSpacing(px);\n\n return this;\n }\n\n /**\n * Returns the effective gap between the glyph and the label — the\n * caller/setter value, else the class default (2).\n *\n * @returns The gap in pixels.\n */\n getGap(): number {\n return (this._options.gap ?? this._defaultOptions.gap)!;\n }\n\n /**\n * Returns the leading glyph component.\n *\n * @returns The [`Glyph`](/api/component/display/classes/Glyph) instance.\n */\n getGlyphComponent(): Glyph {\n return this._glyph;\n }\n\n /**\n * Returns the trailing label component.\n *\n * @returns The [`Label`](/api/component/input/classes/Label) instance.\n */\n getLabelComponent(): Label {\n return this._label;\n }\n}\n\nconst IconLabelCallable = callable(IconLabel);\ntype IconLabelCallable = IconLabel;\nexport {\n IconLabel as _IconLabel,\n IconLabelCallable as IconLabel\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { StyleRule } from \"~/core/StyleTarget.js\";\nimport { callable } from \"~/core/Callable.js\";\nimport { Util } from \"~/core/Util.js\";\n\nStyleRule.ensureKeyframes(\n 'ts-ui-progress-indeterminate',\n '0% { transform: translateX(-100%); } 100% { transform: translateX(400%); }'\n);\n\n/**\n * Construction-time options for {@link ProgressBar}.\n *\n * @category Components\n */\nexport interface ProgressBarOptions extends ComponentOptions {\n value?: number;\n indeterminate?: boolean;\n}\n\n/**\n * A horizontal progress indicator with a determinate (0–100%) and an indeterminate\n * (continuously animated) mode.\n *\n * The component renders a track child filling its inner bounds and a fill child\n * inside the track whose width is driven by the current value. In indeterminate\n * mode the fill is sized to roughly a quarter of the track and slides across\n * via a CSS keyframe animation.\n *\n * @category Components\n */\nclass ProgressBar extends Component {\n\n private _track: Component;\n private _fill : Component;\n private _value: number;\n private _indeterminate: boolean;\n\n /**\n * Constructs a ProgressBar.\n *\n * @param value - Initial progress value in [0, 100]. Defaults to 0.\n * @param indeterminate - When true the bar animates continuously and value is ignored.\n */\n constructor(value: number = 0, indeterminate: boolean = false, options?: ProgressBarOptions) {\n // Child components are built first; options are applied via applyOptions at the constructor tail.\n // eslint-disable-next-line local/forward-super-options\n super();\n\n this._value = Util.clamp(value, 0, 100);\n this._indeterminate = indeterminate;\n\n this.setBackgroundColor(\"var(--ts-ui-progress-track-bg, rgb(220, 220, 220))\");\n this.setBorderRadius(\"var(--ts-ui-progress-track-radius, 4px)\");\n\n this._track = new Component();\n this._track.setOverflow(\"hidden\");\n this._track.setBorderRadius(\"var(--ts-ui-progress-track-radius, 4px)\");\n\n this._fill = new Component();\n this._fill.setBackgroundColor(\"var(--ts-ui-progress-fill-bg, rgb(30, 100, 200))\");\n\n this._track.addComponent(this._fill);\n super.addComponent(this._track);\n\n this.getAria().setRole(\"progressbar\");\n this.getAria().setValueMin(0);\n this.getAria().setValueMax(100);\n this.getAria().setValueNow(this._value);\n\n if (this._indeterminate) {\n this.applyIndeterminate(true);\n }\n\n if (options) {\n this.applyOptions(options);\n }\n }\n\n /**\n * Applies a {@link ProgressBarOptions} bag, dispatching value and\n * indeterminate state after inherited Component fields.\n *\n * @param options - The options bag carrying the values to apply.\n */\n protected applyOptions(options: ProgressBarOptions): this {\n super.applyOptions(options);\n\n if (options.indeterminate !== undefined) {\n this.setIndeterminate(options.indeterminate);\n }\n\n if (options.value !== undefined) {\n this.setValue(options.value);\n }\n\n return this;\n }\n\n /**\n * Returns a baseline near the bottom of the bar so that, when placed in an\n * [`HBox`](/api/layout/classes/HBox) next to text labels, the bar sits with its bottom roughly on the\n * surrounding text baseline (CSS replaced-element behaviour, with a 2 px\n * lift so it doesn't sit visually lower than the text descenders).\n *\n * @returns The current preferred height minus 2, or `null` before a size is set.\n */\n getBaseline(): number | null {\n const size = this.getPreferredSize();\n\n return size ? size.height - 2 : null;\n }\n\n /**\n * Returns the current progress value (0–100).\n *\n * @returns The current percentage, or 0 when indeterminate.\n */\n getValue(): number {\n return this._indeterminate ? 0 : this._value;\n }\n\n /**\n * Sets the progress value and updates the fill width. Clamps to [0, 100].\n * Has no visual effect while in indeterminate mode.\n *\n * @param value - Progress percentage in [0, 100].\n */\n setValue(value: number): this {\n const clamped = Util.clamp(value, 0, 100);\n if (clamped === this._value) {\n return this;\n }\n\n this._value = clamped;\n this.getAria().setValueNow(clamped);\n\n if (!this._indeterminate) {\n this.scheduleLayout();\n }\n\n return this;\n }\n\n /**\n * Returns whether the bar is in indeterminate (animated) mode.\n *\n * @returns True if indeterminate mode is active.\n */\n isIndeterminate(): boolean {\n return this._indeterminate;\n }\n\n /**\n * Activates or deactivates indeterminate animation mode.\n *\n * @param value - True to activate indeterminate mode.\n */\n setIndeterminate(value: boolean): this {\n if (this._indeterminate === value) {\n return this;\n }\n\n this._indeterminate = value;\n this.applyIndeterminate(value);\n this.flushLayout();\n\n return this;\n }\n\n /**\n * Lays out the track and fill child components.\n *\n * @returns This component, for method chaining.\n */\n doLayout(): this {\n // The content box, not the inner size: the inner size is the right\n // extent but carries no origin, so a padded bar would put the track at\n // the inner edge of its border and ignore the padding it just\n // subtracted. The fill's own origin stays at zero — it is a child of\n // the track, so it is already inside the track's box.\n const box = this.getContentBounds();\n if (!box) {\n super.doLayout();\n return this;\n }\n\n this._track.setX(box.x);\n this._track.setY(box.y);\n this._track.setSize({ width: box.width, height: box.height });\n\n if (this._indeterminate) {\n const segment = Math.max(20, Math.round(box.width * 0.25));\n this._fill.setX(0);\n this._fill.setY(0);\n this._fill.setSize({ width: segment, height: box.height });\n } else {\n const fillWidth = Math.round(box.width * this._value / 100);\n this._fill.setX(0);\n this._fill.setY(0);\n this._fill.setSize({ width: fillWidth, height: box.height });\n }\n\n super.doLayout();\n\n return this;\n }\n\n /**\n * Toggles the CSS animation property on the fill element to enter or leave\n * indeterminate mode and updates the fill colour accordingly.\n *\n * @param value - True to start the animation, false to stop it.\n */\n private applyIndeterminate(value: boolean): void {\n if (value) {\n this._fill.setBackgroundColor(\"var(--ts-ui-progress-indeterminate-bg, rgb(30, 100, 200))\");\n this._fill.setAnimation(\"ts-ui-progress-indeterminate 1.4s ease-in-out infinite\");\n } else {\n this._fill.setBackgroundColor(\"var(--ts-ui-progress-fill-bg, rgb(30, 100, 200))\");\n this._fill.clearAnimation();\n }\n }\n}\n\nconst ProgressBarCallable = callable(ProgressBar);\ntype ProgressBarCallable = ProgressBar;\nexport {\n ProgressBar as _ProgressBar,\n ProgressBarCallable as ProgressBar\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { AbstractStore } from \"~/data/AbstractStore.js\";\nimport { HBox } from \"~/layout/HBox.js\";\nimport { Button } from \"~/component/button/Button.js\";\nimport { Text } from \"~/component/input/Text.js\";\nimport { callable } from \"~/core/Callable.js\";\nimport { Glyph } from \"~/component/display/Glyph.js\";\nimport { angles_left } from \"~/glyphs/solid/angles_left.js\";\nimport { angle_left } from \"~/glyphs/solid/angle_left.js\";\nimport { angle_right } from \"~/glyphs/solid/angle_right.js\";\nimport { angles_right } from \"~/glyphs/solid/angles_right.js\";\n\nGlyph.register(angles_left, angle_left, angle_right, angles_right);\n\n/**\n * Construction-time options for {@link PaginationBar}.\n *\n * @category Components\n */\nexport interface PaginationBarOptions extends ComponentOptions {\n pageSize?: number;\n pageIndex?: number;\n totalCount?: number;\n}\n\n/**\n * A horizontal navigation bar for stepping through pages of a paginated\n * {@link AbstractStore}.\n *\n * @remarks\n * The bar displays first / previous / next / last buttons and a `Page X of Y`\n * label, all driven by the store's pagination state. It subscribes to the\n * store's `'pagechange'` and `'load'` events and updates button-enabled\n * state automatically. The bar can be embedded anywhere — [`TablePanel`](/api/component/table/classes/TablePanel)'s\n * `setPaginationBar()` is one consumer, but it is not required.\n *\n * @example\n * ```typescript\n * import { AjaxProxy, Store } from '@jimka/typescript-ui/data';\n * import { PaginationBar } from '@jimka/typescript-ui/component/display';\n * import { TablePanel } from '@jimka/typescript-ui/component/table';\n *\n * const store = new Store({ model, proxy: new AjaxProxy({ url: '/api/users' }) });\n * store.setPageSize(25);\n *\n * const panel = new TablePanel(store);\n * panel.setPaginationBar(new PaginationBar(store));\n *\n * void store.load();\n * ```\n *\n * @category Components\n */\nclass PaginationBar extends Component<PaginationBarOptions> {\n\n private _store: AbstractStore;\n\n private _firstBtn: Button;\n private _prevBtn: Button;\n private _nextBtn: Button;\n private _lastBtn: Button;\n private _pageText: Text;\n\n private readonly _onStoreUpdate: () => void = () => this.refresh();\n\n /**\n * Constructs a pagination bar bound to the given store.\n *\n * @param store - The store whose pagination state drives the bar.\n */\n constructor(store: AbstractStore, options?: PaginationBarOptions) {\n // Child components are built first; options are applied via applyOptions at the constructor tail.\n // eslint-disable-next-line local/forward-super-options\n super();\n\n this._store = store;\n\n const layout = new HBox();\n layout.setComponentSpacing(4);\n this.setLayoutManager(layout);\n\n this._firstBtn = new Button({ glyph: \"angles-left\" });\n this._prevBtn = new Button({ glyph: \"angle-left\" });\n this._pageText = new Text(\"Page x of y\");\n this._nextBtn = new Button({ glyph: \"angle-right\" });\n this._lastBtn = new Button({ glyph: \"angles-right\" });\n\n this._firstBtn.setPreferredSize({ width: 28, height: 28 });\n this._prevBtn.setPreferredSize({ width: 28, height: 28 });\n this._nextBtn.setPreferredSize({ width: 28, height: 28 });\n this._lastBtn.setPreferredSize({ width: 28, height: 28 });\n\n // Match the buttons' 28px row height so the page label baseline lines up.\n this._pageText.centerInHeight(28);\n\n this._firstBtn.on(\"action\", () => { this._store.goToPage(1); });\n this._prevBtn.on(\"action\", () => this._store.prevPage());\n this._nextBtn.on(\"action\", () => this._store.nextPage());\n this._lastBtn.on(\"action\", () => {\n const total = this._store.getTotalPages();\n this._store.goToPage(total ?? this._store.getPage());\n });\n\n this.addComponent(this._firstBtn);\n this.addComponent(this._prevBtn);\n this.addComponent(this._pageText);\n this.addComponent(this._nextBtn);\n this.addComponent(this._lastBtn);\n\n this._store.on('pagechange', this._onStoreUpdate);\n this._store.on('load', this._onStoreUpdate);\n this._store.on('datachange', this._onStoreUpdate);\n this._store.on('add', this._onStoreUpdate);\n this._store.on('remove', this._onStoreUpdate);\n this._store.on('sync', this._onStoreUpdate);\n\n this.refresh();\n\n if (options) {\n this.applyOptions(options);\n }\n }\n\n /**\n * Applies a {@link PaginationBarOptions} bag, dispatching pagination\n * properties to the bound store after inherited Component fields.\n *\n * @param options - The options bag carrying the values to apply.\n */\n protected applyOptions(options: PaginationBarOptions): this {\n super.applyOptions(options);\n\n if (options.pageSize !== undefined) {\n this._store.setPageSize(options.pageSize);\n }\n\n if (options.pageIndex !== undefined) {\n this._store.goToPage(options.pageIndex);\n }\n\n return this;\n }\n\n /**\n * Detaches the store listeners installed by this bar, then defers to the\n * base class for the rest of teardown.\n *\n * @remarks\n * Call `dispose()` when permanently removing the bar (e.g. before\n * discarding it, or when replacing it via [`TablePanel.setPaginationBar`](/api/component/table/classes/TablePanel#setpaginationbar)). After\n * disposal the bar will no longer track the store.\n */\n protected destructor(): void {\n this._store.off('pagechange', this._onStoreUpdate);\n this._store.off('load', this._onStoreUpdate);\n this._store.off('datachange', this._onStoreUpdate);\n this._store.off('add', this._onStoreUpdate);\n this._store.off('remove', this._onStoreUpdate);\n this._store.off('sync', this._onStoreUpdate);\n\n super.destructor();\n }\n\n /**\n * Updates the page label and button-enabled states from the current store state.\n *\n * @remarks\n * When the store has pending unsynced changes, all four navigation buttons\n * are disabled — leaving the page would silently discard in-flight edits.\n * Buttons re-enable once the store is synced or rejected.\n */\n private refresh(): void {\n const page = this._store.getPage();\n const totalPages = this._store.getTotalPages();\n const dirty = this._store.hasPendingChanges();\n const text = totalPages != null\n ? `Page ${page} of ${totalPages}`\n : `Page ${page}`;\n\n this._pageText.setText(text);\n this._firstBtn.setEnabled(!dirty && page > 1);\n this._prevBtn.setEnabled(!dirty && page > 1);\n this._nextBtn.setEnabled(!dirty && (totalPages == null || page < totalPages));\n this._lastBtn.setEnabled(!dirty && totalPages != null && page < totalPages);\n }\n}\n\nconst PaginationBarCallable = callable(PaginationBar);\ntype PaginationBarCallable = PaginationBar;\nexport {\n PaginationBar as _PaginationBar,\n PaginationBarCallable as PaginationBar\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle, MediaState } from \"~/core/DOM.js\";\nimport { ListenerBag } from \"~/core/ListenerBag.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * The media events {@link Video} re-emits through its custom `on` / `off`\n * surface. All are non-bubbling DOM media events, wired natively at render time\n * via `DOM.sink.addListener` (they never reach the `Event` class's window-level\n * capture handler) and fanned back out through the {@link ListenerBag}.\n *\n * @category Components\n */\nexport type VideoMediaEvent =\n | \"timeupdate\"\n | \"play\"\n | \"pause\"\n | \"ended\"\n | \"loadedmetadata\"\n | \"durationchange\"\n | \"volumechange\"\n | \"ratechange\";\n\n/** The re-emitted media event types, iterated when wiring native listeners. */\nconst VIDEO_MEDIA_EVENTS: readonly VideoMediaEvent[] = [\n \"timeupdate\",\n \"play\",\n \"pause\",\n \"ended\",\n \"loadedmetadata\",\n \"durationchange\",\n \"volumechange\",\n \"ratechange\",\n];\n\n// Full volume — the browser's own default for a fresh media element, mirrored so\n// a Video with no `volume` option reports the same value the element would.\nconst DEFAULT_VOLUME = 1;\n\n// Normal playback speed — the browser's default `playbackRate`, mirrored so a\n// Video with no `playbackRate` option reports the element's own default.\nconst DEFAULT_PLAYBACK_RATE = 1;\n\n/**\n * Construction-time options for {@link Video}.\n *\n * @category Components\n */\nexport interface VideoOptions extends ComponentOptions {\n src?: string;\n poster?: string;\n autoplay?: boolean;\n loop?: boolean;\n muted?: boolean;\n preload?: \"none\" | \"metadata\" | \"auto\";\n /** Audio volume, clamped to `[0, 1]`. */\n volume?: number;\n playbackRate?: number;\n /**\n * Construction-time listener bag — the declarative form of `on()`, one key\n * per re-emitted media event.\n */\n listeners?: {\n timeupdate?: () => void;\n play?: () => void;\n pause?: () => void;\n ended?: () => void;\n loadedmetadata?: () => void;\n durationchange?: () => void;\n volumechange?: () => void;\n ratechange?: () => void;\n };\n}\n\n/**\n * Class-level defaults forwarded to `super` so the cascade hits Component's\n * applyOptions with `{ tag: \"video\" }` already merged into `_defaultOptions`\n * (mirrors {@link Image}'s `tag: \"img\"`).\n */\nconst _defaultVideoOptions: Partial<VideoOptions> = {\n tag: \"video\",\n};\n\n/**\n * A native `<video>` surface primitive — the media twin of [`Image`](/api/component/display/classes/Image).\n *\n * Owns the typed media setters, a live playback-state read\n * ({@link Video.getMediaState}), and the native media-event bridge: because DOM\n * media events (`timeupdate`, `play`, …) do not bubble, they are wired directly\n * on the element through the DOM seam at render time and re-emitted through this\n * component's custom `on` / `off` surface. A bare video surface is independently\n * useful; the `VideoPlayer` composite layers a control bar on top of it.\n *\n * @category Components\n */\nclass Video extends Component<VideoOptions> {\n\n /** Custom-event fan-out for the re-emitted media events. */\n private _listeners: ListenerBag<VideoMediaEvent> = new ListenerBag<VideoMediaEvent>();\n\n /**\n * Per-type native handlers, held so the exact reference registered at render\n * can be removed on {@link Video.dispose}. Built once in the constructor.\n */\n private readonly _mediaHandlers: Map<VideoMediaEvent, () => void> = new Map();\n\n /**\n * Constructs a video surface.\n *\n * @param options - Optional construction options.\n * @param subclassDefaults - Per-subclass default bag layered over this\n * class's defaults; subclasses forward their `_defaultXxxOptions`\n * constant here.\n */\n constructor(options?: VideoOptions, subclassDefaults?: Partial<VideoOptions>) {\n super(options, { ..._defaultVideoOptions, ...(subclassDefaults ?? {}) });\n\n this.clearInsets();\n this.getAria().setLabel(\"Video\");\n\n this.buildMediaHandlers();\n this.applyListeners(options?.listeners);\n }\n\n /**\n * Forwards the option-backed media fields to their setters after inherited\n * Component fields cascade through `super.applyOptions`. The setters cache on\n * `_options`; DOM writes no-op until the element exists and are replayed by\n * {@link Video.init}.\n *\n * @param options - The options bag carrying the values to apply.\n *\n * @returns This component, for method chaining.\n */\n protected applyOptions(options: VideoOptions): this {\n super.applyOptions(options);\n\n if (options.src !== undefined) this.setSrc(options.src);\n if (options.poster !== undefined) this.setPoster(options.poster);\n if (options.autoplay !== undefined) this.setAutoplay(options.autoplay);\n if (options.loop !== undefined) this.setLoop(options.loop);\n if (options.preload !== undefined) this.setPreload(options.preload);\n if (options.muted !== undefined) this.setMuted(options.muted);\n if (options.volume !== undefined) this.setVolume(options.volume);\n if (options.playbackRate !== undefined) this.setPlaybackRate(options.playbackRate);\n\n return this;\n }\n\n /**\n * Returns the current media source URL.\n *\n * @returns The `src`, or `null` when unset.\n */\n getSrc(): string | null {\n return this._options.src ?? null;\n }\n\n /**\n * Sets the media source URL (writes the `src` attribute).\n *\n * @param src - The media source URL.\n *\n * @returns This component, for method chaining.\n */\n setSrc(src: string): this {\n this._options.src = src;\n this.setElementAttribute(\"src\", src);\n\n return this;\n }\n\n /**\n * Returns the poster image URL.\n *\n * @returns The `poster`, or `null` when unset.\n */\n getPoster(): string | null {\n return this._options.poster ?? null;\n }\n\n /**\n * Sets the poster image shown before playback (writes the `poster` attribute).\n *\n * @param url - The poster image URL.\n *\n * @returns This component, for method chaining.\n */\n setPoster(url: string): this {\n this._options.poster = url;\n this.setElementAttribute(\"poster\", url);\n\n return this;\n }\n\n /**\n * Whether the media autoplays once it can.\n *\n * @returns The `autoplay` state.\n */\n isAutoplay(): boolean {\n return this._options.autoplay ?? false;\n }\n\n /**\n * Sets whether the media autoplays (toggles the boolean `autoplay` attribute).\n *\n * @param value - Whether to autoplay.\n *\n * @returns This component, for method chaining.\n */\n setAutoplay(value: boolean): this {\n this._options.autoplay = value;\n this.setElementAttribute(\"autoplay\", value ? \"\" : null);\n\n return this;\n }\n\n /**\n * Whether the media loops on reaching the end.\n *\n * @returns The `loop` state.\n */\n isLoop(): boolean {\n return this._options.loop ?? false;\n }\n\n /**\n * Sets whether the media loops (toggles the boolean `loop` attribute).\n *\n * @param value - Whether to loop.\n *\n * @returns This component, for method chaining.\n */\n setLoop(value: boolean): this {\n this._options.loop = value;\n this.setElementAttribute(\"loop\", value ? \"\" : null);\n\n return this;\n }\n\n /**\n * Whether audio is muted.\n *\n * @returns The muted state.\n */\n isMuted(): boolean {\n return this._options.muted ?? false;\n }\n\n /**\n * Sets the muted state. Toggles the boolean `muted` attribute (initial-state\n * channel) and drives the live `muted` IDL property through the seam so a\n * change takes effect on an already-loaded element.\n *\n * @param value - Whether to mute audio.\n *\n * @returns This component, for method chaining.\n */\n setMuted(value: boolean): this {\n this._options.muted = value;\n this.setElementAttribute(\"muted\", value ? \"\" : null);\n\n const element = this.getElement();\n\n if (element) {\n DOM.sink.setMuted(element, value);\n }\n\n return this;\n }\n\n /**\n * Returns the preload strategy.\n *\n * @returns The `preload` value, or `null` when unset.\n */\n getPreload(): string | null {\n return this._options.preload ?? null;\n }\n\n /**\n * Sets the preload strategy (writes the `preload` attribute).\n *\n * @param value - One of `\"none\"`, `\"metadata\"`, `\"auto\"`.\n *\n * @returns This component, for method chaining.\n */\n setPreload(value: \"none\" | \"metadata\" | \"auto\"): this {\n this._options.preload = value;\n this.setElementAttribute(\"preload\", value);\n\n return this;\n }\n\n /**\n * Returns the audio volume.\n *\n * @returns The volume in `[0, 1]`.\n */\n getVolume(): number {\n return this._options.volume ?? DEFAULT_VOLUME;\n }\n\n /**\n * Sets the audio volume, clamped to `[0, 1]`. Drives the live `volume` IDL\n * property through the seam (there is no `volume` attribute).\n *\n * @param value - The desired volume; saturates outside `[0, 1]`.\n *\n * @returns This component, for method chaining.\n */\n setVolume(value: number): this {\n const clamped = Math.max(0, Math.min(1, value));\n\n this._options.volume = clamped;\n\n const element = this.getElement();\n\n if (element) {\n DOM.sink.setVolume(element, clamped);\n }\n\n return this;\n }\n\n /**\n * Returns the playback speed multiplier.\n *\n * @returns The playback rate (`1` is normal speed).\n */\n getPlaybackRate(): number {\n return this._options.playbackRate ?? DEFAULT_PLAYBACK_RATE;\n }\n\n /**\n * Sets the playback speed multiplier. Drives the live `playbackRate` IDL\n * property through the seam (there is no `playbackRate` attribute).\n *\n * @param value - The playback rate (`1` is normal speed).\n *\n * @returns This component, for method chaining.\n */\n setPlaybackRate(value: number): this {\n this._options.playbackRate = value;\n\n const element = this.getElement();\n\n if (element) {\n DOM.sink.setPlaybackRate(element, value);\n }\n\n return this;\n }\n\n /**\n * Seeks to a playback position. Runtime-only — the live playhead is not\n * consumer configuration, so it is not on {@link VideoOptions}.\n *\n * @param seconds - The target position in seconds.\n *\n * @returns This component, for method chaining.\n */\n setCurrentTime(seconds: number): this {\n const element = this.getElement();\n\n if (element) {\n DOM.sink.setCurrentTime(element, seconds);\n }\n\n return this;\n }\n\n /**\n * Starts (or resumes) playback.\n *\n * @returns This component, for method chaining.\n */\n play(): this {\n const element = this.getElement();\n\n if (element) {\n DOM.sink.mediaPlay(element);\n }\n\n return this;\n }\n\n /**\n * Pauses playback.\n *\n * @returns This component, for method chaining.\n */\n pause(): this {\n const element = this.getElement();\n\n if (element) {\n DOM.sink.mediaPause(element);\n }\n\n return this;\n }\n\n /**\n * Reads the live playback state through the DOM read seam. Before the element\n * renders, reports a paused snapshot from the cached configuration.\n *\n * @returns The current {@link MediaState}.\n */\n getMediaState(): MediaState {\n const element = this.getElement();\n\n if (element) {\n return DOM.source.getMediaState(element);\n }\n\n return {\n currentTime: 0,\n duration: 0,\n paused: true,\n ended: false,\n volume: this.getVolume(),\n muted: this.isMuted(),\n playbackRate: this.getPlaybackRate(),\n };\n }\n\n /**\n * Registers a listener for one of this surface's re-emitted media events.\n *\n * @param event - The media event name.\n * @param listener - The callback invoked when the event fires.\n *\n * @returns This component, for method chaining.\n */\n on(event: VideoMediaEvent, listener: () => void): this {\n this._listeners.add(event, listener);\n\n return this;\n }\n\n /**\n * Removes a previously registered media-event listener.\n *\n * @param event - The event the listener was registered for.\n * @param listener - The exact callback reference to remove.\n *\n * @returns This component, for method chaining.\n */\n off(event: VideoMediaEvent, listener: () => void): this {\n this._listeners.remove(event, listener);\n\n return this;\n }\n\n /**\n * Fans a media event out to its registered listeners.\n *\n * @param event - The event to emit.\n */\n protected emit(event: VideoMediaEvent): void {\n this._listeners.fire(event);\n }\n\n /**\n * Replays the cached media options onto the freshly created element and wires\n * the native, non-bubbling media listeners. The base class's\n * `setElementAttribute` now also caches and replays the attribute-backed\n * options (`src`, `poster`, `preload`, `autoplay`, `loop`, `muted`), making\n * that part of the replay redundant — kept anyway, see\n * {@link replayMediaOptions} for why this call still matters for `volume` /\n * `playbackRate` / live-property `muted`.\n *\n * @param element - The element being initialised, when provided by the caller.\n *\n * @returns This component, for method chaining.\n */\n protected init(element?: Handle): this {\n super.init(element);\n\n const el = element ?? this.getElement();\n\n if (!el) {\n return this;\n }\n\n this.replayMediaOptions(el);\n this.attachMediaListeners(el);\n\n return this;\n }\n\n /**\n * Detaches the native media listeners installed at render, then defers to\n * the base class for the rest of teardown. Call before discarding the\n * surface so no stray native listener survives.\n */\n protected destructor(): void {\n const element = this.getElement();\n\n if (element) {\n for (const [type, handler] of this._mediaHandlers) {\n DOM.sink.removeListener(element, type, handler);\n }\n }\n\n super.destructor();\n }\n\n /**\n * Builds one stable re-emit handler per media event, stored so the exact\n * reference registered at render can be removed on disposal.\n */\n private buildMediaHandlers(): void {\n for (const type of VIDEO_MEDIA_EVENTS) {\n this._mediaHandlers.set(type, () => this.emit(type));\n }\n }\n\n /**\n * Re-applies every cached media option onto the freshly-rendered element.\n *\n * @remarks The `setAttr` block below (`src`/`poster`/`preload`/`autoplay`/\n * `loop`/`muted`) is now redundant with the base class's\n * `setElementAttribute` cache, which `Component.init()` already replays\n * onto this same `element` parameter — kept here for parity with the rest\n * of this method rather than split out. `volume`, `playbackRate`, and\n * `muted` as a live IDL property have no reflecting attribute, so they are\n * never covered by that cache and this method remains the only place they\n * get applied to a freshly-created element. Writes go to the **passed**\n * `element`, never through the `getElement()`-based setters: during\n * `init()` the element has been created but `render()` stores it as the\n * component's element only *after* `init()` returns, so `getElement()`\n * (which resolves by document id) returns nothing yet. (The offline\n * modelled source resolves detached elements by id, which is why this was\n * invisible to the recording-sink tests and had to be caught live.)\n *\n * @param element - The rendered (still-detached) video element.\n */\n private replayMediaOptions(element: Handle): void {\n const options = this._options;\n const setAttr: Record<string, string> = {};\n\n if (options.src !== undefined) setAttr.src = options.src;\n if (options.poster !== undefined) setAttr.poster = options.poster;\n if (options.preload !== undefined) setAttr.preload = options.preload;\n if (options.autoplay) setAttr.autoplay = \"\";\n if (options.loop) setAttr.loop = \"\";\n if (options.muted) setAttr.muted = \"\";\n\n if (Object.keys(setAttr).length > 0) {\n DOM.sink.apply(element, { setAttr });\n }\n\n // Live IDL properties: volume/playbackRate have no reflecting attribute,\n // and muted must also be driven as a property to take effect immediately.\n if (options.muted !== undefined) DOM.sink.setMuted(element, options.muted);\n if (options.volume !== undefined) DOM.sink.setVolume(element, options.volume);\n if (options.playbackRate !== undefined) DOM.sink.setPlaybackRate(element, options.playbackRate);\n }\n\n /**\n * Registers each per-type native handler on the element through the DOM seam.\n *\n * @param element - The rendered video element.\n */\n private attachMediaListeners(element: Handle): void {\n for (const [type, handler] of this._mediaHandlers) {\n DOM.sink.addListener(element, type, handler);\n }\n }\n}\n\nconst VideoCallable = callable(Video);\ntype VideoCallable = Video;\nexport {\n Video as _Video,\n VideoCallable as Video\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport type { Video } from \"~/component/display/Video.js\";\n\n/**\n * Pluggable media-loading strategy for a {@link Video} surface.\n *\n * @remarks\n * The engine seam decouples *how a source is loaded* from the {@link Video}\n * primitive and the `VideoPlayer` composite. The default {@link ProgressiveEngine}\n * simply writes the `src` attribute and lets the browser fetch a progressive\n * MP4 / WebM. A future adaptive-streaming engine (hls.js / dash.js) would attach\n * to the raw media element and drive its buffer — which needs the live\n * `HTMLVideoElement` the DOM seam deliberately hides, so that escape is a\n * documented prerequisite for streaming and out of scope here. Implement this\n * interface to add a new strategy without touching the component.\n *\n * @category Components\n */\nexport interface PlaybackEngine {\n /**\n * Loads a source into the given video surface.\n *\n * @param video - The video surface to load into.\n * @param src - The media source URL.\n */\n load(video: Video, src: string): void;\n\n /**\n * Releases any resources the engine holds (detaches a streaming instance,\n * cancels in-flight fetches). Called before a new `load` and on disposal.\n */\n destroy(): void;\n}\n\n/**\n * The default {@link PlaybackEngine}: progressive download. `load` writes the\n * `src` attribute (the browser auto-fetches a progressive MP4 / WebM); `destroy`\n * is a no-op because a progressive source holds no engine-side resources.\n *\n * @category Components\n */\nclass ProgressiveEngine implements PlaybackEngine {\n /**\n * Points the video surface at `src` by writing its `src` attribute.\n *\n * @param video - The video surface to load into.\n * @param src - The media source URL.\n */\n load(video: Video, src: string): void {\n video.setSrc(src);\n }\n\n /**\n * No-op: a progressive source holds no engine-side resources to release.\n */\n destroy(): void {\n // Progressive playback keeps no engine state; nothing to tear down.\n }\n}\n\nexport { ProgressiveEngine };\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle, MediaState } from \"~/core/DOM.js\";\nimport { ListenerBag } from \"~/core/ListenerBag.js\";\nimport { callable } from \"~/core/Callable.js\";\nimport type { Size } from \"~/primitive/Size.js\";\nimport { Border } from \"~/layout/Border.js\";\nimport { HBox } from \"~/layout/HBox.js\";\nimport { Placement } from \"~/primitive/Placement.js\";\nimport { Button } from \"~/component/button/Button.js\";\nimport { Slider } from \"~/component/input/Slider.js\";\nimport { Text } from \"~/component/input/Text.js\";\nimport { Glyph } from \"~/component/display/Glyph.js\";\nimport { Video } from \"~/component/display/Video.js\";\nimport { ProgressiveEngine } from \"~/component/display/PlaybackEngine.js\";\nimport type { PlaybackEngine } from \"~/component/display/PlaybackEngine.js\";\nimport { play } from \"~/glyphs/solid/play.js\";\nimport { pause } from \"~/glyphs/solid/pause.js\";\nimport { volume_high } from \"~/glyphs/solid/volume_high.js\";\nimport { volume_mute } from \"~/glyphs/solid/volume_mute.js\";\nimport { expand } from \"~/glyphs/solid/expand.js\";\nimport { compress } from \"~/glyphs/solid/compress.js\";\n\nGlyph.register(play, pause, volume_high, volume_mute, expand, compress);\n\n// Square side of each glyph-only control button, matching PaginationBar's 28px\n// navigation buttons so the control bar reads as one row of same-sized controls.\nconst CONTROL_BUTTON_SIZE = 28;\n\n// Fixed width of the volume slider. Narrower than the default 200px Slider so it\n// stays a compact secondary control beside the flexible seek scrubber.\nconst VOLUME_SLIDER_WIDTH = 80;\n\n// Height the time readout centres within, matching the control-button row height\n// so its baseline lines up with the buttons.\nconst CONTROL_ROW_HEIGHT = 28;\n\n// Volume-slider granularity — 20 steps across [0, 1] is fine enough for smooth\n// adjustment without sub-percent jitter.\nconst VOLUME_STEP = 0.05;\n\n// Gap between control-bar children, matching PaginationBar's inter-button spacing.\nconst CONTROL_SPACING = 6;\n\n/**\n * Formats a media time in seconds as `h:mm:ss` (when at least an hour) or `m:ss`.\n * A non-finite or negative input (no metadata yet, a live stream) renders as\n * `\"0:00\"`. Pure and module-level so it is trivially unit-testable.\n *\n * @param seconds - The time in seconds.\n * @returns The formatted `m:ss` / `h:mm:ss` string.\n *\n * @category Components\n */\nexport function formatMediaTime(seconds: number): string {\n if (!Number.isFinite(seconds) || seconds < 0) {\n return \"0:00\";\n }\n\n const total = Math.floor(seconds);\n const secs = total % 60;\n const mins = Math.floor(total / 60) % 60;\n const hours = Math.floor(total / 3600);\n const secsText = String(secs).padStart(2, \"0\");\n\n if (hours >= 1) {\n const minsText = String(mins).padStart(2, \"0\");\n\n return `${hours}:${minsText}:${secsText}`;\n }\n\n return `${mins}:${secsText}`;\n}\n\n/**\n * The custom events {@link VideoPlayer} emits — the player-level lifecycle\n * subset of the underlying {@link Video}'s media events.\n *\n * @category Components\n */\nexport type VideoPlayerEvent = \"play\" | \"pause\" | \"ended\";\n\n/**\n * Construction-time options for {@link VideoPlayer}.\n *\n * @category Components\n */\nexport interface VideoPlayerOptions extends ComponentOptions {\n src?: string;\n poster?: string;\n autoplay?: boolean;\n loop?: boolean;\n muted?: boolean;\n /** Audio volume, clamped to `[0, 1]`. */\n volume?: number;\n playbackRate?: number;\n /** Show the custom control bar. Default `true`. */\n controls?: boolean;\n /** Media-loading strategy. Defaults to a {@link ProgressiveEngine}. */\n engine?: PlaybackEngine;\n /**\n * Construction-time listener bag — the declarative form of `on()`.\n */\n listeners?: {\n play?: () => void;\n pause?: () => void;\n ended?: () => void;\n };\n}\n\n/**\n * A video player: a native `<video>` surface ({@link Video}) framed by a control\n * bar built entirely from this library's own components — play / pause, a seek\n * scrubber, a current-time / duration readout, volume, mute, and fullscreen. The\n * native browser `controls` chrome is not used.\n *\n * @remarks\n * The player is a [`Border`](/api/layout/classes/Border) layout with the video\n * in the centre region and the control bar docked south. All coordination — the\n * play-state machine, the scrubber⇄currentTime sync (guarded against a feedback\n * loop), volume / mute sync, fullscreen state, and media-loading engine\n * dispatch — lives here; the control bar itself is a composed `HBox` of existing\n * primitives. Source loading routes through a pluggable {@link PlaybackEngine}\n * (progressive download by default) so an adaptive-streaming engine can be\n * attached without rewriting the component.\n *\n * @example\n * ```typescript\n * import { VideoPlayer } from '@jimka/typescript-ui/component/display';\n *\n * const player = new VideoPlayer({ src: '/media/clip.mp4', poster: '/media/poster.png' });\n * player.on('ended', () => console.log('done'));\n * ```\n *\n * @category Components\n */\nclass VideoPlayer extends Component<VideoPlayerOptions> {\n\n private _video: Video;\n private _controls!: Component;\n private _playBtn!: Button;\n private _scrubber!: Slider;\n private _timeText!: Text;\n private _muteBtn!: Button;\n private _volume!: Slider;\n private _fullscreenBtn!: Button;\n\n /** Media-loading strategy; progressive download unless overridden. */\n private _engine: PlaybackEngine = new ProgressiveEngine();\n\n /** Whether the video is currently playing (driven by media events). */\n private _playing: boolean = false;\n\n /** Whether the player is currently fullscreen. */\n private _fullscreen: boolean = false;\n\n /**\n * Guards the scrubber⇄timeupdate feedback loop: while the user drags the\n * scrubber, incoming `timeupdate` syncs must not overwrite the thumb.\n */\n private _scrubbing: boolean = false;\n\n /**\n * Guards against a programmatic `setValue` on a slider re-entering its own\n * `action` / `change` handler — every {@link Slider.setValue} fires those\n * events, so a `syncFromState` write would otherwise loop back as a seek.\n */\n private _syncing: boolean = false;\n\n /** Custom-event fan-out for the player's `play` / `pause` / `ended` events. */\n private _listeners: ListenerBag<VideoPlayerEvent> = new ListenerBag<VideoPlayerEvent>();\n\n private readonly _onPlayButton: () => void = () => this.togglePlay();\n private readonly _onMuteButton: () => void = () => this.setMuted(!this.isMuted());\n private readonly _onFullscreenButton: () => void = () => this.toggleFullscreen();\n private readonly _onScrub: () => void = () => this.beginScrub();\n private readonly _onVolumeChange: (v: number) => void = (v) => this.onVolumeSlider(v);\n\n private readonly _onVideoPlay: () => void = () => this.onVideoPlay();\n private readonly _onVideoPause: () => void = () => this.onVideoPause();\n private readonly _onVideoEnded: () => void = () => this.onVideoEnded();\n private readonly _onVideoTimeUpdate: () => void = () => this.onVideoTimeUpdate();\n private readonly _onVideoSync: () => void = () => this.syncFromVideo();\n private readonly _onFullscreenChange: () => void = () => this.syncFullscreen();\n\n /**\n * Constructs a video player.\n *\n * @param options - Optional construction options.\n */\n constructor(options?: VideoPlayerOptions) {\n // Child components are built first; options are applied via applyOptions\n // at the constructor tail, after the control children exist.\n // eslint-disable-next-line local/forward-super-options\n super();\n\n this.setLayoutManager(new Border());\n this.getAria().setRole(\"region\");\n this.getAria().setLabel(\"Video player\");\n\n this._video = new Video();\n\n this.buildControlBar();\n this.wireControlListeners();\n this.wireVideoListeners();\n\n this.addComponent(this._video, { placement: Placement.CENTER });\n this.addComponent(this._controls, { placement: Placement.SOUTH });\n\n this.syncFromState(this._video.getMediaState());\n\n if (options) {\n this.applyOptions(options);\n }\n\n this.applyListeners(options?.listeners);\n }\n\n /**\n * Applies a {@link VideoPlayerOptions} bag after inherited Component fields\n * cascade. Dispatched from the constructor tail (not the `super()` cascade),\n * so the control children the setters touch already exist. The engine is set\n * before `src` so a supplied engine handles the initial load.\n *\n * @param options - The options bag carrying the values to apply.\n *\n * @returns This component, for method chaining.\n */\n protected applyOptions(options: VideoPlayerOptions): this {\n super.applyOptions(options);\n\n // Configure the video directly rather than through the UI-syncing public\n // setters: a control-bar slider write fires a DOM event that requires a\n // rendered element, so the control bar is instead synced from the video\n // at render time (see init). Construction stays JS-only.\n if (options.engine !== undefined) this._engine = options.engine;\n if (options.src !== undefined) this.setSrc(options.src);\n if (options.poster !== undefined) this._video.setPoster(options.poster);\n if (options.autoplay !== undefined) this._video.setAutoplay(options.autoplay);\n if (options.loop !== undefined) this._video.setLoop(options.loop);\n if (options.muted !== undefined) this._video.setMuted(options.muted);\n if (options.volume !== undefined) this._video.setVolume(options.volume);\n if (options.playbackRate !== undefined) this._video.setPlaybackRate(options.playbackRate);\n if (options.controls !== undefined) this.setControlsVisible(options.controls);\n\n return this;\n }\n\n /**\n * Returns the current media source URL.\n *\n * @returns The `src`, or `null` when unset.\n */\n getSrc(): string | null {\n return this._video.getSrc();\n }\n\n /**\n * Sets the media source, routing the load through the playback engine: the\n * previous engine session is destroyed, then the new source is loaded.\n *\n * @param src - The media source URL.\n *\n * @returns This component, for method chaining.\n */\n setSrc(src: string): this {\n this._engine.destroy();\n this._engine.load(this._video, src);\n\n return this;\n }\n\n /**\n * Returns the audio volume.\n *\n * @returns The volume in `[0, 1]`.\n */\n getVolume(): number {\n return this._video.getVolume();\n }\n\n /**\n * Sets the audio volume (clamped to `[0, 1]` by the video surface) and\n * refreshes the control bar.\n *\n * @param value - The desired volume.\n *\n * @returns This component, for method chaining.\n */\n setVolume(value: number): this {\n this._video.setVolume(value);\n this.syncFromVideo();\n\n return this;\n }\n\n /**\n * Whether audio is muted.\n *\n * @returns The muted state.\n */\n isMuted(): boolean {\n return this._video.isMuted();\n }\n\n /**\n * Sets the muted state and refreshes the control bar.\n *\n * @param value - Whether to mute audio.\n *\n * @returns This component, for method chaining.\n */\n setMuted(value: boolean): this {\n this._video.setMuted(value);\n this.syncFromVideo();\n\n return this;\n }\n\n /**\n * Returns the playback speed multiplier.\n *\n * @returns The playback rate (`1` is normal speed).\n */\n getPlaybackRate(): number {\n return this._video.getPlaybackRate();\n }\n\n /**\n * Sets the playback speed multiplier.\n *\n * @param value - The playback rate (`1` is normal speed).\n *\n * @returns This component, for method chaining.\n */\n setPlaybackRate(value: number): this {\n this._video.setPlaybackRate(value);\n\n return this;\n }\n\n /**\n * Seeks to a playback position.\n *\n * @param seconds - The target position in seconds.\n *\n * @returns This component, for method chaining.\n */\n setCurrentTime(seconds: number): this {\n this._video.setCurrentTime(seconds);\n\n return this;\n }\n\n /**\n * Reads the live playback position.\n *\n * @returns The current time in seconds.\n */\n getCurrentTime(): number {\n return this._video.getMediaState().currentTime;\n }\n\n /**\n * Reads the live media duration.\n *\n * @returns The duration in seconds (`NaN` before metadata loads).\n */\n getDuration(): number {\n return this._video.getMediaState().duration;\n }\n\n /**\n * Whether the video is currently playing.\n *\n * @returns `true` while playing.\n */\n isPlaying(): boolean {\n return this._playing;\n }\n\n /**\n * Starts (or resumes) playback.\n *\n * @returns This component, for method chaining.\n */\n play(): this {\n this._video.play();\n\n return this;\n }\n\n /**\n * Pauses playback.\n *\n * @returns This component, for method chaining.\n */\n pause(): this {\n this._video.pause();\n\n return this;\n }\n\n /**\n * Toggles between play and pause.\n *\n * @returns This component, for method chaining.\n */\n togglePlay(): this {\n return this._playing ? this.pause() : this.play();\n }\n\n /**\n * Whether the custom control bar is visible.\n *\n * @returns `true` when the control bar shows.\n */\n isControlsVisible(): boolean {\n return this._options.controls ?? true;\n }\n\n /**\n * Shows or hides the custom control bar.\n *\n * @param value - Whether the control bar is visible.\n *\n * @returns This component, for method chaining.\n */\n setControlsVisible(value: boolean): this {\n this._options.controls = value;\n this._controls.setVisible(value);\n\n return this;\n }\n\n /**\n * Whether the player is currently fullscreen.\n *\n * @returns `true` while fullscreen.\n */\n isFullscreen(): boolean {\n return this._fullscreen;\n }\n\n /**\n * Requests fullscreen for the player. Must be called from a user gesture\n * (e.g. the fullscreen button's action); a programmatic call is rejected by\n * the browser.\n *\n * @returns This component, for method chaining.\n */\n enterFullscreen(): this {\n const element = this.getElement();\n\n if (element) {\n DOM.sink.requestFullscreen(element);\n }\n\n return this;\n }\n\n /**\n * Exits fullscreen.\n *\n * @returns This component, for method chaining.\n */\n exitFullscreen(): this {\n DOM.sink.exitFullscreen();\n\n return this;\n }\n\n /**\n * Toggles fullscreen.\n *\n * @returns This component, for method chaining.\n */\n toggleFullscreen(): this {\n return this._fullscreen ? this.exitFullscreen() : this.enterFullscreen();\n }\n\n /**\n * Registers a listener for one of the player's lifecycle events.\n *\n * @param event - The event name.\n * @param listener - The callback invoked when the event fires.\n *\n * @returns This component, for method chaining.\n */\n on(event: VideoPlayerEvent, listener: () => void): this {\n this._listeners.add(event, listener);\n\n return this;\n }\n\n /**\n * Removes a previously registered lifecycle listener.\n *\n * @param event - The event the listener was registered for.\n * @param listener - The exact callback reference to remove.\n *\n * @returns This component, for method chaining.\n */\n off(event: VideoPlayerEvent, listener: () => void): this {\n this._listeners.remove(event, listener);\n\n return this;\n }\n\n /**\n * Fans a lifecycle event out to its registered listeners.\n *\n * @param event - The event to emit.\n */\n protected emit(event: VideoPlayerEvent): void {\n this._listeners.fire(event);\n }\n\n /**\n * Wires the `fullscreenchange` listener on the root element (it bubbles to\n * `document`, unlike the non-bubbling media events) so external Esc-key or\n * browser-driven fullscreen exits keep the player's state in sync.\n *\n * @param element - The element being initialised, when provided by the caller.\n *\n * @returns This component, for method chaining.\n */\n protected init(element?: Handle): this {\n super.init(element);\n\n const el = element ?? this.getElement();\n\n if (!el) {\n return this;\n }\n\n DOM.sink.addListener(el, \"fullscreenchange\", this._onFullscreenChange);\n\n // The video and its control sliders are now rendered, so populating the\n // control bar from the configured media state can safely fire the sliders'\n // value-change events (which a pre-render write cannot).\n this.syncFromVideo();\n\n return this;\n }\n\n /**\n * Detaches the native + video listeners installed by this player, then\n * defers to the base class for the rest of teardown. Call before\n * discarding it so no stray native listener survives.\n */\n protected destructor(): void {\n // `_video` is registered via `addComponent`, so `super.destructor()`'s\n // child recursion below already disposes it — an explicit call here\n // would run `Video.destructor()` a second time.\n const element = this.getElement();\n\n if (element) {\n DOM.sink.removeListener(element, \"fullscreenchange\", this._onFullscreenChange);\n }\n\n super.destructor();\n }\n\n /**\n * Maps a {@link MediaState} snapshot onto the control-bar controls: the\n * scrubber range + position (skipped while the user is scrubbing), the time\n * readout, the play / mute glyphs, and the volume slider. Pure with respect\n * to media state — every media-event handler and the unit tests call it.\n *\n * @param state - The media-state snapshot to reflect.\n */\n private syncFromState(state: MediaState): void {\n this._syncing = true;\n\n try {\n if (!this._scrubbing) {\n this._scrubber.setMax(state.duration || 0);\n this._scrubber.setValue(state.currentTime);\n }\n\n this._timeText.setText(`${formatMediaTime(state.currentTime)} / ${formatMediaTime(state.duration)}`);\n this._playBtn.setGlyph(state.paused ? \"play\" : \"pause\");\n this._muteBtn.setGlyph(state.muted || state.volume === 0 ? \"volume-mute\" : \"volume-high\");\n this._volume.setValue(state.muted ? 0 : state.volume);\n } finally {\n this._syncing = false;\n }\n }\n\n /** Reads the video's live state and reflects it onto the control bar. */\n private syncFromVideo(): void {\n this.syncFromState(this._video.getMediaState());\n }\n\n /** Builds the south-docked control bar as an `HBox` of existing primitives. */\n private buildControlBar(): void {\n this._playBtn = this.makeControlButton(\"play\", \"Play\");\n this._muteBtn = this.makeControlButton(\"volume-high\", \"Mute\");\n this._fullscreenBtn = this.makeControlButton(\"expand\", \"Fullscreen\");\n\n this._scrubber = new Slider({ min: 0, max: 0, value: 0 });\n this._scrubber.getAria().setLabel(\"Seek\");\n\n this._volume = new Slider({ min: 0, max: 1, step: VOLUME_STEP, value: 1 });\n this._volume.getAria().setLabel(\"Volume\");\n this._volume.setPreferredSize({ width: VOLUME_SLIDER_WIDTH, height: this._volume.getPreferredSize()!.height });\n this._volume.setMaxSize({ width: VOLUME_SLIDER_WIDTH, height: this._volume.getPreferredSize()!.height });\n\n this._timeText = new Text(\"0:00 / 0:00\");\n this._timeText.centerInHeight(CONTROL_ROW_HEIGHT);\n\n const bar = new HBox();\n\n bar.setComponentSpacing(CONTROL_SPACING);\n\n this._controls = new Component();\n this._controls.setLayoutManager(bar);\n // Give the control bar an opaque, theme-tracking surface. Transparent, it\n // borrowed the page background in-page (readable) but showed the black\n // :fullscreen backdrop in fullscreen, hiding the dark time text and glyphs.\n // The body background is what already showed through in-page, so this is\n // invisible there yet keeps the controls readable over the fullscreen video.\n this._controls.setBackgroundColor(\"var(--ts-ui-body-bg, rgb(255, 255, 255))\");\n this._controls.addComponent(this._playBtn);\n this._controls.addComponent(this._scrubber, { weight: 1 });\n this._controls.addComponent(this._timeText);\n this._controls.addComponent(this._muteBtn);\n this._controls.addComponent(this._volume);\n this._controls.addComponent(this._fullscreenBtn);\n }\n\n /**\n * Builds a fixed-size glyph-only control button with an accessible label.\n *\n * @param glyph - The glyph name to show.\n * @param label - The accessible name (drives `aria-label` and the tooltip).\n * @returns The configured button.\n */\n private makeControlButton(glyph: string, label: string): Button {\n const button = new Button({ glyph, text: label, showText: false });\n\n button.setPreferredSize({ width: CONTROL_BUTTON_SIZE, height: CONTROL_BUTTON_SIZE });\n\n return button;\n }\n\n /** Wires the control-bar children's interaction events to the player. */\n private wireControlListeners(): void {\n this._playBtn.on(\"action\", this._onPlayButton);\n this._muteBtn.on(\"action\", this._onMuteButton);\n this._fullscreenBtn.on(\"action\", this._onFullscreenButton);\n this._scrubber.on(\"action\", this._onScrub);\n this._volume.on(\"change\", this._onVolumeChange);\n }\n\n /** Subscribes to the video surface's re-emitted media events. */\n private wireVideoListeners(): void {\n this._video.on(\"play\", this._onVideoPlay);\n this._video.on(\"pause\", this._onVideoPause);\n this._video.on(\"ended\", this._onVideoEnded);\n this._video.on(\"timeupdate\", this._onVideoTimeUpdate);\n this._video.on(\"durationchange\", this._onVideoSync);\n this._video.on(\"loadedmetadata\", this._onVideoSync);\n this._video.on(\"volumechange\", this._onVideoSync);\n this._video.on(\"ratechange\", this._onVideoSync);\n }\n\n /** Marks the scrubber as being dragged and seeks the video to its value. */\n private beginScrub(): void {\n if (this._syncing) {\n return;\n }\n\n this._scrubbing = true;\n this._video.setCurrentTime(this._scrubber.getValue());\n }\n\n /**\n * Handles a volume-slider change: applies the volume to the video (ignoring\n * the programmatic echo from a sync write).\n *\n * @param value - The new slider value in `[0, 1]`.\n */\n private onVolumeSlider(value: number): void {\n if (this._syncing) {\n return;\n }\n\n this._video.setVolume(value);\n this.syncFromVideo();\n }\n\n /** Media `play`: records the play state, refreshes, and re-emits. */\n private onVideoPlay(): void {\n this._playing = true;\n this.syncFromVideo();\n this.emit(\"play\");\n }\n\n /** Media `pause`: records the pause state, refreshes, and re-emits. */\n private onVideoPause(): void {\n this._playing = false;\n this.syncFromVideo();\n this.emit(\"pause\");\n }\n\n /** Media `ended`: records the stopped state, refreshes, and re-emits. */\n private onVideoEnded(): void {\n this._playing = false;\n this.syncFromVideo();\n this.emit(\"ended\");\n }\n\n /** Media `timeupdate`: refreshes, then releases the scrubbing guard. */\n private onVideoTimeUpdate(): void {\n this.syncFromVideo();\n this._scrubbing = false;\n }\n\n /**\n * Reconciles the player with the document's fullscreen state: relayouts to\n * fill (or shrink back from) the fullscreen viewport and swaps the fullscreen\n * glyph. Driven by the `fullscreenchange` listener, so a browser-initiated\n * exit (Esc) is handled the same as the button.\n *\n * The browser's `:fullscreen` UA rules blow the root element up to fill the\n * screen, but the absolute layout keeps sizing the children for the\n * pre-fullscreen bounds — leaving the video in the corner over a black\n * backdrop. Resizing the player to the viewport re-runs the `Border` layout\n * so the video stretches to fill and the controls stay docked at the bottom;\n * the saved in-page bounds are restored on exit.\n */\n private syncFullscreen(): void {\n const fullscreen = DOM.source.getFullscreenElement();\n const element = this.getElement();\n\n this._fullscreen = element != null && fullscreen === element;\n this._fullscreenBtn.setGlyph(this._fullscreen ? \"compress\" : \"expand\");\n\n // Re-lay the children against the now fullscreen-aware inner size (see\n // getInnerSize): entering stretches the video to fill the viewport,\n // exiting reverts it to the in-page size.\n this.doLayout();\n }\n\n /**\n * Reports the content area a layout manager fills. While fullscreen the\n * browser's `:fullscreen` UA rules blow the root element up to the viewport,\n * but its parent-committed box stays at the in-page size — so the `Border`\n * layout would keep positioning the video and controls for the small box,\n * leaving the video in a corner over a black backdrop. Returning the viewport\n * extent here makes every layout pass (including parent-driven relayouts that\n * re-commit the in-page box) stretch the children to fill the screen; exiting\n * fullscreen falls back to the inherited inner size.\n *\n * @returns The inner content size, or `null` before the element renders.\n */\n getInnerSize(): Size | null {\n if (this._fullscreen && this.getElement()) {\n const viewport = DOM.source.getViewportSize();\n const perimeter = this.getPerimeterSize();\n\n return {\n width: viewport.width - perimeter.left - perimeter.right,\n height: viewport.height - perimeter.top - perimeter.bottom,\n };\n }\n\n return super.getInnerSize();\n }\n}\n\nconst VideoPlayerCallable = callable(VideoPlayer);\ntype VideoPlayerCallable = VideoPlayer;\nexport {\n VideoPlayer as _VideoPlayer,\n VideoPlayerCallable as VideoPlayer\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"display.es.js","names":[],"sources":["../../../src/typescript/lib/component/display/Canvas.ts","../../../src/typescript/lib/component/display/WebGLCanvas.ts","../../../src/typescript/lib/component/display/Image.ts","../../../src/typescript/lib/component/display/Markdown.ts","../../../src/typescript/lib/component/display/MarkdownMinimap.ts","../../../src/typescript/lib/component/display/MarkdownViewer.ts","../../../src/typescript/lib/component/display/IconLabel.ts","../../../src/typescript/lib/component/display/ProgressBar.ts","../../../src/typescript/lib/component/display/PaginationBar.ts","../../../src/typescript/lib/component/display/Video.ts","../../../src/typescript/lib/component/display/PlaybackEngine.ts","../../../src/typescript/lib/component/display/VideoPlayer.ts"],"sourcesContent":["// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * Draw callback: receives the live 2D context and the logical (CSS-px) size.\n *\n * @param ctx - The canvas 2D rendering context, pre-scaled so one unit is one\n * CSS pixel (the device-pixel-ratio transform is already applied).\n * @param width - The drawing surface width in CSS pixels.\n * @param height - The drawing surface height in CSS pixels.\n * @param elapsedMs - Milliseconds since the current animation run started, or\n * `0` when the canvas is not animating. Derive motion from this rather than\n * advancing a counter once per call: frames arrive at the display's refresh\n * rate, so a per-call increment runs three times faster on a 180Hz monitor\n * than on a 60Hz one. Outside the animation loop (a resize, a DPR change, an\n * explicit {@link Canvas.redraw}) this repeats the most recent frame's value,\n * so a redraw re-renders the same moment rather than jumping.\n *\n * @category Components\n */\nexport type CanvasDrawCallback = (\n ctx: CanvasRenderingContext2D,\n width: number,\n height: number,\n elapsedMs: number,\n) => void;\n\n/**\n * Construction-time options for {@link Canvas}.\n *\n * @category Components\n */\nexport interface CanvasOptions extends ComponentOptions {\n\n /** Draw hook, re-invoked on demand and after every resize / DPR change. */\n onDraw?: CanvasDrawCallback;\n\n /**\n * Keeps the animation loop running while the canvas is not effectively\n * on-screen (e.g. on an inactive `Tab` panel). Default `false`: the loop\n * pauses automatically when hidden and resumes when shown again.\n */\n animateWhenHidden?: boolean;\n\n /**\n * Upper bound, in frames per second, on how often the animation loop\n * redraws. Defaults to 30 — enough for smooth motion at a predictable cost,\n * and independent of the display's refresh rate, so the same animation\n * costs the same on a 60Hz and a 180Hz monitor. Pass `0` to opt out and\n * redraw on every animation frame the browser delivers. A positive value\n * skips frames that arrive sooner than `1000 / maxFps` after the last\n * one; the loop itself keeps running, so the cap trades smoothness for\n * CPU rather than pausing anything.\n */\n maxFps?: number;\n}\n\n/**\n * Class-level defaults forwarded to `super` so the cascade hits Component's\n * applyOptions with `{ tag: \"canvas\" }` already merged into `_defaultOptions`.\n */\nconst _defaultCanvasOptions: Partial<CanvasOptions> = {\n tag: \"canvas\",\n maxFps: 30,\n};\n\n// Sentinel for the last-synced width/height/dpr guard. A real size is always\n// >= 0 and a real dpr always >= 1, so -1 can never equal a genuine value and\n// therefore forces the first `syncBackingStore` to run rather than short-circuit.\nconst NOT_YET_SYNCED = -1;\n\n/**\n * A raster drawing surface backed by a `<canvas>` element and a live\n * `CanvasRenderingContext2D`.\n *\n * The component keeps two sizes in lockstep: the CSS size the framework commits\n * (`setWidth` / `setHeight`) and the backing store (the element's `width` /\n * `height` attributes, sized CSS × device-pixel-ratio for a crisp HiDPI\n * result). Callers draw in CSS pixels via the {@link CanvasDrawCallback} `onDraw`\n * hook — the dpr transform is applied for them. Because reassigning the backing\n * store wipes it, `onDraw` is re-invoked after every resize / DPR change; content\n * that must survive a resize belongs there rather than in a one-off\n * {@link Canvas.getContext} draw.\n *\n * `Canvas` is **live-only**: a rendering context cannot be modelled offline or\n * forwarded across a worker, so under a modelled sink `getContext` returns `null`\n * and every draw path no-ops.\n *\n * @category Components\n */\nclass Canvas extends Component<CanvasOptions> {\n\n /** Cached 2D context; `null` offline or before the element renders. */\n private _ctx: CanvasRenderingContext2D | null = null;\n\n /** Active animation-frame handle; `null` when the loop is idle. */\n private _rafId: number | null = null;\n\n // Frame clock for the animation loop. `_animationStartMs` is null until the\n // first frame of a run anchors it, so elapsed time starts at 0 for that\n // frame however long the run waited to be scheduled. `_lastDrawMs` gates\n // the maxFps cap and `_elapsedMs` is what the draw hook is handed, kept as\n // a field so redraws from outside the loop repeat the last frame's value.\n private _animationStartMs: number | null = null;\n private _lastDrawMs : number | null = null;\n private _elapsedMs = 0;\n\n /** Consumer/auto intent to animate; the loop actually runs only while also\n * effectively visible (or animateWhenHidden is set). Plain initializer:\n * never written during the super() cascade (only startAnimation /\n * stopAnimation, which run post-render, write it), so it needs no\n * `declare`. */\n private _animationRequested = false;\n\n /** Last-synced CSS width; guards against a redundant backing-store wipe. */\n private _syncedWidth: number = NOT_YET_SYNCED;\n\n /** Last-synced CSS height; guards against a redundant backing-store wipe. */\n private _syncedHeight: number = NOT_YET_SYNCED;\n\n /** Last-synced device-pixel ratio; guards against a redundant wipe. */\n private _syncedDpr: number = NOT_YET_SYNCED;\n\n /** Generation counter so only the newest DPR-watch arm acts on a change. */\n private _dprToken: number = 0;\n\n /**\n * Constructs a raster canvas.\n *\n * @param options - Optional construction options.\n * @param subclassDefaults - Per-subclass default bag layered over this\n * class's defaults; subclasses forward their `_defaultXxxOptions`\n * constant here.\n */\n constructor(options?: CanvasOptions, subclassDefaults?: Partial<CanvasOptions>) {\n super(options, { ..._defaultCanvasOptions, ...(subclassDefaults ?? {}) });\n\n this.clearInsets();\n }\n\n /**\n * Forwards the consumer-configurable `onDraw` hook to its setter.\n *\n * @param options - The construction options.\n * @returns This component, for method chaining.\n */\n protected applyOptions(options: CanvasOptions): this {\n super.applyOptions(options);\n\n if (options.onDraw !== undefined) {\n this.setOnDraw(options.onDraw);\n }\n\n if (options.animateWhenHidden !== undefined) {\n this.setAnimateWhenHidden(options.animateWhenHidden);\n }\n\n if (options.maxFps !== undefined) {\n this.setMaxFps(options.maxFps);\n }\n\n return this;\n }\n\n /**\n * Returns the 2D rendering context, lazily obtaining it from the seam on\n * first access once the element exists.\n *\n * @returns The context, or `null` offline / before the element renders.\n */\n getContext(): CanvasRenderingContext2D | null {\n if (this._ctx) {\n return this._ctx;\n }\n\n const element = this.getElement();\n if (!element) {\n return null;\n }\n\n this._ctx = DOM.sink.getContext(element, \"2d\") as CanvasRenderingContext2D | null;\n\n return this._ctx;\n }\n\n /**\n * Sets (or clears) the draw hook and triggers an immediate redraw.\n *\n * @param handler - The draw callback, or `null` to clear it.\n * @returns This component, for method chaining.\n */\n setOnDraw(handler: CanvasDrawCallback | null): this {\n this._options.onDraw = handler ?? undefined;\n this.redraw();\n\n return this;\n }\n\n /**\n * Returns the current draw hook.\n *\n * @returns The draw callback, or `null` when none is set.\n */\n getOnDraw(): CanvasDrawCallback | null {\n return this._options.onDraw ?? this._defaultOptions.onDraw ?? null;\n }\n\n /**\n * Clears the surface (in CSS pixels) and re-invokes `onDraw` against the\n * current context. Public so a consumer can force a repaint after mutating\n * its own model without a resize. No-ops when the context is unavailable.\n *\n * @returns This component, for method chaining.\n */\n redraw(): this {\n const ctx = this.getContext();\n if (!ctx) {\n return this;\n }\n\n const width = this.getWidth();\n const height = this.getHeight();\n\n ctx.clearRect(0, 0, width, height);\n this.getOnDraw()?.(ctx, width, height, this._elapsedMs);\n\n return this;\n }\n\n /**\n * Starts a per-frame redraw loop. Idempotent: a second call while already\n * animating does not schedule a second frame. Records the intent to\n * animate; the loop only actually runs while the canvas is also\n * effectively on-screen (or {@link setAnimateWhenHidden} opts out).\n *\n * @returns This component, for method chaining.\n */\n startAnimation(): this {\n this._animationRequested = true;\n this._animationStartMs = null;\n this._lastDrawMs = null;\n this._elapsedMs = 0;\n this.reconcileAnimation();\n\n return this;\n }\n\n /**\n * Stops the per-frame redraw loop, cancelling any pending frame. Clears\n * the intent to animate, so a later resize/show cannot resurrect it.\n *\n * @returns This component, for method chaining.\n */\n stopAnimation(): this {\n this._animationRequested = false;\n this.reconcileAnimation();\n\n return this;\n }\n\n /**\n * Whether the per-frame redraw loop is currently running. `false` while\n * paused for being effectively hidden, even if animation was requested.\n *\n * @returns `true` while animating.\n */\n isAnimating(): boolean {\n return this._rafId !== null;\n }\n\n /**\n * Keeps the animation loop running while the canvas is not effectively\n * on-screen. Reconciles immediately, so toggling it can start or pause\n * the loop right away.\n *\n * @param value - `true` to animate regardless of visibility.\n * @returns This component, for method chaining.\n */\n setAnimateWhenHidden(value: boolean): this {\n this._options.animateWhenHidden = value;\n this.reconcileAnimation();\n\n return this;\n }\n\n /**\n * Caps how often the animation loop redraws, in frames per second. Takes\n * effect on the next frame; the loop keeps running either way, so this\n * thins redraws rather than pausing anything. Because frames are delivered\n * at the display's refresh rate, an uncapped loop costs proportionally more\n * on a high-refresh monitor — a cap makes that cost predictable.\n *\n * @param fps - Maximum redraws per second, or `0` to remove the cap entirely.\n * Negative values are treated as `0`. The class default is 30.\n * @returns This component, for method chaining.\n */\n setMaxFps(fps: number): this {\n // On the options bag rather than a private field, matching\n // `animateWhenHidden` above: `applyOptions` runs inside the `super()`\n // cascade, before this class's field initializers, so a plain\n // `_maxFps = 0` initializer would overwrite a construction-time value.\n this._options.maxFps = Math.max(0, fps);\n\n return this;\n }\n\n /**\n * Returns the current redraw cap in frames per second.\n *\n * @returns The cap, or `0` when the loop is uncapped. Resolves the class\n * default (30) when no explicit value was set.\n */\n getMaxFps(): number {\n // Consults `_defaultOptions` as well as `_options`, matching the\n // framework's getter convention (`getZIndex` is\n // `_options.zIndex ?? _defaultOptions.zIndex ?? 0`). A class-level\n // default bag lands in `_defaultOptions`, never in `_options`, so\n // reading only the latter would silently ignore a default-supplied cap\n // and leave the loop uncapped from the first frame.\n return this._options.maxFps ?? this._defaultOptions.maxFps ?? 0;\n }\n\n /**\n * Returns whether the animation loop keeps running while hidden.\n *\n * @returns `true` if the loop ignores effective visibility.\n */\n getAnimateWhenHidden(): boolean {\n return this._options.animateWhenHidden ?? this._defaultOptions.animateWhenHidden ?? false;\n }\n\n /**\n * Reusable seam shared with the WebGL sibling: resizes the backing store to\n * CSS × dpr, re-applies the dpr transform (reassigning the attributes resets\n * all context state), and redraws. Called from `doLayout` on every size\n * change. Reads only cached CSS sizes — never DOM geometry, which inside\n * `doLayout` is still buffered — and short-circuits when width/height/dpr are\n * unchanged so idle layout passes never wipe the buffer.\n */\n protected syncBackingStore(): void {\n const ctx = this.getContext();\n if (!ctx) {\n return;\n }\n\n const width = this.getWidth();\n const height = this.getHeight();\n const dpr = DOM.source.getDevicePixelRatio();\n\n if (width === this._syncedWidth && height === this._syncedHeight && dpr === this._syncedDpr) {\n return;\n }\n\n const element = this.getElement()!;\n\n DOM.sink.apply(element, { setAttr: {\n width: String(Math.round(width * dpr)),\n height: String(Math.round(height * dpr)),\n }});\n\n // Reassigning the backing-store attributes reset the context, so re-apply\n // the dpr scale (identity skew/translate, dpr on both axes) — one context\n // unit is then one CSS pixel and callers draw in logical coordinates.\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n this._syncedWidth = width;\n this._syncedHeight = height;\n this._syncedDpr = dpr;\n\n this.redraw();\n }\n\n /**\n * Delegates to the base layout then syncs the backing store — the single\n * settled hook that fires once after both axes are committed, on initial\n * mount and every subsequent resize.\n *\n * @returns This component, for method chaining.\n */\n doLayout(): this {\n super.doLayout();\n this.syncBackingStore();\n\n return this;\n }\n\n /**\n * Renders the element and arms the DPR-change watcher.\n *\n * @returns The created element handle.\n */\n protected render(): Handle {\n const element = super.render();\n\n this.watchDevicePixelRatio();\n\n return element;\n }\n\n /**\n * Stops the animation loop before the inherited destructor detaches the\n * element, so no stray frame survives teardown.\n */\n protected destructor(): void {\n this.stopAnimation();\n super.destructor();\n }\n\n /**\n * Whether the loop should be scheduled right now: animation was\n * requested, and either the canvas is effectively on-screen or the\n * consumer opted out of pausing via `animateWhenHidden`.\n */\n private shouldAnimate(): boolean {\n return this._animationRequested\n && (this.getAnimateWhenHidden() || this.isEffectivelyVisible());\n }\n\n /**\n * Brings the raw rAF loop into agreement with `shouldAnimate()`.\n * Idempotent — safe to call every `doLayout` and from the option setter.\n * A no-op during the construction cascade because `_animationRequested`\n * is still false.\n */\n private reconcileAnimation(): void {\n if (this.shouldAnimate()) {\n if (this._rafId === null) {\n this._rafId = DOM.sink.requestAnimationFrame(this.animationStep);\n }\n } else if (this._rafId != null) {\n // Loose comparison: during the super() cascade (a construction-time\n // animateWhenHidden option), this class's own field initializers\n // haven't run yet, so _rafId briefly reads as `undefined` rather than\n // its declared `null` default — treat both as \"nothing scheduled\".\n DOM.sink.cancelAnimationFrame(this._rafId);\n this._rafId = null;\n }\n }\n\n /**\n * One animation-frame step: self-pauses when animation is no longer\n * requested, otherwise redraws and reschedules. Effective-visibility\n * pausing is handled by {@link onEffectiveVisibilityChange} reconciling the\n * loop on the change event, not by this step re-checking visibility every\n * frame. Arrow field so the rAF callback keeps a stable bound ref.\n */\n private readonly animationStep = (timestamp: number): void => {\n if (!this._animationRequested) {\n this._rafId = null;\n return;\n }\n\n if (this._animationStartMs === null) {\n this._animationStartMs = timestamp;\n }\n\n // A skipped frame still reschedules: the cap thins out redraws, it does\n // not stop the loop, so raising maxFps again takes effect immediately.\n const maxFps = this.getMaxFps();\n const minIntervalMs = maxFps > 0 ? 1000 / maxFps : 0;\n const dueForRedraw = this._lastDrawMs === null\n || timestamp - this._lastDrawMs >= minIntervalMs;\n\n if (dueForRedraw) {\n this._lastDrawMs = timestamp;\n this._elapsedMs = timestamp - this._animationStartMs;\n this.redraw();\n }\n\n this._rafId = DOM.sink.requestAnimationFrame(this.animationStep);\n };\n\n /**\n * Reacts to an effective-visibility change by reconciling the animation\n * loop — the replacement for the old per-frame `isEffectivelyVisible()`\n * poll inside `doLayout`.\n *\n * @param effective - The component's new effective-visibility state.\n */\n protected onEffectiveVisibilityChange(effective: boolean): void {\n super.onEffectiveVisibilityChange(effective);\n this.reconcileAnimation();\n }\n\n /**\n * Arms a one-shot `matchMedia` watch for the current device-pixel ratio. A\n * window dragged to a different-DPI monitor changes the ratio without a\n * resize/relayout, which `doLayout` would miss; on change this re-syncs the\n * backing store and re-arms for the new ratio. A generation token keeps only\n * the newest arm active, so re-arming cannot fan out — and the seam has no\n * unsubscribe, so each change leaves one inert native listener behind\n * (bounded by the number of DPR changes in a session).\n */\n private watchDevicePixelRatio(): void {\n const token = ++this._dprToken;\n const dpr = DOM.source.getDevicePixelRatio();\n\n DOM.source\n .matchMedia(`(resolution: ${dpr}dppx)`)\n .addChangeListener(() => this.onDevicePixelRatioChange(token));\n }\n\n /**\n * Handles a device-pixel-ratio change from the newest armed watch: re-syncs\n * the backing store at the fresh ratio and re-arms. Superseded arms (a stale\n * token) and post-teardown fires (no element) are inert.\n *\n * @param token - The generation token this listener was armed with.\n */\n private onDevicePixelRatioChange(token: number): void {\n if (token !== this._dprToken) {\n return;\n }\n\n if (!this.getElement()) {\n return;\n }\n\n this.syncBackingStore();\n this.watchDevicePixelRatio();\n }\n}\n\nconst CanvasCallable = callable(Canvas);\ntype CanvasCallable = Canvas;\nexport {\n Canvas as _Canvas,\n CanvasCallable as Canvas\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport { Event } from \"~/core/Event.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * GL-resource (re)build hook: receives the live WebGL2 context. Called once on\n * first context acquisition and again after every context restore — build (or\n * rebuild) shaders, programs, buffers, VAOs, and textures here.\n *\n * @param gl - The live `WebGL2RenderingContext`.\n *\n * @category Components\n */\nexport type WebGLContextInitCallback = (gl: WebGL2RenderingContext) => void;\n\n/**\n * Per-frame draw hook: receives the live WebGL2 context and the logical\n * (CSS-px) size. Issue draw calls here; the component has already set the\n * drawing-buffer viewport in device pixels.\n *\n * @param gl - The live `WebGL2RenderingContext`.\n * @param width - The drawing surface width in CSS pixels.\n * @param height - The drawing surface height in CSS pixels.\n *\n * @category Components\n */\nexport type WebGLFrameCallback = (\n gl: WebGL2RenderingContext,\n width: number,\n height: number,\n elapsedMs: number,\n) => void;\n\n/**\n * Construction-time options for {@link WebGLCanvas}.\n *\n * @category Components\n */\nexport interface WebGLCanvasOptions extends ComponentOptions {\n\n /** GL-resource (re)build hook; runs on init and after each context restore. */\n onContextInit?: WebGLContextInitCallback;\n\n /**\n * Per-frame draw hook. Its fourth argument is the milliseconds elapsed\n * since the current animation run started — derive motion from that rather\n * than advancing a counter once per call, since frames arrive at the\n * display's refresh rate and a per-call increment runs three times faster\n * on a 180Hz monitor than on a 60Hz one.\n */\n onFrame?: WebGLFrameCallback;\n\n /**\n * Keeps the animation loop running while the canvas is not effectively\n * on-screen (e.g. on an inactive `Tab` panel). Default `false`: the loop\n * pauses automatically when hidden and resumes when shown again.\n */\n animateWhenHidden?: boolean;\n\n /**\n * Upper bound, in frames per second, on how often the animation loop\n * renders. Defaults to 30 — enough for smooth motion at a predictable cost,\n * and independent of the display's refresh rate, so the same animation\n * costs the same on a 60Hz and a 180Hz monitor. Pass `0` to opt out and\n * render on every animation frame the browser delivers. A positive value\n * skips frames that arrive sooner than `1000 / maxFps` after the last\n * one; the loop itself keeps running, so the cap trades smoothness for\n * CPU rather than pausing anything.\n */\n maxFps?: number;\n}\n\n/**\n * Class-level defaults forwarded to `super` so the cascade hits Component's\n * applyOptions with `{ tag: \"canvas\" }` already merged into `_defaultOptions`.\n */\nconst _defaultWebGLCanvasOptions: Partial<WebGLCanvasOptions> = {\n tag: \"canvas\",\n maxFps: 30,\n};\n\n// Sentinel for the last-synced width/height/dpr guard. A real size is always\n// >= 0 and a real dpr always >= 1, so -1 can never equal a genuine value and\n// therefore forces the first `syncBackingStore` to run rather than short-circuit.\nconst NOT_YET_SYNCED = -1;\n\n/**\n * A GPU drawing surface backed by a `<canvas>` element and a live\n * `WebGL2RenderingContext`.\n *\n * The component owns the canvas element, the GL context, the animation loop,\n * and context-loss recovery; the consumer owns shaders, buffers, and draw calls\n * through two hooks. {@link WebGLCanvasOptions.onContextInit | onContextInit}\n * (re)builds GPU resources — it runs once on first acquisition and again after\n * every context restore. {@link WebGLCanvasOptions.onFrame | onFrame} draws each\n * frame; the drawing-buffer viewport is already set in device pixels, and the\n * hook receives the logical (CSS-px) size for projection math.\n *\n * Backing-store sizing mirrors the 2D sibling: the element's `width` / `height`\n * attributes are kept at CSS × device-pixel-ratio for a crisp HiDPI result, and\n * every resize re-emits `gl.viewport(0, 0, backingW, backingH)` in device pixels\n * (reassigning the attributes resizes the drawing buffer but leaves GL resources\n * intact). The render loop starts automatically on the first connected layout,\n * pauses automatically while the surface is not effectively on-screen (e.g. on\n * an inactive `Tab` panel) — resuming once it's shown again — and stops on\n * teardown; call {@link WebGLCanvas.startAnimation | startAnimation} /\n * {@link WebGLCanvas.stopAnimation | stopAnimation} to drive a static or on-demand\n * surface explicitly.\n *\n * `WebGLCanvas` is **live-only**: a GL context cannot be modelled offline or\n * forwarded across a worker, so under a modelled sink `getContext` returns `null`\n * and every render path no-ops. It is WebGL2 only.\n *\n * @category Components\n */\nclass WebGLCanvas extends Component<WebGLCanvasOptions> {\n\n /** Cached WebGL2 context; `null` offline or before the element renders. */\n private _gl: WebGL2RenderingContext | null = null;\n\n /** Active animation-frame handle; `null` when the loop is idle. */\n private _rafId: number | null = null;\n\n // Frame clock for the animation loop. `_animationStartMs` is null until the\n // first frame of a run anchors it, so elapsed time starts at 0 for that\n // frame however long the run waited to be scheduled. `_lastDrawMs` gates\n // the maxFps cap and `_elapsedMs` is what the frame hook is handed, kept as\n // a field so renders from outside the loop repeat the last frame's value.\n private _animationStartMs: number | null = null;\n private _lastDrawMs : number | null = null;\n private _elapsedMs = 0;\n\n /** Consumer/auto intent to animate; the loop actually runs only while also\n * effectively visible (or animateWhenHidden is set). Plain initializer:\n * never written during the super() cascade (only startAnimation /\n * stopAnimation, which run post-render, write it), so it needs no\n * `declare`. */\n private _animationRequested = false;\n\n /** True between `webglcontextlost` and `webglcontextrestored`; frames skip. */\n private _contextLost: boolean = false;\n\n private readonly _onContextLost: () => void = () => {\n this._contextLost = true;\n };\n\n /**\n * False until `onContextInit` has run for the current context; reset on\n * restore (and by `setOnContextInit`) so the next frame re-runs the hook.\n * Written by `setOnContextInit`, which `applyOptions` can dispatch during the\n * `super()` cascade, so it is `declare`d and assigned in the constructor body.\n */\n declare private _contextInitialised: boolean;\n\n /** Last-synced CSS width; guards against a redundant buffer resize. */\n private _syncedWidth: number = NOT_YET_SYNCED;\n\n /** Last-synced CSS height; guards against a redundant buffer resize. */\n private _syncedHeight: number = NOT_YET_SYNCED;\n\n /** Last-synced device-pixel ratio; guards against a redundant buffer resize. */\n private _syncedDpr: number = NOT_YET_SYNCED;\n\n /** Generation counter so only the newest DPR-watch arm acts on a change. */\n private _dprToken: number = 0;\n\n /**\n * Constructs a WebGL2 canvas.\n *\n * @param options - Optional construction options.\n * @param subclassDefaults - Per-subclass default bag layered over this\n * class's defaults; subclasses forward their `_defaultXxxOptions`\n * constant here.\n */\n constructor(options?: WebGLCanvasOptions, subclassDefaults?: Partial<WebGLCanvasOptions>) {\n super(options, { ..._defaultWebGLCanvasOptions, ...(subclassDefaults ?? {}) });\n\n this._contextInitialised = false;\n\n this.clearInsets();\n }\n\n /**\n * Forwards the consumer-configurable hooks to their setters.\n *\n * @param options - The construction options.\n * @returns This component, for method chaining.\n */\n protected applyOptions(options: WebGLCanvasOptions): this {\n super.applyOptions(options);\n\n if (options.onContextInit !== undefined) {\n this.setOnContextInit(options.onContextInit);\n }\n\n if (options.onFrame !== undefined) {\n this.setOnFrame(options.onFrame);\n }\n\n if (options.animateWhenHidden !== undefined) {\n this.setAnimateWhenHidden(options.animateWhenHidden);\n }\n\n if (options.maxFps !== undefined) {\n this.setMaxFps(options.maxFps);\n }\n\n return this;\n }\n\n /**\n * Returns the WebGL2 rendering context, lazily obtaining it from the seam on\n * first access once the element exists and narrowing the generic seam result\n * to `WebGL2RenderingContext`.\n *\n * @returns The context, or `null` offline / before the element renders.\n */\n getContext(): WebGL2RenderingContext | null {\n if (this._gl) {\n return this._gl;\n }\n\n const element = this.getElement();\n if (!element) {\n return null;\n }\n\n this._gl = DOM.sink.getContext(element, \"webgl2\") as WebGL2RenderingContext | null;\n\n return this._gl;\n }\n\n /**\n * Sets (or clears) the GL-resource build hook. Marks the context\n * uninitialised so the new hook runs on the next frame.\n *\n * @param handler - The context-init callback, or `null` to clear it.\n * @returns This component, for method chaining.\n */\n setOnContextInit(handler: WebGLContextInitCallback | null): this {\n this._options.onContextInit = handler ?? undefined;\n this._contextInitialised = false;\n\n return this;\n }\n\n /**\n * Returns the current GL-resource build hook.\n *\n * @returns The context-init callback, or `null` when none is set.\n */\n getOnContextInit(): WebGLContextInitCallback | null {\n return this._options.onContextInit ?? this._defaultOptions.onContextInit ?? null;\n }\n\n /**\n * Sets (or clears) the per-frame draw hook.\n *\n * @param handler - The frame callback, or `null` to clear it.\n * @returns This component, for method chaining.\n */\n setOnFrame(handler: WebGLFrameCallback | null): this {\n this._options.onFrame = handler ?? undefined;\n\n return this;\n }\n\n /**\n * Returns the current per-frame draw hook.\n *\n * @returns The frame callback, or `null` when none is set.\n */\n getOnFrame(): WebGLFrameCallback | null {\n return this._options.onFrame ?? this._defaultOptions.onFrame ?? null;\n }\n\n /**\n * Starts the per-frame render loop. Idempotent: a second call while already\n * animating does not schedule a second frame. Records the intent to\n * animate; the loop only actually runs while the canvas is also\n * effectively on-screen (or {@link setAnimateWhenHidden} opts out).\n *\n * @returns This component, for method chaining.\n */\n startAnimation(): this {\n this._animationRequested = true;\n this._animationStartMs = null;\n this._lastDrawMs = null;\n this._elapsedMs = 0;\n this.reconcileAnimation();\n\n return this;\n }\n\n /**\n * Stops the per-frame render loop, cancelling any pending frame. Clears\n * the intent to animate, so a later resize/show cannot resurrect it.\n *\n * @returns This component, for method chaining.\n */\n stopAnimation(): this {\n this._animationRequested = false;\n this.reconcileAnimation();\n\n return this;\n }\n\n /**\n * Whether the per-frame render loop is currently running. `false` while\n * paused for being effectively hidden, even if animation was requested.\n *\n * @returns `true` while animating.\n */\n isAnimating(): boolean {\n return this._rafId !== null;\n }\n\n /**\n * Keeps the animation loop running while the canvas is not effectively\n * on-screen. Reconciles immediately, so toggling it can start or pause\n * the loop right away.\n *\n * @param value - `true` to animate regardless of visibility.\n * @returns This component, for method chaining.\n */\n setAnimateWhenHidden(value: boolean): this {\n this._options.animateWhenHidden = value;\n this.reconcileAnimation();\n\n return this;\n }\n\n /**\n * Returns whether the animation loop keeps running while hidden.\n *\n * @returns `true` if the loop ignores effective visibility.\n */\n getAnimateWhenHidden(): boolean {\n return this._options.animateWhenHidden ?? this._defaultOptions.animateWhenHidden ?? false;\n }\n\n /**\n * Caps how often the animation loop renders, in frames per second. Takes\n * effect on the next frame; the loop keeps running either way, so this\n * thins renders rather than pausing anything. Because frames are delivered\n * at the display's refresh rate, an uncapped loop costs proportionally more\n * on a high-refresh monitor — a cap makes that cost predictable.\n *\n * @param fps - Maximum renders per second, or `0` to remove the cap entirely.\n * Negative values are treated as `0`. The class default is 30.\n * @returns This component, for method chaining.\n */\n setMaxFps(fps: number): this {\n // On the options bag rather than a private field, matching\n // `animateWhenHidden`: `applyOptions` runs inside the `super()`\n // cascade, before this class's field initializers, so a plain\n // `_maxFps = 0` initializer would overwrite a construction-time value.\n this._options.maxFps = Math.max(0, fps);\n\n return this;\n }\n\n /**\n * Returns the current render cap in frames per second.\n *\n * @returns The cap, or `0` when the loop is uncapped. Resolves the class\n * default (30) when no explicit value was set.\n */\n getMaxFps(): number {\n // Consults `_defaultOptions` as well as `_options`, matching the\n // framework's getter convention (`getZIndex` is\n // `_options.zIndex ?? _defaultOptions.zIndex ?? 0`). A class-level\n // default bag lands in `_defaultOptions`, never in `_options`, so\n // reading only the latter would silently ignore a default-supplied cap\n // and leave the loop uncapped from the first frame.\n return this._options.maxFps ?? this._defaultOptions.maxFps ?? 0;\n }\n\n /**\n * Resizes the backing store to CSS × dpr, sets `gl.viewport` in device\n * pixels (WebGL's replacement for the 2D transform), and re-emits one frame.\n * Called from `doLayout` on every size change. Reads only cached CSS sizes —\n * never DOM geometry, which inside `doLayout` is still buffered — and\n * short-circuits when width/height/dpr are unchanged so idle layout passes\n * never resize the drawing buffer. Reassigning the attributes resizes the\n * buffer but leaves GL resources intact, so only the viewport is refreshed.\n */\n protected syncBackingStore(): void {\n const gl = this.getContext();\n if (!gl) {\n return;\n }\n\n const width = this.getWidth();\n const height = this.getHeight();\n const dpr = DOM.source.getDevicePixelRatio();\n\n if (width === this._syncedWidth && height === this._syncedHeight && dpr === this._syncedDpr) {\n return;\n }\n\n const backingW = Math.round(width * dpr);\n const backingH = Math.round(height * dpr);\n\n DOM.sink.apply(this.getElement()!, { setAttr: {\n width: String(backingW),\n height: String(backingH),\n }});\n\n // Device pixels, not CSS px — the drawing buffer is CSS × dpr and the\n // viewport must cover it fully for a crisp, unstretched result.\n gl.viewport(0, 0, backingW, backingH);\n\n this._syncedWidth = width;\n this._syncedHeight = height;\n this._syncedDpr = dpr;\n\n this.renderFrame();\n }\n\n /**\n * Delegates to the base layout then syncs the backing store — the single\n * settled hook that fires once after both axes are committed, on initial\n * mount and every subsequent resize.\n *\n * @returns This component, for method chaining.\n */\n doLayout(): this {\n super.doLayout();\n this.syncBackingStore();\n\n return this;\n }\n\n /**\n * Renders the element, wires context loss / restore, arms the DPR-change\n * watcher, and starts the render loop on the first connected layout.\n *\n * @returns The created element handle.\n */\n protected render(): Handle {\n const element = super.render();\n\n // `prevent: true` is REQUIRED — without it the browser never fires\n // `webglcontextrestored`.\n Event.addListener(this, \"webglcontextlost\", { prevent: true, handler: this._onContextLost });\n Event.addListener(this, \"webglcontextrestored\", () => {\n this._contextLost = false;\n this._contextInitialised = false;\n this.syncBackingStore();\n });\n\n this.watchDevicePixelRatio();\n this.onFirstLayout(() => this.startAnimation());\n\n return element;\n }\n\n /**\n * Stops the render loop before the inherited destructor detaches the\n * element, so no stray frame survives teardown.\n */\n protected destructor(): void {\n this.stopAnimation();\n super.destructor();\n }\n\n /**\n * Whether the loop should be scheduled right now: animation was\n * requested, and either the canvas is effectively on-screen or the\n * consumer opted out of pausing via `animateWhenHidden`.\n */\n private shouldAnimate(): boolean {\n return this._animationRequested\n && (this.getAnimateWhenHidden() || this.isEffectivelyVisible());\n }\n\n /**\n * Brings the raw rAF loop into agreement with `shouldAnimate()`.\n * Idempotent — safe to call every `doLayout` and from the option setter.\n * A no-op during the construction cascade because `_animationRequested`\n * is still false.\n */\n private reconcileAnimation(): void {\n if (this.shouldAnimate()) {\n if (this._rafId === null) {\n this._rafId = DOM.sink.requestAnimationFrame(this.animationStep);\n }\n } else if (this._rafId != null) {\n // Loose comparison: during the super() cascade (a construction-time\n // animateWhenHidden option), this class's own field initializers\n // haven't run yet, so _rafId briefly reads as `undefined` rather than\n // its declared `null` default — treat both as \"nothing scheduled\".\n DOM.sink.cancelAnimationFrame(this._rafId);\n this._rafId = null;\n }\n }\n\n /**\n * One animation-frame step: self-pauses when animation is no longer\n * requested, otherwise renders a frame and reschedules. Effective-visibility\n * pausing is handled by {@link onEffectiveVisibilityChange} reconciling the\n * loop on the change event, not by this step re-checking visibility every\n * frame. Arrow field so the rAF callback keeps a stable bound ref.\n */\n private readonly animationStep = (timestamp: number): void => {\n if (!this._animationRequested) {\n this._rafId = null;\n return;\n }\n\n if (this._animationStartMs === null) {\n this._animationStartMs = timestamp;\n }\n\n // A skipped frame still reschedules: the cap thins out renders, it does\n // not stop the loop, so raising maxFps again takes effect immediately.\n const maxFps = this.getMaxFps();\n const minIntervalMs = maxFps > 0 ? 1000 / maxFps : 0;\n const dueForRender = this._lastDrawMs === null\n || timestamp - this._lastDrawMs >= minIntervalMs;\n\n if (dueForRender) {\n this._lastDrawMs = timestamp;\n this._elapsedMs = timestamp - this._animationStartMs;\n this.renderFrame();\n }\n\n this._rafId = DOM.sink.requestAnimationFrame(this.animationStep);\n };\n\n /**\n * Reacts to an effective-visibility change by reconciling the animation\n * loop — the replacement for the old per-frame `isEffectivelyVisible()`\n * poll inside `doLayout`.\n *\n * @param effective - The component's new effective-visibility state.\n */\n protected onEffectiveVisibilityChange(effective: boolean): void {\n super.onEffectiveVisibilityChange(effective);\n this.reconcileAnimation();\n }\n\n /**\n * One frame: lazily runs `onContextInit` the first time (and after a\n * restore), then invokes `onFrame` with the logical CSS-px size. Skips while\n * the context is lost or unavailable.\n */\n private renderFrame(): void {\n const gl = this.getContext();\n if (!gl || this._contextLost) {\n return;\n }\n\n if (!this._contextInitialised) {\n this.getOnContextInit()?.(gl);\n this._contextInitialised = true;\n }\n\n this.getOnFrame()?.(gl, this.getWidth(), this.getHeight(), this._elapsedMs);\n }\n\n /**\n * Arms a one-shot `matchMedia` watch for the current device-pixel ratio. A\n * window dragged to a different-DPI monitor changes the ratio without a\n * resize/relayout, which `doLayout` would miss; on change this re-syncs the\n * backing store and re-arms for the new ratio. A generation token keeps only\n * the newest arm active, so re-arming cannot fan out — and the seam has no\n * unsubscribe, so each change leaves one inert native listener behind\n * (bounded by the number of DPR changes in a session).\n */\n private watchDevicePixelRatio(): void {\n const token = ++this._dprToken;\n const dpr = DOM.source.getDevicePixelRatio();\n\n DOM.source\n .matchMedia(`(resolution: ${dpr}dppx)`)\n .addChangeListener(() => this.onDevicePixelRatioChange(token));\n }\n\n /**\n * Handles a device-pixel-ratio change from the newest armed watch: re-syncs\n * the backing store at the fresh ratio and re-arms. Superseded arms (a stale\n * token) and post-teardown fires (no element) are inert.\n *\n * @param token - The generation token this listener was armed with.\n */\n private onDevicePixelRatioChange(token: number): void {\n if (token !== this._dprToken) {\n return;\n }\n\n if (!this.getElement()) {\n return;\n }\n\n this.syncBackingStore();\n this.watchDevicePixelRatio();\n }\n}\n\nconst WebGLCanvasCallable = callable(WebGLCanvas);\ntype WebGLCanvasCallable = WebGLCanvas;\nexport {\n WebGLCanvas as _WebGLCanvas,\n WebGLCanvasCallable as WebGLCanvas\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport { Size } from \"~/primitive/Size.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * Construction-time options for {@link Image}.\n *\n * @remarks Supplying `preferredSize` locks the rendered size and disables the\n * default natural-dimension auto-fit behaviour.\n *\n * @category Components\n */\nexport interface ImageOptions extends ComponentOptions {\n}\n\n// Upper bound for the auto-derived `minSize` per axis. Small images report\n// their intrinsic size (so a 16×16 favicon stays sharp at full natural size);\n// larger images cap here so their parent layout can always shrink them down.\nconst IMAGE_AUTO_MIN_CAP_PX = 100;\n\n/**\n * Class-level defaults forwarded to `super` so the cascade hits Component's\n * applyOptions with `{ tag: \"img\" }` already merged into `_defaultOptions`.\n */\nconst _defaultImageOptions: Partial<ImageOptions> = {\n tag: \"img\",\n};\n\n/**\n * An image component backed by an `<img>` element.\n *\n * Reports its preferred size from the image's natural intrinsic dimensions once loaded.\n *\n * @category Components\n */\nclass Image extends Component<ImageOptions> {\n\n private _src: String;\n\n /**\n * @param src - Image source URL.\n * @param options - Optional construction options.\n * @param subclassDefaults - Per-subclass default bag layered over this\n * class's defaults; subclasses forward their `_defaultXxxOptions`\n * constant here.\n */\n constructor(src: String, options?: ImageOptions, subclassDefaults?: Partial<ImageOptions>) {\n super(options, { ..._defaultImageOptions, ...(subclassDefaults ?? {}) });\n\n this._src = src;\n this.clearInsets();\n }\n\n /**\n * Returns the component's element handle.\n *\n * @param createIfMissing - Optional. When true, renders the element if it does not yet exist.\n *\n * @returns The component's element handle.\n */\n getElement(createIfMissing: boolean = false): Handle | undefined {\n return super.getElement(createIfMissing);\n }\n\n /**\n * Returns the natural intrinsic size of the image from the DOM element.\n *\n * @returns A Size with the image's naturalWidth and naturalHeight.\n */\n getPreferredSize(): Size | null {\n let element = this.getElement();\n\n const natural = DOM.source.getNaturalSize(element!);\n return {\n width: natural.width,\n height: natural.height\n };\n }\n\n /**\n * Returns a minimum size derived from the image's intrinsic dimensions\n * (mirrors the `Math.min(natural, 100)` cap that `Text` applies), so\n * small images keep their full size while large images stay shrinkable\n * by their parent layout. An explicit `setMinSize` from the caller\n * wins via `Component.getMinSize`'s `_options.minSize` priority.\n *\n * @returns The minimum `{width, height}` from intrinsic dims, or a\n * `20×20` pre-load fallback before the image has decoded.\n */\n getMinSize(): Size | null {\n const explicit = super.getMinSize();\n if (this._options.minSize) {\n return explicit;\n }\n\n const element = this.getElement();\n if (!element || !DOM.source.getNaturalSize(element).width) {\n return { width: 20, height: 20 };\n }\n\n const natural = DOM.source.getNaturalSize(element);\n return {\n width: Math.min(natural.width, IMAGE_AUTO_MIN_CAP_PX),\n height: Math.min(natural.height, IMAGE_AUTO_MIN_CAP_PX),\n };\n }\n\n /**\n * Renders the img element and sets its src attribute.\n *\n * @returns The created HTMLImageElement with its src initialised.\n */\n render(): Handle {\n let element = super.render();\n\n DOM.sink.apply(element, { setAttr: { src: this._src.valueOf() } });\n\n return element;\n }\n}\n\nconst ImageCallable = callable(Image);\ntype ImageCallable = Image;\nexport {\n Image as _Image,\n ImageCallable as Image\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle } from \"~/core/DOM.js\";\nimport { Event } from \"~/core/Event.js\";\nimport { StyleRule } from \"~/core/StyleTarget.js\";\nimport { ThemeManager } from \"~/core/Theme.js\";\nimport { callable } from \"~/core/Callable.js\";\nimport { Size } from \"~/primitive/Size.js\";\nimport { lexer } from \"marked\";\nimport type { Token, Tokens } from \"marked\";\n// Type-only: erased at compile time. `CodeEditor` itself is loaded through a\n// narrow dynamic import (see `loadCodeEditorUpgrade`) so a static top-level\n// value import here would force every `Markdown` consumer's bundler to\n// resolve CodeMirror the moment it imports `Markdown` at all.\nimport type { CodeEditor } from \"~/component/editor/CodeEditor.js\";\n\n/**\n * Shared class names for the prose elements built by {@link Markdown}. Kept as\n * constants so the render helpers and the module-level rule builder reference\n * the same strings.\n */\nconst CODE_CLASS = \"ts-ui-md-code\";\nconst PRE_CLASS = \"ts-ui-md-pre\";\nconst QUOTE_CLASS = \"ts-ui-md-quote\";\nconst LIST_CLASS = \"ts-ui-md-list\";\nconst LINK_CLASS = \"ts-ui-md-link\";\nconst HEADING_CLASS = \"ts-ui-md-heading\";\nconst TABLE_WRAP_CLASS = \"ts-ui-md-table-wrap\";\nconst TABLE_CLASS = \"ts-ui-md-table\";\nconst TH_CLASS = \"ts-ui-md-th\";\nconst TD_CLASS = \"ts-ui-md-td\";\nconst ALIGN_LEFT_CLASS = \"ts-ui-md-align-left\";\nconst ALIGN_CENTER_CLASS = \"ts-ui-md-align-center\";\nconst ALIGN_RIGHT_CLASS = \"ts-ui-md-align-right\";\n/**\n * Wraps a fenced block that upgrades to a live `CodeEditor`. `position:\n * relative` gives the absolutely-positioned `CodeEditor` child a local\n * positioning context wherever the block sits in the token tree (top-level,\n * inside a blockquote, inside a list item) — without it, the editor would\n * resolve `top`/`left` against `Markdown`'s own root instead.\n */\nconst CODE_HOST_CLASS = \"ts-ui-md-code-host\";\n\n// Markdown heading levels span h1..h6; a deeper `#######` run is not a heading\n// in CommonMark, but clamp anyway so a stray `depth` never mints an invalid tag.\nconst HEADING_MIN_DEPTH = 1;\nconst HEADING_MAX_DEPTH = 6;\n\n/**\n * Maps a fenced code block's info-string language (lowercased, first word) to\n * the [`CodeEditor`](/components/CodeEditor) registry id it upgrades to. Only\n * the five ids `languages.ts` registers are\n * reachable; every other alias, and any language with no registered editor\n * support, is intentionally absent — {@link mapFenceLangToEditorId} returns\n * `null` for those rather than guessing, since passing an unregistered id to\n * `CodeEditor` would silently render unhighlighted text.\n */\nconst FENCE_LANG_ALIASES: Record<string, string> = {\n js: \"javascript\", javascript: \"javascript\", jsx: \"javascript\", mjs: \"javascript\", cjs: \"javascript\",\n ts: \"javascript\", typescript: \"javascript\", tsx: \"javascript\",\n json: \"json\",\n html: \"html\", htm: \"html\",\n sql: \"sql\",\n md: \"markdown\", markdown: \"markdown\",\n};\n\n/**\n * Cap on how many rows a fenced block's upgraded CodeEditor grows to before\n * its own vertical scrollbar takes over, rather than the wrapper continuing\n * to grow — keeps one long fenced block from pushing the rest of the prose\n * far down the page. Not a theme token: this is the only call site that\n * needs it, and BaseTheme.ts has no existing \"row count\" token shape to\n * extend (see Architecture Decisions).\n */\nconst CODE_BLOCK_MAX_AUTO_ROWS = 20;\n\n/**\n * Threshold, in pixels, for warning when a fenced block's `CodeEditor`\n * corrects its mount-time guessed height (measured from the plain-text\n * placeholder `<pre>`) to CodeMirror's real, measured content height. Two\n * live reproductions of a \"spasms and never settles\" report measured this\n * correction at 20px and 29px on an ordinary page — this sits above that\n * noise floor's sub-pixel/fractional rounding (CodeMirror's own line\n * metrics can report fractional heights like 19.5938px) while still\n * catching either measured case, so a future report of the same shape\n * comes with hard numbers instead of a cold trail.\n */\nconst GUESS_HEIGHT_CORRECTION_WARN_PX = 8;\n\n/**\n * How many viewport-heights below the fold a fenced block's `CodeEditor`\n * upgrade starts before scrolling actually reaches it (see\n * {@link Markdown.isBlockNearViewport}) — applied below the fold only, never\n * above. One viewport-height is roughly 1.5 seconds of runway at a typical\n * 900px pane and 60fps scroll speed: enough that a reader scrolling down\n * never catches an unhighlighted block, while costing at most a screenful of\n * extra upgrades over the strict minimum. A fixed pixel margin was rejected\n * because it would make a tall monitor prefetch proportionally less.\n */\nconst CODE_UPGRADE_LOOKAHEAD_VIEWPORTS = 1;\n\n/**\n * Resolves a fenced code block's info-string language to the `CodeEditor`\n * registry id it should upgrade to, per {@link FENCE_LANG_ALIASES}.\n *\n * @param lang - The fence's info string as reported by `marked` (e.g. `\"js\"`,\n * `\"ts {1,3}\"`), or `undefined` when the fence carries none.\n * @returns The mapped `CodeEditor` language id, or `null` when `lang` is\n * unset or names a language with no registered editor support — the caller\n * treats both identically (render the plain `<pre>`).\n */\nfunction mapFenceLangToEditorId(lang: string | undefined): string | null {\n if (!lang) {\n return null;\n }\n\n // Only the first whitespace-delimited word is the language token per\n // CommonMark; a shebang-style modifier after it (`js {1,3}`) must not\n // defeat the match.\n const word = lang.trim().split(/\\s+/, 1)[0]?.toLowerCase();\n\n return word ? (FENCE_LANG_ALIASES[word] ?? null) : null;\n}\n\nlet _classRulesEnsured = false;\n\n/**\n * Injects the shared prose class rules on first use. Idempotent — guarded by the\n * module-level `_classRulesEnsured` flag, mirroring `Glyph`'s keyframe singleton.\n *\n * @remarks Presentation references the framework theme tokens actually used by\n * the rules — `--ts-ui-font-mono` (code/pre font), `--ts-ui-border-radius`\n * (code/pre corners), `--ts-ui-border-color` (the blockquote bar), and the\n * accent `--ts-ui-indicator-focus` (link colour) — each with a fallback that\n * works in both light and dark themes; the code/pre background is a theme-neutral\n * translucent grey wash rather than a token, since no surface token exists. Every\n * spacing constant is genuine structural spacing (a code padding, a list marker\n * gutter, a blockquote bar), expressed in `em` so it scales with the surrounding\n * font — not a cosmetic inset.\n */\nfunction ensureMarkdownClassRules(): void {\n if (_classRulesEnsured) {\n return;\n }\n\n _classRulesEnsured = true;\n\n new StyleRule({\n scope: \"class\",\n name: CODE_CLASS,\n styles: {\n fontFamily: \"var(--ts-ui-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)\",\n // Translucent grey wash reads as \"code\" on both light and dark\n // backgrounds without needing a dedicated surface token.\n background: \"rgba(127, 127, 127, 0.16)\",\n borderRadius: \"var(--ts-ui-border-radius, 3px)\",\n // Snug em-relative padding so the wash hugs inline code glyphs.\n padding: \"0.1em 0.3em\",\n // Reset the prose line-height the root sets for reading — code reads\n // as fixed-width text, not continuous prose.\n lineHeight: \"normal\",\n },\n });\n\n new StyleRule({\n scope: \"class\",\n name: PRE_CLASS,\n styles: {\n fontFamily: \"var(--ts-ui-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace)\",\n background: \"rgba(127, 127, 127, 0.16)\",\n borderRadius: \"var(--ts-ui-border-radius, 3px)\",\n // Block padding gives the fenced code room; it is the code frame's\n // structural inset, not a visual nudge.\n padding: \"0.6em 0.8em\",\n // Preserve authored whitespace/newlines and scroll long lines rather\n // than reflow them.\n whiteSpace: \"pre\",\n overflow: \"auto\",\n // Reset the prose line-height the root sets for reading — fenced\n // code reads as fixed-width text, not continuous prose.\n lineHeight: \"normal\",\n },\n });\n\n new StyleRule({\n scope: \"class\",\n name: QUOTE_CLASS,\n styles: {\n // 3px quote bar — the framework's thin-border weight — plus an\n // em-relative gutter that indents the quoted prose off the bar.\n borderLeft: \"3px solid var(--ts-ui-border-color, rgba(127, 127, 127, 0.4))\",\n paddingLeft: \"1em\",\n marginLeft: \"0\",\n },\n });\n\n new StyleRule({\n scope: \"class\",\n name: LIST_CLASS,\n // Room for the list marker; structural indentation, em-relative so it\n // tracks the font size.\n styles: { paddingLeft: \"1.5em\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: LINK_CLASS,\n // The framework's single accent hue (shared with focus/selection).\n styles: { color: \"var(--ts-ui-indicator-focus, #2563eb)\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: HEADING_CLASS,\n // Semibold so headings read as headings independent of any UA-style\n // reset the host page may apply.\n styles: { fontWeight: \"600\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: TABLE_WRAP_CLASS,\n // A table's columns cannot reflow below their content width, so the\n // wrapper scrolls horizontally instead of letting the table spill\n // past the component's assigned width — the same story fenced code\n // already tells with its own frame.\n styles: { maxWidth: \"100%\", overflowX: \"auto\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: TABLE_CLASS,\n styles: { borderCollapse: \"collapse\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: TH_CLASS,\n styles: {\n border: \"1px solid var(--ts-ui-border-color, rgba(127, 127, 127, 0.4))\",\n // Structural cell padding, em-relative so it tracks the font size.\n padding: \"0.3em 0.6em\",\n fontWeight: \"600\",\n // Overrides the browser's centred <th> default so an unaligned\n // header cell reads left, matching its unaligned body cells.\n textAlign: \"left\",\n },\n });\n\n new StyleRule({\n scope: \"class\",\n name: TD_CLASS,\n styles: {\n border: \"1px solid var(--ts-ui-border-color, rgba(127, 127, 127, 0.4))\",\n padding: \"0.3em 0.6em\",\n },\n });\n\n new StyleRule({\n scope: \"class\",\n name: ALIGN_LEFT_CLASS,\n styles: { textAlign: \"left\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: ALIGN_CENTER_CLASS,\n styles: { textAlign: \"center\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: ALIGN_RIGHT_CLASS,\n styles: { textAlign: \"right\" },\n });\n\n new StyleRule({\n scope: \"class\",\n name: CODE_HOST_CLASS,\n styles: { position: \"relative\" },\n });\n}\n\n/**\n * Maps marked's per-column alignment to the class that applies it.\n *\n * @param align - The column's alignment, as reported per-cell by marked's\n * table token.\n * @returns The alignment class, or `null` when the column carries no\n * alignment marker.\n */\nfunction alignmentClass(align: \"center\" | \"left\" | \"right\" | null): string | null {\n switch (align) {\n case \"left\": return ALIGN_LEFT_CLASS;\n case \"center\": return ALIGN_CENTER_CLASS;\n case \"right\": return ALIGN_RIGHT_CLASS;\n default: return null;\n }\n}\n\n/**\n * GitHub/VitePress-compatible slug: lowercase, non-alphanumerics collapsed to\n * single hyphens, ends trimmed. Does not dedupe — the caller folds in a `-N`\n * suffix via a per-render counter so every id on the page is unique.\n *\n * @param text - The heading's plain text.\n * @returns The slug, with no leading, trailing, or doubled hyphens.\n */\nfunction slugify(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\n/**\n * Shared by {@link Markdown.appendHeading} and {@link extractMarkdownHeadings}\n * — the single place a heading's slug is deduped against ids already used\n * earlier in the same render/extraction pass, so both produce identical ids\n * from identical inputs.\n *\n * @param text - The heading's plain text.\n * @param headingIds - The current pass's dedupe counter, keyed by slug.\n * @returns The slug, suffixed with `-N` when it was already seen this pass.\n */\nfunction nextHeadingId(text: string, headingIds: Map<string, number>): string {\n const slug = slugify(text);\n const seen = headingIds.get(slug) ?? 0;\n\n headingIds.set(slug, seen + 1);\n\n return seen === 0 ? slug : `${slug}-${seen}`;\n}\n\n/**\n * How a link href should be rendered: the final href, and whether it leaves\n * the site the {@link Markdown} instance is embedded in.\n *\n * @category Components\n */\nexport interface MarkdownLinkResolution {\n href: string;\n external: boolean;\n}\n\n/**\n * Maps an authored Markdown href to its rendered form. See\n * {@link MarkdownOptions.linkResolver}.\n *\n * @category Components\n */\nexport type MarkdownLinkResolver = (href: string) => MarkdownLinkResolution;\n\n/**\n * The default {@link MarkdownLinkResolver}: every href is rendered exactly as\n * authored and marked external, reproducing the component's pre-`linkResolver`\n * behaviour (`target=\"_blank\" rel=\"noopener noreferrer\"` on every link).\n *\n * @param href - The authored link href.\n * @returns The href unchanged, marked external.\n */\nfunction defaultLinkResolver(href: string): MarkdownLinkResolution {\n return { href, external: true };\n}\n\n/**\n * Construction-time options for {@link Markdown}.\n *\n * @category Components\n */\nexport interface MarkdownOptions extends ComponentOptions {\n /** The Markdown source string to render. */\n markdown?: string;\n\n /**\n * Maps an authored link href to its rendered form. Defaults to a resolver\n * that renders every href unchanged and marks it external, preserving\n * today's behaviour (every link opens in a new tab). A consumer embedding\n * `Markdown` in an app with its own routing (e.g. a docs site) can rewrite\n * in-site hrefs and suppress the new tab for them, while leaving external\n * links external.\n */\n linkResolver?: MarkdownLinkResolver;\n\n /**\n * Per-instance override of the prose column's max width (e.g. `\"60ch\"`,\n * `60`). `null` or omitted uses the theme's `--ts-ui-md-max-measure`\n * default.\n */\n maxMeasure?: string | number | null;\n\n /**\n * Multiplies the prose's base font size; headings scale with it via their\n * own relative sizing. Default `1`.\n */\n fontScale?: number;\n}\n\n/**\n * The fixed identity of one fenced block's pending upgrade — the placeholder\n * handles it will replace, its literal text and mapped language, and the\n * render generation it belongs to (see {@link Markdown._renderGeneration}).\n * Shared shape between {@link QueuedCodeUpgrade} (waiting on visibility,\n * import not yet started) and {@link PendingCodeUpgrade} (import already\n * resolved, still waiting on visibility to apply).\n */\ninterface CodeUpgradeIdentity {\n wrapper: Handle;\n pre: Handle;\n code: Handle;\n text: string;\n languageId: string;\n generation: number;\n}\n\n/**\n * A supported-language fenced block whose `CodeEditor` dynamic import has not\n * started yet because `Markdown` was not effectively visible when its\n * deferred kickoff (queued by {@link Markdown.appendCode} through\n * `onFirstLayout`) ran. Flushed by {@link Markdown.onEffectiveVisibilityChange}\n * once visibility flips to `true` — edge-triggered, not polled, mirroring\n * `Canvas.onEffectiveVisibilityChange`'s animation-loop reconcile.\n */\ntype QueuedCodeUpgrade = CodeUpgradeIdentity;\n\n/**\n * A supported-language fenced block still waiting for `CodeEditor` to\n * become visible after its dynamic import already resolved (queued by\n * {@link Markdown.loadCodeEditorUpgrade} when the import settles while\n * `Markdown` is not effectively visible).\n */\ninterface PendingCodeUpgrade extends CodeUpgradeIdentity {\n // Constructor type built from the type-only `CodeEditor` import — `typeof\n // CodeEditor` is not available here because the value itself is never\n // statically imported.\n CodeEditorClass: new (\n value: string,\n options: { readOnly: true; language: string; autoHeightMaxRows?: number },\n ) => CodeEditor;\n}\n\n/**\n * A display component that renders a Markdown source string as a live DOM\n * subtree.\n *\n * @remarks\n * Parsing uses the `marked` library's lexer only (`marked.lexer(src)`): the\n * component walks the returned token AST and builds every prose element\n * (`<h1>`–`<h6>`, `<p>`, `<ul>`/`<ol>`/`<li>`, `<blockquote>`, `<pre>`/`<code>`,\n * `<strong>`, `<em>`, `<a>`, `<table>`) through the DOM sink. There is no\n * HTML-string assignment path, so untrusted Markdown can never inject markup,\n * and the render runs against the modelled DOM source in tests.\n *\n * The v1 token set covers headings, paragraphs, ordered/unordered lists,\n * blockquotes, fenced/inline code, bold, italic, links, and GFM pipe tables\n * (including per-column alignment). Any other token type (images, raw HTML,\n * the remaining GFM extensions) falls through to a defined fallback that\n * renders the token's plain text — never a crash, never markup.\n *\n * A fenced code block whose info string names a language\n * [`CodeEditor`](/components/CodeEditor) has a registered grammar for\n * (`js`/`ts`/`json`/`html`/`sql`/`markdown`, plus aliases) upgrades from the\n * plain `<pre>` to a live, read-only, syntax-highlighted `CodeEditor` once it\n * loads; an unrecognised language, or no info string, keeps the plain\n * `<pre>`. The upgrade is lazy in two ways —\n * `CodeEditor`'s CodeMirror dependency loads through a dynamic import that\n * fires only when a fenced block actually needs it, deferred until this\n * component's first connected, displayed layout, and further deferred per\n * block until its wrapper comes within one viewport-height of the visible\n * area — so a `Markdown` with no fenced code (or only unsupported languages)\n * pays no extra bundle cost, and a long document upgrades only the blocks\n * the reader actually scrolls to.\n *\n * Links render as plain `<a href target=\"_blank\" rel=\"noopener noreferrer\">`\n * with native navigation; the component exposes no event surface in v1.\n *\n * Wrapping and scrolling. The prose wraps to the width it is assigned —\n * paragraphs reflow at word boundaries and overlong unbreakable tokens (URLs)\n * break, so content never overflows horizontally; fenced code blocks instead\n * preserve their lines and scroll inside their own frame. Because prose reflows,\n * the component measures its rendered content height at the assigned width and\n * reports it through {@link Markdown.getMinSize} / {@link Markdown.getPreferredSize},\n * so it grows a size-negotiating scroll host to the full prose height — drop one\n * in a vertically-scrolling [`Panel`](/api/component/container/classes/Panel)\n * (`setAutoScroll(\"y\")`) and it scrolls. The height is re-measured on content,\n * width, and theme change; only the height axis is derived (the width stays\n * freely assignable). The measured height is reported as a *minimum*, so an\n * explicit `preferredSize`/`setMinSize` taller than the content still wins; to\n * cap the component below its content, give it a bounded scroll host.\n *\n * @example\n * ```typescript\n * import { Markdown } from '@jimka/typescript-ui/component/display';\n *\n * panel.addComponent(new Markdown('# Title\\n\\nSome **bold** text.'));\n * ```\n *\n * @category Components\n */\nclass Markdown extends Component<MarkdownOptions> {\n\n /**\n * Raw child nodes built into the root through the sink. Tracked so they are\n * released with the component and torn down / rebuilt on {@link setMarkdown}.\n */\n private _contentHandles: Handle[] = [];\n\n /**\n * Measured content height in px (outer/border-box), cached as per-instance\n * derived state — intrinsic runtime bookkeeping, so it lives here rather than\n * in {@link MarkdownOptions}. `null` until the first measure records it; folded\n * into {@link getMinSize} / {@link getPreferredSize} to drive a scroll host.\n */\n private _measuredHeight: number | null = null;\n\n /** Handle to detach the {@link ThemeManager.onThemeChange} listener on {@link dispose}. */\n private readonly _unsubscribeTheme: () => void;\n\n /** Live `CodeEditor` upgrades, raw-DOM-appended per fenced block; see {@link applyCodeEditorUpgrade}. */\n private _codeEditors: Array<{ editor: CodeEditor; wrapper: Handle }> = [];\n\n /** Supported-language fenced blocks still waiting to upgrade; see {@link PendingCodeUpgrade}. */\n private _pendingCodeUpgrades: PendingCodeUpgrade[] = [];\n\n /**\n * Supported-language fenced blocks whose dynamic import hasn't started\n * because `Markdown` wasn't effectively visible when their deferred\n * kickoff ran; see {@link QueuedCodeUpgrade} and\n * {@link onEffectiveVisibilityChange}.\n */\n private _awaitingVisibilityKickoffs: QueuedCodeUpgrade[] = [];\n\n /**\n * Supported-language fenced blocks whose dynamic import hasn't started\n * because their wrapper is not yet within {@link CODE_UPGRADE_LOOKAHEAD_VIEWPORTS}\n * of the viewport; see {@link isBlockNearViewport} and {@link onViewportPass}.\n */\n private _awaitingViewportKickoffs: QueuedCodeUpgrade[] = [];\n\n /** Whether the scroll/resize viewport listeners are currently registered. */\n private _viewportWatchArmed = false;\n\n /** Whether a viewport pass is already queued on the next layout flush. */\n private _viewportPassScheduled = false;\n\n /** Whether a coalesced content-height measure is already queued. */\n private _measureScheduled = false;\n\n /**\n * Arrow field, not a prototype method: {@link Component.afterNextLayout}\n * calls its callback bare with no receiver.\n */\n private readonly handleViewportPass: () => void = () => this.onViewportPass();\n\n /**\n * Arrow field, not a prototype method: {@link Component.afterNextLayout}\n * calls its callback bare with no receiver.\n */\n private readonly handleScheduledMeasure: () => void = () => this.onScheduledMeasure();\n\n /**\n * Bumped by {@link clearContent}; a resolved dynamic import compares its\n * captured generation against this to detect a render it no longer\n * belongs to (a later {@link setMarkdown}, or disposal), mirroring\n * `DiagramView.relayout`'s generation token.\n */\n private _renderGeneration = 0;\n\n /**\n * Normalised form of {@link MarkdownOptions.maxMeasure} — a bare `number`\n * is stored here with its `\"ch\"` suffix appended, so the canonical string\n * form is computed once and reused by every {@link setMaxMeasure} write.\n * `null` (the default) means \"use the theme's `--ts-ui-md-max-measure`\".\n */\n private _maxMeasure: string | null = null;\n\n /**\n * Constructs a Markdown component for the given source string.\n *\n * @param markdown - The Markdown source to render (optional; defaults to \"\").\n * @param options - Optional component options bag.\n */\n constructor(markdown?: string, options?: MarkdownOptions) {\n super(options);\n\n // Positional argument: cache it only when the caller didn't also pass\n // `options.markdown` (which the super-time cascade already stored).\n if (markdown !== undefined && this._options.markdown === undefined) {\n this._options.markdown = markdown;\n }\n\n // Flowed prose must wrap: Component defaults `white-space` to \"nowrap\",\n // which would lay the document out as unwrapping single lines that\n // overflow horizontally. Reflow at word boundaries and break overlong\n // unbreakable tokens (URLs) so nothing spills sideways; fenced code keeps\n // its own `white-space: pre` + self-scroll from the `pre` class rule.\n this.setWhiteSpace(\"normal\");\n this.setElementCSSRule(\"overflowWrap\", \"break-word\");\n\n // Rendered prose is read-only content the reader copies (a code\n // sample, an error message, a changelog entry), not interactive UI\n // chrome. `Markdown`'s children are raw DOM nodes rather than\n // `Component`s, so they carry no `user-select` of their own and\n // inherit this value.\n this.setUserSelect(\"text\");\n // Same reasoning as the user-select opt-in above: the children are\n // raw DOM, so they inherit the cursor from the root too.\n this.setCursor(\"text\");\n\n // Prose reads continuously, unlike the framework's UI controls (tuned\n // for scanned single-line text), so it wants looser leading. Inherits\n // to every prose descendant; code/pre reset it back to \"normal\" via\n // their own class rules.\n this.setElementCSSRule(\"lineHeight\", \"var(--ts-ui-md-line-height, 1.8)\");\n\n // Caps the prose column to a comfortable reading measure regardless of\n // how wide the assigned layout box is; oversized tables/code fall back\n // to their own class-rule horizontal scroll within the capped column.\n // Dispatched through the setter (even for the unset default) so a\n // later `setMaxMeasure(null)` reverts to the same theme-var default\n // this constructor seeds.\n this.setMaxMeasure(this._options.maxMeasure ?? null);\n\n // Scales the prose's base font size; headings scale with it via their\n // own relative sizing (see setFontScale).\n this.setFontScale(this._options.fontScale ?? 1);\n\n // Prose metrics (font, spacing) are theme-bound, so a theme swap can\n // change the rendered height, and `--ts-ui-md-max-measure`'s `ch` unit\n // means it can change a code block's width too — react to both when\n // it fires (mirrors Text).\n this._unsubscribeTheme = ThemeManager.onThemeChange(() => this.onThemeChanged());\n\n // First measurement rides the first connected layout: only then is the\n // element attached and width-assigned, so the `scrollHeight` read is\n // meaningful. Subsequent re-measures come from setWidth / setMarkdown / theme.\n this.onFirstLayout(() => this.measureContentHeight());\n }\n\n /**\n * Applies a {@link MarkdownOptions} bag. Inherited Component fields cascade\n * through `super.applyOptions`; the `markdown` source is cached pure to\n * `_options` and picked up by `render()` (or a later `setMarkdown`).\n *\n * @param options - The options bag carrying the values to apply.\n * @returns This component, for method chaining.\n */\n protected applyOptions(options: MarkdownOptions): this {\n super.applyOptions(options);\n\n if (options.markdown !== undefined) {\n this._options.markdown = options.markdown;\n }\n\n if (options.linkResolver !== undefined) {\n this._options.linkResolver = options.linkResolver;\n }\n\n if (options.maxMeasure !== undefined) {\n this._options.maxMeasure = options.maxMeasure;\n }\n\n if (options.fontScale !== undefined) {\n this._options.fontScale = options.fontScale;\n }\n\n return this;\n }\n\n /**\n * Returns the current Markdown source, or \"\" when unset.\n *\n * @returns The cached Markdown source string.\n */\n getMarkdown(): string {\n return this._options.markdown ?? \"\";\n }\n\n /**\n * Sets the resolver used to render every link's href. Does not re-render\n * already-built content; call {@link setMarkdown} (with the same source, if\n * needed) to re-render links with the new resolver.\n *\n * @param resolver - The new link resolver.\n * @returns This component, for method chaining.\n */\n setLinkResolver(resolver: MarkdownLinkResolver): this {\n this._options.linkResolver = resolver;\n\n return this;\n }\n\n /**\n * Returns the current link resolver, or the default resolver (every href\n * unchanged, marked external) when none was set — never `null`.\n *\n * @returns The active link resolver.\n */\n getLinkResolver(): MarkdownLinkResolver {\n return this._options.linkResolver ?? defaultLinkResolver;\n }\n\n /**\n * Overrides the prose column's max width. Pass `null` to revert to the\n * theme's `--ts-ui-md-max-measure` default.\n *\n * @param value - A CSS width string (e.g. `\"60ch\"`), a bare number of\n * `ch` units, or `null` to revert to the theme default.\n * @returns This component, for method chaining.\n */\n setMaxMeasure(value: string | number | null): this {\n this._options.maxMeasure = value;\n this._maxMeasure = typeof value === \"number\" ? `${value}ch` : value;\n this.setElementCSSRule(\"maxWidth\", this._maxMeasure ?? \"var(--ts-ui-md-max-measure, 70ch)\");\n\n return this;\n }\n\n /**\n * Returns the current max-measure override, or `null` when unset (the\n * theme default applies).\n *\n * @returns The cached {@link MarkdownOptions.maxMeasure} value, or `null`.\n */\n getMaxMeasure(): string | number | null {\n return this._options.maxMeasure ?? null;\n }\n\n /**\n * Scales the prose's base font size; headings and other relatively-sized\n * elements scale with it. Pass `1` to clear the override.\n *\n * @param value - The multiplier applied to the base font size.\n * @returns This component, for method chaining.\n */\n setFontScale(value: number): this {\n this._options.fontScale = value;\n this.setElementCSSRule(\"fontSize\", value === 1 ? null : (value * 100) + \"%\");\n\n return this;\n }\n\n /**\n * Returns the current font-scale multiplier, or `1` (no scaling) when unset.\n *\n * @returns The cached {@link MarkdownOptions.fontScale} value, or `1`.\n */\n getFontScale(): number {\n return this._options.fontScale ?? 1;\n }\n\n /**\n * Sets the Markdown source, re-lexing and rebuilding the rendered subtree\n * when the element already exists. When it does not (pre-render), the source\n * is cached and picked up by `render()`.\n *\n * @param markdown - The new Markdown source string.\n * @returns This component, for method chaining.\n */\n setMarkdown(markdown: string): this {\n this._options.markdown = markdown;\n\n const element = this.getElement();\n\n if (!element) {\n return this;\n }\n\n this.clearContent();\n ensureMarkdownClassRules();\n this.appendBlockTokens(element, lexer(markdown), new Map<string, number>());\n\n // Content changed — the flowed height did too; re-measure and let a host grow.\n this.measureContentHeight();\n\n return this;\n }\n\n /**\n * Folds the measured content height into the inherited minimum as a height\n * floor, so a scroll host grows to the full prose extent (via\n * `Fit.inflateForOverflow`, which reads the child's `getMinSize`). Only the\n * height axis is folded; the width minimum stays `0` so the prose can reflow\n * at any assigned width. An explicit {@link setMinSize} still wins when larger.\n *\n * @returns The min size with the measured height folded in, or the inherited\n * minimum when nothing has been measured yet.\n */\n getMinSize(): Size | null {\n const base = super.getMinSize();\n\n if (this._measuredHeight === null) {\n return base;\n }\n\n if (!base) {\n return { width: 0, height: this._measuredHeight };\n }\n\n return { width: base.width, height: Math.max(base.height, this._measuredHeight) };\n }\n\n /**\n * Reports the measured content height as the preferred height when the caller\n * has set no explicit `preferredSize`, keeping the component's preferred extent\n * honest inside a sizing parent. An explicit `preferredSize` constraint wins.\n *\n * @returns The preferred size with the measured height applied, or the\n * inherited preferred size when a constraint is set or nothing is measured.\n */\n getPreferredSize(): Size | null {\n const base = super.getPreferredSize();\n\n if (this._measuredHeight === null || this.getPreferredSizeConstraint() !== null) {\n return base;\n }\n\n if (!base) {\n return { width: 0, height: this._measuredHeight };\n }\n\n return { width: base.width, height: this._measuredHeight };\n }\n\n /**\n * Re-measures the flowed content height when the assigned width changes: prose\n * height is width-dependent, so a narrower box reflows taller. The re-measure\n * reads the just-committed width back from the DOM before reading the height.\n *\n * @param width - The new width in pixels.\n * @returns This component, for method chaining.\n */\n setWidth(width: number): this {\n const changed = width !== this.getWidth();\n\n super.setWidth(width);\n\n if (changed) {\n this.resyncCodeEditorWidths();\n this.measureContentHeight();\n }\n\n return this;\n }\n\n /**\n * Reacts to a theme swap: re-syncs live editors' widths (a `ch`-unit\n * `--ts-ui-md-max-measure` moves with the font) and re-measures the\n * content height (prose metrics are theme-bound).\n */\n private onThemeChanged(): void {\n this.resyncCodeEditorWidths();\n this.measureContentHeight();\n }\n\n /**\n * Detaches the theme-change listener, then defers to the base class for\n * the rest of teardown. Call when a dynamically-built Markdown is\n * permanently removed from the page, mirroring `CodeEditor.destructor`.\n */\n protected destructor(): void {\n // Disposes every live CodeEditor: raw-DOM-appended (never through\n // addComponent), so the base class's own child-recursion below never\n // reaches them — Markdown must dispose them explicitly.\n this.clearContent();\n this._unsubscribeTheme();\n super.destructor();\n }\n\n /**\n * Measures the rendered subtree's content height at the element's assigned\n * width and folds it into the component's reported size, then schedules a\n * re-layout so a scroll host can grow to fit. This is the component's only\n * live seam read (`scrollHeight`) — isolated here as the single forced-layout\n * point — and no-ops before the element exists (the first connected layout\n * retries via {@link onFirstLayout}). Idempotent: an unchanged height suppresses\n * the re-layout so repeated measures cannot loop.\n */\n private measureContentHeight(): void {\n const element = this.getElement();\n if (!element) {\n return;\n }\n\n // Flush any pending style writes — in particular a `width` queued by\n // LayoutManager.commitBounds, which disables auto-commit\n // (setAutoCommitStyle(false)) for the duration of a layout pass.\n // Markdown.setWidth calls measureContentHeight synchronously from\n // inside that window, so without this flush the scrollHeight read\n // below would measure against the previous frame's width. Same fix\n // as Panel.doLayout's pre-measureScrollbarGutter flush.\n this.commitElementStyle();\n\n this.flushPendingCodeUpgrades();\n\n // Read the true content height, not the committed box. `scrollHeight` is\n // floored at the element's own `clientHeight`, so measuring the live\n // (already height-committed) box would only ever report *growth* — a\n // document that reflows wider or is edited shorter could never shrink its\n // extent, leaving stale dead space. Collapse the box to its content\n // first — the width was already flushed above, so this second flush only\n // needs to commit the `height: auto` write below. The raw style write is\n // a transient probe restored below, not persistent state, so it\n // deliberately bypasses the typed `setHeight` (which takes only a number).\n const restoreHeight = this.getHeight();\n this.setElementStyle(\"height\", \"auto\");\n this.commitElementStyle();\n\n // `scrollHeight` is content + padding (border-box excludes the border),\n // so reach the outer height by adding only the border.\n const border = this.getBorderSize();\n const measured = DOM.source.getScrollMetrics(element).scrollHeight + border.top + border.bottom;\n\n // Restore the laid-out height so the box isn't left content-collapsed\n // between now and the next layout pass (which re-commits it anyway).\n this.setElementStyle(\"height\", restoreHeight + \"px\");\n this.commitElementStyle();\n\n if (measured === this._measuredHeight) {\n return;\n }\n\n this._measuredHeight = measured;\n (this.getParentComponent() ?? this).scheduleLayout();\n }\n\n /**\n * Renders the root element, then lexes the current source and builds the\n * prose subtree into it. Empty/blank source lexes to an empty token list, so\n * the root renders with no prose children.\n *\n * @returns The rendered root element handle.\n */\n protected render(): Handle {\n const element = super.render();\n\n ensureMarkdownClassRules();\n this.appendBlockTokens(element, lexer(this.getMarkdown()), new Map<string, number>());\n\n return element;\n }\n\n /**\n * Creates a tracked child element: minted through the sink, registered for\n * release with the component, and recorded in `_contentHandles` so it is\n * torn down on the next {@link setMarkdown} rebuild.\n *\n * @param tag - The HTML tag to create.\n * @returns The new element handle.\n */\n private create(tag: string): Handle {\n const handle = DOM.sink.createElement(tag);\n\n this.trackHandle(handle);\n this._contentHandles.push(handle);\n\n return handle;\n }\n\n /**\n * Removes every content node from the DOM, releases its handle, and empties\n * the tracked set, so a rebuild neither leaks registry entries nor lets\n * `_ownedHandles` accumulate stale references across edits.\n */\n private clearContent(): void {\n for (const handle of this._contentHandles) {\n DOM.sink.removeElement(handle);\n this.untrackHandle(handle);\n DOM.sink.release(handle);\n }\n\n this._contentHandles.length = 0;\n\n // Bumped so any dynamic import already in flight for this render\n // (see loadCodeEditorUpgrade) recognises itself as stale and no-ops\n // instead of applying against handles this rebuild just tore down.\n this._renderGeneration += 1;\n\n for (const { editor } of this._codeEditors) {\n editor.dispose();\n }\n\n this._codeEditors.length = 0;\n this._pendingCodeUpgrades.length = 0;\n this._awaitingVisibilityKickoffs.length = 0;\n this._awaitingViewportKickoffs.length = 0;\n this.disarmViewportWatch();\n }\n\n /**\n * Swaps a fenced block's placeholder `<pre>`/`<code>` for a live,\n * syntax-highlighted `CodeEditor`, sized to exactly fill the `wrapper`\n * (see the \"position: relative wrapper\" architecture decision — an\n * absolutely positioned child does not contribute to its ancestor's auto\n * height, so the wrapper's own height is pinned explicitly below).\n *\n * @param CodeEditorClass - The dynamically-imported `CodeEditor` constructor.\n * @param wrapper - The `ts-ui-md-code-host` wrapper the placeholder `<pre>` sits in.\n * @param pre - The placeholder `<pre>` handle being replaced.\n * @param code - The placeholder `<code>` handle being replaced.\n * @param text - The fenced block's literal source text.\n * @param languageId - The mapped `CodeEditor` registry id.\n */\n private applyCodeEditorUpgrade(\n CodeEditorClass: PendingCodeUpgrade[\"CodeEditorClass\"],\n wrapper: Handle,\n pre: Handle,\n code: Handle,\n text: string,\n languageId: string,\n ): void {\n const metrics = DOM.source.getScrollMetrics(pre);\n const width = metrics.clientWidth;\n const height = metrics.scrollHeight;\n\n for (const handle of [pre, code]) {\n DOM.sink.removeElement(handle);\n this.untrackHandle(handle);\n DOM.sink.release(handle);\n\n const index = this._contentHandles.indexOf(handle);\n if (index !== -1) {\n this._contentHandles.splice(index, 1);\n }\n }\n\n const editor = new CodeEditorClass(text, {\n readOnly: true,\n language: languageId,\n autoHeightMaxRows: CODE_BLOCK_MAX_AUTO_ROWS,\n });\n\n editor.setX(0).setY(0).setWidth(width).setHeight(height);\n\n let correctionWarned = false;\n\n editor.on(\"heightchange\", (payload) => {\n if (!correctionWarned) {\n correctionWarned = true;\n\n const delta = Math.abs(payload.height - height);\n\n if (delta > GUESS_HEIGHT_CORRECTION_WARN_PX) {\n console.warn(\n `Markdown: fenced \"${languageId}\" code block's CodeEditor corrected its guessed ` +\n `height by ${Math.round(delta)}px (${height}px → ${payload.height}px) on mount.`,\n );\n }\n }\n\n this.handleCodeEditorHeightChange(wrapper, payload.height);\n });\n DOM.sink.appendChild(wrapper, editor.getElement(true)!);\n DOM.sink.apply(wrapper, { style: { height: height + \"px\" } });\n\n this._codeEditors.push({ editor, wrapper });\n }\n\n /**\n * Re-pins the `ts-ui-md-code-host` wrapper's height to a live CodeEditor's\n * own auto-grown height synchronously, then schedules a coalesced\n * re-measure of Markdown's own content height (see {@link scheduleContentMeasure})\n * so the taller/shorter block folds into the reported size. Wired in\n * {@link applyCodeEditorUpgrade} onto each editor's `\"heightchange\"` event.\n *\n * @param wrapper - The wrapper the fired editor sits in.\n * @param height - The editor's new height in pixels.\n */\n private handleCodeEditorHeightChange(wrapper: Handle, height: number): void {\n DOM.sink.apply(wrapper, { style: { height: height + \"px\" } });\n this.scheduleContentMeasure();\n }\n\n /**\n * Applies any {@link PendingCodeUpgrade} that has become visible since it\n * was queued. Called as the first line of {@link measureContentHeight}, so\n * a freshly-applied wrapper's height is committed before `Markdown`'s own\n * content height is measured.\n */\n private flushPendingCodeUpgrades(): void {\n this._pendingCodeUpgrades = this._pendingCodeUpgrades.filter((pending) => {\n if (!this.isEffectivelyVisible()) {\n return true;\n }\n\n this.applyCodeEditorUpgrade(\n pending.CodeEditorClass, pending.wrapper, pending.pre, pending.code,\n pending.text, pending.languageId,\n );\n\n return false;\n });\n }\n\n /**\n * Re-syncs every already-applied editor's width from its wrapper's\n * current `clientWidth` — what keeps a code block's width tracking\n * `Markdown`'s own width. Called only from the two things that can\n * actually change a wrapper's width: {@link setWidth} and the\n * theme-change handler (a font swap moves the `ch`-unit max-measure).\n *\n * @remarks Skipped entirely while not effectively visible: a hidden\n * subtree's `clientWidth` reads `0`, and writing that through would\n * collapse a previously-applied editor with nothing to correct it on\n * re-show (no width actually changes on re-show, so `setWidth`'s \"only\n * re-measure when changed\" guard would never re-run this method).\n * Skipping leaves the last-good width intact instead.\n */\n private resyncCodeEditorWidths(): void {\n this.commitElementStyle();\n\n if (!this.isEffectivelyVisible()) {\n return;\n }\n\n for (const { editor, wrapper } of this._codeEditors) {\n editor.setWidth(DOM.source.getScrollMetrics(wrapper).clientWidth);\n }\n }\n\n /**\n * Starts a fenced block's `CodeEditor` upgrade once it clears two gates in\n * series. First, effective visibility: if `Markdown` is not currently\n * effectively visible, the entry queues in {@link _awaitingVisibilityKickoffs},\n * flushed later by {@link onEffectiveVisibilityChange} once visibility\n * flips to `true`, edge-triggered rather than polled. This is the single\n * check-once gate the deferred kickoff registered in {@link appendCode}\n * runs through: `onFirstLayout` alone only guarantees \"connected +\n * displayed\" for a component's very first render — once the element is\n * already connected (e.g. a second `setMarkdown()` on a previously-shown,\n * now-hidden instance), it takes `Component.afterNextLayout`'s fast path,\n * which fires unconditionally on the next flush with no displayed-gating\n * — so the visibility check has to happen here, not be assumed from\n * having been called at all. Second, proximity to the viewport: once\n * effectively visible, an entry whose wrapper is not yet within\n * {@link CODE_UPGRADE_LOOKAHEAD_VIEWPORTS} of the viewport queues in\n * {@link _awaitingViewportKickoffs} instead, flushed by {@link onViewportPass}\n * as scrolling or resizing brings it into range.\n *\n * @param entry - The fenced block's placeholder handles, text, mapped\n * language, and the render generation it belongs to.\n */\n private startCodeEditorImport(entry: QueuedCodeUpgrade): void {\n if (!this.isEffectivelyVisible()) {\n this._awaitingVisibilityKickoffs.push(entry);\n\n return;\n }\n\n if (!this.isBlockNearViewport(entry.wrapper)) {\n this._awaitingViewportKickoffs.push(entry);\n this.armViewportWatch();\n\n return;\n }\n\n void this.loadCodeEditorUpgrade(entry.wrapper, entry.pre, entry.code, entry.text, entry.languageId, entry.generation);\n }\n\n /**\n * Tests whether a fenced block's wrapper is close enough to the visible\n * window to start its `CodeEditor` upgrade now — not entirely above the\n * top of the window, and not further than {@link CODE_UPGRADE_LOOKAHEAD_VIEWPORTS}\n * viewport-heights below the fold. The lookahead applies below the fold\n * only: swapping the placeholder for a live editor changes the block's\n * height, and giving the margin no upward component means that in\n * ordinary downward reading every upgrade happens at or below the\n * reader's position, so the movement lands on off-screen content.\n *\n * @param wrapper - The fenced block's `ts-ui-md-code-host` wrapper.\n * @returns `true` when the wrapper is within range of the viewport.\n */\n private isBlockNearViewport(wrapper: Handle): boolean {\n const rect = DOM.source.getElementRect(wrapper);\n const viewportHeight = DOM.source.getViewportSize().height;\n const cutoff = viewportHeight * (1 + CODE_UPGRADE_LOOKAHEAD_VIEWPORTS);\n\n return rect.bottom >= 0 && rect.top <= cutoff;\n }\n\n /**\n * Registers the scroll/resize viewport listeners that drive {@link onViewportPass},\n * if not already armed. Idempotent — safe to call from every enqueue.\n */\n private armViewportWatch(): void {\n if (this._viewportWatchArmed) {\n return;\n }\n\n this._viewportWatchArmed = true;\n Event.addViewportListener(this, \"scroll\", this.handleViewportChange);\n Event.addViewportListener(this, \"resize\", this.handleViewportChange);\n }\n\n /**\n * Removes the scroll/resize viewport listeners registered by\n * {@link armViewportWatch}, once nothing remains queued to watch for.\n */\n private disarmViewportWatch(): void {\n if (!this._viewportWatchArmed) {\n return;\n }\n\n this._viewportWatchArmed = false;\n Event.removeViewportListener(this, \"scroll\", this.handleViewportChange);\n Event.removeViewportListener(this, \"resize\", this.handleViewportChange);\n }\n\n /**\n * Plain prototype-method reference passed to {@link Event.addViewportListener} /\n * {@link Event.removeViewportListener}, which invoke it with this\n * component bound as `this` — unlike {@link handleViewportPass}, this\n * cannot be an arrow field (see the class-level remark on the two\n * callback shapes).\n */\n private handleViewportChange(): void {\n this.scheduleViewportPass();\n }\n\n /**\n * Coalesces a burst of scroll/resize events into one {@link onViewportPass}\n * per layout flush.\n */\n private scheduleViewportPass(): void {\n if (this._viewportPassScheduled || this._awaitingViewportKickoffs.length === 0) {\n return;\n }\n\n this._viewportPassScheduled = true;\n Component.afterNextLayout(this.handleViewportPass);\n }\n\n /**\n * Walks {@link _awaitingViewportKickoffs} in document order, starting the\n * upgrade for every entry within range of the viewport and breaking at\n * the first one past the lookahead cutoff (later entries are further\n * down, since fenced blocks are appended in document order). Reads every\n * entry's rect before starting any upgrade, so the pass costs at most one\n * forced reflow rather than interleaving reads with the layout-affecting\n * writes an upgrade triggers.\n */\n private onViewportPass(): void {\n this._viewportPassScheduled = false;\n\n if (this._awaitingViewportKickoffs.length === 0) {\n this.disarmViewportWatch();\n\n return;\n }\n\n if (!this.isEffectivelyVisible()) {\n return;\n }\n\n this.commitElementStyle();\n\n const viewportHeight = DOM.source.getViewportSize().height;\n const cutoff = viewportHeight * (1 + CODE_UPGRADE_LOOKAHEAD_VIEWPORTS);\n const queue = this._awaitingViewportKickoffs;\n const due: QueuedCodeUpgrade[] = [];\n const remaining: QueuedCodeUpgrade[] = [];\n\n for (let i = 0; i < queue.length; i++) {\n const entry = queue[i]!;\n const rect = DOM.source.getElementRect(entry.wrapper);\n\n if (rect.top > cutoff) {\n remaining.push(...queue.slice(i));\n\n break;\n }\n\n (rect.bottom >= 0 ? due : remaining).push(entry);\n }\n\n this._awaitingViewportKickoffs = remaining;\n\n if (remaining.length === 0) {\n this.disarmViewportWatch();\n }\n\n for (const entry of due) {\n void this.loadCodeEditorUpgrade(entry.wrapper, entry.pre, entry.code, entry.text, entry.languageId, entry.generation);\n }\n }\n\n /**\n * Flushes any {@link QueuedCodeUpgrade} once this component becomes\n * effectively visible, starting the dynamic import for each — the\n * edge-triggered replacement for a per-frame visibility poll, mirroring\n * `Canvas.onEffectiveVisibilityChange`'s animation-loop reconcile.\n *\n * @param effective - The component's new effective-visibility state.\n */\n protected onEffectiveVisibilityChange(effective: boolean): void {\n super.onEffectiveVisibilityChange(effective);\n\n if (!effective || this._awaitingVisibilityKickoffs.length === 0) {\n return;\n }\n\n const queued = this._awaitingVisibilityKickoffs;\n\n this._awaitingVisibilityKickoffs = [];\n\n for (const entry of queued) {\n this.startCodeEditorImport(entry);\n }\n }\n\n /**\n * Loads `CodeEditor` through a narrow dynamic import — the two specific\n * modules it needs (`CodeEditor.js` itself, and `languages.js` for its\n * side-effect language registration), never the `component/editor`\n * barrel, which would also pull in the unrelated Lexical-based\n * `MarkdownEditor` stack. Once the import resolves, applies the upgrade\n * immediately if `Markdown` is still showing the render that queued this\n * call and is effectively visible, or queues it in {@link _pendingCodeUpgrades}\n * otherwise.\n *\n * @param wrapper - The `ts-ui-md-code-host` wrapper the placeholder `<pre>` sits in.\n * @param pre - The placeholder `<pre>` handle.\n * @param code - The placeholder `<code>` handle.\n * @param text - The fenced block's literal source text.\n * @param languageId - The mapped `CodeEditor` registry id.\n * @param generation - The {@link _renderGeneration} captured when this\n * fenced block was queued in {@link appendCode}.\n */\n private async loadCodeEditorUpgrade(\n wrapper: Handle,\n pre: Handle,\n code: Handle,\n text: string,\n languageId: string,\n generation: number,\n ): Promise<void> {\n const [{ CodeEditor: CodeEditorClass }] = await Promise.all([\n import(\"~/component/editor/CodeEditor.js\"),\n import(\"~/component/editor/languages.js\"),\n ]);\n\n if (generation !== this._renderGeneration) {\n // A later setMarkdown() (or disposal, which also bumps the\n // generation) rebuilt since this block was queued — the wrapper/\n // pre/code handles this call closed over no longer belong to a\n // live render.\n return;\n }\n\n if (this.isEffectivelyVisible()) {\n this.applyCodeEditorUpgrade(CodeEditorClass, wrapper, pre, code, text, languageId);\n this.scheduleContentMeasure();\n } else {\n this._pendingCodeUpgrades.push({ CodeEditorClass, wrapper, pre, code, text, languageId, generation });\n }\n }\n\n /**\n * Coalesces a burst of upgrade-driven re-measures — {@link loadCodeEditorUpgrade}\n * and {@link handleCodeEditorHeightChange} each fire once per upgraded\n * block — into one {@link measureContentHeight} call per layout flush,\n * rather than one full-document reflow per block.\n */\n private scheduleContentMeasure(): void {\n if (this._measureScheduled) {\n return;\n }\n\n this._measureScheduled = true;\n Component.afterNextLayout(this.handleScheduledMeasure);\n }\n\n /**\n * Runs the coalesced {@link measureContentHeight} queued by\n * {@link scheduleContentMeasure}, then re-evaluates the viewport queue:\n * the measure's reflow can move a still-queued block relative to the\n * fold without any scroll or resize event firing to trigger the pass\n * itself.\n */\n private onScheduledMeasure(): void {\n this._measureScheduled = false;\n this.measureContentHeight();\n this.scheduleViewportPass();\n }\n\n /**\n * Walks a list of block-level tokens, appending each into `parent`.\n *\n * @param parent - The element handle to append the built blocks into.\n * @param tokens - The block-level tokens to render.\n * @param headingIds - The current render pass's heading-id dedupe counter,\n * keyed by slug — see {@link appendHeading}. Threaded as a parameter\n * (never a field) so it cannot survive past the render pass that created it.\n */\n private appendBlockTokens(parent: Handle, tokens: Token[], headingIds: Map<string, number>): void {\n for (const token of tokens) {\n this.appendBlockToken(parent, token, headingIds);\n }\n }\n\n /**\n * Dispatches a single block-level token to its builder. Unknown/unsupported\n * types fall through to a plain-text node — never markup, never a crash.\n *\n * @param parent - The element handle to append into.\n * @param token - The block-level token.\n * @param headingIds - The current render pass's heading-id dedupe counter.\n */\n private appendBlockToken(parent: Handle, token: Token, headingIds: Map<string, number>): void {\n switch (token.type) {\n case \"heading\": this.appendHeading(parent, token as Tokens.Heading, headingIds); break;\n case \"paragraph\": this.appendParagraph(parent, token as Tokens.Paragraph); break;\n case \"list\": this.appendList(parent, token as Tokens.List, headingIds); break;\n case \"blockquote\": this.appendBlockquote(parent, token as Tokens.Blockquote, headingIds); break;\n case \"code\": this.appendCode(parent, token as Tokens.Code); break;\n case \"table\": this.appendTable(parent, token as Tokens.Table); break;\n\n // Blank line between blocks — nothing to render.\n case \"space\": break;\n\n default: this.appendTextNode(parent, token.raw ?? \"\"); break;\n }\n }\n\n /**\n * Builds an `<h1>`..`<h6>` element (tag from the token depth) carrying the\n * heading's inline content and a slugified `id`, so an in-page `#fragment`\n * link can target it.\n *\n * @param parent - The element handle to append into.\n * @param token - The heading token.\n * @param headingIds - The current render pass's dedupe counter: a slug seen\n * before gets a `-N` suffix so every id on the page is unique. Local to\n * one render pass — {@link render} and {@link setMarkdown} each start a\n * fresh `Map`, so a re-render does not accumulate suffixes.\n */\n private appendHeading(parent: Handle, token: Tokens.Heading, headingIds: Map<string, number>): void {\n const depth = Math.min(Math.max(token.depth, HEADING_MIN_DEPTH), HEADING_MAX_DEPTH);\n const heading = this.create(\"h\" + depth);\n const id = nextHeadingId(token.text, headingIds);\n\n DOM.sink.apply(heading, { addClass: [HEADING_CLASS], setAttr: { id } });\n this.appendInlineTokens(heading, token.tokens);\n DOM.sink.appendChild(parent, heading);\n }\n\n /**\n * Builds a `<p>` carrying the paragraph's inline content.\n *\n * @param parent - The element handle to append into.\n * @param token - The paragraph token.\n */\n private appendParagraph(parent: Handle, token: Tokens.Paragraph): void {\n const paragraph = this.create(\"p\");\n\n this.appendInlineTokens(paragraph, token.tokens);\n DOM.sink.appendChild(parent, paragraph);\n }\n\n /**\n * Builds a `<ul>` (or `<ol>` when the token is ordered) with an `<li>` per\n * item.\n *\n * @param parent - The element handle to append into.\n * @param token - The list token.\n * @param headingIds - The current render pass's heading-id dedupe counter.\n */\n private appendList(parent: Handle, token: Tokens.List, headingIds: Map<string, number>): void {\n const list = this.create(token.ordered ? \"ol\" : \"ul\");\n\n DOM.sink.apply(list, { addClass: [LIST_CLASS] });\n\n for (const item of token.items) {\n this.appendListItem(list, item, headingIds);\n }\n\n DOM.sink.appendChild(parent, list);\n }\n\n /**\n * Builds an `<li>` and walks its children, which are block tokens for a\n * loose list and inline-bearing `text` tokens for a tight one — so both\n * render.\n *\n * @param list - The `<ul>`/`<ol>` element handle to append into.\n * @param item - The list-item token.\n * @param headingIds - The current render pass's heading-id dedupe counter.\n */\n private appendListItem(list: Handle, item: Tokens.ListItem, headingIds: Map<string, number>): void {\n const listItem = this.create(\"li\");\n\n for (const token of item.tokens) {\n if (token.type === \"text\") {\n const text = token as Tokens.Text;\n\n if (text.tokens && text.tokens.length > 0) {\n this.appendInlineTokens(listItem, text.tokens);\n } else {\n this.appendInlineToken(listItem, text, item.tokens.length === 1);\n }\n } else {\n this.appendBlockToken(listItem, token, headingIds);\n }\n }\n\n DOM.sink.appendChild(list, listItem);\n }\n\n /**\n * Builds a wrapper `<div>` › `<table>` with a `<thead>` holding the header\n * row and a `<tbody>` holding one row per body entry. The wrapper scrolls\n * horizontally so an overlong table cannot spill sideways.\n *\n * @param parent - The element handle to append into.\n * @param token - The table token.\n */\n private appendTable(parent: Handle, token: Tokens.Table): void {\n const wrapper = this.create(\"div\");\n\n DOM.sink.apply(wrapper, { addClass: [TABLE_WRAP_CLASS] });\n\n const table = this.create(\"table\");\n\n DOM.sink.apply(table, { addClass: [TABLE_CLASS] });\n\n const thead = this.create(\"thead\");\n\n this.appendTableRow(thead, token.header, true);\n DOM.sink.appendChild(table, thead);\n\n const tbody = this.create(\"tbody\");\n\n for (const row of token.rows) {\n this.appendTableRow(tbody, row, false);\n }\n\n DOM.sink.appendChild(table, tbody);\n DOM.sink.appendChild(wrapper, table);\n DOM.sink.appendChild(parent, wrapper);\n }\n\n /**\n * Builds a `<tr>` with one `<th>` (header) or `<td>` (body) per cell,\n * carrying the cell's alignment class (when the column is aligned) and\n * inline content.\n *\n * @param section - The `<thead>`/`<tbody>` element handle to append into.\n * @param cells - The row's cells.\n * @param header - Whether this is the header row (`<th>` cells) or a body\n * row (`<td>` cells).\n */\n private appendTableRow(section: Handle, cells: Tokens.TableCell[], header: boolean): void {\n const row = this.create(\"tr\");\n\n for (const cell of cells) {\n const cellElement = this.create(header ? \"th\" : \"td\");\n const classes = [header ? TH_CLASS : TD_CLASS];\n const align = alignmentClass(cell.align);\n\n if (align) {\n classes.push(align);\n }\n\n DOM.sink.apply(cellElement, { addClass: classes });\n this.appendInlineTokens(cellElement, cell.tokens);\n DOM.sink.appendChild(row, cellElement);\n }\n\n DOM.sink.appendChild(section, row);\n }\n\n /**\n * Builds a `<blockquote>` and recurses into its block-level children.\n *\n * @param parent - The element handle to append into.\n * @param token - The blockquote token.\n * @param headingIds - The current render pass's heading-id dedupe counter.\n */\n private appendBlockquote(parent: Handle, token: Tokens.Blockquote, headingIds: Map<string, number>): void {\n const quote = this.create(\"blockquote\");\n\n DOM.sink.apply(quote, { addClass: [QUOTE_CLASS] });\n this.appendBlockTokens(quote, token.tokens, headingIds);\n DOM.sink.appendChild(parent, quote);\n }\n\n /**\n * Builds a `<pre>` › `<code>` carrying the fenced block's literal text\n * verbatim (newlines preserved). When the fence's info string maps to a\n * registered `CodeEditor` language (see {@link mapFenceLangToEditorId}),\n * the `<pre>` is additionally wrapped in a `ts-ui-md-code-host` div and a\n * deferred upgrade to a live, syntax-highlighted `CodeEditor` is queued —\n * see {@link loadCodeEditorUpgrade}. An unmapped language, or no info\n * string, renders exactly as before: a bare `<pre>` with no wrapper and\n * no dynamic import triggered.\n *\n * @param parent - The element handle to append into.\n * @param token - The code token.\n */\n private appendCode(parent: Handle, token: Tokens.Code): void {\n const pre = this.create(\"pre\");\n\n DOM.sink.apply(pre, { addClass: [PRE_CLASS] });\n\n const code = this.create(\"code\");\n\n DOM.sink.apply(code, { text: token.text });\n DOM.sink.appendChild(pre, code);\n\n const languageId = mapFenceLangToEditorId(token.lang);\n\n if (languageId === null) {\n DOM.sink.appendChild(parent, pre);\n\n return;\n }\n\n const wrapper = this.create(\"div\");\n\n DOM.sink.apply(wrapper, { addClass: [CODE_HOST_CLASS] });\n DOM.sink.appendChild(wrapper, pre);\n DOM.sink.appendChild(parent, wrapper);\n\n // Not a direct call: the kickoff itself, not just the DOM swap, waits\n // for Markdown's first connected layout. `startCodeEditorImport`\n // re-checks visibility itself rather than trusting `onFirstLayout`\n // alone to guarantee \"connected + displayed\" — see its own docblock\n // for why that guarantee doesn't hold for every registration.\n const entry: QueuedCodeUpgrade = { wrapper, pre, code, text: token.text, languageId, generation: this._renderGeneration };\n\n this.onFirstLayout(() => this.startCodeEditorImport(entry));\n }\n\n /**\n * Walks a list of inline-level tokens, appending each into `parent`.\n *\n * @param parent - The element handle to append into.\n * @param tokens - The inline-level tokens to render.\n */\n private appendInlineTokens(parent: Handle, tokens: Token[]): void {\n const sole = tokens.length === 1;\n\n for (const token of tokens) {\n this.appendInlineToken(parent, token, sole);\n }\n }\n\n /**\n * Dispatches a single inline-level token. A lone plain-text run writes\n * straight onto `parent`; interleaved runs are wrapped in a `<span>` (the\n * sink has no raw text-node primitive, so a text sibling of an element needs\n * its own element). Unknown types fall through to a text node.\n *\n * @param parent - The element handle to append into.\n * @param token - The inline-level token.\n * @param sole - Whether this is the only token being appended into `parent`,\n * allowing a direct text write instead of a wrapping span.\n */\n private appendInlineToken(parent: Handle, token: Token, sole: boolean): void {\n switch (token.type) {\n case \"text\": {\n const text = token as Tokens.Text;\n\n if (text.tokens && text.tokens.length > 0) {\n this.appendInlineTokens(parent, text.tokens);\n } else if (sole) {\n DOM.sink.apply(parent, { text: text.text });\n } else {\n this.appendTextNode(parent, text.text);\n }\n\n break;\n }\n\n case \"strong\": this.appendInlineWrapper(parent, \"strong\", (token as Tokens.Strong).tokens); break;\n case \"em\": this.appendInlineWrapper(parent, \"em\", (token as Tokens.Em).tokens); break;\n\n case \"codespan\": {\n const code = this.create(\"code\");\n\n DOM.sink.apply(code, { addClass: [CODE_CLASS], text: (token as Tokens.Codespan).text });\n DOM.sink.appendChild(parent, code);\n\n break;\n }\n\n case \"link\": this.appendLink(parent, token as Tokens.Link); break;\n\n default: this.appendTextNode(parent, (token as Tokens.Text).text ?? token.raw ?? \"\"); break;\n }\n }\n\n /**\n * Builds an inline emphasis wrapper (`<strong>` / `<em>`) around its inline\n * children.\n *\n * @param parent - The element handle to append into.\n * @param tag - The wrapper tag.\n * @param tokens - The inline children.\n */\n private appendInlineWrapper(parent: Handle, tag: string, tokens: Token[]): void {\n const wrapper = this.create(tag);\n\n this.appendInlineTokens(wrapper, tokens);\n DOM.sink.appendChild(parent, wrapper);\n }\n\n /**\n * Builds an `<a>` from the link token's href, passed through\n * {@link getLinkResolver}, and the link's inline text. `target`/`rel` are\n * set only when the resolution is external — the default resolver marks\n * everything external, reproducing the pre-`linkResolver` behaviour.\n *\n * @param parent - The element handle to append into.\n * @param token - The link token.\n */\n private appendLink(parent: Handle, token: Tokens.Link): void {\n const anchor = this.create(\"a\");\n const resolution = this.getLinkResolver()(token.href);\n const setAttr: Record<string, string> = { href: resolution.href };\n\n if (resolution.external) {\n setAttr.target = \"_blank\";\n setAttr.rel = \"noopener noreferrer\";\n }\n\n DOM.sink.apply(anchor, { addClass: [LINK_CLASS], setAttr });\n this.appendInlineTokens(anchor, token.tokens);\n DOM.sink.appendChild(parent, anchor);\n }\n\n /**\n * Appends a plain-text run wrapped in a `<span>`. Used for interleaved text\n * runs and the unsupported-token fallback, where a bare text node cannot be\n * a sibling of element children through the sink.\n *\n * @param parent - The element handle to append into.\n * @param text - The text content.\n */\n private appendTextNode(parent: Handle, text: string): void {\n const span = this.create(\"span\");\n\n DOM.sink.apply(span, { text });\n DOM.sink.appendChild(parent, span);\n }\n}\n\n/**\n * One heading extracted from Markdown source by {@link extractMarkdownHeadings}.\n *\n * @category Components\n */\nexport interface MarkdownHeading {\n /** The slugified id — byte-identical to the `id` {@link Markdown} renders onto the corresponding heading element. */\n id: string;\n /** The heading's plain text. */\n text: string;\n /** The heading's level, clamped to `[1, 6]`. */\n depth: number;\n}\n\n/**\n * Flattens a heading's inline token tree to plain text — the same content\n * {@link Markdown.appendInlineTokens} would render, minus the markup: a\n * `strong`/`em` wrapper contributes its inner text with no `**`/`_` marks, a\n * `codespan` contributes its code text with no backticks, and a `link`\n * contributes its label text with no `[]()` syntax. `heading.text` (the raw\n * source substring) is deliberately not used here — it still carries that\n * markup, which is correct for {@link nextHeadingId} (matching `appendHeading`'s\n * own slug input) but wrong for display.\n *\n * @param tokens - A heading's inline-level tokens (`Tokens.Heading.tokens`).\n * @returns The heading's rendered plain text.\n */\nfunction inlineText(tokens: Token[]): string {\n return tokens.map((token) => {\n switch (token.type) {\n case \"text\": return (token as Tokens.Text).tokens ? inlineText((token as Tokens.Text).tokens!) : (token as Tokens.Text).text;\n case \"strong\": return inlineText((token as Tokens.Strong).tokens);\n case \"em\": return inlineText((token as Tokens.Em).tokens);\n case \"codespan\": return (token as Tokens.Codespan).text;\n case \"link\": return inlineText((token as Tokens.Link).tokens);\n default: return (token as Tokens.Text).text ?? token.raw ?? \"\";\n }\n }).join(\"\");\n}\n\n/**\n * Recursively walks `tokens` for heading tokens, the same block-token shapes\n * {@link Markdown.appendBlockToken} recurses into for headings: top-level, and\n * nested inside a blockquote or a (loose) list item.\n *\n * @param tokens - The block tokens to walk.\n * @param headingIds - The current pass's dedupe counter — see `nextHeadingId`.\n * @param out - The array headings are appended to, in document order.\n */\nfunction collectHeadings(tokens: Token[], headingIds: Map<string, number>, out: MarkdownHeading[]): void {\n for (const token of tokens) {\n if (token.type === \"heading\") {\n const heading = token as Tokens.Heading;\n const depth = Math.min(Math.max(heading.depth, HEADING_MIN_DEPTH), HEADING_MAX_DEPTH);\n\n out.push({ id: nextHeadingId(heading.text, headingIds), text: inlineText(heading.tokens), depth });\n } else if (token.type === \"blockquote\") {\n collectHeadings((token as Tokens.Blockquote).tokens, headingIds, out);\n } else if (token.type === \"list\") {\n for (const item of (token as Tokens.List).items) {\n collectHeadings(item.tokens, headingIds, out);\n }\n }\n }\n}\n\n/**\n * Computes the heading outline of a Markdown source string, without building\n * any DOM — the ids produced are byte-identical to the `id` {@link Markdown}\n * renders onto the corresponding `<h1>`-`<h6>` element for the same source,\n * since both go through `nextHeadingId`.\n *\n * @param source - The Markdown source to extract headings from.\n * @returns The source's headings, in document order.\n *\n * @category Components\n */\nexport function extractMarkdownHeadings(source: string): MarkdownHeading[] {\n const headings: MarkdownHeading[] = [];\n\n collectHeadings(lexer(source), new Map<string, number>(), headings);\n\n return headings;\n}\n\n/**\n * Sub-pixel tolerance for \"at or above the pane's top\". A scroll-to-heading\n * lands its target via a delta computed from sub-pixel-precise\n * `getBoundingClientRect()` reads, but the native `scrollTop` it's applied\n * through can round the requested value — landing the heading a fraction of\n * a pixel past the pane's top, enough to fail a strict `<=` and fall back to\n * the previous heading.\n */\nconst ACTIVE_HEADING_TOP_TOLERANCE_PX = 1;\n\n/**\n * Resolves which heading in `headings` is at or nearest above\n * `scrollElement`'s viewport top — the last heading, in document order,\n * whose top edge is at or above the scroll container's own top (within\n * `ACTIVE_HEADING_TOP_TOLERANCE_PX`). Mirrors\n * `DocsContent.scrollToHeading`'s lookup technique in the read direction.\n *\n * Once `scrollElement` has scrolled to its maximum, the first heading that\n * hasn't yet reached the pane's own top is active outright, instead of\n * whichever heading last crossed it: a heading near the document's end (or\n * several, clustered together) may have less than a full viewport of\n * content left below it, so no amount of scrolling can bring it exactly to\n * the pane's top, and the top-crossing rule alone would otherwise resolve to\n * a much earlier heading than whichever one the scroll actually landed on.\n *\n * @param scrollElement - The scroll-owning element to read the pane's own top from.\n * @param headings - The document's headings, in document order.\n * @returns The active heading's id, or `null` when the pane's top is above every heading.\n *\n * @category Components\n */\nexport function findActiveHeading(scrollElement: Handle, headings: MarkdownHeading[]): string | null {\n const paneTop = DOM.source.getElementRect(scrollElement).top;\n const metrics = DOM.source.getScrollMetrics(scrollElement);\n const atMaxScroll = metrics.scrollHeight > metrics.clientHeight\n && metrics.scrollTop >= metrics.scrollHeight - metrics.clientHeight - ACTIVE_HEADING_TOP_TOLERANCE_PX;\n\n let active: string | null = null;\n\n for (const heading of headings) {\n const el = DOM.source.getElementById(heading.id);\n\n if (!el || !DOM.source.contains(scrollElement, el)) {\n continue;\n }\n\n if (DOM.source.getElementRect(el).top <= paneTop + ACTIVE_HEADING_TOP_TOLERANCE_PX) {\n active = heading.id;\n } else {\n // Headings are in document order; every later one is further\n // below. At max scroll, though, this first not-yet-reached\n // heading is already on screen — nothing can scroll it up any\n // further — so it wins outright rather than leaving whichever\n // earlier heading last crossed the top still active.\n if (atMaxScroll) {\n active = heading.id;\n }\n\n break;\n }\n }\n\n return active;\n}\n\nconst MarkdownCallable = callable(Markdown);\ntype MarkdownCallable = Markdown;\nexport {\n Markdown as _Markdown,\n MarkdownCallable as Markdown,\n // Not re-exported from the package barrel (`component/display/index.ts`):\n // a test-only hook, not part of the public API surface.\n mapFenceLangToEditorId,\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component } from \"~/core/Component.js\";\nimport { FloatingPanel, FloatingPanelOptions } from \"~/component/container/FloatingPanel.js\";\nimport { Tree } from \"~/component/tree/Tree.js\";\nimport type { TreeNode } from \"~/component/tree/TreeNode.js\";\nimport { Text } from \"~/component/input/Text.js\";\nimport { LabelTreeNodeRenderer } from \"~/component/tree/renderer/Label.js\";\nimport { VBox } from \"~/layout/VBox.js\";\nimport { Fit } from \"~/layout/Fit.js\";\nimport { Insets } from \"~/primitive/Insets.js\";\nimport { UNBOUNDED } from \"~/primitive/Size.js\";\nimport type { Size } from \"~/primitive/Size.js\";\nimport { ListenerBag } from \"~/core/ListenerBag.js\";\nimport { callable } from \"~/core/Callable.js\";\nimport type { MarkdownHeading } from \"~/component/display/Markdown.js\";\n\n/**\n * Structural interface a scroll-owning host exposes so a {@link\n * MarkdownMinimap} can highlight the heading currently on screen without\n * depending on `Markdown`, `Panel`, or any other concrete scroll owner.\n */\nexport interface HeadingScrollSource {\n on(event: \"activeheadingchange\", listener: (headingId: string | null) => void): unknown;\n off(event: \"activeheadingchange\", listener: (headingId: string | null) => void): unknown;\n}\n\n/** String-literal union of the events emitted by {@link MarkdownMinimap}. */\nexport type MarkdownMinimapEvent = \"select\";\n\n/**\n * Construction-time options for {@link MarkdownMinimap}.\n *\n * @category Components\n */\nexport interface MarkdownMinimapOptions extends FloatingPanelOptions {\n /** Deepest heading depth shown; deeper headings are dropped entirely. Default `3`. */\n maxHeadingDepth?: number;\n\n /** The scroll-owning source whose active-heading changes drive the highlighted row. */\n scrollSource?: HeadingScrollSource;\n\n listeners?: {\n select?: (headingId: string) => void;\n };\n}\n\n/** Deepest heading depth shown when the caller doesn't specify one. */\nconst DEFAULT_MAX_HEADING_DEPTH = 3;\n\n/** Header row text above the tree. */\nconst HEADER_TEXT = \"On this page\";\n\n/**\n * Height cap: the header plus roughly 20 rows at `Tree`'s fixed 24px row\n * height. `Tree.getPreferredSize` now reports a height derived from the\n * flattened row count with no ceiling of its own (see `Tree.ts`), so an\n * outline longer than this needs to scroll instead of committing an\n * unbounded height that can run past the viewport with nothing to scroll it\n * back into view. Deliberately no `autoScroll` on this outer panel to\n * provide that scrolling: `Tree` is already a virtualized, self-scrolling\n * list (its own row pool plus scrollbar overlays — see `Tree.ts`'s class\n * doc), so marking this panel's own axis as overflowing too would tell\n * `BoxLayout.computeShrink` to skip shrinking `Tree` to fit (it explicitly\n * skips the shrink when the axis is overflowing), leaving `Tree` laid out at\n * its full uncapped content height with both this panel and `Tree` then\n * scrolling the same content independently. Without `autoScroll` here, the\n * VBox shrinks `Tree` down to whatever's left under this cap once it\n * overflows, and `Tree`'s own scrolling takes over from there — one\n * scrollbar, not two.\n */\nconst DEFAULT_MAX_HEIGHT_PX = 500;\n\n/**\n * Width floor: enough for the header text and a reasonably short heading\n * label without wrapping to nothing. `Anchor` bypasses the cell clamp\n * entirely (see `FloatingPanel`'s own docs), so this is a no-op for a\n * floating `MarkdownMinimap`; it only matters for a docked one placed in a\n * shrinking layout (e.g. an `HBox` row) — without it, that layout manager's\n * own shrink-to-min distribution has nothing to stop this panel collapsing\n * to an unreadable sliver on a narrow viewport.\n */\nconst DEFAULT_MIN_WIDTH_PX = 160;\n\n/**\n * Default preferred width. Wider than `Tree`'s own generic content-derived\n * default (200px, `Tree.ts`'s `DEFAULT_PREFERRED_WIDTH`) so a heading label\n * — which can run considerably longer than a typical Tree row elsewhere in\n * this codebase — has more room before truncating.\n */\nconst DEFAULT_WIDTH_PX = 240;\n\n/**\n * Row-label font size in pixels — smaller than `Text`'s own 14px ambient\n * default (the prose this outlines renders at that default), so the outline\n * reads as a secondary navigation aid rather than a second copy of the text\n * at the same visual weight. A plain pixel number, not a relative unit:\n * `Text.setFontSize`'s string overload binds to a named CSS custom property\n * (a theme token) rather than accepting an arbitrary CSS length, so\n * `\"0.85em\"` would silently fail to apply.\n */\nconst ROW_FONT_SIZE = 12;\n\n/** Padding around the header row's text — top/bottom give it room beyond its bare line height. */\nconst HEADER_PADDING = new Insets(8, 12, 4, 12);\n\nconst _defaultMarkdownMinimapOptions: Partial<MarkdownMinimapOptions> = {\n maxHeadingDepth: DEFAULT_MAX_HEADING_DEPTH,\n // Caps the otherwise-unbounded content-derived height (see\n // DEFAULT_MAX_HEIGHT_PX). Class default (not an imperative setter call)\n // so a caller-supplied maxSize still wins.\n maxSize: { width: UNBOUNDED, height: DEFAULT_MAX_HEIGHT_PX },\n minSize: { width: DEFAULT_MIN_WIDTH_PX, height: 0 },\n};\n\n/**\n * A floating card showing a document's heading outline as a `Tree`.\n *\n * Builds a real `TreeNode[]` hierarchy from a flat `MarkdownHeading[]` list\n * (see {@link setHeadings}) and, when constructed with a {@link\n * HeadingScrollSource}, highlights whichever heading is currently on screen.\n * Emits a semantic `\"select\"` event carrying the clicked heading's id rather\n * than navigating itself — the caller decides what \"select\" means (scroll an\n * owned `Markdown`, call a router), the same division of responsibility\n * `Tree` itself keeps between emitting `\"selection\"` and acting on it.\n *\n * @category Components\n */\nclass MarkdownMinimap extends FloatingPanel<MarkdownMinimapOptions> {\n\n private readonly _tree: Tree;\n private readonly _listeners: ListenerBag<MarkdownMinimapEvent> = new ListenerBag<MarkdownMinimapEvent>();\n private readonly _scrollSource: HeadingScrollSource | null;\n\n /** Shown heading id -> its `TreeNode`. */\n private _nodesById: Map<string, TreeNode> = new Map();\n\n /** Every heading id, shown or not, -> the nearest ancestor id that IS shown (or `null`). */\n private _nearestShown: Map<string, string | null> = new Map();\n\n private readonly handleSelection: (nodes: TreeNode[]) => void = (nodes) => this.onTreeSelection(nodes);\n private readonly handleActiveHeadingChange: (headingId: string | null) => void = (id) => this.applyActiveHeading(id);\n\n constructor(options?: MarkdownMinimapOptions, subclassDefaults?: Partial<MarkdownMinimapOptions>) {\n super(options, { ..._defaultMarkdownMinimapOptions, ...(subclassDefaults ?? {}) });\n\n // The opaque card surface lives here, on the outer panel; the inner\n // Tree stays transparent (see class doc) so there is exactly one\n // opaque box, not two stacked ones.\n this.setBackgroundColor(\"var(--ts-ui-input-bg, rgb(255, 255, 255))\");\n this.setShadow(\"var(--ts-ui-popover-shadow, 2px 4px 12px rgba(0, 0, 0, 0.18))\");\n this.setBorderRadius(\"var(--ts-ui-border-radius, 4px)\");\n // stretching: true fills the header and the tree to the panel's own\n // width — the same reason DocsSidebar's own Tree-hosting layouts pass it.\n this.setLayoutManager(new VBox({ spacing: 4, stretching: true }));\n\n const headerText = new Text(HEADER_TEXT);\n headerText.setFontSize(12);\n headerText.setFontWeight(\"700\");\n // Dims the ambient (theme-aware) text colour rather than hardcoding a\n // grey, so this reads correctly in both light and dark themes.\n headerText.setOpacity(0.6);\n\n // `padding` on a bare `Text` is invisible to its own getPreferredSize\n // (which reports only measured font metrics — see Text.ts), so the\n // padding has to live on a wrapping row instead, or VBox allocates\n // the header only its bare line height and the real CSS padding then\n // clips into that too-small box.\n const headerRow = new Component({ layoutManager: new Fit(), padding: HEADER_PADDING });\n headerRow.addComponent(headerText);\n this.addComponent(headerRow);\n\n // \"clip\": a heading label is read, not scrolled sideways to see in\n // full — an outline is a navigation aid, not a place to read a long\n // title one horizontal-scroll-drag at a time.\n this._tree = new Tree({ backgroundColor: \"transparent\", rowOverflow: \"clip\" });\n this._tree.setRendererFactory(() => {\n const renderer = new LabelTreeNodeRenderer();\n renderer.getLabel().setFontSize(ROW_FONT_SIZE);\n\n return renderer;\n });\n this._tree.on(\"selection\", this.handleSelection);\n this.addComponent(this._tree);\n\n this._scrollSource = options?.scrollSource ?? null;\n this._scrollSource?.on(\"activeheadingchange\", this.handleActiveHeadingChange);\n\n this.applyListeners(options?.listeners);\n }\n\n /**\n * Reports `DEFAULT_WIDTH_PX` as the preferred width when the\n * caller has set no explicit `preferredSize` and the computed width\n * (driven by `Tree`'s own generic, content-agnostic default) is\n * narrower — height keeps its normal (content-derived, capped) VBox\n * computation.\n *\n * @returns The preferred `{width, height}`.\n */\n getPreferredSize(): Size | null {\n if (this.getPreferredSizeConstraint() !== null) {\n return super.getPreferredSize();\n }\n\n const computed = super.getPreferredSize();\n\n if (!computed) {\n return computed;\n }\n\n return { width: Math.max(computed.width, DEFAULT_WIDTH_PX), height: computed.height };\n }\n\n /**\n * Dispatches {@link MarkdownMinimapOptions.maxHeadingDepth}; every other\n * option is inherited from {@link FloatingPanel}.\n *\n * @param options - The options bag carrying the values to apply.\n * @returns This panel, for method chaining.\n */\n protected applyOptions(options: MarkdownMinimapOptions): this {\n super.applyOptions(options);\n\n if (options.maxHeadingDepth !== undefined) {\n this._options.maxHeadingDepth = options.maxHeadingDepth;\n }\n\n return this;\n }\n\n /**\n * The deepest heading depth shown; a heading at or past `maxHeadingDepth +\n * 1` has no row.\n *\n * @returns The cached {@link MarkdownMinimapOptions.maxHeadingDepth}, or the class default when never set.\n */\n getMaxHeadingDepth(): number {\n return this._options.maxHeadingDepth ?? this._defaultOptions.maxHeadingDepth ?? DEFAULT_MAX_HEADING_DEPTH;\n }\n\n /**\n * Replaces the shown outline, rebuilding the tree from `headings`. A\n * heading past {@link getMaxHeadingDepth} is dropped entirely — it nests\n * under no other row, and no row of its own survives either — while still\n * anchoring any of its own descendants (that are within depth) to its\n * nearest shown ancestor.\n *\n * @param headings - The document's headings, in document order.\n * @returns This panel, for method chaining.\n */\n setHeadings(headings: MarkdownHeading[]): this {\n const maxDepth = this.getMaxHeadingDepth();\n const roots: TreeNode[] = [];\n const stack: Array<{ depth: number; node: TreeNode }> = [];\n const nodesById = new Map<string, TreeNode>();\n const nearestShown = new Map<string, string | null>();\n\n for (const heading of headings) {\n while (stack.length > 0 && stack[stack.length - 1].depth >= heading.depth) {\n stack.pop();\n }\n\n const ancestor = stack.length > 0 ? stack[stack.length - 1].node : null;\n const shown = heading.depth <= maxDepth;\n\n if (shown) {\n const node: TreeNode = { label: heading.text, data: heading.id, children: [] };\n\n if (ancestor) {\n ancestor.children!.push(node);\n } else {\n roots.push(node);\n }\n\n nodesById.set(heading.id, node);\n nearestShown.set(heading.id, heading.id);\n stack.push({ depth: heading.depth, node });\n } else {\n nearestShown.set(heading.id, ancestor ? (nearestShown.get(ancestor.data as string) ?? null) : null);\n }\n }\n\n this._nodesById = nodesById;\n this._nearestShown = nearestShown;\n\n this._tree.setNodes(roots);\n this._tree.expandAll();\n\n return this;\n }\n\n on(event: \"select\", listener: (headingId: string) => void): this {\n this._listeners.add(event, listener);\n\n return this;\n }\n\n off(event: \"select\", listener: (headingId: string) => void): this {\n this._listeners.remove(event, listener);\n\n return this;\n }\n\n protected emit(event: \"select\", headingId: string): void {\n this._listeners.fire(event, headingId);\n }\n\n /**\n * Maps a `Tree` `\"selection\"` event onto this minimap's own `\"select\"`\n * event, carrying just the clicked heading's id.\n *\n * @param nodes - The tree's newly selected nodes; only the first is used.\n */\n private onTreeSelection(nodes: TreeNode[]): void {\n const node = nodes[0];\n\n if (!node || node.data === undefined) {\n return;\n }\n\n this.emit(\"select\", node.data as string);\n }\n\n /**\n * Highlights the row for `id`'s nearest shown ancestor (or `id` itself\n * when it is shown). A `null` id — no heading is above the scroll pane's\n * top yet — is a no-op, leaving the previously selected row standing.\n *\n * @param id - The scroll source's newly active heading id, or `null`.\n */\n private applyActiveHeading(id: string | null): void {\n if (id === null) {\n return;\n }\n\n const resolvedId = this._nearestShown.get(id) ?? null;\n\n if (resolvedId === null) {\n return;\n }\n\n const node = this._nodesById.get(resolvedId);\n\n if (node) {\n this._tree.selectNode(node);\n }\n }\n\n /**\n * Unwires the `scrollSource` listener before the inherited destructor\n * disposes the tree — a `scrollSource` outliving this minimap must not\n * keep firing into torn-down state.\n */\n protected destructor(): void {\n this._scrollSource?.off(\"activeheadingchange\", this.handleActiveHeadingChange);\n\n super.destructor();\n }\n}\n\nconst MarkdownMinimapCallable = callable(MarkdownMinimap);\ntype MarkdownMinimapCallable = MarkdownMinimap;\nexport {\n MarkdownMinimap as _MarkdownMinimap,\n MarkdownMinimapCallable as MarkdownMinimap,\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Panel, PanelOptions } from \"~/core/Panel.js\";\nimport { Anchor } from \"~/layout/Anchor.js\";\nimport { AnchorConstraints } from \"~/layout/AnchorConstraints.js\";\nimport { VBox } from \"~/layout/VBox.js\";\nimport { Insets } from \"~/primitive/Insets.js\";\nimport { FloatingPanel } from \"~/component/container/FloatingPanel.js\";\nimport { Button } from \"~/component/button/Button.js\";\nimport { Glyph } from \"~/component/display/Glyph.js\";\nimport { compress } from \"~/glyphs/solid/compress.js\";\nimport { expand } from \"~/glyphs/solid/expand.js\";\nimport { magnifying_glass_plus } from \"~/glyphs/solid/magnifying_glass_plus.js\";\nimport { magnifying_glass_minus } from \"~/glyphs/solid/magnifying_glass_minus.js\";\nimport { arrows_rotate } from \"~/glyphs/solid/arrows_rotate.js\";\nimport { Event } from \"~/core/Event.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport { ListenerBag } from \"~/core/ListenerBag.js\";\nimport { Util } from \"~/core/Util.js\";\nimport { callable } from \"~/core/Callable.js\";\nimport { Markdown, extractMarkdownHeadings, findActiveHeading } from \"~/component/display/Markdown.js\";\nimport type { MarkdownLinkResolver, MarkdownHeading } from \"~/component/display/Markdown.js\";\nimport { MarkdownMinimap } from \"~/component/display/MarkdownMinimap.js\";\nimport type { HeadingScrollSource } from \"~/component/display/MarkdownMinimap.js\";\n\nGlyph.register(compress, expand, magnifying_glass_plus, magnifying_glass_minus, arrows_rotate);\n\n/**\n * Prose column width presets, in `ch` units — narrow / default / wide. `70`\n * matches the theme's own `--ts-ui-md-max-measure` default, so stepping back\n * to the middle preset reads the same as never having touched the control.\n */\nconst WIDTH_PRESETS_CH = [60, 70, 90];\n\n/** Font-scale presets the zoom controls step through. */\nconst ZOOM_PRESETS = [0.85, 1.0, 1.15, 1.3];\n\n/** Index into {@link WIDTH_PRESETS_CH} a freshly constructed viewer starts at. */\nconst DEFAULT_WIDTH_INDEX = 1;\n\n/** Index into {@link ZOOM_PRESETS} a freshly constructed viewer starts at. */\nconst DEFAULT_ZOOM_INDEX = 1;\n\n/**\n * Left margin kept on the internal `Markdown`'s own content box, so the\n * prose starts indented from the pane's edge the way text sits on a printed\n * page or in a word processor, rather than flush against it.\n */\nconst PROSE_LEFT_MARGIN_PX = 32;\n\n/**\n * Construction-time options for {@link MarkdownViewer}.\n *\n * @category Components\n */\nexport interface MarkdownViewerOptions extends PanelOptions {\n /** The Markdown source string to render. */\n markdown?: string;\n\n /** Maps an authored link href to its rendered form; forwarded to the internal `Markdown`. */\n linkResolver?: MarkdownLinkResolver;\n\n /** Deepest heading depth the minimap shows. Default `3`, forwarded to the internal `MarkdownMinimap`. */\n maxHeadingDepth?: number;\n\n /** Whether the floating heading outline shows. Default `true`. */\n showMinimap?: boolean;\n\n /** Whether the floating width/zoom controls show. Default `true`. */\n showControls?: boolean;\n\n listeners?: {\n activeheadingchange?: (headingId: string | null) => void;\n };\n}\n\nconst _defaultMarkdownViewerOptions: Partial<MarkdownViewerOptions> = {\n showMinimap: true,\n showControls: true,\n};\n\n/**\n * A single-document Markdown viewer with a floating outline minimap and\n * width/zoom controls, both pinned over the prose.\n *\n * Wraps one `Markdown` instance in an `Anchor` layout, with the minimap\n * top-right and the controls bottom-right. Any consumer embedding one\n * `Markdown` instance gets both for free by using `MarkdownViewer` instead\n * of `Markdown` directly. Exposes the same `\"activeheadingchange\"` event\n * `DocsContent` exposes, computed from its own native scroll the same way —\n * see {@link HeadingScrollSource}. `MarkdownMinimap` consumes that event to\n * highlight whichever heading is currently on screen without depending on\n * this class concretely.\n *\n * @category Components\n */\nclass MarkdownViewer extends Panel<MarkdownViewerOptions> implements HeadingScrollSource {\n\n private readonly _markdown: Markdown;\n private readonly _minimap: MarkdownMinimap;\n private _controls!: FloatingPanel;\n private _narrowerBtn!: Button;\n private _widerBtn!: Button;\n private _zoomOutBtn!: Button;\n private _zoomInBtn!: Button;\n private _resetBtn!: Button;\n\n private _headings: MarkdownHeading[];\n private _widthIndex: number = DEFAULT_WIDTH_INDEX;\n private _zoomIndex: number = DEFAULT_ZOOM_INDEX;\n private _lastActiveHeadingId: string | null = null;\n\n /**\n * The scrollTop {@link scrollToHeading} last landed the pane on, or\n * `null` once a later native scroll has moved past it. Lets\n * `onNativeScroll` recognise \"nothing has organically scrolled since\n * that click\" and skip re-deriving the active heading from geometry —\n * see both methods' own doc comments for why that re-derivation alone\n * cannot be trusted here.\n */\n private _pendingClickScrollTop: number | null = null;\n\n private readonly _listeners: ListenerBag<\"activeheadingchange\"> = new ListenerBag<\"activeheadingchange\">();\n\n private readonly handleNativeScroll: () => void = () => this.onNativeScroll();\n private readonly handleMinimapSelect: (id: string) => void = (id) => this.scrollToHeading(id);\n private readonly _onNarrower: () => void = () => this.stepWidth(-1);\n private readonly _onWider: () => void = () => this.stepWidth(1);\n private readonly _onZoomOut: () => void = () => this.stepZoom(-1);\n private readonly _onZoomIn: () => void = () => this.stepZoom(1);\n private readonly _onReset: () => void = () => this.resetViewerProperties();\n\n constructor(options?: MarkdownViewerOptions, subclassDefaults?: Partial<MarkdownViewerOptions>) {\n super(options, {\n ..._defaultMarkdownViewerOptions,\n ...(subclassDefaults ?? {}),\n // Last, so a subclass default can't silently swap out the\n // Anchor every addComponent call below depends on. (A caller\n // passing its own `options.layoutManager` still wins, per\n // Component's own dispatch — the same pre-existing tradeoff\n // DiagramView's own Anchor-dependent constructor carries.)\n layoutManager: new Anchor(),\n autoScroll: \"y\",\n } as Partial<MarkdownViewerOptions>);\n\n this._markdown = new Markdown(options?.markdown, {\n linkResolver: options?.linkResolver,\n padding: new Insets(0, 0, 0, PROSE_LEFT_MARGIN_PX),\n });\n\n const markdownConstraints = new AnchorConstraints();\n markdownConstraints.left = 0;\n markdownConstraints.right = 0;\n this.addComponent(this._markdown, markdownConstraints);\n\n this._headings = extractMarkdownHeadings(options?.markdown ?? \"\");\n\n this._minimap = new MarkdownMinimap({ scrollSource: this, maxHeadingDepth: options?.maxHeadingDepth, corner: \"top-right\" });\n this._minimap.setHeadings(this._headings);\n this._minimap.on(\"select\", this.handleMinimapSelect);\n this.addComponent(this._minimap, this._minimap.getAnchorConstraints());\n\n this.buildControls();\n this.wireControlListeners();\n this.addComponent(this._controls, this._controls.getAnchorConstraints());\n\n Event.addSubtreeListener(this, \"scroll\", this.handleNativeScroll);\n\n this.setMinimapVisible(this._options.showMinimap ?? this._defaultOptions.showMinimap ?? true);\n this.setControlsVisible(this._options.showControls ?? this._defaultOptions.showControls ?? true);\n\n this.applyListeners(options?.listeners);\n }\n\n /**\n * Lays out `Markdown` / the minimap / the controls as usual, then\n * re-hugs the minimap against `_markdown`'s freshly committed geometry —\n * `MarkdownMinimap.placeNextTo` needs calling after every pass that can\n * move either this viewer's own width or `_markdown`'s rendered width\n * (see its own doc comment for why it's an owner-driven call rather than\n * a `MarkdownMinimap`-internal `doLayout` override).\n *\n * @returns This viewer, for method chaining.\n */\n doLayout(): this {\n super.doLayout();\n\n // Guards against the super() cascade's own options dispatch (e.g.\n // setAutoScroll) triggering a layout pass before this constructor has\n // reached the point of assigning _minimap/_markdown.\n this._minimap?.placeNextTo(this._markdown ?? null);\n\n return this;\n }\n\n /**\n * Dispatches `showMinimap` / `showControls`; every other option either\n * routes to the internal `Markdown` (constructor-only) or is inherited\n * from `Panel`.\n *\n * @param options - The options bag carrying the values to apply.\n * @returns This viewer, for method chaining.\n */\n protected applyOptions(options: MarkdownViewerOptions): this {\n super.applyOptions(options);\n\n // Cached only: the minimap/controls don't exist yet during the\n // super() cascade. The constructor dispatches\n // setMinimapVisible/setControlsVisible itself once they're built —\n // mirrors DiagramView's own `controls` option.\n if (options.showMinimap !== undefined) {\n this._options.showMinimap = options.showMinimap;\n }\n\n if (options.showControls !== undefined) {\n this._options.showControls = options.showControls;\n }\n\n return this;\n }\n\n /**\n * Read-only escape hatch; change content via {@link setMarkdown}, not by\n * calling `getMarkdown().setMarkdown(...)` directly — that would desync\n * the minimap.\n *\n * @returns The internal `Markdown` instance.\n */\n getMarkdown(): Markdown {\n return this._markdown;\n }\n\n /**\n * Replaces the rendered source, recomputes headings, and refreshes the minimap.\n *\n * @param markdown - The new Markdown source string.\n * @returns This viewer, for method chaining.\n */\n setMarkdown(markdown: string): this {\n this._markdown.setMarkdown(markdown);\n this._headings = extractMarkdownHeadings(markdown);\n this._minimap.setHeadings(this._headings);\n\n return this;\n }\n\n /**\n * Whether the floating heading-outline minimap shows.\n *\n * @returns `true` when the minimap is visible.\n */\n isMinimapVisible(): boolean {\n return this._options.showMinimap ?? this._defaultOptions.showMinimap ?? true;\n }\n\n /**\n * Shows or hides the floating heading-outline minimap.\n *\n * @param value - Whether the minimap is visible.\n * @returns This viewer, for method chaining.\n */\n setMinimapVisible(value: boolean): this {\n this._options.showMinimap = value;\n this._minimap.setVisible(value);\n\n return this;\n }\n\n /**\n * Whether the floating width/zoom control cluster shows.\n *\n * @returns `true` when the control cluster is visible.\n */\n isControlsVisible(): boolean {\n return this._options.showControls ?? this._defaultOptions.showControls ?? true;\n }\n\n /**\n * Shows or hides the floating width/zoom control cluster.\n *\n * @param value - Whether the control cluster is visible.\n * @returns This viewer, for method chaining.\n */\n setControlsVisible(value: boolean): this {\n this._options.showControls = value;\n this._controls.setVisible(value);\n\n return this;\n }\n\n on(event: \"activeheadingchange\", listener: (headingId: string | null) => void): this {\n this._listeners.add(event, listener);\n\n return this;\n }\n\n off(event: \"activeheadingchange\", listener: (headingId: string | null) => void): this {\n this._listeners.remove(event, listener);\n\n return this;\n }\n\n protected emit(event: \"activeheadingchange\", headingId: string | null): void {\n this._listeners.fire(event, headingId);\n }\n\n /** Builds the corner-pinned width-narrower/wider, zoom-out/in, and reset control cluster. */\n private buildControls(): void {\n this._narrowerBtn = this.makeControlButton(\"compress\", \"Narrower\");\n this._widerBtn = this.makeControlButton(\"expand\", \"Wider\");\n this._zoomOutBtn = this.makeControlButton(\"magnifying-glass-minus\", \"Zoom out\");\n this._zoomInBtn = this.makeControlButton(\"magnifying-glass-plus\", \"Zoom in\");\n this._resetBtn = this.makeControlButton(\"arrows-rotate\", \"Reset\");\n\n this._controls = new FloatingPanel({ corner: \"bottom-right\", layoutManager: new VBox({ spacing: 4 }) });\n this._controls.addComponent(this._narrowerBtn);\n this._controls.addComponent(this._widerBtn);\n this._controls.addComponent(this._zoomOutBtn);\n this._controls.addComponent(this._zoomInBtn);\n this._controls.addComponent(this._resetBtn);\n }\n\n /**\n * Builds a glyph-only control button with an accessible label, mirroring\n * `DiagramView`'s own control-cluster buttons.\n *\n * @param glyph - The glyph name to show.\n * @param label - The accessible name (drives `aria-label` and the tooltip).\n * @returns The configured button.\n */\n private makeControlButton(glyph: string, label: string): Button {\n return new Button({ glyph, text: label, showText: false });\n }\n\n /** Wires the control cluster's buttons to their viewer-property step methods. */\n private wireControlListeners(): void {\n this._narrowerBtn.on(\"action\", this._onNarrower);\n this._widerBtn.on(\"action\", this._onWider);\n this._zoomOutBtn.on(\"action\", this._onZoomOut);\n this._zoomInBtn.on(\"action\", this._onZoomIn);\n this._resetBtn.on(\"action\", this._onReset);\n }\n\n /**\n * Moves `_widthIndex` by one step, clamped to `WIDTH_PRESETS_CH`'s bounds,\n * and applies the resulting preset to the internal `Markdown`.\n *\n * @param direction - `1` for wider, `-1` for narrower.\n */\n private stepWidth(direction: 1 | -1): void {\n this._widthIndex = Util.clamp(this._widthIndex + direction, 0, WIDTH_PRESETS_CH.length - 1);\n this._markdown.setMaxMeasure(WIDTH_PRESETS_CH[this._widthIndex] + \"ch\");\n // setMaxMeasure writes a CSS rule directly and schedules no layout of\n // its own, so the minimap's hug would otherwise go stale against the\n // prose's new rendered width — see MarkdownMinimap.placeNextTo.\n this._minimap.placeNextTo(this._markdown);\n }\n\n /**\n * Moves `_zoomIndex` by one step, clamped to `ZOOM_PRESETS`'s bounds, and\n * applies the resulting preset to the internal `Markdown`.\n *\n * @param direction - `1` for zoom in, `-1` for zoom out.\n */\n private stepZoom(direction: 1 | -1): void {\n this._zoomIndex = Util.clamp(this._zoomIndex + direction, 0, ZOOM_PRESETS.length - 1);\n this._markdown.setFontScale(ZOOM_PRESETS[this._zoomIndex]);\n // setFontScale can also change the prose's rendered width (ch-based\n // maxMeasure scales with font size) — see stepWidth's own comment.\n this._minimap.placeNextTo(this._markdown);\n }\n\n /**\n * Resets both viewer properties to their default index and clears their\n * overrides entirely (`setMaxMeasure(null)` / `setFontScale(1)`) rather\n * than re-applying the default preset, so a live theme change afterward\n * still takes effect.\n */\n private resetViewerProperties(): void {\n this._widthIndex = DEFAULT_WIDTH_INDEX;\n this._zoomIndex = DEFAULT_ZOOM_INDEX;\n this._markdown.setMaxMeasure(null);\n this._markdown.setFontScale(1);\n this._minimap.placeNextTo(this._markdown);\n }\n\n /**\n * Computes the active heading from the current native scroll position and\n * emits `\"activeheadingchange\"` only when it differs from the previous\n * tick. A no-op while the pane is still sitting exactly where {@link\n * scrollToHeading} last left it (see `_pendingClickScrollTop`'s own doc\n * comment) — geometry alone can't be trusted to reproduce that click's\n * own target there, so this defers to whatever it already set.\n */\n private onNativeScroll(): void {\n const scrollElement = this.getScrollElement();\n\n if (!scrollElement) {\n return;\n }\n\n if (this._pendingClickScrollTop !== null) {\n // Reads the live DOM value, not the cached getScrollTop(): an\n // organic scroll (wheel, scrollbar drag) updates the pane's real\n // scrollTop without ever going through setScrollTop, so the cache\n // would otherwise still read the click's own landing spot forever.\n if (DOM.source.getScrollTop(scrollElement) === this._pendingClickScrollTop) {\n return;\n }\n\n this._pendingClickScrollTop = null;\n }\n\n const id = findActiveHeading(scrollElement, this._headings);\n\n if (id === this._lastActiveHeadingId) {\n return;\n }\n\n this._lastActiveHeadingId = id;\n this.emit(\"activeheadingchange\", id);\n }\n\n /**\n * Scrolls this viewer so `id`'s heading sits at the pane's own top edge —\n * the geometry technique `DocsContent.scrollToHeading` uses, kept local to\n * this class rather than shared (see the plan's Non-Goals). Marks `id`\n * active immediately rather than waiting for the resulting native scroll\n * event to drive that through `findActiveHeading`: a heading close to the\n * document's end can share its clamped landing scrollTop with a\n * neighbouring heading, and geometry alone then can't tell which of them\n * this click actually targeted (`findActiveHeading`'s own doc comment).\n *\n * @param id - The heading id to scroll to.\n */\n private scrollToHeading(id: string): void {\n const scrollElement = this.getScrollElement();\n\n if (!scrollElement) {\n return;\n }\n\n const heading = DOM.source.getElementById(id);\n\n if (!heading || !DOM.source.contains(scrollElement, heading)) {\n return;\n }\n\n const headingTop = DOM.source.getElementRect(heading).top;\n const paneTop = DOM.source.getElementRect(scrollElement).top;\n\n this.setScrollTop(this.getScrollTop() + (headingTop - paneTop));\n this._pendingClickScrollTop = this.getScrollTop();\n\n if (id !== this._lastActiveHeadingId) {\n this._lastActiveHeadingId = id;\n this.emit(\"activeheadingchange\", id);\n }\n }\n}\n\nconst MarkdownViewerCallable = callable(MarkdownViewer);\ntype MarkdownViewerCallable = MarkdownViewer;\nexport {\n MarkdownViewer as _MarkdownViewer,\n MarkdownViewerCallable as MarkdownViewer,\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { HBox } from \"~/layout/HBox.js\";\nimport { Insets } from \"~/primitive/Insets.js\";\nimport { Label } from \"~/component/input/Label.js\";\nimport { Glyph } from \"~/component/display/Glyph.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * Construction-time options for {@link IconLabel}.\n *\n * @category Components\n */\nexport interface IconLabelOptions extends ComponentOptions {\n glyph?: string;\n text?: string;\n forId?: string;\n gap?: number;\n}\n\n/**\n * User-overridable defaults forwarded to `super` via the options bag. The\n * cascade dispatches each present setter once with the final value.\n * `layoutManager` is *not* listed — each instance needs its own fresh HBox.\n * `gap`/`glyph`/`text`/`forId` are late-built state (their setters reach into\n * children) and are written pure by `applyOptions`, then dispatched from the\n * constructor body once the row exists.\n */\nconst _defaultIconLabelOptions: Partial<IconLabelOptions> = {\n gap: 2,\n insets: new Insets(0, 0, 0, 0),\n};\n\n/**\n * A small composite pairing a leading [`Glyph`](/api/component/display/classes/Glyph)\n * with a trailing [`Label`](/api/component/input/classes/Label), laid out\n * horizontally with a configurable gap (default 2).\n *\n * Use this when the icon belongs to a form control: the inner element is a\n * real `<label for=\"…\">`, so the browser still focuses the associated input\n * when the label text is clicked. For icon-with-text that has no form-control\n * association, use [`IconText`](/api/component/display/classes/IconText) instead.\n *\n * @example\n * ```typescript\n * import { IconLabel } from '@jimka/typescript-ui/component/display';\n * import { TextField } from '@jimka/typescript-ui/component/input';\n *\n * const field = new TextField();\n * panel.addComponent(new IconLabel('times', 'Email:', field.getId()));\n * panel.addComponent(field);\n * ```\n *\n * @category Components\n */\nclass IconLabel extends Component<IconLabelOptions> {\n\n private _glyph!: Glyph;\n private _label!: Label;\n\n /**\n * Constructs an `IconLabel` pairing the named glyph with a `<label for=\"…\">`.\n *\n * @param glyph - Registry glyph name. Must be present in the internal `Glyphs` registry.\n * @param text - Label text shown to the right of the glyph.\n * @param forId - Element id of the form control this label is associated with.\n * Must be non-empty; mirrors [`Label`](/api/component/input/classes/Label)'s constructor contract.\n * @param options - Optional configuration bag (gap override, common Component fields).\n * @param subclassDefaults - Per-subclass default bag layered over this\n * class's defaults; subclasses forward their `_defaultXxxOptions`\n * constant here.\n */\n constructor(\n glyph: string,\n text: string,\n forId: string,\n options?: IconLabelOptions,\n subclassDefaults?: Partial<IconLabelOptions>,\n ) {\n super(options, { ..._defaultIconLabelOptions, ...(subclassDefaults ?? {}) });\n\n // Per-instance layout manager seeded with the effective gap (caller\n // override, else the class default) so it flows into the HBox spacing.\n this.setLayoutManager(new HBox({ spacing: this.getGap() }));\n\n // Build children with the effective values up front so the late-built\n // dispatch below has nothing to overwrite. The bag-written values\n // from the cascade take precedence over the positional arguments.\n // `setGlyph` would rebuild the inner Glyph, and `setText`/`setForId`\n // would push the same value into the Label a second time — so we\n // resolve the effective value here once.\n const effectiveGlyph = this._options.glyph ?? glyph;\n const effectiveText = this._options.text ?? text;\n const effectiveForId = this._options.forId ?? forId;\n\n this._glyph = new Glyph(effectiveGlyph);\n this._label = new Label(effectiveText, effectiveForId);\n\n this.addComponent(this._glyph);\n this.addComponent(this._label);\n\n // Late-built state: bag-written by `applyOptions`. Only `gap` needs\n // post-construction dispatch — the HBox's spacing was seeded from\n // the bag at construction, but a later cascade-time write to\n // `_options.gap` (consumer override) needs to push into the HBox.\n if (this._options.gap !== undefined) {\n (this.getLayoutManager() as HBox).setComponentSpacing(this._options.gap);\n }\n }\n\n /**\n * Applies an {@link IconLabelOptions} bag. Inherited Component fields\n * cascade through `super.applyOptions`; the gap/glyph/text/forId fields\n * are written pure to `_options` here and dispatched from the constructor\n * body once children exist.\n *\n * @param options - The options bag carrying the values to apply.\n */\n protected applyOptions(options: IconLabelOptions): this {\n super.applyOptions(options);\n\n if (options.gap !== undefined) this._options.gap = options.gap;\n if (options.glyph !== undefined) this._options.glyph = options.glyph;\n if (options.text !== undefined) this._options.text = options.text;\n if (options.forId !== undefined) this._options.forId = options.forId;\n\n return this;\n }\n\n /**\n * Replaces the leading glyph with a fresh instance for the given registry name.\n *\n * @param name - Registry glyph name. Must be present in the internal registry.\n *\n * @returns This component, for method chaining.\n */\n setGlyph(name: string): this {\n this.removeComponent(this._glyph);\n\n this._glyph = new Glyph(name);\n this.insertComponent(this._glyph, 0);\n\n return this;\n }\n\n /**\n * Updates the trailing label text.\n *\n * @param text - The new label string.\n *\n * @returns This component, for method chaining.\n */\n setText(text: string): this {\n this._label.setText(text);\n\n return this;\n }\n\n /**\n * Updates the trailing label's `for` association.\n *\n * @param id - Element id of the form control this label should be associated with.\n *\n * @returns This component, for method chaining.\n */\n setForId(id: string): this {\n this._label.setForId(id);\n\n return this;\n }\n\n /**\n * Sets the pixel gap between the glyph and the label.\n *\n * @param px - Gap in pixels.\n *\n * @returns This component, for method chaining.\n */\n setGap(px: number): this {\n this._options.gap = px;\n (this.getLayoutManager() as HBox).setComponentSpacing(px);\n\n return this;\n }\n\n /**\n * Returns the effective gap between the glyph and the label — the\n * caller/setter value, else the class default (2).\n *\n * @returns The gap in pixels.\n */\n getGap(): number {\n return (this._options.gap ?? this._defaultOptions.gap)!;\n }\n\n /**\n * Returns the leading glyph component.\n *\n * @returns The [`Glyph`](/api/component/display/classes/Glyph) instance.\n */\n getGlyphComponent(): Glyph {\n return this._glyph;\n }\n\n /**\n * Returns the trailing label component.\n *\n * @returns The [`Label`](/api/component/input/classes/Label) instance.\n */\n getLabelComponent(): Label {\n return this._label;\n }\n}\n\nconst IconLabelCallable = callable(IconLabel);\ntype IconLabelCallable = IconLabel;\nexport {\n IconLabel as _IconLabel,\n IconLabelCallable as IconLabel\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { StyleRule } from \"~/core/StyleTarget.js\";\nimport { callable } from \"~/core/Callable.js\";\nimport { Util } from \"~/core/Util.js\";\n\nStyleRule.ensureKeyframes(\n 'ts-ui-progress-indeterminate',\n '0% { transform: translateX(-100%); } 100% { transform: translateX(400%); }'\n);\n\n/**\n * Construction-time options for {@link ProgressBar}.\n *\n * @category Components\n */\nexport interface ProgressBarOptions extends ComponentOptions {\n value?: number;\n indeterminate?: boolean;\n}\n\n/**\n * A horizontal progress indicator with a determinate (0–100%) and an indeterminate\n * (continuously animated) mode.\n *\n * The component renders a track child filling its inner bounds and a fill child\n * inside the track whose width is driven by the current value. In indeterminate\n * mode the fill is sized to roughly a quarter of the track and slides across\n * via a CSS keyframe animation.\n *\n * @category Components\n */\nclass ProgressBar extends Component {\n\n private _track: Component;\n private _fill : Component;\n private _value: number;\n private _indeterminate: boolean;\n\n /**\n * Constructs a ProgressBar.\n *\n * @param value - Initial progress value in [0, 100]. Defaults to 0.\n * @param indeterminate - When true the bar animates continuously and value is ignored.\n */\n constructor(value: number = 0, indeterminate: boolean = false, options?: ProgressBarOptions) {\n // Child components are built first; options are applied via applyOptions at the constructor tail.\n // eslint-disable-next-line local/forward-super-options\n super();\n\n this._value = Util.clamp(value, 0, 100);\n this._indeterminate = indeterminate;\n\n this.setBackgroundColor(\"var(--ts-ui-progress-track-bg, rgb(220, 220, 220))\");\n this.setBorderRadius(\"var(--ts-ui-progress-track-radius, 4px)\");\n\n this._track = new Component();\n this._track.setOverflow(\"hidden\");\n this._track.setBorderRadius(\"var(--ts-ui-progress-track-radius, 4px)\");\n\n this._fill = new Component();\n this._fill.setBackgroundColor(\"var(--ts-ui-progress-fill-bg, rgb(30, 100, 200))\");\n\n this._track.addComponent(this._fill);\n super.addComponent(this._track);\n\n this.getAria().setRole(\"progressbar\");\n this.getAria().setValueMin(0);\n this.getAria().setValueMax(100);\n this.getAria().setValueNow(this._value);\n\n if (this._indeterminate) {\n this.applyIndeterminate(true);\n }\n\n if (options) {\n this.applyOptions(options);\n }\n }\n\n /**\n * Applies a {@link ProgressBarOptions} bag, dispatching value and\n * indeterminate state after inherited Component fields.\n *\n * @param options - The options bag carrying the values to apply.\n */\n protected applyOptions(options: ProgressBarOptions): this {\n super.applyOptions(options);\n\n if (options.indeterminate !== undefined) {\n this.setIndeterminate(options.indeterminate);\n }\n\n if (options.value !== undefined) {\n this.setValue(options.value);\n }\n\n return this;\n }\n\n /**\n * Returns a baseline near the bottom of the bar so that, when placed in an\n * [`HBox`](/api/layout/classes/HBox) next to text labels, the bar sits with its bottom roughly on the\n * surrounding text baseline (CSS replaced-element behaviour, with a 2 px\n * lift so it doesn't sit visually lower than the text descenders).\n *\n * @returns The current preferred height minus 2, or `null` before a size is set.\n */\n getBaseline(): number | null {\n const size = this.getPreferredSize();\n\n return size ? size.height - 2 : null;\n }\n\n /**\n * Returns the current progress value (0–100).\n *\n * @returns The current percentage, or 0 when indeterminate.\n */\n getValue(): number {\n return this._indeterminate ? 0 : this._value;\n }\n\n /**\n * Sets the progress value and updates the fill width. Clamps to [0, 100].\n * Has no visual effect while in indeterminate mode.\n *\n * @param value - Progress percentage in [0, 100].\n */\n setValue(value: number): this {\n const clamped = Util.clamp(value, 0, 100);\n if (clamped === this._value) {\n return this;\n }\n\n this._value = clamped;\n this.getAria().setValueNow(clamped);\n\n if (!this._indeterminate) {\n this.scheduleLayout();\n }\n\n return this;\n }\n\n /**\n * Returns whether the bar is in indeterminate (animated) mode.\n *\n * @returns True if indeterminate mode is active.\n */\n isIndeterminate(): boolean {\n return this._indeterminate;\n }\n\n /**\n * Activates or deactivates indeterminate animation mode.\n *\n * @param value - True to activate indeterminate mode.\n */\n setIndeterminate(value: boolean): this {\n if (this._indeterminate === value) {\n return this;\n }\n\n this._indeterminate = value;\n this.applyIndeterminate(value);\n this.flushLayout();\n\n return this;\n }\n\n /**\n * Lays out the track and fill child components.\n *\n * @returns This component, for method chaining.\n */\n doLayout(): this {\n // The content box, not the inner size: the inner size is the right\n // extent but carries no origin, so a padded bar would put the track at\n // the inner edge of its border and ignore the padding it just\n // subtracted. The fill's own origin stays at zero — it is a child of\n // the track, so it is already inside the track's box.\n const box = this.getContentBounds();\n if (!box) {\n super.doLayout();\n return this;\n }\n\n this._track.setX(box.x);\n this._track.setY(box.y);\n this._track.setSize({ width: box.width, height: box.height });\n\n if (this._indeterminate) {\n const segment = Math.max(20, Math.round(box.width * 0.25));\n this._fill.setX(0);\n this._fill.setY(0);\n this._fill.setSize({ width: segment, height: box.height });\n } else {\n const fillWidth = Math.round(box.width * this._value / 100);\n this._fill.setX(0);\n this._fill.setY(0);\n this._fill.setSize({ width: fillWidth, height: box.height });\n }\n\n super.doLayout();\n\n return this;\n }\n\n /**\n * Toggles the CSS animation property on the fill element to enter or leave\n * indeterminate mode and updates the fill colour accordingly.\n *\n * @param value - True to start the animation, false to stop it.\n */\n private applyIndeterminate(value: boolean): void {\n if (value) {\n this._fill.setBackgroundColor(\"var(--ts-ui-progress-indeterminate-bg, rgb(30, 100, 200))\");\n this._fill.setAnimation(\"ts-ui-progress-indeterminate 1.4s ease-in-out infinite\");\n } else {\n this._fill.setBackgroundColor(\"var(--ts-ui-progress-fill-bg, rgb(30, 100, 200))\");\n this._fill.clearAnimation();\n }\n }\n}\n\nconst ProgressBarCallable = callable(ProgressBar);\ntype ProgressBarCallable = ProgressBar;\nexport {\n ProgressBar as _ProgressBar,\n ProgressBarCallable as ProgressBar\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { AbstractStore } from \"~/data/AbstractStore.js\";\nimport { HBox } from \"~/layout/HBox.js\";\nimport { Button } from \"~/component/button/Button.js\";\nimport { Text } from \"~/component/input/Text.js\";\nimport { callable } from \"~/core/Callable.js\";\nimport { Glyph } from \"~/component/display/Glyph.js\";\nimport { angles_left } from \"~/glyphs/solid/angles_left.js\";\nimport { angle_left } from \"~/glyphs/solid/angle_left.js\";\nimport { angle_right } from \"~/glyphs/solid/angle_right.js\";\nimport { angles_right } from \"~/glyphs/solid/angles_right.js\";\n\nGlyph.register(angles_left, angle_left, angle_right, angles_right);\n\n/**\n * Construction-time options for {@link PaginationBar}.\n *\n * @category Components\n */\nexport interface PaginationBarOptions extends ComponentOptions {\n pageSize?: number;\n pageIndex?: number;\n totalCount?: number;\n}\n\n/**\n * A horizontal navigation bar for stepping through pages of a paginated\n * {@link AbstractStore}.\n *\n * @remarks\n * The bar displays first / previous / next / last buttons and a `Page X of Y`\n * label, all driven by the store's pagination state. It subscribes to the\n * store's `'pagechange'` and `'load'` events and updates button-enabled\n * state automatically. The bar can be embedded anywhere — [`TablePanel`](/api/component/table/classes/TablePanel)'s\n * `setPaginationBar()` is one consumer, but it is not required.\n *\n * @example\n * ```typescript\n * import { AjaxProxy, Store } from '@jimka/typescript-ui/data';\n * import { PaginationBar } from '@jimka/typescript-ui/component/display';\n * import { TablePanel } from '@jimka/typescript-ui/component/table';\n *\n * const store = new Store({ model, proxy: new AjaxProxy({ url: '/api/users' }) });\n * store.setPageSize(25);\n *\n * const panel = new TablePanel(store);\n * panel.setPaginationBar(new PaginationBar(store));\n *\n * void store.load();\n * ```\n *\n * @category Components\n */\nclass PaginationBar extends Component<PaginationBarOptions> {\n\n private _store: AbstractStore;\n\n private _firstBtn: Button;\n private _prevBtn: Button;\n private _nextBtn: Button;\n private _lastBtn: Button;\n private _pageText: Text;\n\n private readonly _onStoreUpdate: () => void = () => this.refresh();\n\n /**\n * Constructs a pagination bar bound to the given store.\n *\n * @param store - The store whose pagination state drives the bar.\n */\n constructor(store: AbstractStore, options?: PaginationBarOptions) {\n // Child components are built first; options are applied via applyOptions at the constructor tail.\n // eslint-disable-next-line local/forward-super-options\n super();\n\n this._store = store;\n\n const layout = new HBox();\n layout.setComponentSpacing(4);\n this.setLayoutManager(layout);\n\n this._firstBtn = new Button({ glyph: \"angles-left\" });\n this._prevBtn = new Button({ glyph: \"angle-left\" });\n this._pageText = new Text(\"Page x of y\");\n this._nextBtn = new Button({ glyph: \"angle-right\" });\n this._lastBtn = new Button({ glyph: \"angles-right\" });\n\n this._firstBtn.setPreferredSize({ width: 28, height: 28 });\n this._prevBtn.setPreferredSize({ width: 28, height: 28 });\n this._nextBtn.setPreferredSize({ width: 28, height: 28 });\n this._lastBtn.setPreferredSize({ width: 28, height: 28 });\n\n // Match the buttons' 28px row height so the page label baseline lines up.\n this._pageText.centerInHeight(28);\n\n this._firstBtn.on(\"action\", () => { this._store.goToPage(1); });\n this._prevBtn.on(\"action\", () => this._store.prevPage());\n this._nextBtn.on(\"action\", () => this._store.nextPage());\n this._lastBtn.on(\"action\", () => {\n const total = this._store.getTotalPages();\n this._store.goToPage(total ?? this._store.getPage());\n });\n\n this.addComponent(this._firstBtn);\n this.addComponent(this._prevBtn);\n this.addComponent(this._pageText);\n this.addComponent(this._nextBtn);\n this.addComponent(this._lastBtn);\n\n this._store.on('pagechange', this._onStoreUpdate);\n this._store.on('load', this._onStoreUpdate);\n this._store.on('datachange', this._onStoreUpdate);\n this._store.on('add', this._onStoreUpdate);\n this._store.on('remove', this._onStoreUpdate);\n this._store.on('sync', this._onStoreUpdate);\n\n this.refresh();\n\n if (options) {\n this.applyOptions(options);\n }\n }\n\n /**\n * Applies a {@link PaginationBarOptions} bag, dispatching pagination\n * properties to the bound store after inherited Component fields.\n *\n * @param options - The options bag carrying the values to apply.\n */\n protected applyOptions(options: PaginationBarOptions): this {\n super.applyOptions(options);\n\n if (options.pageSize !== undefined) {\n this._store.setPageSize(options.pageSize);\n }\n\n if (options.pageIndex !== undefined) {\n this._store.goToPage(options.pageIndex);\n }\n\n return this;\n }\n\n /**\n * Detaches the store listeners installed by this bar, then defers to the\n * base class for the rest of teardown.\n *\n * @remarks\n * Call `dispose()` when permanently removing the bar (e.g. before\n * discarding it, or when replacing it via [`TablePanel.setPaginationBar`](/api/component/table/classes/TablePanel#setpaginationbar)). After\n * disposal the bar will no longer track the store.\n */\n protected destructor(): void {\n this._store.off('pagechange', this._onStoreUpdate);\n this._store.off('load', this._onStoreUpdate);\n this._store.off('datachange', this._onStoreUpdate);\n this._store.off('add', this._onStoreUpdate);\n this._store.off('remove', this._onStoreUpdate);\n this._store.off('sync', this._onStoreUpdate);\n\n super.destructor();\n }\n\n /**\n * Updates the page label and button-enabled states from the current store state.\n *\n * @remarks\n * When the store has pending unsynced changes, all four navigation buttons\n * are disabled — leaving the page would silently discard in-flight edits.\n * Buttons re-enable once the store is synced or rejected.\n */\n private refresh(): void {\n const page = this._store.getPage();\n const totalPages = this._store.getTotalPages();\n const dirty = this._store.hasPendingChanges();\n const text = totalPages != null\n ? `Page ${page} of ${totalPages}`\n : `Page ${page}`;\n\n this._pageText.setText(text);\n this._firstBtn.setEnabled(!dirty && page > 1);\n this._prevBtn.setEnabled(!dirty && page > 1);\n this._nextBtn.setEnabled(!dirty && (totalPages == null || page < totalPages));\n this._lastBtn.setEnabled(!dirty && totalPages != null && page < totalPages);\n }\n}\n\nconst PaginationBarCallable = callable(PaginationBar);\ntype PaginationBarCallable = PaginationBar;\nexport {\n PaginationBar as _PaginationBar,\n PaginationBarCallable as PaginationBar\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle, MediaState } from \"~/core/DOM.js\";\nimport { ListenerBag } from \"~/core/ListenerBag.js\";\nimport { callable } from \"~/core/Callable.js\";\n\n/**\n * The media events {@link Video} re-emits through its custom `on` / `off`\n * surface. All are non-bubbling DOM media events, wired natively at render time\n * via `DOM.sink.addListener` (they never reach the `Event` class's window-level\n * capture handler) and fanned back out through the {@link ListenerBag}.\n *\n * @category Components\n */\nexport type VideoMediaEvent =\n | \"timeupdate\"\n | \"play\"\n | \"pause\"\n | \"ended\"\n | \"loadedmetadata\"\n | \"durationchange\"\n | \"volumechange\"\n | \"ratechange\";\n\n/** The re-emitted media event types, iterated when wiring native listeners. */\nconst VIDEO_MEDIA_EVENTS: readonly VideoMediaEvent[] = [\n \"timeupdate\",\n \"play\",\n \"pause\",\n \"ended\",\n \"loadedmetadata\",\n \"durationchange\",\n \"volumechange\",\n \"ratechange\",\n];\n\n// Full volume — the browser's own default for a fresh media element, mirrored so\n// a Video with no `volume` option reports the same value the element would.\nconst DEFAULT_VOLUME = 1;\n\n// Normal playback speed — the browser's default `playbackRate`, mirrored so a\n// Video with no `playbackRate` option reports the element's own default.\nconst DEFAULT_PLAYBACK_RATE = 1;\n\n/**\n * Construction-time options for {@link Video}.\n *\n * @category Components\n */\nexport interface VideoOptions extends ComponentOptions {\n src?: string;\n poster?: string;\n autoplay?: boolean;\n loop?: boolean;\n muted?: boolean;\n preload?: \"none\" | \"metadata\" | \"auto\";\n /** Audio volume, clamped to `[0, 1]`. */\n volume?: number;\n playbackRate?: number;\n /**\n * Construction-time listener bag — the declarative form of `on()`, one key\n * per re-emitted media event.\n */\n listeners?: {\n timeupdate?: () => void;\n play?: () => void;\n pause?: () => void;\n ended?: () => void;\n loadedmetadata?: () => void;\n durationchange?: () => void;\n volumechange?: () => void;\n ratechange?: () => void;\n };\n}\n\n/**\n * Class-level defaults forwarded to `super` so the cascade hits Component's\n * applyOptions with `{ tag: \"video\" }` already merged into `_defaultOptions`\n * (mirrors {@link Image}'s `tag: \"img\"`).\n */\nconst _defaultVideoOptions: Partial<VideoOptions> = {\n tag: \"video\",\n};\n\n/**\n * A native `<video>` surface primitive — the media twin of [`Image`](/api/component/display/classes/Image).\n *\n * Owns the typed media setters, a live playback-state read\n * ({@link Video.getMediaState}), and the native media-event bridge: because DOM\n * media events (`timeupdate`, `play`, …) do not bubble, they are wired directly\n * on the element through the DOM seam at render time and re-emitted through this\n * component's custom `on` / `off` surface. A bare video surface is independently\n * useful; the `VideoPlayer` composite layers a control bar on top of it.\n *\n * @category Components\n */\nclass Video extends Component<VideoOptions> {\n\n /** Custom-event fan-out for the re-emitted media events. */\n private _listeners: ListenerBag<VideoMediaEvent> = new ListenerBag<VideoMediaEvent>();\n\n /**\n * Per-type native handlers, held so the exact reference registered at render\n * can be removed on {@link Video.dispose}. Built once in the constructor.\n */\n private readonly _mediaHandlers: Map<VideoMediaEvent, () => void> = new Map();\n\n /**\n * Constructs a video surface.\n *\n * @param options - Optional construction options.\n * @param subclassDefaults - Per-subclass default bag layered over this\n * class's defaults; subclasses forward their `_defaultXxxOptions`\n * constant here.\n */\n constructor(options?: VideoOptions, subclassDefaults?: Partial<VideoOptions>) {\n super(options, { ..._defaultVideoOptions, ...(subclassDefaults ?? {}) });\n\n this.clearInsets();\n this.getAria().setLabel(\"Video\");\n\n this.buildMediaHandlers();\n this.applyListeners(options?.listeners);\n }\n\n /**\n * Forwards the option-backed media fields to their setters after inherited\n * Component fields cascade through `super.applyOptions`. The setters cache on\n * `_options`; DOM writes no-op until the element exists and are replayed by\n * {@link Video.init}.\n *\n * @param options - The options bag carrying the values to apply.\n *\n * @returns This component, for method chaining.\n */\n protected applyOptions(options: VideoOptions): this {\n super.applyOptions(options);\n\n if (options.src !== undefined) this.setSrc(options.src);\n if (options.poster !== undefined) this.setPoster(options.poster);\n if (options.autoplay !== undefined) this.setAutoplay(options.autoplay);\n if (options.loop !== undefined) this.setLoop(options.loop);\n if (options.preload !== undefined) this.setPreload(options.preload);\n if (options.muted !== undefined) this.setMuted(options.muted);\n if (options.volume !== undefined) this.setVolume(options.volume);\n if (options.playbackRate !== undefined) this.setPlaybackRate(options.playbackRate);\n\n return this;\n }\n\n /**\n * Returns the current media source URL.\n *\n * @returns The `src`, or `null` when unset.\n */\n getSrc(): string | null {\n return this._options.src ?? null;\n }\n\n /**\n * Sets the media source URL (writes the `src` attribute).\n *\n * @param src - The media source URL.\n *\n * @returns This component, for method chaining.\n */\n setSrc(src: string): this {\n this._options.src = src;\n this.setElementAttribute(\"src\", src);\n\n return this;\n }\n\n /**\n * Returns the poster image URL.\n *\n * @returns The `poster`, or `null` when unset.\n */\n getPoster(): string | null {\n return this._options.poster ?? null;\n }\n\n /**\n * Sets the poster image shown before playback (writes the `poster` attribute).\n *\n * @param url - The poster image URL.\n *\n * @returns This component, for method chaining.\n */\n setPoster(url: string): this {\n this._options.poster = url;\n this.setElementAttribute(\"poster\", url);\n\n return this;\n }\n\n /**\n * Whether the media autoplays once it can.\n *\n * @returns The `autoplay` state.\n */\n isAutoplay(): boolean {\n return this._options.autoplay ?? false;\n }\n\n /**\n * Sets whether the media autoplays (toggles the boolean `autoplay` attribute).\n *\n * @param value - Whether to autoplay.\n *\n * @returns This component, for method chaining.\n */\n setAutoplay(value: boolean): this {\n this._options.autoplay = value;\n this.setElementAttribute(\"autoplay\", value ? \"\" : null);\n\n return this;\n }\n\n /**\n * Whether the media loops on reaching the end.\n *\n * @returns The `loop` state.\n */\n isLoop(): boolean {\n return this._options.loop ?? false;\n }\n\n /**\n * Sets whether the media loops (toggles the boolean `loop` attribute).\n *\n * @param value - Whether to loop.\n *\n * @returns This component, for method chaining.\n */\n setLoop(value: boolean): this {\n this._options.loop = value;\n this.setElementAttribute(\"loop\", value ? \"\" : null);\n\n return this;\n }\n\n /**\n * Whether audio is muted.\n *\n * @returns The muted state.\n */\n isMuted(): boolean {\n return this._options.muted ?? false;\n }\n\n /**\n * Sets the muted state. Toggles the boolean `muted` attribute (initial-state\n * channel) and drives the live `muted` IDL property through the seam so a\n * change takes effect on an already-loaded element.\n *\n * @param value - Whether to mute audio.\n *\n * @returns This component, for method chaining.\n */\n setMuted(value: boolean): this {\n this._options.muted = value;\n this.setElementAttribute(\"muted\", value ? \"\" : null);\n\n const element = this.getElement();\n\n if (element) {\n DOM.sink.setMuted(element, value);\n }\n\n return this;\n }\n\n /**\n * Returns the preload strategy.\n *\n * @returns The `preload` value, or `null` when unset.\n */\n getPreload(): string | null {\n return this._options.preload ?? null;\n }\n\n /**\n * Sets the preload strategy (writes the `preload` attribute).\n *\n * @param value - One of `\"none\"`, `\"metadata\"`, `\"auto\"`.\n *\n * @returns This component, for method chaining.\n */\n setPreload(value: \"none\" | \"metadata\" | \"auto\"): this {\n this._options.preload = value;\n this.setElementAttribute(\"preload\", value);\n\n return this;\n }\n\n /**\n * Returns the audio volume.\n *\n * @returns The volume in `[0, 1]`.\n */\n getVolume(): number {\n return this._options.volume ?? DEFAULT_VOLUME;\n }\n\n /**\n * Sets the audio volume, clamped to `[0, 1]`. Drives the live `volume` IDL\n * property through the seam (there is no `volume` attribute).\n *\n * @param value - The desired volume; saturates outside `[0, 1]`.\n *\n * @returns This component, for method chaining.\n */\n setVolume(value: number): this {\n const clamped = Math.max(0, Math.min(1, value));\n\n this._options.volume = clamped;\n\n const element = this.getElement();\n\n if (element) {\n DOM.sink.setVolume(element, clamped);\n }\n\n return this;\n }\n\n /**\n * Returns the playback speed multiplier.\n *\n * @returns The playback rate (`1` is normal speed).\n */\n getPlaybackRate(): number {\n return this._options.playbackRate ?? DEFAULT_PLAYBACK_RATE;\n }\n\n /**\n * Sets the playback speed multiplier. Drives the live `playbackRate` IDL\n * property through the seam (there is no `playbackRate` attribute).\n *\n * @param value - The playback rate (`1` is normal speed).\n *\n * @returns This component, for method chaining.\n */\n setPlaybackRate(value: number): this {\n this._options.playbackRate = value;\n\n const element = this.getElement();\n\n if (element) {\n DOM.sink.setPlaybackRate(element, value);\n }\n\n return this;\n }\n\n /**\n * Seeks to a playback position. Runtime-only — the live playhead is not\n * consumer configuration, so it is not on {@link VideoOptions}.\n *\n * @param seconds - The target position in seconds.\n *\n * @returns This component, for method chaining.\n */\n setCurrentTime(seconds: number): this {\n const element = this.getElement();\n\n if (element) {\n DOM.sink.setCurrentTime(element, seconds);\n }\n\n return this;\n }\n\n /**\n * Starts (or resumes) playback.\n *\n * @returns This component, for method chaining.\n */\n play(): this {\n const element = this.getElement();\n\n if (element) {\n DOM.sink.mediaPlay(element);\n }\n\n return this;\n }\n\n /**\n * Pauses playback.\n *\n * @returns This component, for method chaining.\n */\n pause(): this {\n const element = this.getElement();\n\n if (element) {\n DOM.sink.mediaPause(element);\n }\n\n return this;\n }\n\n /**\n * Reads the live playback state through the DOM read seam. Before the element\n * renders, reports a paused snapshot from the cached configuration.\n *\n * @returns The current {@link MediaState}.\n */\n getMediaState(): MediaState {\n const element = this.getElement();\n\n if (element) {\n return DOM.source.getMediaState(element);\n }\n\n return {\n currentTime: 0,\n duration: 0,\n paused: true,\n ended: false,\n volume: this.getVolume(),\n muted: this.isMuted(),\n playbackRate: this.getPlaybackRate(),\n };\n }\n\n /**\n * Registers a listener for one of this surface's re-emitted media events.\n *\n * @param event - The media event name.\n * @param listener - The callback invoked when the event fires.\n *\n * @returns This component, for method chaining.\n */\n on(event: VideoMediaEvent, listener: () => void): this {\n this._listeners.add(event, listener);\n\n return this;\n }\n\n /**\n * Removes a previously registered media-event listener.\n *\n * @param event - The event the listener was registered for.\n * @param listener - The exact callback reference to remove.\n *\n * @returns This component, for method chaining.\n */\n off(event: VideoMediaEvent, listener: () => void): this {\n this._listeners.remove(event, listener);\n\n return this;\n }\n\n /**\n * Fans a media event out to its registered listeners.\n *\n * @param event - The event to emit.\n */\n protected emit(event: VideoMediaEvent): void {\n this._listeners.fire(event);\n }\n\n /**\n * Replays the cached media options onto the freshly created element and wires\n * the native, non-bubbling media listeners. The base class's\n * `setElementAttribute` now also caches and replays the attribute-backed\n * options (`src`, `poster`, `preload`, `autoplay`, `loop`, `muted`), making\n * that part of the replay redundant — kept anyway, see\n * {@link replayMediaOptions} for why this call still matters for `volume` /\n * `playbackRate` / live-property `muted`.\n *\n * @param element - The element being initialised, when provided by the caller.\n *\n * @returns This component, for method chaining.\n */\n protected init(element?: Handle): this {\n super.init(element);\n\n const el = element ?? this.getElement();\n\n if (!el) {\n return this;\n }\n\n this.replayMediaOptions(el);\n this.attachMediaListeners(el);\n\n return this;\n }\n\n /**\n * Detaches the native media listeners installed at render, then defers to\n * the base class for the rest of teardown. Call before discarding the\n * surface so no stray native listener survives.\n */\n protected destructor(): void {\n const element = this.getElement();\n\n if (element) {\n for (const [type, handler] of this._mediaHandlers) {\n DOM.sink.removeListener(element, type, handler);\n }\n }\n\n super.destructor();\n }\n\n /**\n * Builds one stable re-emit handler per media event, stored so the exact\n * reference registered at render can be removed on disposal.\n */\n private buildMediaHandlers(): void {\n for (const type of VIDEO_MEDIA_EVENTS) {\n this._mediaHandlers.set(type, () => this.emit(type));\n }\n }\n\n /**\n * Re-applies every cached media option onto the freshly-rendered element.\n *\n * @remarks The `setAttr` block below (`src`/`poster`/`preload`/`autoplay`/\n * `loop`/`muted`) is now redundant with the base class's\n * `setElementAttribute` cache, which `Component.init()` already replays\n * onto this same `element` parameter — kept here for parity with the rest\n * of this method rather than split out. `volume`, `playbackRate`, and\n * `muted` as a live IDL property have no reflecting attribute, so they are\n * never covered by that cache and this method remains the only place they\n * get applied to a freshly-created element. Writes go to the **passed**\n * `element`, never through the `getElement()`-based setters: during\n * `init()` the element has been created but `render()` stores it as the\n * component's element only *after* `init()` returns, so `getElement()`\n * (which resolves by document id) returns nothing yet. (The offline\n * modelled source resolves detached elements by id, which is why this was\n * invisible to the recording-sink tests and had to be caught live.)\n *\n * @param element - The rendered (still-detached) video element.\n */\n private replayMediaOptions(element: Handle): void {\n const options = this._options;\n const setAttr: Record<string, string> = {};\n\n if (options.src !== undefined) setAttr.src = options.src;\n if (options.poster !== undefined) setAttr.poster = options.poster;\n if (options.preload !== undefined) setAttr.preload = options.preload;\n if (options.autoplay) setAttr.autoplay = \"\";\n if (options.loop) setAttr.loop = \"\";\n if (options.muted) setAttr.muted = \"\";\n\n if (Object.keys(setAttr).length > 0) {\n DOM.sink.apply(element, { setAttr });\n }\n\n // Live IDL properties: volume/playbackRate have no reflecting attribute,\n // and muted must also be driven as a property to take effect immediately.\n if (options.muted !== undefined) DOM.sink.setMuted(element, options.muted);\n if (options.volume !== undefined) DOM.sink.setVolume(element, options.volume);\n if (options.playbackRate !== undefined) DOM.sink.setPlaybackRate(element, options.playbackRate);\n }\n\n /**\n * Registers each per-type native handler on the element through the DOM seam.\n *\n * @param element - The rendered video element.\n */\n private attachMediaListeners(element: Handle): void {\n for (const [type, handler] of this._mediaHandlers) {\n DOM.sink.addListener(element, type, handler);\n }\n }\n}\n\nconst VideoCallable = callable(Video);\ntype VideoCallable = Video;\nexport {\n Video as _Video,\n VideoCallable as Video\n};\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport type { Video } from \"~/component/display/Video.js\";\n\n/**\n * Pluggable media-loading strategy for a {@link Video} surface.\n *\n * @remarks\n * The engine seam decouples *how a source is loaded* from the {@link Video}\n * primitive and the `VideoPlayer` composite. The default {@link ProgressiveEngine}\n * simply writes the `src` attribute and lets the browser fetch a progressive\n * MP4 / WebM. A future adaptive-streaming engine (hls.js / dash.js) would attach\n * to the raw media element and drive its buffer — which needs the live\n * `HTMLVideoElement` the DOM seam deliberately hides, so that escape is a\n * documented prerequisite for streaming and out of scope here. Implement this\n * interface to add a new strategy without touching the component.\n *\n * @category Components\n */\nexport interface PlaybackEngine {\n /**\n * Loads a source into the given video surface.\n *\n * @param video - The video surface to load into.\n * @param src - The media source URL.\n */\n load(video: Video, src: string): void;\n\n /**\n * Releases any resources the engine holds (detaches a streaming instance,\n * cancels in-flight fetches). Called before a new `load` and on disposal.\n */\n destroy(): void;\n}\n\n/**\n * The default {@link PlaybackEngine}: progressive download. `load` writes the\n * `src` attribute (the browser auto-fetches a progressive MP4 / WebM); `destroy`\n * is a no-op because a progressive source holds no engine-side resources.\n *\n * @category Components\n */\nclass ProgressiveEngine implements PlaybackEngine {\n /**\n * Points the video surface at `src` by writing its `src` attribute.\n *\n * @param video - The video surface to load into.\n * @param src - The media source URL.\n */\n load(video: Video, src: string): void {\n video.setSrc(src);\n }\n\n /**\n * No-op: a progressive source holds no engine-side resources to release.\n */\n destroy(): void {\n // Progressive playback keeps no engine state; nothing to tear down.\n }\n}\n\nexport { ProgressiveEngine };\n","// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0\n\nimport { Component, ComponentOptions } from \"~/core/Component.js\";\nimport { DOM } from \"~/core/DOM.js\";\nimport type { Handle, MediaState } from \"~/core/DOM.js\";\nimport { ListenerBag } from \"~/core/ListenerBag.js\";\nimport { callable } from \"~/core/Callable.js\";\nimport type { Size } from \"~/primitive/Size.js\";\nimport { Border } from \"~/layout/Border.js\";\nimport { HBox } from \"~/layout/HBox.js\";\nimport { Placement } from \"~/primitive/Placement.js\";\nimport { Button } from \"~/component/button/Button.js\";\nimport { Slider } from \"~/component/input/Slider.js\";\nimport { Text } from \"~/component/input/Text.js\";\nimport { Glyph } from \"~/component/display/Glyph.js\";\nimport { Video } from \"~/component/display/Video.js\";\nimport { ProgressiveEngine } from \"~/component/display/PlaybackEngine.js\";\nimport type { PlaybackEngine } from \"~/component/display/PlaybackEngine.js\";\nimport { play } from \"~/glyphs/solid/play.js\";\nimport { pause } from \"~/glyphs/solid/pause.js\";\nimport { volume_high } from \"~/glyphs/solid/volume_high.js\";\nimport { volume_mute } from \"~/glyphs/solid/volume_mute.js\";\nimport { expand } from \"~/glyphs/solid/expand.js\";\nimport { compress } from \"~/glyphs/solid/compress.js\";\n\nGlyph.register(play, pause, volume_high, volume_mute, expand, compress);\n\n// Square side of each glyph-only control button, matching PaginationBar's 28px\n// navigation buttons so the control bar reads as one row of same-sized controls.\nconst CONTROL_BUTTON_SIZE = 28;\n\n// Fixed width of the volume slider. Narrower than the default 200px Slider so it\n// stays a compact secondary control beside the flexible seek scrubber.\nconst VOLUME_SLIDER_WIDTH = 80;\n\n// Height the time readout centres within, matching the control-button row height\n// so its baseline lines up with the buttons.\nconst CONTROL_ROW_HEIGHT = 28;\n\n// Volume-slider granularity — 20 steps across [0, 1] is fine enough for smooth\n// adjustment without sub-percent jitter.\nconst VOLUME_STEP = 0.05;\n\n// Gap between control-bar children, matching PaginationBar's inter-button spacing.\nconst CONTROL_SPACING = 6;\n\n/**\n * Formats a media time in seconds as `h:mm:ss` (when at least an hour) or `m:ss`.\n * A non-finite or negative input (no metadata yet, a live stream) renders as\n * `\"0:00\"`. Pure and module-level so it is trivially unit-testable.\n *\n * @param seconds - The time in seconds.\n * @returns The formatted `m:ss` / `h:mm:ss` string.\n *\n * @category Components\n */\nexport function formatMediaTime(seconds: number): string {\n if (!Number.isFinite(seconds) || seconds < 0) {\n return \"0:00\";\n }\n\n const total = Math.floor(seconds);\n const secs = total % 60;\n const mins = Math.floor(total / 60) % 60;\n const hours = Math.floor(total / 3600);\n const secsText = String(secs).padStart(2, \"0\");\n\n if (hours >= 1) {\n const minsText = String(mins).padStart(2, \"0\");\n\n return `${hours}:${minsText}:${secsText}`;\n }\n\n return `${mins}:${secsText}`;\n}\n\n/**\n * The custom events {@link VideoPlayer} emits — the player-level lifecycle\n * subset of the underlying {@link Video}'s media events.\n *\n * @category Components\n */\nexport type VideoPlayerEvent = \"play\" | \"pause\" | \"ended\";\n\n/**\n * Construction-time options for {@link VideoPlayer}.\n *\n * @category Components\n */\nexport interface VideoPlayerOptions extends ComponentOptions {\n src?: string;\n poster?: string;\n autoplay?: boolean;\n loop?: boolean;\n muted?: boolean;\n /** Audio volume, clamped to `[0, 1]`. */\n volume?: number;\n playbackRate?: number;\n /** Show the custom control bar. Default `true`. */\n controls?: boolean;\n /** Media-loading strategy. Defaults to a {@link ProgressiveEngine}. */\n engine?: PlaybackEngine;\n /**\n * Construction-time listener bag — the declarative form of `on()`.\n */\n listeners?: {\n play?: () => void;\n pause?: () => void;\n ended?: () => void;\n };\n}\n\n/**\n * A video player: a native `<video>` surface ({@link Video}) framed by a control\n * bar built entirely from this library's own components — play / pause, a seek\n * scrubber, a current-time / duration readout, volume, mute, and fullscreen. The\n * native browser `controls` chrome is not used.\n *\n * @remarks\n * The player is a [`Border`](/api/layout/classes/Border) layout with the video\n * in the centre region and the control bar docked south. All coordination — the\n * play-state machine, the scrubber⇄currentTime sync (guarded against a feedback\n * loop), volume / mute sync, fullscreen state, and media-loading engine\n * dispatch — lives here; the control bar itself is a composed `HBox` of existing\n * primitives. Source loading routes through a pluggable {@link PlaybackEngine}\n * (progressive download by default) so an adaptive-streaming engine can be\n * attached without rewriting the component.\n *\n * @example\n * ```typescript\n * import { VideoPlayer } from '@jimka/typescript-ui/component/display';\n *\n * const player = new VideoPlayer({ src: '/media/clip.mp4', poster: '/media/poster.png' });\n * player.on('ended', () => console.log('done'));\n * ```\n *\n * @category Components\n */\nclass VideoPlayer extends Component<VideoPlayerOptions> {\n\n private _video: Video;\n private _controls!: Component;\n private _playBtn!: Button;\n private _scrubber!: Slider;\n private _timeText!: Text;\n private _muteBtn!: Button;\n private _volume!: Slider;\n private _fullscreenBtn!: Button;\n\n /** Media-loading strategy; progressive download unless overridden. */\n private _engine: PlaybackEngine = new ProgressiveEngine();\n\n /** Whether the video is currently playing (driven by media events). */\n private _playing: boolean = false;\n\n /** Whether the player is currently fullscreen. */\n private _fullscreen: boolean = false;\n\n /**\n * Guards the scrubber⇄timeupdate feedback loop: while the user drags the\n * scrubber, incoming `timeupdate` syncs must not overwrite the thumb.\n */\n private _scrubbing: boolean = false;\n\n /**\n * Guards against a programmatic `setValue` on a slider re-entering its own\n * `action` / `change` handler — every {@link Slider.setValue} fires those\n * events, so a `syncFromState` write would otherwise loop back as a seek.\n */\n private _syncing: boolean = false;\n\n /** Custom-event fan-out for the player's `play` / `pause` / `ended` events. */\n private _listeners: ListenerBag<VideoPlayerEvent> = new ListenerBag<VideoPlayerEvent>();\n\n private readonly _onPlayButton: () => void = () => this.togglePlay();\n private readonly _onMuteButton: () => void = () => this.setMuted(!this.isMuted());\n private readonly _onFullscreenButton: () => void = () => this.toggleFullscreen();\n private readonly _onScrub: () => void = () => this.beginScrub();\n private readonly _onVolumeChange: (v: number) => void = (v) => this.onVolumeSlider(v);\n\n private readonly _onVideoPlay: () => void = () => this.onVideoPlay();\n private readonly _onVideoPause: () => void = () => this.onVideoPause();\n private readonly _onVideoEnded: () => void = () => this.onVideoEnded();\n private readonly _onVideoTimeUpdate: () => void = () => this.onVideoTimeUpdate();\n private readonly _onVideoSync: () => void = () => this.syncFromVideo();\n private readonly _onFullscreenChange: () => void = () => this.syncFullscreen();\n\n /**\n * Constructs a video player.\n *\n * @param options - Optional construction options.\n */\n constructor(options?: VideoPlayerOptions) {\n // Child components are built first; options are applied via applyOptions\n // at the constructor tail, after the control children exist.\n // eslint-disable-next-line local/forward-super-options\n super();\n\n this.setLayoutManager(new Border());\n this.getAria().setRole(\"region\");\n this.getAria().setLabel(\"Video player\");\n\n this._video = new Video();\n\n this.buildControlBar();\n this.wireControlListeners();\n this.wireVideoListeners();\n\n this.addComponent(this._video, { placement: Placement.CENTER });\n this.addComponent(this._controls, { placement: Placement.SOUTH });\n\n this.syncFromState(this._video.getMediaState());\n\n if (options) {\n this.applyOptions(options);\n }\n\n this.applyListeners(options?.listeners);\n }\n\n /**\n * Applies a {@link VideoPlayerOptions} bag after inherited Component fields\n * cascade. Dispatched from the constructor tail (not the `super()` cascade),\n * so the control children the setters touch already exist. The engine is set\n * before `src` so a supplied engine handles the initial load.\n *\n * @param options - The options bag carrying the values to apply.\n *\n * @returns This component, for method chaining.\n */\n protected applyOptions(options: VideoPlayerOptions): this {\n super.applyOptions(options);\n\n // Configure the video directly rather than through the UI-syncing public\n // setters: a control-bar slider write fires a DOM event that requires a\n // rendered element, so the control bar is instead synced from the video\n // at render time (see init). Construction stays JS-only.\n if (options.engine !== undefined) this._engine = options.engine;\n if (options.src !== undefined) this.setSrc(options.src);\n if (options.poster !== undefined) this._video.setPoster(options.poster);\n if (options.autoplay !== undefined) this._video.setAutoplay(options.autoplay);\n if (options.loop !== undefined) this._video.setLoop(options.loop);\n if (options.muted !== undefined) this._video.setMuted(options.muted);\n if (options.volume !== undefined) this._video.setVolume(options.volume);\n if (options.playbackRate !== undefined) this._video.setPlaybackRate(options.playbackRate);\n if (options.controls !== undefined) this.setControlsVisible(options.controls);\n\n return this;\n }\n\n /**\n * Returns the current media source URL.\n *\n * @returns The `src`, or `null` when unset.\n */\n getSrc(): string | null {\n return this._video.getSrc();\n }\n\n /**\n * Sets the media source, routing the load through the playback engine: the\n * previous engine session is destroyed, then the new source is loaded.\n *\n * @param src - The media source URL.\n *\n * @returns This component, for method chaining.\n */\n setSrc(src: string): this {\n this._engine.destroy();\n this._engine.load(this._video, src);\n\n return this;\n }\n\n /**\n * Returns the audio volume.\n *\n * @returns The volume in `[0, 1]`.\n */\n getVolume(): number {\n return this._video.getVolume();\n }\n\n /**\n * Sets the audio volume (clamped to `[0, 1]` by the video surface) and\n * refreshes the control bar.\n *\n * @param value - The desired volume.\n *\n * @returns This component, for method chaining.\n */\n setVolume(value: number): this {\n this._video.setVolume(value);\n this.syncFromVideo();\n\n return this;\n }\n\n /**\n * Whether audio is muted.\n *\n * @returns The muted state.\n */\n isMuted(): boolean {\n return this._video.isMuted();\n }\n\n /**\n * Sets the muted state and refreshes the control bar.\n *\n * @param value - Whether to mute audio.\n *\n * @returns This component, for method chaining.\n */\n setMuted(value: boolean): this {\n this._video.setMuted(value);\n this.syncFromVideo();\n\n return this;\n }\n\n /**\n * Returns the playback speed multiplier.\n *\n * @returns The playback rate (`1` is normal speed).\n */\n getPlaybackRate(): number {\n return this._video.getPlaybackRate();\n }\n\n /**\n * Sets the playback speed multiplier.\n *\n * @param value - The playback rate (`1` is normal speed).\n *\n * @returns This component, for method chaining.\n */\n setPlaybackRate(value: number): this {\n this._video.setPlaybackRate(value);\n\n return this;\n }\n\n /**\n * Seeks to a playback position.\n *\n * @param seconds - The target position in seconds.\n *\n * @returns This component, for method chaining.\n */\n setCurrentTime(seconds: number): this {\n this._video.setCurrentTime(seconds);\n\n return this;\n }\n\n /**\n * Reads the live playback position.\n *\n * @returns The current time in seconds.\n */\n getCurrentTime(): number {\n return this._video.getMediaState().currentTime;\n }\n\n /**\n * Reads the live media duration.\n *\n * @returns The duration in seconds (`NaN` before metadata loads).\n */\n getDuration(): number {\n return this._video.getMediaState().duration;\n }\n\n /**\n * Whether the video is currently playing.\n *\n * @returns `true` while playing.\n */\n isPlaying(): boolean {\n return this._playing;\n }\n\n /**\n * Starts (or resumes) playback.\n *\n * @returns This component, for method chaining.\n */\n play(): this {\n this._video.play();\n\n return this;\n }\n\n /**\n * Pauses playback.\n *\n * @returns This component, for method chaining.\n */\n pause(): this {\n this._video.pause();\n\n return this;\n }\n\n /**\n * Toggles between play and pause.\n *\n * @returns This component, for method chaining.\n */\n togglePlay(): this {\n return this._playing ? this.pause() : this.play();\n }\n\n /**\n * Whether the custom control bar is visible.\n *\n * @returns `true` when the control bar shows.\n */\n isControlsVisible(): boolean {\n return this._options.controls ?? true;\n }\n\n /**\n * Shows or hides the custom control bar.\n *\n * @param value - Whether the control bar is visible.\n *\n * @returns This component, for method chaining.\n */\n setControlsVisible(value: boolean): this {\n this._options.controls = value;\n this._controls.setVisible(value);\n\n return this;\n }\n\n /**\n * Whether the player is currently fullscreen.\n *\n * @returns `true` while fullscreen.\n */\n isFullscreen(): boolean {\n return this._fullscreen;\n }\n\n /**\n * Requests fullscreen for the player. Must be called from a user gesture\n * (e.g. the fullscreen button's action); a programmatic call is rejected by\n * the browser.\n *\n * @returns This component, for method chaining.\n */\n enterFullscreen(): this {\n const element = this.getElement();\n\n if (element) {\n DOM.sink.requestFullscreen(element);\n }\n\n return this;\n }\n\n /**\n * Exits fullscreen.\n *\n * @returns This component, for method chaining.\n */\n exitFullscreen(): this {\n DOM.sink.exitFullscreen();\n\n return this;\n }\n\n /**\n * Toggles fullscreen.\n *\n * @returns This component, for method chaining.\n */\n toggleFullscreen(): this {\n return this._fullscreen ? this.exitFullscreen() : this.enterFullscreen();\n }\n\n /**\n * Registers a listener for one of the player's lifecycle events.\n *\n * @param event - The event name.\n * @param listener - The callback invoked when the event fires.\n *\n * @returns This component, for method chaining.\n */\n on(event: VideoPlayerEvent, listener: () => void): this {\n this._listeners.add(event, listener);\n\n return this;\n }\n\n /**\n * Removes a previously registered lifecycle listener.\n *\n * @param event - The event the listener was registered for.\n * @param listener - The exact callback reference to remove.\n *\n * @returns This component, for method chaining.\n */\n off(event: VideoPlayerEvent, listener: () => void): this {\n this._listeners.remove(event, listener);\n\n return this;\n }\n\n /**\n * Fans a lifecycle event out to its registered listeners.\n *\n * @param event - The event to emit.\n */\n protected emit(event: VideoPlayerEvent): void {\n this._listeners.fire(event);\n }\n\n /**\n * Wires the `fullscreenchange` listener on the root element (it bubbles to\n * `document`, unlike the non-bubbling media events) so external Esc-key or\n * browser-driven fullscreen exits keep the player's state in sync.\n *\n * @param element - The element being initialised, when provided by the caller.\n *\n * @returns This component, for method chaining.\n */\n protected init(element?: Handle): this {\n super.init(element);\n\n const el = element ?? this.getElement();\n\n if (!el) {\n return this;\n }\n\n DOM.sink.addListener(el, \"fullscreenchange\", this._onFullscreenChange);\n\n // The video and its control sliders are now rendered, so populating the\n // control bar from the configured media state can safely fire the sliders'\n // value-change events (which a pre-render write cannot).\n this.syncFromVideo();\n\n return this;\n }\n\n /**\n * Detaches the native + video listeners installed by this player, then\n * defers to the base class for the rest of teardown. Call before\n * discarding it so no stray native listener survives.\n */\n protected destructor(): void {\n // `_video` is registered via `addComponent`, so `super.destructor()`'s\n // child recursion below already disposes it — an explicit call here\n // would run `Video.destructor()` a second time.\n const element = this.getElement();\n\n if (element) {\n DOM.sink.removeListener(element, \"fullscreenchange\", this._onFullscreenChange);\n }\n\n super.destructor();\n }\n\n /**\n * Maps a {@link MediaState} snapshot onto the control-bar controls: the\n * scrubber range + position (skipped while the user is scrubbing), the time\n * readout, the play / mute glyphs, and the volume slider. Pure with respect\n * to media state — every media-event handler and the unit tests call it.\n *\n * @param state - The media-state snapshot to reflect.\n */\n private syncFromState(state: MediaState): void {\n this._syncing = true;\n\n try {\n if (!this._scrubbing) {\n this._scrubber.setMax(state.duration || 0);\n this._scrubber.setValue(state.currentTime);\n }\n\n this._timeText.setText(`${formatMediaTime(state.currentTime)} / ${formatMediaTime(state.duration)}`);\n this._playBtn.setGlyph(state.paused ? \"play\" : \"pause\");\n this._muteBtn.setGlyph(state.muted || state.volume === 0 ? \"volume-mute\" : \"volume-high\");\n this._volume.setValue(state.muted ? 0 : state.volume);\n } finally {\n this._syncing = false;\n }\n }\n\n /** Reads the video's live state and reflects it onto the control bar. */\n private syncFromVideo(): void {\n this.syncFromState(this._video.getMediaState());\n }\n\n /** Builds the south-docked control bar as an `HBox` of existing primitives. */\n private buildControlBar(): void {\n this._playBtn = this.makeControlButton(\"play\", \"Play\");\n this._muteBtn = this.makeControlButton(\"volume-high\", \"Mute\");\n this._fullscreenBtn = this.makeControlButton(\"expand\", \"Fullscreen\");\n\n this._scrubber = new Slider({ min: 0, max: 0, value: 0 });\n this._scrubber.getAria().setLabel(\"Seek\");\n\n this._volume = new Slider({ min: 0, max: 1, step: VOLUME_STEP, value: 1 });\n this._volume.getAria().setLabel(\"Volume\");\n this._volume.setPreferredSize({ width: VOLUME_SLIDER_WIDTH, height: this._volume.getPreferredSize()!.height });\n this._volume.setMaxSize({ width: VOLUME_SLIDER_WIDTH, height: this._volume.getPreferredSize()!.height });\n\n this._timeText = new Text(\"0:00 / 0:00\");\n this._timeText.centerInHeight(CONTROL_ROW_HEIGHT);\n\n const bar = new HBox();\n\n bar.setComponentSpacing(CONTROL_SPACING);\n\n this._controls = new Component();\n this._controls.setLayoutManager(bar);\n // Give the control bar an opaque, theme-tracking surface. Transparent, it\n // borrowed the page background in-page (readable) but showed the black\n // :fullscreen backdrop in fullscreen, hiding the dark time text and glyphs.\n // The body background is what already showed through in-page, so this is\n // invisible there yet keeps the controls readable over the fullscreen video.\n this._controls.setBackgroundColor(\"var(--ts-ui-body-bg, rgb(255, 255, 255))\");\n this._controls.addComponent(this._playBtn);\n this._controls.addComponent(this._scrubber, { weight: 1 });\n this._controls.addComponent(this._timeText);\n this._controls.addComponent(this._muteBtn);\n this._controls.addComponent(this._volume);\n this._controls.addComponent(this._fullscreenBtn);\n }\n\n /**\n * Builds a fixed-size glyph-only control button with an accessible label.\n *\n * @param glyph - The glyph name to show.\n * @param label - The accessible name (drives `aria-label` and the tooltip).\n * @returns The configured button.\n */\n private makeControlButton(glyph: string, label: string): Button {\n const button = new Button({ glyph, text: label, showText: false });\n\n button.setPreferredSize({ width: CONTROL_BUTTON_SIZE, height: CONTROL_BUTTON_SIZE });\n\n return button;\n }\n\n /** Wires the control-bar children's interaction events to the player. */\n private wireControlListeners(): void {\n this._playBtn.on(\"action\", this._onPlayButton);\n this._muteBtn.on(\"action\", this._onMuteButton);\n this._fullscreenBtn.on(\"action\", this._onFullscreenButton);\n this._scrubber.on(\"action\", this._onScrub);\n this._volume.on(\"change\", this._onVolumeChange);\n }\n\n /** Subscribes to the video surface's re-emitted media events. */\n private wireVideoListeners(): void {\n this._video.on(\"play\", this._onVideoPlay);\n this._video.on(\"pause\", this._onVideoPause);\n this._video.on(\"ended\", this._onVideoEnded);\n this._video.on(\"timeupdate\", this._onVideoTimeUpdate);\n this._video.on(\"durationchange\", this._onVideoSync);\n this._video.on(\"loadedmetadata\", this._onVideoSync);\n this._video.on(\"volumechange\", this._onVideoSync);\n this._video.on(\"ratechange\", this._onVideoSync);\n }\n\n /** Marks the scrubber as being dragged and seeks the video to its value. */\n private beginScrub(): void {\n if (this._syncing) {\n return;\n }\n\n this._scrubbing = true;\n this._video.setCurrentTime(this._scrubber.getValue());\n }\n\n /**\n * Handles a volume-slider change: applies the volume to the video (ignoring\n * the programmatic echo from a sync write).\n *\n * @param value - The new slider value in `[0, 1]`.\n */\n private onVolumeSlider(value: number): void {\n if (this._syncing) {\n return;\n }\n\n this._video.setVolume(value);\n this.syncFromVideo();\n }\n\n /** Media `play`: records the play state, refreshes, and re-emits. */\n private onVideoPlay(): void {\n this._playing = true;\n this.syncFromVideo();\n this.emit(\"play\");\n }\n\n /** Media `pause`: records the pause state, refreshes, and re-emits. */\n private onVideoPause(): void {\n this._playing = false;\n this.syncFromVideo();\n this.emit(\"pause\");\n }\n\n /** Media `ended`: records the stopped state, refreshes, and re-emits. */\n private onVideoEnded(): void {\n this._playing = false;\n this.syncFromVideo();\n this.emit(\"ended\");\n }\n\n /** Media `timeupdate`: refreshes, then releases the scrubbing guard. */\n private onVideoTimeUpdate(): void {\n this.syncFromVideo();\n this._scrubbing = false;\n }\n\n /**\n * Reconciles the player with the document's fullscreen state: relayouts to\n * fill (or shrink back from) the fullscreen viewport and swaps the fullscreen\n * glyph. Driven by the `fullscreenchange` listener, so a browser-initiated\n * exit (Esc) is handled the same as the button.\n *\n * The browser's `:fullscreen` UA rules blow the root element up to fill the\n * screen, but the absolute layout keeps sizing the children for the\n * pre-fullscreen bounds — leaving the video in the corner over a black\n * backdrop. Resizing the player to the viewport re-runs the `Border` layout\n * so the video stretches to fill and the controls stay docked at the bottom;\n * the saved in-page bounds are restored on exit.\n */\n private syncFullscreen(): void {\n const fullscreen = DOM.source.getFullscreenElement();\n const element = this.getElement();\n\n this._fullscreen = element != null && fullscreen === element;\n this._fullscreenBtn.setGlyph(this._fullscreen ? \"compress\" : \"expand\");\n\n // Re-lay the children against the now fullscreen-aware inner size (see\n // getInnerSize): entering stretches the video to fill the viewport,\n // exiting reverts it to the in-page size.\n this.doLayout();\n }\n\n /**\n * Reports the content area a layout manager fills. While fullscreen the\n * browser's `:fullscreen` UA rules blow the root element up to the viewport,\n * but its parent-committed box stays at the in-page size — so the `Border`\n * layout would keep positioning the video and controls for the small box,\n * leaving the video in a corner over a black backdrop. Returning the viewport\n * extent here makes every layout pass (including parent-driven relayouts that\n * re-commit the in-page box) stretch the children to fill the screen; exiting\n * fullscreen falls back to the inherited inner size.\n *\n * @returns The inner content size, or `null` before the element renders.\n */\n getInnerSize(): Size | null {\n if (this._fullscreen && this.getElement()) {\n const viewport = DOM.source.getViewportSize();\n const perimeter = this.getPerimeterSize();\n\n return {\n width: viewport.width - perimeter.left - perimeter.right,\n height: viewport.height - perimeter.top - perimeter.bottom,\n };\n }\n\n return super.getInnerSize();\n }\n}\n\nconst VideoPlayerCallable = callable(VideoPlayer);\ntype VideoPlayerCallable = VideoPlayer;\nexport {\n VideoPlayer as _VideoPlayer,\n VideoPlayerCallable as VideoPlayer\n};\n"],"mappings":"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"}
|