Nested Panes

Render panes inside panes. A dashboard that contains a PersonCard, a TaskList, and a Chart — each driven by the data's @type.

The problem

A pane is a full-screen view. But real apps have nested data — a Project contains People, a Person has Tasks, Tasks have Comments. You want each piece to render itself using the right pane, wherever it appears.

resolvePane()

The shell exports resolvePane — give it a node, it finds the right pane and renders it:

import { resolvePane } from '../losos/shell.js'

// Inside a parent pane's render function:
var personNode = store.get('#person1')
var div = document.createElement('div')
container.appendChild(div)
await resolvePane(personNode, lionStore, div, rawData)

resolvePane checks three sources in order:

1. Local panes

Panes loaded via <script data-pane> tags in your HTML. The same canHandle check the shell uses for tabs:

<script type="module" data-pane src="panes/person-pane.js"></script>
<script type="module" data-pane src="panes/task-pane.js"></script>

If any local pane's canHandle returns true for the node, it renders. This is the fastest path — no network requests.

2. ui:view on the node

The data itself declares how to render it. Add a ui:view property pointing to a pane URL:

{
  "@id": "#person1",
  "@type": "Person",
  "name": "Alice",
  "ui:view": "https://example.com/panes/person-card.js"
}

The shell dynamically imports the pane from the URL, caches it, and renders. This means data can travel with its own UI — a JSON-LD document can specify exactly how it should be displayed, anywhere.

This follows the W3C UI vocabulary pattern used by the Solid OS project.

3. Registry

A type-to-pane mapping. LOSOS ships a default registry:

// losos/registry.js
export default {
  'wf:Tracker': '../panes/todo-pane.js',
  'ical:Vtodo': '../panes/todo-pane.js'
}

Extend it in your app:

import { registry } from './losos/shell.js'

registry['schema:Person'] = './panes/person-pane.js'
registry['schema:Event'] = './panes/event-pane.js'

When resolvePane encounters a node with @type: "schema:Person", and no local pane handles it and no ui:view is set, it checks the registry, dynamically imports the pane, caches it, and renders.

Lookup order

PrioritySourceWhen to use
1stLocal panesPanes bundled with your app
2ndui:view on nodeData that travels with its own UI
3rdRegistryDefault views for common types

First match wins. If nothing matches, resolvePane logs a warning and returns null.

Example: Dashboard with nested panes

export default {
  label: 'Dashboard',
  icon: '\uD83D\uDCCA',

  canHandle(subject, store) {
    return store.type(store.get(subject.value))?.includes('Dashboard')
  },

  async render(subject, lionStore, container, rawData) {
    var { resolvePane } = await import('../losos/shell.js')
    var { html, render } = await import('../losos/html.js')

    var data = rawData
    var root = data  // or use a reactive store

    // Render the dashboard layout
    render(container, html`
      <h1>${data.title}</h1>
      <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
        <div id="person-slot"></div>
        <div id="tasks-slot"></div>
      </div>
    `)

    // Resolve nested panes for each slot
    var personNode = lionStore.get('#person1')
    if (personNode) {
      await resolvePane(personNode, lionStore,
        container.querySelector('#person-slot'), rawData)
    }

    var tasksNode = lionStore.get('#tasks')
    if (tasksNode) {
      await resolvePane(tasksNode, lionStore,
        container.querySelector('#tasks-slot'), rawData)
    }
  }
}

The dashboard doesn't know how to render a Person or a Task list. It creates slots and lets resolvePane find the right pane for each @type.

The linked data advantage

In React, you'd import PersonCard and TaskList by name. If the data changes shape, you rewrite the imports. In LOSOS, the data's @type drives which pane renders. Change the type, the UI follows automatically. Add a new type with a ui:view, and it renders itself — no code changes in the parent.

This is the core idea behind Tim Berners-Lee's data-driven UI vision — the data knows how to present itself.