import { css, html, PropertyValues } from "lit";
import { property, state } from "lit/decorators.js";
import { consume } from "@lit/context";
import { BaseElement } from "../../core/base-element.ts";
import "../ct-chip/ct-chip.ts";
import type { Cell, MemorySpace, Runtime } from "@commontools/runner";
import { NAME } from "@commontools/runner";
import { parseLLMFriendlyLink } from "@commontools/runner";
import { runtimeContext, spaceContext } from "../../runtime-context.ts";
/**
* CTCellLink - Renders a link or cell as a clickable pill
*
* @element ct-cell-link
*
* @property {string} link - The serialized path to a cell (e.g. /of:bafy.../path)
* @property {Cell} cell - The live Cell reference
*
* @example
*
*
*/
export class CTCellLink extends BaseElement {
static override styles = [
BaseElement.baseStyles,
css`
:host {
display: inline-block;
vertical-align: middle;
}
ct-chip {
cursor: pointer;
max-width: 100%;
}
`,
];
@property({ type: String })
link?: string;
@property({ type: String })
label?: string;
@property({ attribute: false })
cell?: Cell;
@consume({ context: runtimeContext, subscribe: true })
@property({ attribute: false })
runtime?: Runtime;
@consume({ context: spaceContext, subscribe: true })
@property({ attribute: false })
space?: MemorySpace;
@state()
private _resolvedCell?: Cell;
@state()
private _name?: string;
@state()
private _handle?: string;
private _unsubscribe?: () => void;
override disconnectedCallback() {
super.disconnectedCallback();
this._cleanupSubscription();
}
private _cleanupSubscription() {
if (this._unsubscribe) {
this._unsubscribe();
this._unsubscribe = undefined;
}
}
protected override willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (
changedProperties.has("cell") || changedProperties.has("link") ||
changedProperties.has("runtime") || changedProperties.has("space")
) {
this._resolveCell();
}
}
protected override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("_resolvedCell")) {
this._updateSubscription();
this._updateDisplayInfo();
}
// Also update display info when link changes without resolving to a new cell
if (
changedProperties.has("link") && !changedProperties.has("_resolvedCell")
) {
this._updateDisplayInfo();
}
}
private _resolveCell() {
if (this.cell) {
this._resolvedCell = this.cell;
return;
}
if (this.link && this.runtime) {
try {
const parsedLink = parseLLMFriendlyLink(this.link, this.space);
// We need to cast because parseLLMFriendlyLink returns NormalizedLink (if space optional)
// but getCellFromLink might expect NormalizedFullLink or handle it.
// Based on runtime.ts, getCellFromLink handles NormalizedLink but casts to NormalizedFullLink internally for createCell.
// If space is missing in parsedLink, createCell might fail if it strictly needs it.
// However, we pass what we have.
this._resolvedCell = this.runtime.getCellFromLink(parsedLink);
} catch (e) {
console.error("Failed to resolve link:", e);
this._resolvedCell = undefined;
}
} else {
this._resolvedCell = undefined;
}
}
private _updateSubscription() {
this._cleanupSubscription();
if (this._resolvedCell) {
// Subscribe to the cell to get updates for NAME
// We assume the cell value is an object that might have NAME symbol
this._unsubscribe = this._resolvedCell.sink((val) => {
this._updateNameFromValue(val);
});
}
}
private _updateNameFromValue(val: unknown) {
if (val && typeof val === "object" && NAME in val) {
this._name = (val as any)[NAME];
} else {
this._name = undefined;
}
this.requestUpdate();
}
private _updateDisplayInfo() {
if (this._resolvedCell) {
const link = this._resolvedCell.getAsNormalizedFullLink();
// Create a short handle from the ID
const id = link.id;
const shortId = id.split(":").pop()?.slice(-6) ?? "???";
this._handle = `#${shortId}`;
} else if (this.link) {
// Fallback if we can't resolve the cell but have a link string
try {
const parsed = parseLLMFriendlyLink(this.link);
const id = parsed.id;
const shortId = id ? id.split(":").pop()?.slice(0, 6) ?? "???" : "???";
this._handle = `#${shortId}`;
} catch {
this._handle = this.link;
}
} else {
this._handle = undefined;
}
}
private _handleClick(e: Event) {
e.stopPropagation();
if (this._resolvedCell && this._resolvedCell.runtime) {
this._resolvedCell.runtime.navigateCallback?.(this._resolvedCell);
} else if (this.runtime && this._resolvedCell) {
this.runtime.navigateCallback?.(this._resolvedCell);
}
}
override render() {
// Priority: label (from markdown) > [NAME] field > handle > "Unknown Link"
const displayText = this.label
? this.label
: this._name
? `${this._name} ${this._handle}`
: (this._handle || "Unknown Link");
return html`
${displayText}
`;
}
}
globalThis.customElements.define("ct-cell-link", CTCellLink);
declare global {
interface HTMLElementTagNameMap {
"ct-cell-link": CTCellLink;
}
}