@jimka/typescript-ui 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -18
- package/dist/lib/AbstractBooleanInput-C6CidMMX.js +2 -0
- package/dist/lib/AbstractBooleanInput-C6CidMMX.js.map +1 -0
- package/dist/lib/AbstractInput-CfjW_HM2.js +2 -0
- package/dist/lib/AbstractInput-CfjW_HM2.js.map +1 -0
- package/dist/lib/AnimatedDropdown-BFz-DO7R.js +2 -0
- package/dist/lib/AnimatedDropdown-BFz-DO7R.js.map +1 -0
- package/dist/lib/Border-5Ki2DCWz.js +2 -0
- package/dist/lib/Border-5Ki2DCWz.js.map +1 -0
- package/dist/lib/BoxLayout-8ryMD4j5.js +2 -0
- package/dist/lib/BoxLayout-8ryMD4j5.js.map +1 -0
- package/dist/lib/Button-Cdq2ySPL.js +2 -0
- package/dist/lib/Button-Cdq2ySPL.js.map +1 -0
- package/dist/lib/Card-AKIUAWYS.js +2 -0
- package/dist/lib/Card-AKIUAWYS.js.map +1 -0
- package/dist/lib/ComboBox-e_YVsWBr.js +2 -0
- package/dist/lib/ComboBox-e_YVsWBr.js.map +1 -0
- package/dist/lib/Component-Bs6TvaGZ.js +2 -0
- package/dist/lib/Component-Bs6TvaGZ.js.map +1 -0
- package/dist/lib/Container-CzyZeMyZ.js +2 -0
- package/dist/lib/Container-CzyZeMyZ.js.map +1 -0
- package/dist/lib/DOM-DeF_dWe2.js +2 -0
- package/dist/lib/DOM-DeF_dWe2.js.map +1 -0
- package/dist/lib/DragManager-c6M_mE3e.js +2 -0
- package/dist/lib/DragManager-c6M_mE3e.js.map +1 -0
- package/dist/lib/FieldDecorator-Bl0h9UEF.js +2 -0
- package/dist/lib/FieldDecorator-Bl0h9UEF.js.map +1 -0
- package/dist/lib/Fit-CUS1JXy3.js +2 -0
- package/dist/lib/Fit-CUS1JXy3.js.map +1 -0
- package/dist/lib/Glyph-6Ki3A1_a.js +2 -0
- package/dist/lib/Glyph-6Ki3A1_a.js.map +1 -0
- package/dist/lib/Grid-D8luwRPI.js +2 -0
- package/dist/lib/Grid-D8luwRPI.js.map +1 -0
- package/dist/lib/GridConstraints-CzC0w4i-.js +2 -0
- package/dist/lib/GridConstraints-CzC0w4i-.js.map +1 -0
- package/dist/lib/HBox-BWMf9h6r.js +2 -0
- package/dist/lib/HBox-BWMf9h6r.js.map +1 -0
- package/dist/lib/Header-CQjomjo0.js +2 -0
- package/dist/lib/Header-CQjomjo0.js.map +1 -0
- package/dist/lib/IconText-BXJK0MOq.js +2 -0
- package/dist/lib/IconText-BXJK0MOq.js.map +1 -0
- package/dist/lib/LayoutSerialization-C_zcwaN3.js +2 -0
- package/dist/lib/LayoutSerialization-C_zcwaN3.js.map +1 -0
- package/dist/lib/LayoutSizes-XdhpAOmc.js +2 -0
- package/dist/lib/LayoutSizes-XdhpAOmc.js.map +1 -0
- package/dist/lib/List-BgxnLWnl.js +2 -0
- package/dist/lib/List-BgxnLWnl.js.map +1 -0
- package/dist/lib/Menu-BdnwRY0M.js +2 -0
- package/dist/lib/Menu-BdnwRY0M.js.map +1 -0
- package/dist/lib/MenuButton-D15cSbMf.js +2 -0
- package/dist/lib/MenuButton-D15cSbMf.js.map +1 -0
- package/dist/lib/Panel-DApa7vA6.js +2 -0
- package/dist/lib/Panel-DApa7vA6.js.map +1 -0
- package/dist/lib/Position-DJKB0N3l.js +2 -0
- package/dist/lib/Position-DJKB0N3l.js.map +1 -0
- package/dist/lib/ProgressSpinner-CNYcsBhw.js +2 -0
- package/dist/lib/ProgressSpinner-CNYcsBhw.js.map +1 -0
- package/dist/lib/RadioButton-CepkneSA.js +2 -0
- package/dist/lib/RadioButton-CepkneSA.js.map +1 -0
- package/dist/lib/RovingTabIndex-D-zOuptw.js +2 -0
- package/dist/lib/RovingTabIndex-D-zOuptw.js.map +1 -0
- package/dist/lib/Scrollbar-DQdCdj94.js +2 -0
- package/dist/lib/Scrollbar-DQdCdj94.js.map +1 -0
- package/dist/lib/Slider-DvDdVvRm.js +2 -0
- package/dist/lib/Slider-DvDdVvRm.js.map +1 -0
- package/dist/lib/Spacer-BqiPiCqz.js +2 -0
- package/dist/lib/Spacer-BqiPiCqz.js.map +1 -0
- package/dist/lib/TabButton-DqYDfN7P.js +2 -0
- package/dist/lib/TabButton-DqYDfN7P.js.map +1 -0
- package/dist/lib/Text-DJoUHukb.js +2 -0
- package/dist/lib/Text-DJoUHukb.js.map +1 -0
- package/dist/lib/Tooltip-5BR7yYEX.js +3 -0
- package/dist/lib/Tooltip-5BR7yYEX.js.map +1 -0
- package/dist/lib/VBox-CdV9VGxR.js +2 -0
- package/dist/lib/VBox-CdV9VGxR.js.map +1 -0
- package/dist/lib/VirtualRowView-C18j90n5.js +2 -0
- package/dist/lib/VirtualRowView-C18j90n5.js.map +1 -0
- package/dist/lib/VirtualScroller-BKMvTisA.js +2 -0
- package/dist/lib/VirtualScroller-BKMvTisA.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/chart.es.js.map +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 +3 -1
- 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 +2 -2
- package/dist/lib/component/table.es.js.map +1 -1
- package/dist/lib/component/tree.es.js +1 -1
- package/dist/lib/component/tree.es.js.map +1 -1
- package/dist/lib/core.es.js +1 -1
- package/dist/lib/core.es.js.map +1 -1
- package/dist/lib/layout.es.js +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 +2 -0
- package/dist/lib/router.es.js.map +1 -0
- package/dist/lib/types/component/container/SplitGutter.d.ts +1 -1
- package/dist/lib/types/component/container/WindowBorder.d.ts +1 -1
- package/dist/lib/types/component/display/Markdown.d.ts +10 -0
- package/dist/lib/types/component/display/index.d.ts +1 -1
- package/dist/lib/types/component/editor/MarkdownEditor.d.ts +6 -0
- package/dist/lib/types/component/editor/markdownTableTransformer.d.ts +2 -0
- package/dist/lib/types/core/DOM.d.ts +6 -0
- package/dist/lib/types/overlay/AbstractWindow.d.ts +1 -1
- package/dist/lib/types/router/RoutePattern.d.ts +18 -0
- package/dist/lib/types/router/Router.d.ts +36 -0
- package/dist/lib/types/router/index.d.ts +2 -0
- package/dist/lib/validation.es.js +1 -1
- package/llms.txt +8 -4
- package/package.json +6 -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/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 *\n * @category Components\n */\nexport type CanvasDrawCallback = (\n ctx: CanvasRenderingContext2D,\n width: number,\n height: 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/**\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};\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 /** 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 */\n constructor(options?: CanvasOptions) {\n super(options, _defaultCanvasOptions);\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 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 ?? 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._options.onDraw?.(ctx, width, height);\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.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 * 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 ?? 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 = (): void => {\n if (!this._animationRequested) {\n this._rafId = null;\n return;\n }\n\n this.redraw();\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) => 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 /** Per-frame draw hook. */\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/**\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};\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 /** 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 */\n constructor(options?: WebGLCanvasOptions) {\n super(options, _defaultWebGLCanvasOptions);\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 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 ?? 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 ?? 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.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 ?? false;\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 = (): void => {\n if (!this._animationRequested) {\n this._rafId = null;\n return;\n }\n\n this.renderFrame();\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._options.onContextInit?.(gl);\n this._contextInitialised = true;\n }\n\n this._options.onFrame?.(gl, this.getWidth(), this.getHeight());\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 constructor(src: String, options?: ImageOptions) {\n super(options, _defaultImageOptions);\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 { 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\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\";\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\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 },\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 },\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\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/**\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>`) through the DOM sink. There is no HTML-string\n * assignment path, so untrusted Markdown can never inject markup, and the\n * 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, and links. Any other token\n * type (tables, images, raw HTML, GFM extensions) falls through to a defined\n * fallback that renders the token's plain text — never a crash, never markup.\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 /**\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 metrics (font, spacing) are theme-bound, so a theme swap can\n // change the rendered height — re-measure when it fires (mirrors Text).\n this._unsubscribeTheme = ThemeManager.onThemeChange(() => this.measureContentHeight());\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 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 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));\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.measureContentHeight();\n }\n\n return this;\n }\n\n /**\n * Detaches the theme-change listener. Call when a dynamically-built Markdown\n * is permanently removed from the page, mirroring `Text.dispose`.\n */\n dispose(): void {\n this._unsubscribeTheme();\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 // 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 flush also commits the buffered width so the read reflects\n // the assigned width (the commitBounds/stale-DOM gotcha). The raw style\n // write is 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()));\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\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 */\n private appendBlockTokens(parent: Handle, tokens: Token[]): void {\n for (const token of tokens) {\n this.appendBlockToken(parent, token);\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 */\n private appendBlockToken(parent: Handle, token: Token): void {\n switch (token.type) {\n case \"heading\": this.appendHeading(parent, token as Tokens.Heading); break;\n case \"paragraph\": this.appendParagraph(parent, token as Tokens.Paragraph); break;\n case \"list\": this.appendList(parent, token as Tokens.List); break;\n case \"blockquote\": this.appendBlockquote(parent, token as Tokens.Blockquote); break;\n case \"code\": this.appendCode(parent, token as Tokens.Code); 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.\n *\n * @param parent - The element handle to append into.\n * @param token - The heading token.\n */\n private appendHeading(parent: Handle, token: Tokens.Heading): void {\n const depth = Math.min(Math.max(token.depth, HEADING_MIN_DEPTH), HEADING_MAX_DEPTH);\n const heading = this.create(\"h\" + depth);\n\n DOM.sink.apply(heading, { addClass: [HEADING_CLASS] });\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 */\n private appendList(parent: Handle, token: Tokens.List): 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);\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 */\n private appendListItem(list: Handle, item: Tokens.ListItem): 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);\n }\n }\n\n DOM.sink.appendChild(list, listItem);\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 */\n private appendBlockquote(parent: Handle, token: Tokens.Blockquote): void {\n const quote = this.create(\"blockquote\");\n\n DOM.sink.apply(quote, { addClass: [QUOTE_CLASS] });\n this.appendBlockTokens(quote, token.tokens);\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).\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 DOM.sink.appendChild(parent, pre);\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>` with a safe `target`/`rel` and the link's inline text.\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\n DOM.sink.apply(anchor, {\n addClass: [LINK_CLASS],\n setAttr: { href: token.href, target: \"_blank\", rel: \"noopener noreferrer\" },\n });\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\nconst MarkdownCallable = callable(Markdown);\ntype MarkdownCallable = Markdown;\nexport {\n Markdown as _Markdown,\n MarkdownCallable as Markdown,\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 */\n constructor(glyph: string, text: string, forId: string, options?: IconLabelOptions) {\n super(options, _defaultIconLabelOptions);\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 const inner = this.getInnerSize();\n if (!inner) {\n super.doLayout();\n return this;\n }\n\n this._track.setX(0);\n this._track.setY(0);\n this._track.setSize({ width: inner.width, height: inner.height });\n\n if (this._indeterminate) {\n const segment = Math.max(20, Math.round(inner.width * 0.25));\n this._fill.setX(0);\n this._fill.setY(0);\n this._fill.setSize({ width: segment, height: inner.height });\n } else {\n const fillWidth = Math.round(inner.width * this._value / 100);\n this._fill.setX(0);\n this._fill.setY(0);\n this._fill.setSize({ width: fillWidth, height: inner.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(28, 28);\n this._prevBtn.setPreferredSize(28, 28);\n this._nextBtn.setPreferredSize(28, 28);\n this._lastBtn.setPreferredSize(28, 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.\n *\n * @remarks\n * Call this when permanently removing the bar (e.g. before discarding it,\n * 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 dispose(): 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\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 */\n constructor(options?: VideoOptions) {\n super(options, _defaultVideoOptions);\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. Setter DOM writes no-op before\n * the element exists, so the constructor-time configuration is applied here.\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. Call before\n * discarding the surface so no stray native listener survives.\n */\n dispose(): void {\n const element = this.getElement();\n\n if (!element) {\n return;\n }\n\n for (const [type, handler] of this._mediaHandlers) {\n DOM.sink.removeListener(element, type, handler);\n }\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 so\n * the DOM writes that no-op'd during detached construction take effect.\n *\n * @remarks Writes go to the **passed** `element`, never through the\n * `getElement()`-based setters. During `init()` the element has been created\n * but is not yet attached to the document, and `render()` stores it as the\n * component's element only *after* `init()` returns — so `getElement()`\n * (which resolves by document id) returns nothing in a live DOM and every\n * setter write would silently no-op, dropping `src` and the rest. Writing to\n * the passed element mirrors the base `init()` replay of cached attributes\n * and aria. (The offline modelled source resolves detached elements by id,\n * which is why this was invisible to the recording-sink tests and had to be\n * 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. Call before\n * discarding it so no stray native listener survives.\n */\n dispose(): void {\n this._video.dispose();\n\n const element = this.getElement();\n\n if (element) {\n DOM.sink.removeListener(element, \"fullscreenchange\", this._onFullscreenChange);\n }\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(VOLUME_SLIDER_WIDTH, this._volume.getPreferredSize()!.height);\n this._volume.setMaxSize(VOLUME_SLIDER_WIDTH, 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(CONTROL_BUTTON_SIZE, 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/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 *\n * @category Components\n */\nexport type CanvasDrawCallback = (\n ctx: CanvasRenderingContext2D,\n width: number,\n height: 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/**\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};\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 /** 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 */\n constructor(options?: CanvasOptions) {\n super(options, _defaultCanvasOptions);\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 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 ?? 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._options.onDraw?.(ctx, width, height);\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.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 * 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 ?? 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 = (): void => {\n if (!this._animationRequested) {\n this._rafId = null;\n return;\n }\n\n this.redraw();\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) => 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 /** Per-frame draw hook. */\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/**\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};\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 /** 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 */\n constructor(options?: WebGLCanvasOptions) {\n super(options, _defaultWebGLCanvasOptions);\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 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 ?? 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 ?? 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.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 ?? false;\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 = (): void => {\n if (!this._animationRequested) {\n this._rafId = null;\n return;\n }\n\n this.renderFrame();\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._options.onContextInit?.(gl);\n this._contextInitialised = true;\n }\n\n this._options.onFrame?.(gl, this.getWidth(), this.getHeight());\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 constructor(src: String, options?: ImageOptions) {\n super(options, _defaultImageOptions);\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 { 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\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// 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\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 },\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 },\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\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 * 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/**\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 * 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 /**\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 metrics (font, spacing) are theme-bound, so a theme swap can\n // change the rendered height — re-measure when it fires (mirrors Text).\n this._unsubscribeTheme = ThemeManager.onThemeChange(() => this.measureContentHeight());\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 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 * 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.measureContentHeight();\n }\n\n return this;\n }\n\n /**\n * Detaches the theme-change listener. Call when a dynamically-built Markdown\n * is permanently removed from the page, mirroring `Text.dispose`.\n */\n dispose(): void {\n this._unsubscribeTheme();\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 // 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 flush also commits the buffered width so the read reflects\n // the assigned width (the commitBounds/stale-DOM gotcha). The raw style\n // write is 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\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 slug = slugify(token.text);\n const seen = headingIds.get(slug) ?? 0;\n\n headingIds.set(slug, seen + 1);\n\n const id = seen === 0 ? slug : `${slug}-${seen}`;\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).\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 DOM.sink.appendChild(parent, pre);\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\nconst MarkdownCallable = callable(Markdown);\ntype MarkdownCallable = Markdown;\nexport {\n Markdown as _Markdown,\n MarkdownCallable as Markdown,\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 */\n constructor(glyph: string, text: string, forId: string, options?: IconLabelOptions) {\n super(options, _defaultIconLabelOptions);\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 const inner = this.getInnerSize();\n if (!inner) {\n super.doLayout();\n return this;\n }\n\n this._track.setX(0);\n this._track.setY(0);\n this._track.setSize({ width: inner.width, height: inner.height });\n\n if (this._indeterminate) {\n const segment = Math.max(20, Math.round(inner.width * 0.25));\n this._fill.setX(0);\n this._fill.setY(0);\n this._fill.setSize({ width: segment, height: inner.height });\n } else {\n const fillWidth = Math.round(inner.width * this._value / 100);\n this._fill.setX(0);\n this._fill.setY(0);\n this._fill.setSize({ width: fillWidth, height: inner.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(28, 28);\n this._prevBtn.setPreferredSize(28, 28);\n this._nextBtn.setPreferredSize(28, 28);\n this._lastBtn.setPreferredSize(28, 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.\n *\n * @remarks\n * Call this when permanently removing the bar (e.g. before discarding it,\n * 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 dispose(): 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\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 */\n constructor(options?: VideoOptions) {\n super(options, _defaultVideoOptions);\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. Setter DOM writes no-op before\n * the element exists, so the constructor-time configuration is applied here.\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. Call before\n * discarding the surface so no stray native listener survives.\n */\n dispose(): void {\n const element = this.getElement();\n\n if (!element) {\n return;\n }\n\n for (const [type, handler] of this._mediaHandlers) {\n DOM.sink.removeListener(element, type, handler);\n }\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 so\n * the DOM writes that no-op'd during detached construction take effect.\n *\n * @remarks Writes go to the **passed** `element`, never through the\n * `getElement()`-based setters. During `init()` the element has been created\n * but is not yet attached to the document, and `render()` stores it as the\n * component's element only *after* `init()` returns — so `getElement()`\n * (which resolves by document id) returns nothing in a live DOM and every\n * setter write would silently no-op, dropping `src` and the rest. Writing to\n * the passed element mirrors the base `init()` replay of cached attributes\n * and aria. (The offline modelled source resolves detached elements by id,\n * which is why this was invisible to the recording-sink tests and had to be\n * 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. Call before\n * discarding it so no stray native listener survives.\n */\n dispose(): void {\n this._video.dispose();\n\n const element = this.getElement();\n\n if (element) {\n DOM.sink.removeListener(element, \"fullscreenchange\", this._onFullscreenChange);\n }\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(VOLUME_SLIDER_WIDTH, this._volume.getPreferredSize()!.height);\n this._volume.setMaxSize(VOLUME_SLIDER_WIDTH, 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(CONTROL_BUTTON_SIZE, 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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"}
|