From 698f3672e09cfbfea8034fa352a5ef75b68f5734 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 11 Jul 2026 20:02:43 +0100 Subject: [PATCH] Lower semantic rosters in browser clients --- packages/eve-browser-lowering/dist/index.d.ts | 7 ++++ packages/eve-browser-lowering/dist/index.js | 34 +++++++++++++++- packages/eve-browser-lowering/src/index.ts | 39 ++++++++++++++++++- packages/eve-browser-lowering/styles.css | 39 +++++++++++++++++++ .../test/browser-lowering.test.mjs | 23 ++++++++++- 5 files changed, 139 insertions(+), 3 deletions(-) diff --git a/packages/eve-browser-lowering/dist/index.d.ts b/packages/eve-browser-lowering/dist/index.d.ts index 951c59d..60fc175 100644 --- a/packages/eve-browser-lowering/dist/index.d.ts +++ b/packages/eve-browser-lowering/dist/index.d.ts @@ -47,6 +47,12 @@ export interface EveCommandIntent { issuedAt: string; clientId: string; } +export interface EveSemanticListItem { + label: string; + status: string; + detail: string; + badges: string[]; +} export interface EveSurfaceWorldInteraction { commandBoundary?: string; receiptSchema?: string; @@ -145,6 +151,7 @@ export interface EveProjectedWorldEntity { } export declare function renderEveSurface(surface: EveSurfaceDocument, host: HTMLElement, options?: EveBrowserLoweringOptions): HTMLElement; export declare function renderEveComponent(node: EveSurfaceComponent, options?: EveBrowserLoweringOptions): HTMLElement; +export declare function projectSemanticListItem(node: EveSurfaceComponent): EveSemanticListItem; export declare function projectWorldScene(node: EveSurfaceComponent): EveProjectedWorldEntity[]; export declare function createWorldActionIntent(command: string, action: Record, options?: EveBrowserLoweringOptions): EveCommandIntent; export declare function applyEveSurfaceStyles(styles: EveSurfaceStyles | undefined, body?: HTMLElement): void; diff --git a/packages/eve-browser-lowering/dist/index.js b/packages/eve-browser-lowering/dist/index.js index 041f250..70e2061 100644 --- a/packages/eve-browser-lowering/dist/index.js +++ b/packages/eve-browser-lowering/dist/index.js @@ -294,9 +294,10 @@ export function renderEveComponent(node, options = currentOptions) { pane.append(renderEveComponent(child, options)); return pane; } - if (kind === "list") { + if (kind === "list" || kind.endsWith(".roster")) { const list = el("section", "cultui-list"); assignId(list, node); + list.dataset.componentKind = kind; const title = stringProp(props.title, ""); if (title) list.append(el("h3", "cultui-list-title", title)); @@ -304,6 +305,28 @@ export function renderEveComponent(node, options = currentOptions) { list.append(renderEveComponent(child, options)); return list; } + if (kind === "list.item" || kind.endsWith(".item")) { + const semantic = projectSemanticListItem(node); + const item = el("article", "cultui-list-item"); + assignId(item, node); + item.dataset.componentKind = kind; + const heading = el("div", "cultui-list-item-heading"); + heading.append(el("strong", "cultui-list-item-label", semantic.label)); + if (semantic.status) + heading.append(el("span", "cultui-list-item-status", semantic.status)); + item.append(heading); + if (semantic.detail) + item.append(el("div", "cultui-list-item-detail", semantic.detail)); + if (semantic.badges.length) { + const badgeRow = el("div", "cultui-list-item-badges"); + for (const badge of semantic.badges) + badgeRow.append(el("span", "cultui-list-item-badge", badge)); + item.append(badgeRow); + } + for (const child of children) + item.append(renderEveComponent(child, options)); + return item; + } if (kind === "bar") { const bar = el("div", "cultui-meter"); assignId(bar, node); @@ -521,6 +544,15 @@ export function renderEveComponent(node, options = currentOptions) { fallback.append(renderEveComponent(child, options)); return fallback; } +export function projectSemanticListItem(node) { + const props = objectProps(node.props); + return { + label: firstString(props.label, props.title, node.text, node.kind, "item"), + status: firstString(props.status, ""), + detail: firstString(props.detail, props.phase, ""), + badges: firstString(props.badges, "").split(",").map(value => value.trim()).filter(Boolean), + }; +} function applyGeneratedLayout(element, layout, style = {}) { const display = firstString(layout.display, ""); if (display) diff --git a/packages/eve-browser-lowering/src/index.ts b/packages/eve-browser-lowering/src/index.ts index aa65949..2e00767 100644 --- a/packages/eve-browser-lowering/src/index.ts +++ b/packages/eve-browser-lowering/src/index.ts @@ -48,6 +48,13 @@ export interface EveCommandIntent { clientId: string; } +export interface EveSemanticListItem { + label: string; + status: string; + detail: string; + badges: string[]; +} + export interface EveSurfaceWorldInteraction { commandBoundary?: string; receiptSchema?: string; @@ -450,15 +457,35 @@ export function renderEveComponent( return pane; } - if (kind === "list") { + if (kind === "list" || kind.endsWith(".roster")) { const list = el("section", "cultui-list"); assignId(list, node); + list.dataset.componentKind = kind; const title = stringProp(props.title, ""); if (title) list.append(el("h3", "cultui-list-title", title)); for (const child of children) list.append(renderEveComponent(child, options)); return list; } + if (kind === "list.item" || kind.endsWith(".item")) { + const semantic = projectSemanticListItem(node); + const item = el("article", "cultui-list-item"); + assignId(item, node); + item.dataset.componentKind = kind; + const heading = el("div", "cultui-list-item-heading"); + heading.append(el("strong", "cultui-list-item-label", semantic.label)); + if (semantic.status) heading.append(el("span", "cultui-list-item-status", semantic.status)); + item.append(heading); + if (semantic.detail) item.append(el("div", "cultui-list-item-detail", semantic.detail)); + if (semantic.badges.length) { + const badgeRow = el("div", "cultui-list-item-badges"); + for (const badge of semantic.badges) badgeRow.append(el("span", "cultui-list-item-badge", badge)); + item.append(badgeRow); + } + for (const child of children) item.append(renderEveComponent(child, options)); + return item; + } + if (kind === "bar") { const bar = el("div", "cultui-meter"); assignId(bar, node); @@ -679,6 +706,16 @@ export function renderEveComponent( return fallback; } +export function projectSemanticListItem(node: EveSurfaceComponent): EveSemanticListItem { + const props = objectProps(node.props); + return { + label: firstString(props.label, props.title, node.text, node.kind, "item"), + status: firstString(props.status, ""), + detail: firstString(props.detail, props.phase, ""), + badges: firstString(props.badges, "").split(",").map(value => value.trim()).filter(Boolean), + }; +} + function applyGeneratedLayout( element: HTMLElement, layout: Record, diff --git a/packages/eve-browser-lowering/styles.css b/packages/eve-browser-lowering/styles.css index 533383b..038aef2 100644 --- a/packages/eve-browser-lowering/styles.css +++ b/packages/eve-browser-lowering/styles.css @@ -518,6 +518,45 @@ body[data-pixel-art="true"] .cultui-background { text-transform: uppercase; } +.cultui-list-item { + display: grid; + gap: 5px; + min-width: 0; + padding: 8px; + border: 1px solid var(--line); + border-radius: 6px; + background: var(--panel); +} + +.cultui-list-item-heading, +.cultui-list-item-badges { + display: flex; + gap: 6px; + align-items: center; + flex-wrap: wrap; + min-width: 0; +} + +.cultui-list-item-label { + min-width: 0; + overflow-wrap: anywhere; +} + +.cultui-list-item-status, +.cultui-list-item-badge { + padding: 2px 5px; + border: 1px solid var(--line-hot); + border-radius: 4px; + color: var(--quiet); + font: 600 10px var(--font-mono); +} + +.cultui-list-item-detail { + color: var(--quiet); + font-size: 12px; + overflow-wrap: anywhere; +} + .cultui-avatar-card { flex: 0 0 104px; display: grid; diff --git a/packages/eve-browser-lowering/test/browser-lowering.test.mjs b/packages/eve-browser-lowering/test/browser-lowering.test.mjs index d7845ef..37020c0 100644 --- a/packages/eve-browser-lowering/test/browser-lowering.test.mjs +++ b/packages/eve-browser-lowering/test/browser-lowering.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; -import { createEveCommandIntent, createWorldActionIntent, normalizeFieldsDocument, projectWorldScene, resolveRequiredPluginAdapters, selectAdvertisedSurface } from "../dist/index.js"; +import { createEveCommandIntent, createWorldActionIntent, normalizeFieldsDocument, projectSemanticListItem, projectWorldScene, resolveRequiredPluginAdapters, selectAdvertisedSurface } from "../dist/index.js"; test("selects the requested surface from provider authority", () => { const provider = { @@ -60,6 +60,27 @@ test("projects provider world entities into a normalized tactical plane", () => assert.ok(entities.every(entity => entity.yPercent >= 6 && entity.yPercent <= 94)); }); +test("projects unknown semantic items through the generic list contract", () => { + const item = projectSemanticListItem({ + id: "worker.one", + kind: "agent.item", + props: { + label: "Foundry Tug", + status: "working", + detail: "Hauling ore", + badges: "haul, tow, explore", + providerSpecificState: "remains provider-owned", + }, + }); + + assert.deepEqual(item, { + label: "Foundry Tug", + status: "working", + detail: "Hauling ore", + badges: ["haul", "tow", "explore"], + }); +}); + test("command intents use the active surface advertisement boundary", () => { const intent = createEveCommandIntent("aetheria.daemon.editor.inspect", { action: {