Zum Inhalt springen

Deine erste App

Diese Anleitung scaffoldet eine funktionierende App, führt sie in der Shell aus und zeigt, wie sie eine Live-Liste von Objekten aus deinem Vault rendert. Sie dauert etwa zehn Minuten.

Aus dem Shell-Repo erzeugt das Scaffold eine vollständige, konforme React-App:

Terminal window
bun run new-app field-notes "Field Notes"

Das erste Argument ist die App-ID (kebab-case); das zweite ist der Anzeigename. Du bekommst:

apps/field-notes/
├── manifest.json # die App-Deklaration
├── package.json # Abhängigkeiten: @brainstorm/sdk, @brainstorm/react-yjs, react
├── tsconfig.json
├── vite.config.ts
├── icon.svg # aus den Initialen der App generiert
└── src/
├── index.html # Einstiegsdokument (liefert eine strikte Content-Security-Policy)
├── main.tsx # React-Root-Mount
├── app.tsx # deine Root-Komponente — eine Live-Objektliste
├── runtime.ts # typsicherer Zugriff auf window.brainstorm
└── styles.css # App-Styles, vom SDK gethemt

Das Scaffold ist bewusst keine leere Seite — es mountet eine echte useVaultEntities-Liste und das Standard-Header-Chrome, sodass du von einer konformen App aus startest, statt die Konventionen später nachzurüsten.

manifest.json deklariert die App und den einen Objekttyp, den sie besitzt:

{
"id": "io.brainstorm.field-notes",
"name": "Field Notes",
"version": "0.1.0",
"sdk": "1",
"entry": "dist/index.html",
"icon": "icon.svg",
"capabilities": [
"storage.kv",
"entities.read:*",
"entities.write:io.brainstorm.field-notes/Item/v1"
],
"registrations": {
"entityTypes": [
{
"id": "io.brainstorm.field-notes/Item/v1",
"schema": {
"type": "object",
"required": ["id", "title", "createdAt", "updatedAt"]
}
}
]
}
}

Siehe Das Manifest für jedes Feld und Fähigkeiten für die Bedeutung dieser Fähigkeits-Strings.

src/main.tsx mountet React. Zwei Importe sind Pflicht und kommen zuerst — das App-Theme-Stylesheet (das das gemeinsame .app-header-Chrome und die Theme-Tokens trägt) und der Menü-Host:

import "@brainstorm/sdk/app-theme.css";
import { mountMenuHost } from "@brainstorm/sdk/menus";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { FieldNotesApp } from "./app";
import "./styles.css";
const root = document.getElementById("root");
if (!root) throw new Error("field-notes: #root not found");
mountMenuHost();
createRoot(root).render(
<StrictMode>
<FieldNotesApp />
</StrictMode>,
);

src/app.tsx ist deine UI. Das Scaffold rendert eine live, reaktive Liste deines eigenen Objekttyps:

import { useVaultEntities } from "@brainstorm/react-yjs";
import { useMemo } from "react";
import { getBrainstorm } from "./runtime";
const APP_TYPE = "io.brainstorm.field-notes/Item/v1";
export function FieldNotesApp() {
const service = getBrainstorm()?.services?.vaultEntities ?? null;
const { entities } = useVaultEntities(service);
const items = useMemo(
() => entities.filter((e) => e.type === APP_TYPE),
[entities],
);
return (
<div className="app">
<header className="app-header" data-testid="app-header">
<div className="app-header__left">
<h1 className="app-header__title">Field Notes</h1>
</div>
<div className="app-header__right" />
</header>
<main className="app-body">
{items.length === 0 ? (
<p>Nothing here yet.</p>
) : (
<ul className="app-list">
{items.map((item) => (
<li key={item.id}>{String(item.properties.title ?? item.id)}</li>
))}
</ul>
)}
</main>
</div>
);
}

Die Liste ist live: useVaultEntities abonniert den Vault, sodass die Liste neu rendert, wenn ein Objekt dieses Typs erstellt oder geändert wird — von deiner App, einer anderen App oder einem anderen Gerät. Du schreibst nie eine manuelle onChange → setState-Schleife; das ist die Reaktivitätsregel.

Eine neue App wird bei der Shell registriert, damit der Dev-Seeder sie beim Start installiert (füge sie zur Liste der Erstanbieter-Apps hinzu, gemäß dem Contributor-Leitfaden des Repos). Dann:

Terminal window
bun run dev

Die Shell baut Erstanbieter-Apps beim Booten neu und installiert sie neu, sodass ein vollständiger Shell-Neustart deine Änderungen ausliefert — ein Fenster neu zu laden liefert den vorigen Build. Du siehst deine App im Launcher; öffne sie, und sie rendert die (leere) Liste.

Erstelle aus der App ein Objekt deines Typs und beobachte, wie sich die Liste selbst aktualisiert:

const bs = getBrainstorm();
await bs.services.entities.create(APP_TYPE, {
title: "First field note",
createdAt: Date.now(),
updatedAt: Date.now(),
});

Kein Refresh, kein Neuladen — die Live-Abfrage, die diesen Typ bereits abonniert hat, rendert neu. Von hier aus: