Skip to content

2. Boundaries and the List

A list of notes on the page. Each note wrapped in a <kit-boundary> so future events from inside that note carry the note’s identity automatically.

Create src/notes.ts:

export type Note = {
id: string
title: string
body: string
updatedAt: number
}
export const seedNotes: Note[] = [
{
id: 'n_welcome',
title: 'Welcome to Quill',
body: 'A small notes app, built without React.',
updatedAt: Date.now() - 1000 * 60 * 60 * 24,
},
{
id: 'n_design',
title: 'Design tokens',
body: 'Components should consume CSS custom properties.',
updatedAt: Date.now() - 1000 * 60 * 60,
},
{
id: 'n_links',
title: 'Links',
body: 'Native dialog: developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog',
updatedAt: Date.now() - 1000 * 60 * 5,
},
]

src/render.ts:

import type { Note } from './notes.js'
export function renderNoteList(notes: Note[]): string {
return `
<kit-boundary surface="note-list" feature="notes">
<ul class="note-list">
${notes.map(renderNoteItem).join('')}
</ul>
</kit-boundary>
`
}
function renderNoteItem(note: Note): string {
return `
<li>
<kit-boundary surface="note-card" entity-type="note" entity-id="${escape(note.id)}">
<article class="note">
<h3>${escape(note.title)}</h3>
<p>${escape(note.body)}</p>
<small>Updated ${new Date(note.updatedAt).toLocaleString()}</small>
</article>
</kit-boundary>
</li>
`
}
function escape(value: string): string {
return value.replace(/[<>&"']/g, (c) => ({
'<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;',
}[c]!))
}

Two boundaries:

  • The outer <kit-boundary surface="note-list" feature="notes"> declares the surface and feature.
  • Each <kit-boundary surface="note-card" entity-type="note" entity-id="..."> declares the specific note. Surfaces stack, so events emitted inside a card carry both note-list and note-card in surfaces, with the deeper one as the primary surface.

Update src/main.ts:

import '@atheory-ai/kitsune-app'
import '@atheory-ai/kitsune-ui'
import { debugModule } from '@atheory-ai/kitsune-dev'
import { dialogModule, notificationModule } from '@atheory-ai/kitsune-ui'
import { seedNotes } from './notes.js'
import { renderNoteList } from './render.js'
await customElements.whenDefined('kit-shell')
const shell = document.querySelector('kit-shell')!
shell.modules = [notificationModule(), dialogModule(), debugModule()]
document.getElementById('main')!.innerHTML = renderNoteList(seedNotes)

Add to src/quill.css:

.note-list {
display: grid;
gap: 0.75rem;
list-style: none;
margin: 0;
padding: 0;
}
.note {
background: light-dark(oklch(98% 0 0), oklch(25% 0 250));
border: 1px solid light-dark(oklch(85% 0 0), oklch(35% 0 250));
border-radius: 0.5rem;
padding: 0.875rem;
}
.note h3 { margin: 0 0 0.25rem; font-size: 1rem; }
.note p { margin: 0 0 0.5rem; }
.note small { color: light-dark(oklch(45% 0 0), oklch(70% 0 0)); }

Three notes render to the page. The boundaries don’t do anything yet — but the moment any element inside emits an event, it’ll come out with full context attached. The list is ready for behavior.

You can verify by running this in the console:

document.querySelector('kit-boundary[surface=note-card]').context
// → { surfaces: ['app', 'note-list', 'note-card'], surface: 'note-card', feature: 'notes', entity: { type: 'note', id: 'n_welcome' } }

The DOM tree carries the meaning.

Make the cards clickable. We’ll use kit-card, declare meta-event for an “open note” interaction, and watch the event carry surface, feature, and entity all on its own.

Next: Chapter 3 — Note Cards →