3. Note Cards
What we want
Section titled “What we want”Each note becomes a clickable card. Clicking it emits a note.opened event that carries the note’s id from the boundary, automatically. We won’t write a click handler — the boundary delegates.
Update the render
Section titled “Update the render”function renderNoteItem(note: Note): string { return ` <li> <kit-boundary surface="note-card" entity-type="note" entity-id="${escape(note.id)}"> <kit-card interactive tabindex="0" role="button" meta-event="note.opened" meta-prop-id="${escape(note.id)}" > <h3 slot="header">${escape(note.title)}</h3> <p>${escape(note.body)}</p> <small slot="footer">Updated ${new Date(note.updatedAt).toLocaleString()}</small> </kit-card> </kit-boundary> </li> `}What changed:
<article>→<kit-card interactive>(a styled, slotted card).- Added
tabindex="0"androle="button"so the card is focusable and screen readers announce it as a button. - Added
meta-event="note.opened"andmeta-prop-id="..."so a click emits the event withpayload: { id: 'n_welcome' }.
Render an empty editor pane
Section titled “Render an empty editor pane”Wrap the whole layout in a two-column grid. src/render.ts:
export function renderApp(notes: Note[]): string { return ` <div class="app-grid"> <section class="sidebar"> ${renderNoteList(notes)} </section> <section id="editor" class="editor"> <kit-boundary surface="empty-editor" feature="notes"> <p class="muted">Select a note from the left.</p> </kit-boundary> </section> </div> `}And update main.ts:
document.getElementById('main')!.innerHTML = renderApp(seedNotes)CSS:
.app-grid { display: grid; gap: 1.5rem; grid-template-columns: minmax(16rem, 24rem) 1fr;}
.muted { color: light-dark(oklch(45% 0 0), oklch(70% 0 0)); }Listen for note.opened and render the editor
Section titled “Listen for note.opened and render the editor”In main.ts, after installing the modules:
import type { Note } from './notes.js'
const editorEl = document.getElementById('editor')!
function renderEditor(note: Note) { editorEl.innerHTML = ` <kit-boundary surface="note-editor" feature="notes" entity-type="note" entity-id="${note.id}"> <article> <h2>${escape(note.title)}</h2> <pre class="body">${escape(note.body)}</pre> <small>Last updated ${new Date(note.updatedAt).toLocaleString()}</small> </article> </kit-boundary> `}
shell.runtime.on('note.opened', (event) => { const id = String(event.payload?.id ?? '') const note = seedNotes.find((n) => n.id === id) if (note) renderEditor(note)})(Keep the escape helper handy — duplicate it from render.ts or import it.)
What we have now
Section titled “What we have now”Click any card. The right pane renders the note. The console shows the event:
[kit:event] note.opened { type: 'note.opened', context: { surfaces: ['app', 'note-list', 'note-card'], surface: 'note-card', feature: 'notes', entity: { type: 'note', id: 'n_welcome' } }, payload: { id: 'n_welcome' }, source: { tagName: 'kit-card' },}We didn’t write a click listener on the card. The boundary delegated. The card declared its meaning. The runtime stitched in context.
A note on role="button" plus tabindex="0"
Section titled “A note on role="button" plus tabindex="0"”The card is presentational. Adding role="button" and tabindex="0" makes it announce as a button to assistive tech and become focusable. It does not automatically activate on Space/Enter — for that, custom elements with role="button" should also handle keyboard activation.
A future iteration of <kit-card> will accept an interactive-as="button" attribute that handles all of this. For now, in production code, prefer wrapping the card content in a real <button> for accessibility.
What’s next
Section titled “What’s next”Time to create notes. We’ll add a form with <kit-field> and use the native form submission flow.