import { css, html } from "lit";
import { property, state } from "lit/decorators.js";
import { BaseView, createDefaultAppState } from "./BaseView.ts";
import { KeyStore } from "@commontools/identity";
import { RuntimeInternals } from "../lib/runtime.ts";
import { DebuggerController } from "../lib/debugger-controller.ts";
import "./DebuggerView.ts";
import { Task, TaskStatus } from "@lit/task";
import { CharmController } from "@commontools/charm/ops";
import { CellEventTarget, CellUpdateEvent } from "../lib/cell-event-target.ts";
import { NAME } from "@commontools/runner";
import { type NameSchema } from "@commontools/runner/schemas";
import { updatePageTitle } from "../lib/navigate.ts";
import { KeyboardController } from "../lib/keyboard-router.ts";
export class XAppView extends BaseView {
static override styles = css`
:host {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.shell-container {
display: flex;
flex-direction: column;
height: 100%;
background-color: white;
border: var(--border-width, 2px) solid var(--border-color, #000);
}
.content-area {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
background-color: white;
min-height: 0; /* Important for flex children */
}
`;
@property({ attribute: false })
app = createDefaultAppState();
@property({ attribute: false })
rt?: RuntimeInternals;
@property({ attribute: false })
keyStore?: KeyStore;
@state()
charmTitle?: string;
@property({ attribute: false })
private titleSubscription?: CellEventTarget;
@state()
private hasSidebarContent = false;
@state()
private _patternError?: Error;
private debuggerController = new DebuggerController(this);
private _keyboard = new KeyboardController(this);
override connectedCallback() {
super.connectedCallback();
this.addEventListener(
"sidebar-content-change",
this.handleSidebarContentChange,
);
}
override disconnectedCallback() {
this.removeEventListener(
"sidebar-content-change",
this.handleSidebarContentChange,
);
super.disconnectedCallback();
}
private handleSidebarContentChange = (e: Event) => {
const event = e as CustomEvent<{ hasSidebarContent: boolean }>;
this.hasSidebarContent = event.detail.hasSidebarContent;
};
// Fetches the space root pattern from the space.
_spaceRootPattern = new Task(this, {
task: async (
[rt],
): Promise<
| CharmController
| undefined
> => {
if (!rt) return;
try {
return await rt.getSpaceRootPattern();
} catch (err) {
console.error("[AppView] Failed to load space root pattern:", err);
throw err;
}
},
args: () => [this.rt],
});
// Gets the selected pattern synchronously - no await needed.
// The charm starts in the background; errors are captured in _patternError.
_selectedPattern = new Task(this, {
task: (
[app, rt],
): CharmController | undefined => {
if (!rt) return;
this._patternError = undefined; // Clear previous error
if ("charmId" in app.view && app.view.charmId) {
const { controller, ready } = rt.getPattern(app.view.charmId);
// Handle errors from the start() promise
ready.catch((err) => {
console.error("[AppView] Failed to start pattern:", err);
this._patternError = err;
this.requestUpdate();
});
return controller;
}
},
args: () => [this.app, this.rt],
});
// This derives a space root pattern as well as an "active" (main)
// pattern for use in child views.
// This hybrid task intentionally only uses completed/fresh
// source patterns to avoid unsyncing state.
_patterns = new Task(this, {
task: function (
[
app,
spaceRootPatternValue,
spaceRootPatternStatus,
selectedPatternValue,
selectedPatternStatus,
],
): {
activePattern: CharmController | undefined;
spaceRootPattern: CharmController | undefined;
} {
const spaceRootPattern = spaceRootPatternStatus === TaskStatus.COMPLETE
? spaceRootPatternValue
: undefined;
// The "active" pattern is the main pattern to be rendered.
// This may be the same as the space root pattern, unless we're
// in a view that specifies a different pattern to use.
const useSpaceRootAsActive = !("charmId" in app.view && app.view.charmId);
const activePattern = useSpaceRootAsActive
? spaceRootPattern
: selectedPatternStatus === TaskStatus.COMPLETE
? selectedPatternValue
: undefined;
return {
activePattern,
spaceRootPattern,
};
},
args: () => [
this.app,
this._spaceRootPattern.value,
this._spaceRootPattern.status,
this._selectedPattern.value,
this._selectedPattern.status,
],
});
#setTitleSubscription(activeCharm?: CharmController) {
if (!activeCharm) {
if (this.titleSubscription) {
this.titleSubscription.removeEventListener(
"update",
this.#onCharmTitleChange,
);
}
this.titleSubscription = undefined;
this.charmTitle = this.app && "spaceName" in this.app.view
? this.app.view.spaceName
: "Common Tools";
} else {
const cell = activeCharm.getCell();
this.titleSubscription = new CellEventTarget(cell.key(NAME));
this.charmTitle = cell.key(NAME).get();
}
}
#onCharmTitleChange = (e: Event) => {
const event = e as CellUpdateEvent;
this.charmTitle = event.detail ?? "";
};
override updated(changedProperties: Map) {
super.updated(changedProperties);
if (changedProperties.has("charmTitle")) {
updatePageTitle(this.charmTitle ?? "");
}
if (changedProperties.has("titleSubscription")) {
const current = this.titleSubscription;
const prev = changedProperties.get(
"titleSubscription",
) as CellEventTarget | undefined;
if (prev) {
prev.removeEventListener("update", this.#onCharmTitleChange);
}
if (current) {
current.addEventListener("update", this.#onCharmTitleChange);
}
}
// Update debugger controller with runtime
if (changedProperties.has("rt") && this.rt) {
this.debuggerController.setRuntime(this.rt);
}
// Update debugger visibility from app state
if (changedProperties.has("app")) {
this.debuggerController.setVisibility(
this.app.config.showDebuggerView ?? false,
);
}
}
// Always defer to the loaded active pattern for the ID,
// but until that loads, use an ID in the view if available.
private getActivePatternId(): string | undefined {
const activePattern = this._patterns.value?.activePattern;
if (activePattern?.id) return activePattern.id;
if ("charmId" in this.app.view && this.app.view.charmId) {
return this.app.view.charmId;
}
}
override render() {
const config = this.app.config ?? {};
const { activePattern, spaceRootPattern } = this._patterns.value ?? {};
this.#setTitleSubscription(activePattern);
const authenticated = html`
`;
const unauthenticated = html`
`;
const charmId = this.getActivePatternId();
const spaceName = this.app && "spaceName" in this.app.view
? this.app.view.spaceName
: undefined;
const content = this.app?.identity ? authenticated : unauthenticated;
return html`
${this.app.identity
? html`
`
: ""}
`;
}
}
globalThis.customElements.define("x-app-view", XAppView);