import { css, html } from "lit"; import { property, state } from "lit/decorators.js"; import { BaseElement } from "../../core/base-element.ts"; import type { Cell } from "@commontools/runner"; /** * CTCellContext - Wraps page regions and associates them with a Cell * * Provides a debugging toolbar that appears when holding Alt and hovering. * The toolbar allows inspecting cell values and addresses. * * @element ct-cell-context * * @property {Cell} cell - The Cell reference to associate with this context * @property {string} label - Optional label for display in the toolbar * * @slot - Default slot for wrapped content * * @example * *
Content here
*
*/ export class CTCellContext extends BaseElement { static override styles = [ BaseElement.baseStyles, css` :host { display: block; position: relative; flex: 1; min-height: 0; } :host([inline]) { display: inline-block; flex: none; } .container { height: 100%; box-sizing: border-box; border: 1px dashed transparent; transition: border-color 0.2s ease; } .container.alt-held { border-color: rgba(128, 128, 128, 0.25); } .container.alt-held:hover { border-color: rgba(128, 128, 128, 0.75); } .toolbar { position: absolute; top: 0; right: 0; z-index: 1000; display: flex; border: 1px solid #000; border-radius: 0; background: rgba(255, 255, 255, 0.95); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.75rem; overflow: hidden; } .toolbar.hidden { display: none; } .toolbar button { border: none; border-right: 1px solid #000; border-radius: 0; padding: 0.25rem 0.5rem; background: transparent; cursor: pointer; font-family: inherit; font-size: inherit; color: #000; } .toolbar button:last-child { border-right: none; } .toolbar button:hover { background: rgba(0, 0, 0, 0.05); } .toolbar button:active { background: rgba(0, 0, 0, 0.1); } .toolbar button.watching { background: #000; color: #fff; } .toolbar button.watching:hover { background: #333; color: #fff; } .toolbar .label { padding: 0.25rem 0.5rem; border-right: 1px solid #000; font-weight: 500; color: #666; } `, ]; @property({ attribute: false }) cell?: Cell; @property({ type: String }) label?: string; @property({ type: Boolean, reflect: true }) inline?: boolean; @state() private _modifierHeld: boolean = false; @state() private _isHovered: boolean = false; @state() private _isWatching: boolean = false; @state() private _updateCount: number = 0; private _boundHandleKeyDown = this._handleKeyDown.bind(this); private _boundHandleKeyUp = this._handleKeyUp.bind(this); private _watchUnsubscribe?: () => void; override connectedCallback() { super.connectedCallback(); // Listen for Alt key at document level document.addEventListener("keydown", this._boundHandleKeyDown); document.addEventListener("keyup", this._boundHandleKeyUp); } override disconnectedCallback() { super.disconnectedCallback(); // Clean up document-level listeners document.removeEventListener("keydown", this._boundHandleKeyDown); document.removeEventListener("keyup", this._boundHandleKeyUp); // Clean up watch subscription if active if (this._watchUnsubscribe) { this._watchUnsubscribe(); this._watchUnsubscribe = undefined; } } private _handleKeyDown(e: KeyboardEvent) { if (e.key === "Alt") { this._modifierHeld = true; } } private _handleKeyUp(e: KeyboardEvent) { if (e.key === "Alt") { this._modifierHeld = false; } } private _handleMouseEnter() { this._isHovered = true; } private _handleMouseLeave() { this._isHovered = false; } private _handleValClick() { if (!this.cell) { console.log("[ct-cell-context] No cell available"); return; } // Set window.$cell for easy console access (like Chrome's $0 for elements) (globalThis as unknown as { $cell: Cell }).$cell = this.cell; console.log("$cell =", this.cell, "→", this.cell.get()); } private _handleIdClick() { if (!this.cell) { console.log("[ct-cell-context] No cell available"); return; } console.log( "[ct-cell-context] Cell address:", this.cell.getAsNormalizedFullLink(), ); } private _handleWatchClick() { if (!this.cell) { console.log("[ct-cell-context] No cell available"); return; } const identifier = this._getCellIdentifier(); if (this._isWatching) { // Unwatch if (this._watchUnsubscribe) { this._watchUnsubscribe(); this._watchUnsubscribe = undefined; } this._isWatching = false; this._updateCount = 0; console.log(`[ct-cell-context] Stopped watching: ${identifier}`); // Emit event for debugger integration this.emit("ct-cell-unwatch", { cell: this.cell, label: this.label }); } else { // Watch this._updateCount = 0; this._watchUnsubscribe = this.cell.sink((value) => { this._updateCount++; console.log( `[ct-cell-context] Cell update #${this._updateCount}:`, value, ); }); this._isWatching = true; console.log(`[ct-cell-context] Started watching: ${identifier}`); // Emit event for debugger integration this.emit("ct-cell-watch", { cell: this.cell, label: this.label }); } } private _getCellIdentifier(): string { if (!this.cell) return "unknown"; if (this.label) return this.label; // Create short ID like ct-cell-link does const link = this.cell.getAsNormalizedFullLink(); const id = link.id; const shortId = id.split(":").pop()?.slice(-6) ?? "???"; return `#${shortId}`; } private get _shouldShowToolbar(): boolean { return this._modifierHeld && this._isHovered; } override render() { return html`
${this.label ? html`
${this.label}
` : ""}
`; } } globalThis.customElements.define("ct-cell-context", CTCellContext); declare global { interface HTMLElementTagNameMap { "ct-cell-context": CTCellContext; } }