Creating Panes
This guide walks you through creating a custom pane for SolidOS.
Pane Structure
Every pane has this structure:
import { PaneDefinition } from 'pane-registry'
import { NamedNode } from 'rdflib'
import { store } from 'solid-logic'
const myPane: PaneDefinition = {
// Required: unique identifier
name: 'my-pane',
// Optional: icon (emoji or URL)
icon: '📋',
// Optional: developer info
dev: {
name: 'Your Name',
repo: 'https://github.com/you/my-pane'
},
// Required: can this pane render the subject?
label: (subject: NamedNode, context: any): string | null => {
// Return a label string if yes, null if no
},
// Required: render the UI
render: (
subject: NamedNode,
dom: Document,
context: any
): HTMLElement => {
// Build and return a DOM element
}
}
export default myPane
Step 1: Define Your Type
Decide what RDF type your pane will render. For this example, we'll create a pane for ex:Recipe:
@prefix ex: <http://example.org/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
<#carbonara> a ex:Recipe ;
rdfs:label "Spaghetti Carbonara" ;
ex:servings 4 ;
ex:prepTime "30 minutes" ;
ex:ingredients (
"400g spaghetti"
"200g guanciale"
"4 egg yolks"
"100g pecorino"
) .
Step 2: Create the Namespace
import { Namespace, sym } from 'rdflib'
const EX = Namespace('http://example.org/')
const RDF = Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#')
const RDFS = Namespace('http://www.w3.org/2000/01/rdf-schema#')
Step 3: Implement the Label Function
The label function determines if your pane can render a subject:
label: (subject: NamedNode, context: any): string | null => {
// Check if the subject is a Recipe
if (store.holds(subject, RDF('type'), EX('Recipe'))) {
return 'Recipe' // Return a label to claim this resource
}
return null // Return null to pass
}
Label Priority
More specific labels get higher priority:
// Good: specific check
if (store.holds(subject, RDF('type'), EX('ItalianRecipe'))) {
return 'Italian Recipe'
}
// Fallback: broader check
if (store.holds(subject, RDF('type'), EX('Recipe'))) {
return 'Recipe'
}
Step 4: Implement the Render Function
Build the UI:
render: (subject: NamedNode, dom: Document, context: any): HTMLElement => {
const container = dom.createElement('div')
container.className = 'recipe-pane'
// Get the label
const label = store.any(subject, RDFS('label'), null, null)
// Create header
const header = dom.createElement('h2')
header.textContent = label?.value || 'Untitled Recipe'
container.appendChild(header)
// Get metadata
const servings = store.any(subject, EX('servings'), null, null)
const prepTime = store.any(subject, EX('prepTime'), null, null)
if (servings || prepTime) {
const meta = dom.createElement('div')
meta.className = 'recipe-meta'
if (servings) {
meta.innerHTML += `<span>Servings: ${servings.value}</span>`
}
if (prepTime) {
meta.innerHTML += `<span>Prep: ${prepTime.value}</span>`
}
container.appendChild(meta)
}
// Get ingredients (RDF list)
const ingredients = getListItems(store, subject, EX('ingredients'))
if (ingredients.length > 0) {
const section = dom.createElement('div')
section.innerHTML = '<h3>Ingredients</h3>'
const list = dom.createElement('ul')
ingredients.forEach(item => {
const li = dom.createElement('li')
li.textContent = item.value
list.appendChild(li)
})
section.appendChild(list)
container.appendChild(section)
}
return container
}
Helper: Reading RDF Lists
function getListItems(store: any, subject: NamedNode, predicate: NamedNode): any[] {
const items: any[] = []
let list = store.any(subject, predicate, null, null)
const RDF_FIRST = sym('http://www.w3.org/1999/02/22-rdf-syntax-ns#first')
const RDF_REST = sym('http://www.w3.org/1999/02/22-rdf-syntax-ns#rest')
const RDF_NIL = sym('http://www.w3.org/1999/02/22-rdf-syntax-ns#nil')
while (list && list.uri !== RDF_NIL.uri) {
const item = store.any(list, RDF_FIRST, null, null)
if (item) items.push(item)
list = store.any(list, RDF_REST, null, null)
}
return items
}
Step 5: Add Styling
Add CSS for your pane:
render: (subject, dom, context) => {
const container = dom.createElement('div')
// Add scoped styles
const style = dom.createElement('style')
style.textContent = `
.recipe-pane {
font-family: system-ui, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.recipe-pane h2 {
color: #2c3e50;
border-bottom: 2px solid #e74c3c;
padding-bottom: 10px;
}
.recipe-meta {
display: flex;
gap: 20px;
color: #7f8c8d;
margin: 15px 0;
}
.recipe-pane ul {
list-style: none;
padding: 0;
}
.recipe-pane li {
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.recipe-pane li:before {
content: "•";
color: #e74c3c;
margin-right: 10px;
}
`
container.appendChild(style)
// ... rest of render
}
Step 6: Make It Editable
Allow users to edit the data:
import * as UI from 'solid-ui'
import { authn } from 'solid-logic'
render: (subject, dom, context) => {
const container = dom.createElement('div')
// Check if user can edit
const user = authn.currentUser()
const canEdit = user !== null // More sophisticated: check ACL
if (canEdit) {
// Editable title field
const titleField = UI.widgets.field(
dom,
store,
subject,
RDFS('label'),
'Recipe Name'
)
container.appendChild(titleField)
// Editable servings
const servingsField = UI.widgets.field(
dom,
store,
subject,
EX('servings'),
'Servings'
)
container.appendChild(servingsField)
} else {
// Read-only display
const label = store.any(subject, RDFS('label'), null, null)
const h2 = dom.createElement('h2')
h2.textContent = label?.value || 'Untitled'
container.appendChild(h2)
}
return container
}
Step 7: Register the Pane
In solid-panes
Edit src/registerPanes.ts:
import recipePane from './recipe/recipePane'
export function registerPanes() {
// ... other registrations
register(recipePane)
}
Standalone
import { paneRegistry } from 'solid-panes'
import recipePane from './recipePane'
paneRegistry.register(recipePane)
Complete Example
import { PaneDefinition } from 'pane-registry'
import { NamedNode, Namespace, sym } from 'rdflib'
import { store, authn } from 'solid-logic'
import * as UI from 'solid-ui'
const EX = Namespace('http://example.org/')
const RDF = Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#')
const RDFS = Namespace('http://www.w3.org/2000/01/rdf-schema#')
const recipePane: PaneDefinition = {
name: 'recipe',
icon: '🍝',
dev: {
name: 'Recipe Pane Developer',
repo: 'https://github.com/example/recipe-pane'
},
label: (subject: NamedNode): string | null => {
if (store.holds(subject, RDF('type'), EX('Recipe'))) {
return 'Recipe'
}
return null
},
render: (subject: NamedNode, dom: Document, context: any): HTMLElement => {
const container = dom.createElement('div')
container.className = 'recipe-pane'
// Styles
const style = dom.createElement('style')
style.textContent = `
.recipe-pane { max-width: 600px; margin: 0 auto; padding: 20px; }
.recipe-pane h2 { color: #2c3e50; }
.recipe-meta { color: #7f8c8d; margin: 10px 0; }
`
container.appendChild(style)
// Header
const label = store.any(subject, RDFS('label'), null, null)
const h2 = dom.createElement('h2')
h2.textContent = label?.value || 'Untitled Recipe'
container.appendChild(h2)
// Meta
const servings = store.any(subject, EX('servings'), null, null)
if (servings) {
const meta = dom.createElement('div')
meta.className = 'recipe-meta'
meta.textContent = `Serves ${servings.value}`
container.appendChild(meta)
}
// Edit button (if logged in)
const user = authn.currentUser()
if (user) {
const editBtn = UI.widgets.button(dom, 'Edit Recipe', () => {
// Open edit mode
})
container.appendChild(editBtn)
}
return container
}
}
export default recipePane
Testing
import { graph, sym, lit } from 'rdflib'
import recipePane from './recipePane'
describe('recipePane', () => {
let testStore
beforeEach(() => {
testStore = graph()
})
test('claims Recipe type', () => {
const subject = sym('https://example.org/recipe#carbonara')
testStore.add(subject, RDF('type'), EX('Recipe'))
// Mock store.holds
const label = recipePane.label(subject, { store: testStore })
expect(label).toBe('Recipe')
})
test('ignores non-Recipe types', () => {
const subject = sym('https://example.org/other')
testStore.add(subject, RDF('type'), sym('http://other.org/Type'))
const label = recipePane.label(subject, { store: testStore })
expect(label).toBeNull()
})
})
See Also
- Pane Anatomy — detailed structure reference
- solid-ui — UI components
- Your First Pane — simpler tutorial