# Taskbar Shell-chrome application that turns the SGApps desktop into a familiar windowed OS. Renders as a **Windows-style taskbar**, **OSX-style dock**, or **mobile launcher**; tracks every opened app; reserves screen space so maximized windows don't hover it; and hosts pluggable widgets (clock, battery, network, launcher, show-desktop) that you can extend with your own. - **Register as**: `taskbar` - **Module path**: `sgapps/applications/window-manager/taskbar` - **Kind**: windowed application (runs inside a `windowManager.open(...)` window) ## Live Demo The iframe boots with the **full** preset (launcher, show-desktop, network, battery, clock) and an in-memory state adapter — nothing it does leaks into your localStorage. Use the quick-action bar below to manipulate the live preview, or scroll down for the per-event Try buttons.
Quick actions on the live preview
> **Tip** — Click the **launcher** widget (first icon on the left) inside the > live preview to open the start-menu popup. Click any app there to open it > inside the iframe — the taskbar will track it as a real window-item. ## Embed ```html ``` The same iframe accepts every `api-request::instance:*` event documented below. Drive it with the standard postMessage / WindowSocket bridge: ```js socket.fire("webapp::instance::request", "config:mode", "dock"); socket.fire("webapp::instance::request", "widgets:add", "clock", { side: "right" }); ``` ## At a glance | Capability | What you can do | Where | |---|---|---| | **Layouts** | Pick `taskbar`, `dock`, `mobile`, or `auto` (live viewport detection) | [Visual modes](#visual-modes) · [Recipe 3](#recipe-3--toggle-layout--position-from-your-own-settings-panel) | | **Position** | Pin to `top`, `bottom`, `left`, or `right` edge; switch between fixed/sticky | [Positions](#positions) | | **Presets** | One-shot `full` / `no-config` / `minimal` configurations | [Configuration presets](#configuration-presets) | | **Window tracking** | Shows every non-pinned window the manager opens; click to focus / minimize | [Tracks running apps](#tracks-running-apps-in-the-main-slot) | | **Built-in widgets** | clock, battery, network, launcher, show-desktop | [Widgets](#widgets) | | **Custom widgets** | Add your own factory and the bar persists / restores it across reloads | [Building a custom widget](#building-a-custom-widget) · [Recipe 4](#recipe-4--build-a-custom-notifications-widget-end-to-end) · [Recipe 5](#recipe-5--polling-widget-cpu--memory--status-feed) | | **Quick-launch icons** | `addItem({ icon, title, side, onClick })` — non-persisted shortcuts | [Recipe 6](#recipe-6--quick-launch-icon-for-a-specific-app) | | **Persistence** | `localStorage` / in-memory / disabled / custom remote backend | [Persistent configuration](#persistent-configuration) · [Recipe 7](#recipe-7--persist-state-to-a-remote-backend) | | **Programmatic control** | Direct `app.method()` calls or window-event API for foreign frames | [Programmatic control](#programmatic-control) · [Window-event API](#window-event-api-api-requestinstance) · [Recipe 11](#recipe-11--drive-a-taskbar-from-a-foreign-script-window-events-only) | | **Lifecycle events** | `layout:changed`, `item:activate`, `widget:added`, `widget:removed`, `widget:unknown` | [Events](#events) · [Recipe 9](#recipe-9--listen-to-window-activity-and-react) | | **Multi-instance** | Run a dock and a status bar side-by-side with separate state keys | [Recipe 12](#recipe-12--side-by-side-dock--slim-status-bar) | | **Demo iframe** | Embed at `/online/webapp/taskbar` and drive every API event over postMessage | [Live Demo](#live-demo) · [Embed](#embed) | ## Launch ```js // Full-featured taskbar with widgets + localStorage persistence windowManager.open({ run: ["taskbar", "--preset=full"] }); // Minimal — just a bar, no persistence, no context menu windowManager.open({ run: ["taskbar", "--preset=no-config"] }); // Dock at the bottom with explicit widget set windowManager.open({ run: [ "taskbar", "--mode=dock", "--position=bottom", "--state-storage=localStorage", "--widgets=launcher:left,show-desktop:left,network:right,battery:right,clock:right" ] }); // Vertical taskbar on the left edge, auto-detect mode windowManager.open({ run: ["taskbar", "--preset=full", "--mode=taskbar", "--position=left"] }); ``` --- ## Visual modes | Mode | What it looks like | When to use | |------|--------------------|-------------| | `taskbar` | Thin horizontal bar (40 px) or vertical strip (48 px) at the configured edge | Windows-like desktop feel | | `dock` | 64 px translucent strip with centered pills separating left widgets / running apps / right widgets; hovered icons lift and glow | OSX-like desktop feel | | `mobile` | Small 72 × 72 floating round button that opens a full-screen app picker; every opened app is auto-maximized | Touch devices / small screens | | `auto` | Picks `mobile` on small or coarse-pointer viewports, `taskbar` otherwise, and switches live when the viewport changes | Apps that should "just work" across device types | ## Positions `top` / `bottom` / `left` / `right` — edge of the viewport the bar pins to. Vertical edges (`left` / `right`) stack widgets in a column and hide their labels in favour of icons. ## Configuration presets | Preset | Context menu | Persistence | Widgets | |--------|--------------|-------------|---------| | `full` | enabled | `localStorage` | launcher / show-desktop / network / battery / clock | | `no-config` | disabled | none | none | | `minimal` | enabled | none | clock | Pass with `--preset=` on launch, or swap at runtime: ```js taskbarWin.emit("api-request::ready", [function (p) { p.then(function (app) { app.preset("no-config"); }); }]); ``` --- ## Features ### Tracks running apps in the main slot Every window opened by the `windowManager` appears as an item (icon + title). Items show: - **Focused**: highlighted background (or a glowing dot under the icon in dock mode). - **Minimized**: faded / italic. - **Click**: brings the window to front; click a focused window to minimize it; click a minimized one to restore. - **Right-click**: opens a contextual menu with **Minimize / Maximize / Fullscreen / Close**. Items that don't fit the bar are automatically moved behind an overflow-arrow popup. The popup scrolls vertically when there are more than ~8 entries. ### Reserves screen space around itself Maximized windows shrink so they don't hover the bar — the taskbar writes the CSS variable `--sgapps-taskbar-size` plus body classes (`has-sgapps-taskbar-bottom`, etc.), and the built-in stylesheet rules subtract that size from any `.maximized` window that isn't the taskbar itself. Mobile mode resets to full screen since the FAB is OK to hover content. ### Pinned windows are skipped Windows that are `pinned()` to a container aren't shown in the task area (they're part of the host widget already). Pin/unpin at runtime is reacted to live. ### Configurable right-click menu on the empty bar ``` ┌──────────────┐ │ Taskbar │ │ Layout ▸ │── Taskbar / Dock / Mobile / Auto (✓ current) │ Position ▸ │── Top / Bottom / Left / Right (✓ current) │ Position type▸│── Fixed / Sticky ├──────────────┤ │ Add widget ▸ │── clock / battery / network / … ▸── Left / Right │ Remove ▸ │── clock (right) / battery (right) / … ├──────────────┤ │ Preset ▸ │── Full / No-config / Minimal │ State storage▸│── Disabled / localStorage / In-memory └──────────────┘ ``` Disable with `--no-context-menu` or `app.contextMenuEnabled(false)`. ### Persistent configuration Layout choices, active widgets, and widget options are saved automatically as the user tinkers. On next load they're restored exactly. Three built-in adapters: - **`localStorage`** — survives reloads; key is `--state-key` (default `sgapps-taskbar-state`) - **`memory`** — lives only for the tab session - **`none`** — no persistence For server-persisted configuration or a custom backing store, pass an object implementing `{ get(), set(state) }`: ```js app.stateStorage({ get: function () { return fetch("/api/taskbar").then(function (r) { return r.json(); }); }, set: function (state) { return fetch("/api/taskbar", { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(state) }); } }); ``` Either method may return a Promise. Errors are swallowed so the adapter can throw freely. --- ## Widgets Widgets are small units that render into the left or right slot. Each one is a submodule under `window-manager/taskbar/widgets/`. | Type | What it does | Options | |------|--------------|---------| | `clock` | Live clock | `format`: `"HH:mm"` (default), `"HH:mm:ss"`, `"full"` | | `battery` | Battery percentage + charging icon (`navigator.getBattery`) | — | | `network` | Online / offline indicator (`navigator.onLine`) | — | | `launcher` | Start-menu popup with a **search field** and apps grouped into **subcategories** (System / Office / Text & Code / Graphics / Media / Engineering / …) sourced from the live `windowManager.applicationsLibrary()` | — | | `show-desktop` | First click minimizes every non-pinned, non-taskbar window; second click restores them | — | Add at launch with `--widgets=type:side,…` or programmatically: ```js app.addWidget("clock", { side: "right", options: { format: "HH:mm:ss" } }); ``` Remove by id: ```js app.removeWidget(someWidgetId); ``` List currently active widgets: ```js app.widgets(); // [ { id: "w-abc", type: "clock", side: "right", options: { format: "HH:mm" } }, ... ] ``` ### The launcher widget Clicking the launcher opens a frosted popup with: - A **search input** at the top that filters apps across every category as you type. - Apps grouped into **subcategories** (heuristic — falls back to the optional `category` field if the app descriptor defines one). - **Enter** launches the first visible result; **Escape** or a click outside closes. - Apps are read live from `windowManager.applicationsLibrary()`, so any runtime-registered app shows up automatically. --- ## Building a custom widget A widget is any factory returning `{ node, destroy? }`. Register it once with any taskbar instance, then add it to a slot. It automatically: - Gets two classes appended to its node — `sgapps-taskbar--widget` and `sgapps-taskbar--widget-`. - Appears in the right-click **Add widget** submenu. - Survives reloads when state persistence is enabled (the runtime saves `{ type, side, options }` and re-instantiates through your factory). ```js // my-ping-widget.js (Application module) module.exports = function pingWidget(options, tbApp) { var node = document.createElement("span"); node.className = "sgapps-taskbar--widget-inner"; var em = document.createElement("em"); node.appendChild(em); var ticks = 0; var timer = setInterval(function () { ticks += 1; em.textContent = "ping " + ticks; }, options.intervalMs || 5000); return { node: node, destroy: function () { clearInterval(timer); } }; }; ``` ```js // wire it up against a live taskbar Application.require(["myPing :: path/to/my-ping-widget"]).then(function (libs) { taskbarWin.emit("api-request::ready", [function (p) { p.then(function (app) { app.registerWidget("ping", libs.myPing); app.addWidget("ping", { side: "right", options: { intervalMs: 1000 } }); }); }]); }); ``` Optional — style via the auto-added class: ```css .sgapps-taskbar--widget-ping em { font-weight: bold; color: #d04; } ``` --- ## Programmatic control Get a reference to the live app through the standard bridge: ```js taskbarWin.emit("api-request::ready", [function (promise) { promise.then(function (app) { // --- layout --- app.mode("dock"); // "auto"|"taskbar"|"dock"|"mobile" app.position("bottom"); // "top"|"bottom"|"left"|"right" app.positionType("fixed"); // "fixed"|"sticky" app.autoMobileMaximize(false); // --- presets --- app.preset("full"); // apply a preset app.presets(); // -> ["full","no-config","minimal"] // --- widgets --- app.widgetTypes(); // registered types app.widgets(); // active widgets snapshot app.addWidget("clock", { side: "right" }); app.removeWidget(id); app.registerWidget("my-thing", factory); // --- branding icons on the bar sides --- app.addItem({ icon: "paper/icons/apps/internet-web-browser", title: "Docs", side: "right", onClick: function () { window.open("https://example.com"); } }); // --- state --- app.stateStorage("localStorage"); // or "memory" / null / { get, set } app.serializeState(); // snapshot of what would be persisted app.saveState(); // force write // --- tracked windows --- app.items(); // { id, win }[] // --- misc --- app.contextMenu(); // shared ContextMenu instance app.contextMenuEnabled(false); // disable empty-bar menu }); }]); ``` --- ## Events ```js app.on("layout:changed", function (conf) { /* { mode, position, positionType } */ }); app.on("item:activate", function (win) { /* user clicked an item */ }); app.on("item:contextmenu", function (ev, win) { /* after built-in menu opens */ }); app.on("widget:added", function (w) { /* { id, type, side, options, instance } */ }); app.on("widget:removed", function (w) { /* same shape */ }); app.on("widget:unknown", function (type) { /* addWidget with an unregistered type */ }); ``` --- ## Window-event API (`api-request::instance:*`) In addition to direct method calls on the live `app`, the taskbar exposes its configuration surface as window-events on the host window. This mirrors the file-manager pattern and is useful when you only have a `taskbarWin` handle (e.g. from a foreign frame or a sandboxed launcher). All events use a node-style `(value?, callback)` signature: pass a value to set, omit it to read. **Same surface, two transports.** Inside an SGApps page you call `taskbarWin.emit("api-request::instance:", [args…])` directly. From a **parent page that hosts the taskbar in an iframe** (like this docs page) you fire the event over the WindowSocket bridge: ```js socket.fire("webapp::instance::request", "", arg1, arg2, …); ``` Every Try button below uses the iframe form against the demo above. ### Get the live app instance ```js taskbarWin.emit("api-request::instance", [function (err, app) { if (err) return console.error(err); // app is the same object you'd get via api-request::ready }]); ``` --- ### Layout & Position #### `config:mode` -- Switch Layout Mode ```js socket.fire("webapp::instance::request", "config:mode", "dock"); // "auto" | "taskbar" | "dock" | "mobile" ``` Read the current mode (omit the value to read instead of set): ```js socket.fire("webapp::instance::request", "config:mode", function (err, mode) { console.log("Current mode:", mode); }); ``` #### `config:position` -- Pin to a Viewport Edge ```js socket.fire("webapp::instance::request", "config:position", "top"); // "top" | "bottom" | "left" | "right" ``` #### `config:positionType` -- Fixed vs Sticky ```js socket.fire("webapp::instance::request", "config:positionType", "fixed"); // "fixed" | "sticky" ``` #### `config:resolvedMode` -- Read the Currently Resolved Mode When `mode` is `"auto"`, `resolvedMode` reports what the bar is *actually* rendering as right now (`"taskbar"` or `"mobile"`). ```js socket.fire("webapp::instance::request", "config:resolvedMode", function (err, mode) { console.log("Resolved:", mode); }); ``` #### `config:autoMobileMaximize` / `config:contextMenuEnabled` -- Toggles ```js socket.fire("webapp::instance::request", "config:autoMobileMaximize", false); socket.fire("webapp::instance::request", "config:contextMenuEnabled", false); ``` --- ### Widgets #### `widgets:add` -- Append a Built-in Widget ```js socket.fire("webapp::instance::request", "widgets:add", "clock", { side: "right", options: { format: "HH:mm:ss" } }, function (err, id) { console.log("widget id:", id); }); ``` #### `widgets:list` -- Inspect Active Widgets ```js socket.fire("webapp::instance::request", "widgets:list", function (err, widgets) { console.log(widgets); }); // -> [{ id, type, side, options }, ...] ``` #### `widgets:types` -- See What Can Be Added ```js socket.fire("webapp::instance::request", "widgets:types", function (err, types) { console.log(types); }); // -> ["clock", "battery", "network", "launcher", "show-desktop", ...] ``` #### `widgets:remove` -- Detach a Widget The Try button below chains `widgets:list` → `widgets:remove` to delete the last widget in the bar: ```js socket.fire("webapp::instance::request", "widgets:remove", widgetId, function (err) { /* removed */ }); ``` --- ### Presets #### `presets:apply` -- One-Shot Configuration Resets the widget set, context menu state, and storage adapter in a single call. Three presets ship by default: | Preset | Context menu | Persistence | Widgets | |---|---|---|---| | `"full"` | enabled | `localStorage` | launcher / show-desktop / network / battery / clock | | `"no-config"` | disabled | none | (empty) | | `"minimal"` | enabled | none | clock | ```js socket.fire("webapp::instance::request", "presets:apply", "full"); ``` #### `presets:list` -- Discover Available Presets ```js socket.fire("webapp::instance::request", "presets:list", function (err, names) { console.log(names); }); ``` --- ### Custom Slot Items `addItem` puts a clickable icon in the left or right slot. Items are not persisted across reloads (re-add them at startup if you want them sticky). #### `items:add` -- Inject a Quick-Launch Icon ```js socket.fire("webapp::instance::request", "items:add", { icon: "paper/icons/apps/internet-web-browser", title: "Docs", side: "right" }, function (err, id) { /* ... */ }); ``` > **Note** — In the iframe demo the `onClick` handler can't cross the > postMessage boundary, so the Try buttons below add icon-only items. In your > own pages, pass an inline `function () { … }` along with the rest of the > config. #### `items:list` -- Inspect Active Items ```js socket.fire("webapp::instance::request", "items:list", function (err, items) { console.log(items); }); ``` --- ### Tracked Windows The taskbar tracks every window opened by the host `windowManager`. To make the bar visually interesting in the iframe demo we expose a thin layer for spawning sample windows from the parent page. #### `windows:open` -- Spawn a Window the Bar Will Track ```js // open by registered app name socket.fire("webapp::instance::request", "windows:open", "file-manager"); // or with arguments socket.fire("webapp::instance::request", "windows:open", ["xdg-open", "https://sgapps.io/docs"]); ``` #### `windows:list` -- See What the Bar Is Tracking ```js socket.fire("webapp::instance::request", "windows:list", function (err, windows) { console.log(windows); }); // -> [{ title, isTaskbar }, ...] ``` #### `windows:closeAll` -- Tear Down Sample Windows Closes every non-pinned window — except the taskbar itself, which protects itself from this call. ```js socket.fire("webapp::instance::request", "windows:closeAll", function (err, n) { console.log("Closed", n, "windows"); }); ``` --- ### State Persistence #### `state:serialize` -- Snapshot the Current Configuration Returns the exact object that would be written to the storage adapter. ```js socket.fire("webapp::instance::request", "state:serialize", function (err, snapshot) { console.log(snapshot); }); ``` #### `state:load` / `state:save` -- Round-Trip Persistence ```js socket.fire("webapp::instance::request", "state:load", function (err, stored) { console.log("From storage:", stored); }); socket.fire("webapp::instance::request", "state:save", function (err) { /* forced write */ }); ``` #### `config:stateStorage` -- Switch Adapter at Runtime ```js socket.fire("webapp::instance::request", "config:stateStorage", "localStorage"); socket.fire("webapp::instance::request", "config:stateStorage", "memory"); socket.fire("webapp::instance::request", "config:stateStorage", "none"); ``` --- ### Composed Demos Higher-level scenarios that chain several events together — handy for showing off the bar's behavior in one click. #### Build a Stocked Dock at the Bottom ```js socket.fire("webapp::instance::request", "presets:apply", "no-config"); socket.fire("webapp::instance::request", "config:mode", "dock"); socket.fire("webapp::instance::request", "config:position", "bottom"); ["launcher","show-desktop"].forEach(function (t) { socket.fire("webapp::instance::request", "widgets:add", t, { side: "left" }); }); ["network","battery","clock"].forEach(function (t) { socket.fire("webapp::instance::request", "widgets:add", t, { side: "right" }); }); socket.fire("webapp::instance::request", "windows:open", ["file-manager"]); socket.fire("webapp::instance::request", "windows:open", ["code-editor"]); ``` #### Reset the Demo to a Clean State ```js // remove every widget, every custom item, close sample windows, reapply preset socket.fire("webapp::instance::request", "windows:closeAll"); socket.fire("webapp::instance::request", "widgets:list", function (e, ws) { (ws || []).forEach(function (w) { socket.fire("webapp::instance::request", "widgets:remove", w.id); }); socket.fire("webapp::instance::request", "presets:apply", "minimal"); socket.fire("webapp::instance::request", "config:mode", "auto"); socket.fire("webapp::instance::request", "config:position", "bottom"); }); ``` #### Convert Bar → Side Dock → Back ```js // move the dock to the left edge, then back to the bottom after 2 seconds socket.fire("webapp::instance::request", "config:mode", "dock"); socket.fire("webapp::instance::request", "config:position", "left"); setTimeout(function () { socket.fire("webapp::instance::request", "config:position", "bottom"); }, 2000); ``` #### Walk Every Mode ```js var modes = ["taskbar", "dock", "mobile", "auto"]; modes.forEach(function (m, i) { setTimeout(function () { socket.fire("webapp::instance::request", "config:mode", m); }, i * 1500); }); ``` ### Full example ```js window.taskbarWin = windowManager.open({ run: ["taskbar", "--mode=auto", "--state-storage=localStorage", "--preset=full"] }); taskbarWin.emit("api-request::ready", [function (whenReady) { whenReady.then(function () { // switch layout taskbarWin.emit("api-request::instance:config:mode", ["dock", function (err, mode) { console.log("now in", mode); }]); // append a custom widget taskbarWin.emit("api-request::instance:widgets:add", ["clock", { side: "right", options: { format: "HH:mm:ss" } }, function (err, id) { console.log("clock id", id); }]); }, console.warn); }]); ``` > **Note** Callbacks are optional everywhere. If you only want to fire-and-forget, > `taskbarWin.emit("api-request::instance:config:mode", ["auto"])` works. --- ## Flows & Recipes Practical, end-to-end snippets you can copy into a page. Every recipe assumes `windowManager` is the global window manager (already present on any SGApps page) and that the taskbar module is reachable via the registered name `taskbar`. > **Tip** — Most recipes can also be driven against the [Live Demo](#live-demo) > at the top of this page using the iframe form > `socket.fire("webapp::instance::request", "", …)`. See the > [Window-event API](#window-event-api-api-requestinstance) for the equivalent > Try buttons. ### Recipe 1 — Get a handle to the live app Two equivalent ways to obtain the running `app` from a freshly opened window. Pick the one that fits your code style — both fire after the taskbar's `render()` resolves, so `app.mode()`, `app.addWidget()`, etc. are safe to call. ```js // (a) Promise-style via api-request::ready var taskbarWin = windowManager.open({ run: ["taskbar", "--preset=full"] }); taskbarWin.emit("api-request::ready", [function (whenReady) { whenReady.then(function (app) { // app is the live TaskbarApplication instance console.log("mode:", app.mode(), "widgets:", app.widgets().length); }, console.error); }]); // (b) Synchronous getter via api-request::instance taskbarWin.emit("api-request::instance", [function (err, app) { if (err) return console.error(err); // app is available now — but render() may not have run yet, // so widgets/state may still be empty until ready resolves. }]); ``` > **When to use which** — `api-request::ready` waits for the full render > pipeline (state load + initial widgets). `api-request::instance` returns the > raw app immediately and is preferred when you want to subscribe to events > before render finishes (so you don't miss `widget:added` for the initial > widget set). --- ### Recipe 2 — Spin up a quiet, persistence-free bar For dashboards, kiosk pages, or embedded scenes where you want the bar to behave the same on every reload regardless of what the user did last session: ```js windowManager.open({ run: [ "taskbar", "--preset=no-config", // no widgets, no context menu "--state-storage=none", // never read or write persistence "--mode=taskbar", "--position=bottom" ] }); ``` If you also want to wipe any old saved state from a previous run that used `localStorage`, do it once at boot: ```js localStorage.removeItem("sgapps-taskbar-state"); ``` --- ### Recipe 3 — Toggle layout + position from your own settings panel Drive the taskbar from a separate UI by holding onto the `app` reference. This is what a "Display preferences" dialog would do. ```js var taskbarWin = windowManager.open({ run: ["taskbar", "--preset=full"] }); var taskbarApp = null; taskbarWin.emit("api-request::ready", [function (p) { p.then(function (app) { taskbarApp = app; }); }]); // Wire your settings UI: document.getElementById("layout-dock").onclick = function () { taskbarApp.mode("dock"); }; document.getElementById("layout-bar").onclick = function () { taskbarApp.mode("taskbar"); }; document.getElementById("layout-mobile").onclick = function () { taskbarApp.mode("mobile"); }; document.getElementById("position-top").onclick = function () { taskbarApp.position("top"); }; document.getElementById("position-bottom").onclick = function () { taskbarApp.position("bottom"); }; document.getElementById("position-left").onclick = function () { taskbarApp.position("left"); }; ``` The same can be done over the window-event API if you only have a `taskbarWin` handle (no direct `app`): ```js function setMode(m) { taskbarWin.emit("api-request::instance:config:mode", [m]); } function setPosition(p) { taskbarWin.emit("api-request::instance:config:position", [p]); } ``` Every change is auto-persisted (when storage is enabled), so the user's choice survives a reload. --- ### Recipe 4 — Build a custom "notifications" widget end-to-end A widget is a factory `function (options, tbApp) -> { node, destroy? }`. Below is a full red-badge notifier that listens to a global event: ```js function notificationsWidgetFactory(options, tbApp) { var node = document.createElement("span"); node.className = "sgapps-taskbar--widget-inner"; var img = document.createElement("img"); img.src = Application.icon("paper/icons/status/user-available", true); var badge = document.createElement("em"); badge.textContent = "0"; node.appendChild(img); node.appendChild(badge); var count = 0; function bump() { count += 1; badge.textContent = String(count); node.classList.add("has-unread"); } function clear() { count = 0; badge.textContent = "0"; node.classList.remove("has-unread"); } node.addEventListener("click", clear); window.addEventListener("my-app:notification", bump); return { node: node, destroy: function () { window.removeEventListener("my-app:notification", bump); } }; } ``` Wire it up against a live taskbar: ```js var taskbarWin = windowManager.open({ run: ["taskbar", "--preset=full"] }); taskbarWin.emit("api-request::ready", [function (p) { p.then(function (app) { app.registerWidget("notifications", notificationsWidgetFactory); app.addWidget("notifications", { side: "right" }); }); }]); // Trigger a notification from anywhere in your app: window.dispatchEvent(new Event("my-app:notification")); ``` Optional — style via the auto-added class: ```css .sgapps-taskbar--widget-notifications.has-unread em { background: #d04; color: #fff; padding: 0 6px; border-radius: 999px; } ``` > **Persistence note** — When state storage is enabled, widgets are > re-instantiated by `type` on reload. Register the factory **before** the > taskbar's render finishes (i.e. in the same script that opens it) so the > entry isn't silently dropped. --- ### Recipe 5 — Polling widget (CPU / memory / status feed) Widgets are just DOM nodes — anything that ticks is a few lines: ```js function statusWidgetFactory(options, tbApp) { var node = document.createElement("span"); node.className = "sgapps-taskbar--widget-inner"; node.title = options.label || "Status"; var em = document.createElement("em"); em.textContent = "…"; node.appendChild(em); var url = options.url || "/api/status"; var ms = options.intervalMs || 5000; function tick() { fetch(url).then(function (r) { return r.json(); }) .then(function (data) { em.textContent = data.label || data.status || "ok"; node.classList.toggle("is-bad", data.severity === "error"); }) .catch(function () { em.textContent = "?"; }); } tick(); var t = setInterval(tick, ms); return { node: node, destroy: function () { clearInterval(t); } }; } // register, then add multiple instances pointing at different endpoints taskbarWin.emit("api-request::ready", [function (p) { p.then(function (app) { app.registerWidget("status", statusWidgetFactory); app.addWidget("status", { side: "right", options: { label: "API", url: "/api/health", intervalMs: 10000 } }); app.addWidget("status", { side: "right", options: { label: "Worker", url: "/worker/health", intervalMs: 30000 } }); }); }]); ``` --- ### Recipe 6 — Quick-launch icon for a specific app `addItem` puts a clickable icon in the left or right slot. Useful for "branding" buttons or for shortcuts to apps you want exactly one click away: ```js taskbarWin.emit("api-request::ready", [function (p) { p.then(function (app) { app.addItem({ icon: "paper/icons/apps/internet-web-browser", title: "Open Docs", side: "right", onClick: function () { windowManager.open({ run: ["xdg-open", "https://sgapps.io/docs"] }); } }); app.addItem({ icon: "paper/icons/apps/preferences-system", title: "Settings", side: "left", onClick: function () { windowManager.open({ run: ["sgapps/applications/settings"] }); }, onContextMenu: function (ev) { ev.preventDefault(); // open your own custom menu here } }); }); }]); ``` Items are not persisted (they're not part of the saved state) — re-add them on each load, the same way you'd re-register custom widget factories. --- ### Recipe 7 — Persist state to a remote backend Replace the built-in `localStorage` adapter with anything implementing `{ get(), set(state) }`. Both methods may return Promises; errors are swallowed, so your adapter can throw freely. ```js var taskbarWin = windowManager.open({ run: ["taskbar", "--preset=full"] }); taskbarWin.emit("api-request::ready", [function (p) { p.then(function (app) { app.stateStorage({ get: function () { return fetch("/api/taskbar/state", { credentials: "include" }) .then(function (r) { return r.ok ? r.json() : null; }); }, set: function (state) { return fetch("/api/taskbar/state", { method: "PUT", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(state) }); } }); // Force an initial save so the backend immediately has the current shape: app.saveState(); }); }]); ``` > **Caveat** — `stateStorage` is applied *after* the initial render, so if you > need server state to be the source of truth on first paint, also pass > `--state-storage=none` at launch and call `app.loadState()` yourself once > the backend handler is wired. --- ### Recipe 8 — Reset to defaults / wipe persisted state The user clicked "Reset taskbar" in your settings dialog: ```js taskbarWin.emit("api-request::ready", [function (p) { p.then(function (app) { // 1) detach every active widget app.widgets().forEach(function (w) { app.removeWidget(w.id); }); // 2) reapply the default preset app.preset("full"); // 3) wipe persistent state for next reload try { localStorage.removeItem(app.stateKey()); } catch (e) {} app.saveState(); }); }]); ``` --- ### Recipe 9 — Listen to window activity and react The taskbar emits high-level events that are easier to consume than raw window manager events: ```js taskbarWin.emit("api-request::ready", [function (p) { p.then(function (app) { app.on("layout:changed", function (conf) { console.log("Layout is now", conf.mode, "at", conf.position); }); app.on("item:activate", function (win) { // user clicked a taskbar entry try { console.log("activated:", win.title()); } catch (e) {} }); app.on("widget:added", function (w) { console.log("added widget", w.type); }); app.on("widget:removed", function (w) { console.log("removed widget", w.type); }); app.on("widget:unknown", function (type) { console.warn("addWidget called with unregistered type:", type); }); }); }]); ``` --- ### Recipe 10 — Hide the taskbar during fullscreen presentations Toggle the bar's window in/out of view without destroying it (so widgets keep their state): ```js function setTaskbarVisible(visible) { var node = taskbarWin.node(); if (!node) return; node.style.display = visible ? "" : "none"; // Also release reserved screen space: document.body.style.setProperty( "--sgapps-taskbar-size", visible ? "" : "0px" ); } document.addEventListener("fullscreenchange", function () { setTaskbarVisible(!document.fullscreenElement); }); ``` For permanent removal, close the window: `taskbarWin.close();` — `destroy()` runs automatically and cleans up widgets, body classes, and CSS variables. --- ### Recipe 11 — Drive a taskbar from a foreign script (window-events only) You only have a window handle (no direct `app`) — for example, the taskbar was opened from one bundle and you want to control it from another. Everything you need is exposed on the window event bus: ```js // Detect the running taskbar window from the manager var taskbarWin = windowManager.windows().filter(function (w) { try { return (w.args() || {}).mode !== undefined && w.title() === ""; } catch (e) { return false; } })[0]; // ... or just keep the handle when you opened it. // Configure remotely taskbarWin.emit("api-request::instance:config:mode", ["dock"]); taskbarWin.emit("api-request::instance:config:position", ["bottom"]); // Add a clock with a callback so we know when it landed taskbarWin.emit("api-request::instance:widgets:add", ["clock", { side: "right", options: { format: "HH:mm:ss" } }, function (err, id) { if (err) return console.error(err); console.log("clock widget id:", id); }]); // Snapshot the current configuration taskbarWin.emit("api-request::instance:state:serialize", [function (err, state) { console.log("current state:", state); }]); ``` --- ### Recipe 12 — Side-by-side: dock + slim status bar Two taskbar windows can run together — give them different state keys so they don't share persisted layout: ```js // Dock at the bottom for app launching windowManager.open({ run: [ "taskbar", "--mode=dock", "--position=bottom", "--widgets=launcher:left,show-desktop:left", "--state-key=sgapps-taskbar-dock" ] }); // Thin status bar at the top for system widgets windowManager.open({ run: [ "taskbar", "--mode=taskbar", "--position=top", "--no-context-menu", "--widgets=network:right,battery:right,clock:right", "--state-key=sgapps-taskbar-statusbar" ] }); ``` The taskbars reserve their own slice of screen space via independent body classes, so maximized windows correctly avoid both. --- ## Complete example — reusable desktop shell ```html ``` --- ## Troubleshooting **Taskbar opens at 240 × 120 at a random location.** Module loader hasn't finished wiring the widgets yet. Launch by name (`run: ["taskbar"]`) instead of importing the module manually — the loader guarantees exports resolve before the window opens. **Maximized apps still cover the bar.** Confirm `document.body` has the `has-sgapps-taskbar-*` classes applied, and that `taskbar.css` is actually loaded (DevTools → Network, filter `taskbar.css`). **State didn't restore after a reload.** Check the selected adapter (`app.stateStorage()`) and compare `app.serializeState()` to `localStorage.getItem("sgapps-taskbar-state")`. Saves only happen after the initial load resolves. **`--mode=auto` (or `--position=…`) appears to be ignored on launch.** Explicit launch flags are applied **after** any persisted state, so they always win. If you're still seeing the old layout, you have a sticky `localStorage` entry from an older build — clear it with `localStorage.removeItem("sgapps-taskbar-state")` (or the value of `--state-key`) and reopen the taskbar. To skip persistence entirely for a session pass `--state-storage=none`. **A custom widget I registered disappeared after reload.** Widgets are re-instantiated by type on reload. If the factory isn't registered again before the taskbar finishes loading, the entry is silently dropped. Either register the factory before opening the taskbar, or call `app.addWidget` explicitly after register. **Overflow arrow never appears.** Make sure you're on the latest build and that your viewport is genuinely too narrow — the overflow budget is `nav.clientWidth − leftSlot.offsetWidth − rightSlot.offsetWidth − 24..52`.