Ada Extensions
Guides

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

AreaWhereWhat fits
compact.leading / compact.trailingIn the closed island, left and right of the notchtext, icon, badge, timer, ring, progress (a small ring), gauge (as 42%)
expandedThe open island cardOne node; usually a stack

Keep compact text short: a few characters. Leave the detail to the card.

Kapalı Adacompact.leading ve compact.trailing çentiğin iki yanında durur
34%
Açılmış Adaexpanded alanına verdiğin düğüm, çentikten açılan kartta çizilir
SistemCanlı
42%CPU
68%RAM
Macintosh HD64%

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

default

Temel metin ve simgeler

Örnek
secondary

Yardımcı metin ve etiketler

Örnek
accent

Vurgu, bağlantı, sayaç

Örnek
success

Tamamlanan, onaylanan

Örnek
warning

Uyarı, bekleyen durum

Örnek
danger

Kritik sınır, hata

Örnek

Text 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, undefined and false children are skipped, so you can write condition && 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/max default 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 and https:// links.
  • image(src, { shape: "rounded" | "circle", size }): HTTPS only, and only from your network: domains; size 16–120, default 40.
Live Data Nodes PreviewVisual appearance of ui.ring, ui.gauge, ui.progress, and ui.chart
74%CPU
58%RAM
Disk Storage64%
// 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
  }
}
Controlvalue
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
Interactive Controls PreviewClickable buttons, live toggle switch, and draggable volume/opacity slider
Active
Speaker Volume65

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.

Interactive List RowsNative macOS island list items generated with ui.list

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.

On this page