Building UI
The compact area and the island card; layout, showing data and controls.
Your extension returns a view made of nodes, not HTML. AdanotchKit.ui holds the helpers that build those nodes; Adanotch draws them with native SwiftUI and matches their colours and fonts to the island.
const { ui } = AdanotchKit;
return {
compact: { leading: ui.icon("cloud.sun.fill"), trailing: ui.text("21°") },
expanded: ui.stack([
ui.text("Istanbul", { style: "title" }),
ui.text("Partly cloudy", { tone: "secondary" }),
]),
};Two areas
| Area | Where | What fits |
|---|---|---|
compact.leading / compact.trailing | In the closed island, left and right of the notch | text, icon, badge, timer, ring, progress (a small ring), gauge (as 42%) |
expanded | The open island card | One node; usually a stack |
Keep compact text short: a few characters. Leave the detail to the card.
Colour and type
There are no raw colours. Every node takes a semantic tone, and Adanotch picks its actual colour:
default · secondary · accent · success · warning · danger
Temel metin ve simgeler
ÖrnekYardımcı metin ve etiketler
ÖrnekVurgu, bağlantı, sayaç
ÖrnekTamamlanan, onaylanan
ÖrnekUyarı, bekleyen durum
ÖrnekKritik sınır, hata
ÖrnekText styles: title · body · caption · mono · display.
ui.text(`${cpu}%`, { style: "display", tone: cpu > 80 ? "danger" : "default" });Icons are SF Symbols names: "cpu", "bolt.fill", "checkmark.circle".
Layout
ui.stack(
[
ui.icon("music.note"),
ui.text(track.title),
ui.spacer(), // pushes its neighbours apart
ui.badge("Live", { tone: "danger" }),
],
{ axis: "h", spacing: 8, align: "center" },
);stack(children, { axis, spacing, align }): vertical by default.null,undefinedandfalsechildren are skipped, so you can writecondition && ui.text(...).grid(children, { columns, spacing }): equal columns.scroll(child, { maxHeight }): scrolling for long content.tabs([{ title, icon, content }]): tabs. Switching tabs doesn't call your extension.divider(),spacer().
Showing data
ui.stack([
ui.stack(
[
ui.ring(cpu, { label: "CPU", tone: "accent" }),
ui.ring(ram, { label: "RAM", tone: "success" }),
ui.chart(history, { style: "area", min: 0, max: 100 }),
],
{ axis: "h", spacing: 12 },
),
ui.gauge("Disk", 64),
ui.progress(uploadPercent, { tone: "accent" }),
]);ring,gauge,progress: values are 0–100.chart(values, { style: "line" | "area" | "bar", min, max }): up to 500 values;min/maxdefault to the data's own range.timer(date, { style: "timer" | "relative" }): counts on its own, with no re-rendering. Counts down to a future date and up from a past one.markdown(text): a small subset of Markdown; bold, italic, inline code andhttps://links.image(src, { shape: "rounded" | "circle", size }): HTTPS only, and only from yournetwork:domains; size 16–120, default 40.
// Time left until the meeting, second by second, without update running.
ui.timer(event.start, { style: "timer", textStyle: "display" });Controls
Every control has an action name. When the user uses a control, onAction(action, ctx, value) runs, then update.
expanded: ui.stack([
ui.button("Refresh", "refresh", { icon: "arrow.clockwise", primary: true }),
ui.toggle("Notifications", "notify", ctx.settings.notify),
ui.slider("volume", volume, { min: 0, max: 100, step: 5, label: "Volume" }),
ui.textField("search", { placeholder: "Search…" }),
ui.picker("range", range, [
{ value: "day", label: "Day" },
{ value: "week", label: "Week" },
]),
]),
onAction(action, ctx, value) {
switch (action) {
case "volume": ctx.storage.set("volume", value); break; // number
case "search": ctx.storage.set("query", value); break; // string, on Return
case "notify": ctx.storage.set("notify", value); break; // boolean
}
}| Control | value |
|---|---|
button(label, action, { value }) | The value you gave (null without one) |
toggle(label, action, isOn) | The new state, a boolean |
slider(action, value, opts) | Where it was let go, a number |
textField(action, opts) | The text, on Return |
textField(action, { changeAction }) | With changeAction, the text as the user types |
picker(action, value, options) | The picked option's value |
list([{ action, value }]) | The row's value |
Search as you type
With changeAction, the text field also reports while the user types: once they pause for a moment (250 ms), onAction(changeAction, ctx, text) runs, then update. Return still sends action as usual.
let query = "";
activity({
update() {
const shown = items.filter((item) => item.title.toLowerCase().includes(query.toLowerCase()));
return {
expanded: ui.stack([
ui.textField("add", { placeholder: "Search or add…", value: query, changeAction: "filter" }),
ui.list(shown),
]),
};
},
onAction(action, ctx, value) {
if (action === "filter") query = String(value); // while typing
if (action === "add") addItem(String(value)); // on Return
},
});Every change is a round trip to your extension; keep update fast, and cache results if you fetch while the user types.
Lists
ui.list(
prs.map((pr) => ({
title: pr.title,
subtitle: `#${pr.number} · ${pr.user}`,
icon: pr.draft ? "pencil.circle" : "arrow.triangle.pull",
trailing: pr.age,
tone: pr.failing ? "danger" : "default",
action: "open",
value: pr.url,
})),
);
async onAction(action, ctx, value) {
if (action === "open") await AdanotchKit.open(String(value));
}A row with an action becomes clickable. open(url) only works within 10 seconds of the user using one of your controls; you can't open pages in the background.
Extra activities and notifications
Beside the main view you can put short-lived things in the island:
// A short notification (the "notifications" permission)
AdanotchKit.notify.island({ text: "Backup done", icon: "checkmark.circle", tone: "success", duration: 4 });
// An extra activity with its own id; calling it again with the same id updates it
AdanotchKit.island.show({
id: "timer-1",
compact: { leading: ui.icon("timer"), trailing: ui.timer(endsAt) },
duration: 60, // without it, it stays until hide(id)
});
AdanotchKit.island.hide("timer-1");You can show up to 8 activities at once.
Every node and its options: UI reference.