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.

Live Demo

<iframe id="tb-demo" src="/online/webapp/taskbar" width="100%" height="700" frameborder="0" style="border:1px solid #ccc; border-radius:4px;"></iframe>
<script>window._wsConnect('tb-demo', 'tbSocket');</script>
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.
<div style="display:flex;flex-wrap:wrap;gap:6px;padding:10px;background:#f6f8fa;border:1px solid #e1e4e8;border-radius:6px;margin:8px 0;"><strong style="width:100%;margin:0 0 4px 0;font-size:12px;color:#666;text-transform:uppercase;letter-spacing:.04em;">Quick actions on the live preview</strong><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:mode','taskbar')">Mode: taskbar</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:mode','dock')">Mode: dock</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:mode','mobile')">Mode: mobile</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:mode','auto')">Mode: auto</button><span style="width:1px;background:#ddd;margin:0 4px;"></span><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:position','top')">Pos: top</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:position','bottom')">Pos: bottom</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:position','left')">Pos: left</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:position','right')">Pos: right</button><span style="width:1px;background:#ddd;margin:0 4px;"></span><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','windows:open',['file-manager'])">+ File Manager</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','windows:open',['code-editor'])">+ Code Editor</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','windows:open',['media-player'])">+ Media Player</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','windows:open',['photo-editor'])">+ Photo Editor</button><span style="width:1px;background:#ddd;margin:0 4px;"></span><button onclick="(function(){var s=window._ws('tbSocket');if(!s)return;s.fire('webapp::instance::request','windows:closeAll');s.fire('webapp::instance::request','widgets:list',function(e,ws){(ws||[]).forEach(function(w){s.fire('webapp::instance::request','widgets:remove',w.id)});s.fire('webapp::instance::request','presets:apply','full');s.fire('webapp::instance::request','config:mode','taskbar');s.fire('webapp::instance::request','config:position','bottom')})})()" style="background:#e9ecef;">↻ Reset demo</button></div>

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

<iframe src="https://sgapps.io/online/webapp/taskbar"
    width="100%" height="600" frameborder="0"></iframe>

The same iframe accepts every api-request::instance:* event documented below.Drive it with the standard postMessage / WindowSocket bridge:

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 · Recipe 3
Position Pin to top, bottom, left, or right edge; switch between fixed/sticky Positions
Presets One-shot full / no-config / minimal configurations Configuration presets
Window tracking Shows every non-pinned window the manager opens; click to focus / minimize Tracks running apps
Built-in widgets clock, battery, network, launcher, show-desktop Widgets
Custom widgets Add your own factory and the bar persists / restores it across reloads Building a custom widget · Recipe 4 · Recipe 5
Quick-launch icons addItem(&#x7B; icon, title, side, onClick &#x7D;) — non-persisted shortcuts Recipe 6
Persistence localStorage / in-memory / disabled / custom remote backend Persistent configuration · Recipe 7
Programmatic control Direct app.method() calls or window-event API for foreign frames Programmatic control · Window-event API · Recipe 11
Lifecycle events layout:changed, item:activate, widget:added, widget:removed, widget:unknown Events · Recipe 9
Multi-instance Run a dock and a status bar side-by-side with separate state keys Recipe 12
Demo iframe Embed at /online/webapp/taskbar and drive every API event over postMessage Live Demo · Embed

Launch

// 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=&#x3C;name&#x3E; on launch, or swap at runtime:

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:

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:

For server-persisted configuration or a custom backing store, pass an object implementing &#x7B; get(), set(state) &#x7D;:

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: &#x22;HH:mm&#x22; (default), &#x22;HH:mm:ss&#x22;, &#x22;full&#x22;
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:

app.addWidget("clock", { side: "right", options: { format: "HH:mm:ss" } });

Remove by id:

app.removeWidget(someWidgetId);

List currently active widgets:

app.widgets();
// [ { id: "w-abc", type: "clock", side: "right", options: { format: "HH:mm" } }, ... ]

The launcher widget

Clicking the launcher opens a frosted popup with:


Building a custom widget

A widget is any factory returning &#x7B; node, destroy&#x3F; &#x7D;. Register it once with any taskbar instance, then add it to a slot. It automatically:

// 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); }
    };
};
// 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:

.sgapps-taskbar--widget-ping em { font-weight: bold; color: #d04; }

Programmatic control

Get a reference to the live app through the standard bridge:

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

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 itsconfiguration surface as window-events on the host window. This mirrors thefile-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 anode-style (value&#x3F;, callback) signature: pass a value to set, omit it to read.

socket.fire("webapp::instance::request", "<event>", arg1, arg2, …);

Every Try button below uses the iframe form against the demo above.

Get the live app instance

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

socket.fire("webapp::instance::request", "config:mode", "dock");
// "auto" | "taskbar" | "dock" | "mobile"

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:mode','taskbar')">Try: Mode = taskbar</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:mode','dock')">Try: Mode = dock</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:mode','mobile')">Try: Mode = mobile</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:mode','auto')">Try: Mode = auto</button>
Read the current mode (omit the value to read instead of set):

socket.fire("webapp::instance::request", "config:mode",
    function (err, mode) { console.log("Current mode:", mode); });

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:mode',function(e,m){alert('Current mode: '+(m||'(unknown)'))})">Try: Read current mode</button>

config:position -- Pin to a Viewport Edge

socket.fire("webapp::instance::request", "config:position", "top");
// "top" | "bottom" | "left" | "right"

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:position','top')">Try: Position = top</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:position','bottom')">Try: Position = bottom</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:position','left')">Try: Position = left</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:position','right')">Try: Position = right</button>

config:positionType -- Fixed vs Sticky

socket.fire("webapp::instance::request", "config:positionType", "fixed");  // "fixed" | "sticky"

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:positionType','fixed')">Try: Type = fixed</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:positionType','sticky')">Try: Type = sticky</button>

config:resolvedMode -- Read the Currently Resolved Mode

When mode is &#x22;auto&#x22;, resolvedMode reports what the bar is actuallyrendering as right now (&#x22;taskbar&#x22; or &#x22;mobile&#x22;).

socket.fire("webapp::instance::request", "config:resolvedMode",
    function (err, mode) { console.log("Resolved:", mode); });

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:resolvedMode',function(e,m){alert('Resolved mode: '+(m||'(unknown)'))})">Try: Read resolved mode</button>

config:autoMobileMaximize / config:contextMenuEnabled -- Toggles

socket.fire("webapp::instance::request", "config:autoMobileMaximize", false);
socket.fire("webapp::instance::request", "config:contextMenuEnabled", false);

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:autoMobileMaximize',false)">Try: Disable auto-maximize</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:autoMobileMaximize',true)">Try: Enable auto-maximize</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:contextMenuEnabled',false)">Try: Disable right-click menu</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:contextMenuEnabled',true)">Try: Enable right-click menu</button>


Widgets

widgets:add -- Append a Built-in Widget

socket.fire("webapp::instance::request", "widgets:add", "clock",
    { side: "right", options: { format: "HH:mm:ss" } },
    function (err, id) { console.log("widget id:", id); });

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','widgets:add','clock',{side:'right',options:{format:'HH:mm:ss'}},function(e,id){if(id)alert('Added clock: '+id)})">Try: Add clock (HH:mm:ss)</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','widgets:add','clock',{side:'right',options:{format:'full'}},function(e,id){if(id)alert('Added full-format clock: '+id)})">Try: Add clock (full)</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','widgets:add','battery',{side:'right'},function(e,id){if(id)alert('Added battery: '+id)})">Try: Add battery</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','widgets:add','network',{side:'right'},function(e,id){if(id)alert('Added network: '+id)})">Try: Add network</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','widgets:add','launcher',{side:'left'},function(e,id){if(id)alert('Added launcher: '+id)})">Try: Add launcher (left)</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','widgets:add','show-desktop',{side:'left'},function(e,id){if(id)alert('Added show-desktop (left)')})">Try: Add show-desktop</button>

widgets:list -- Inspect Active Widgets

socket.fire("webapp::instance::request", "widgets:list",
    function (err, widgets) { console.log(widgets); });
// -> [{ id, type, side, options }, ...]

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','widgets:list',function(e,ws){alert('Widgets ('+(ws||[]).length+'):\n\n'+JSON.stringify(ws,null,2))})">Try: List active widgets</button>

widgets:types -- See What Can Be Added

socket.fire("webapp::instance::request", "widgets:types",
    function (err, types) { console.log(types); });
// -> ["clock", "battery", "network", "launcher", "show-desktop", ...]

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','widgets:types',function(e,t){alert('Registered widget types:\n\n'+(t||[]).join(', '))})">Try: List widget types</button>

widgets:remove -- Detach a Widget

The Try button below chains widgets:list → widgets:remove to delete thelast widget in the bar:

socket.fire("webapp::instance::request", "widgets:remove", widgetId,
    function (err) { /* removed */ });

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','widgets:list',function(e,ws){if(!ws||!ws.length)return alert('No widgets to remove');var last=ws[ws.length-1];window.tbSocket.fire('webapp::instance::request','widgets:remove',last.id,function(e2){alert(e2?('Error: '+e2):('Removed '+last.type+' ('+last.id+')'))})})">Try: Remove last widget</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','widgets:list',function(e,ws){if(!ws||!ws.length)return alert('No widgets to remove');var i=0;var next=function(){if(i>=ws.length)return alert('Cleared '+ws.length+' widget(s).');window.tbSocket.fire('webapp::instance::request','widgets:remove',ws[i++].id,next)};next()})">Try: Remove all widgets</button>


Presets

presets:apply -- One-Shot Configuration

Resets the widget set, context menu state, and storage adapter in a singlecall. Three presets ship by default:

Preset Context menu Persistence Widgets
&#x22;full&#x22; enabled localStorage launcher / show-desktop / network / battery / clock
&#x22;no-config&#x22; disabled none (empty)
&#x22;minimal&#x22; enabled none clock

socket.fire("webapp::instance::request", "presets:apply", "full");

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','presets:apply','full')">Try: Preset = full</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','presets:apply','minimal')">Try: Preset = minimal</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','presets:apply','no-config')">Try: Preset = no-config</button>

presets:list -- Discover Available Presets

socket.fire("webapp::instance::request", "presets:list",
    function (err, names) { console.log(names); });

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','presets:list',function(e,p){alert('Available presets:\n\n'+(p||[]).join(', '))})">Try: List presets</button>


Custom Slot Items

addItem puts a clickable icon in the left or right slot. Items are notpersisted across reloads (re-add them at startup if you want them sticky).

items:add -- Inject a Quick-Launch Icon

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 () &#x7B; … &#x7D; along with the rest of the config.

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','items:add',{icon:'paper/icons/apps/internet-web-browser',title:'Docs',side:'right'},function(e,id){if(id)alert('Added item: '+id)})">Try: Add brand icon (right)</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','items:add',{icon:'paper/icons/apps/preferences-system',title:'Settings',side:'left'},function(e,id){if(id)alert('Added item: '+id)})">Try: Add settings icon (left)</button>

items:list -- Inspect Active Items

socket.fire("webapp::instance::request", "items:list",
    function (err, items) { console.log(items); });

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','items:list',function(e,it){alert('Custom items:\n\n'+JSON.stringify(it,null,2))})">Try: List custom items</button>


Tracked Windows

The taskbar tracks every window opened by the host windowManager. To makethe bar visually interesting in the iframe demo we expose a thin layer forspawning sample windows from the parent page.

windows:open -- Spawn a Window the Bar Will Track

// 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"]);

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','windows:open',['file-manager'])">Try: Open File Manager</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','windows:open',['code-editor'])">Try: Open Code Editor</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','windows:open',['media-player'])">Try: Open Media Player</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','windows:open',['photo-editor'])">Try: Open Photo Editor</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','windows:open',['archive-viewer'])">Try: Open Archive Viewer</button>

windows:list -- See What the Bar Is Tracking

socket.fire("webapp::instance::request", "windows:list",
    function (err, windows) { console.log(windows); });
// -> [{ title, isTaskbar }, ...]

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','windows:list',function(e,ws){alert('Open windows ('+(ws||[]).length+'):\n\n'+(ws||[]).map(function(w){return (w.isTaskbar?'[BAR] ':' ')+(w.title||'(untitled)')}).join('\n'))})">Try: List open windows</button>

windows:closeAll -- Tear Down Sample Windows

Closes every non-pinned window — except the taskbar itself, which protectsitself from this call.

socket.fire("webapp::instance::request", "windows:closeAll",
    function (err, n) { console.log("Closed", n, "windows"); });

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','windows:closeAll',function(e,n){alert('Closed '+(n||0)+' window(s).')})">Try: Close all sample windows</button>


State Persistence

state:serialize -- Snapshot the Current Configuration

Returns the exact object that would be written to the storage adapter.

socket.fire("webapp::instance::request", "state:serialize",
    function (err, snapshot) { console.log(snapshot); });

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','state:serialize',function(e,s){alert('Current state:\n\n'+JSON.stringify(s,null,2))})">Try: Snapshot state</button>

state:load / state:save -- Round-Trip Persistence

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 */ });

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','state:load',function(e,s){alert('Stored state:\n\n'+(s?JSON.stringify(s,null,2):'(none)'))})">Try: Read stored state</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','state:save',function(){alert('Saved.')})">Try: Force save</button>

config:stateStorage -- Switch Adapter at Runtime

socket.fire("webapp::instance::request", "config:stateStorage", "localStorage");
socket.fire("webapp::instance::request", "config:stateStorage", "memory");
socket.fire("webapp::instance::request", "config:stateStorage", "none");

<button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:stateStorage','localStorage')">Try: Use localStorage</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:stateStorage','memory')">Try: Use in-memory</button><button onclick="window._ws('tbSocket')&&window.tbSocket.fire('webapp::instance::request','config:stateStorage','none')">Try: Disable persistence</button>


Composed Demos

Higher-level scenarios that chain several events together — handy for showingoff the bar's behavior in one click.

Build a Stocked Dock at the Bottom

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"]);

<button onclick="(function(){var s=window._ws('tbSocket');if(!s)return;s.fire('webapp::instance::request','presets:apply','no-config');s.fire('webapp::instance::request','config:mode','dock');s.fire('webapp::instance::request','config:position','bottom');['launcher','show-desktop'].forEach(function(t){s.fire('webapp::instance::request','widgets:add',t,{side:'left'})});['network','battery','clock'].forEach(function(t){s.fire('webapp::instance::request','widgets:add',t,{side:'right'})});s.fire('webapp::instance::request','windows:open',['file-manager']);s.fire('webapp::instance::request','windows:open',['code-editor'])})()">Try: Build stocked dock</button>

Reset the Demo to a Clean State

// 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");
});

<button onclick="(function(){var s=window._ws('tbSocket');if(!s)return;s.fire('webapp::instance::request','windows:closeAll');s.fire('webapp::instance::request','widgets:list',function(e,ws){(ws||[]).forEach(function(w){s.fire('webapp::instance::request','widgets:remove',w.id)});s.fire('webapp::instance::request','presets:apply','minimal');s.fire('webapp::instance::request','config:mode','auto');s.fire('webapp::instance::request','config:position','bottom')})})()">Try: Reset demo</button>

Convert Bar → Side Dock → Back

// 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);

<button onclick="(function(){var s=window._ws('tbSocket');if(!s)return;s.fire('webapp::instance::request','config:mode','dock');s.fire('webapp::instance::request','config:position','left');setTimeout(function(){s.fire('webapp::instance::request','config:position','bottom')},2000)})()">Try: Bounce to left edge and back</button>

Walk Every Mode

var modes = ["taskbar", "dock", "mobile", "auto"];
modes.forEach(function (m, i) {
    setTimeout(function () {
        socket.fire("webapp::instance::request", "config:mode", m);
    }, i * 1500);
});

<button onclick="(function(){var s=window._ws('tbSocket');if(!s)return;['taskbar','dock','mobile','auto'].forEach(function(m,i){setTimeout(function(){s.fire('webapp::instance::request','config:mode',m)},i*1500)})})()">Try: Walk every mode (taskbar → dock → mobile → auto)</button>

Full example

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(&#x22;api-request::instance:config:mode&#x22;, [&#x22;auto&#x22;]) works.

Flows & Recipes

Practical, end-to-end snippets you can copy into a page. Every recipe assumeswindowManager is the global window manager (already present on any SGAppspage) and that the taskbar module is reachable via the registered nametaskbar.

Tip — Most recipes can also be driven against the Live Demo at the top of this page using the iframe form socket.fire(&#x22;webapp::instance::request&#x22;, &#x22;&#x3C;event&#x3E;&#x22;, …). See the Window-event API 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'srender() resolves, so app.mode(), app.addWidget(), etc. are safe to call.

// (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 tobehave the same on every reload regardless of what the user did last session:

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 usedlocalStorage, do it once at boot:

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. Thisis what a "Display preferences" dialog would do.

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 taskbarWinhandle (no direct app):

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 choicesurvives a reload.


Recipe 4 — Build a custom "notifications" widget end-to-end

A widget is a factory function (options, tbApp) -&#x3E; &#x7B; node, destroy&#x3F; &#x7D;. Belowis a full red-badge notifier that listens to a global event:

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:

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:

.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:

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:

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 oneach 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&#x7B; get(), set(state) &#x7D;. Both methods may return Promises; errors areswallowed, so your adapter can throw freely.

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:

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 windowmanager events:

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 keeptheir state):

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 wasopened from one bundle and you want to control it from another. Everything youneed is exposed on the window event bus:

// 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 theydon't share persisted layout:

// 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 bodyclasses, so maximized windows correctly avoid both.


Complete example — reusable desktop shell

<!DOCTYPE html>
<html>
<body>
    <!-- SGApps page (window manager already loaded on this domain) -->

    <script>
        // Launch the taskbar
        var taskbarWin = windowManager.open({
            run: ["taskbar", "--preset=full", "--mode=dock", "--position=bottom"]
        });

        // Get an API handle and teach it a new widget
        taskbarWin.emit("api-request::ready", [function (p) {
            p.then(function (app) {

                // Custom "notifications" widget
                app.registerWidget("notifications", function (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);
                        }
                    };
                });

                app.addWidget("notifications", { side: "right" });

                // Brand badge on the left
                app.addItem({
                    icon: "paper/icons/apps/preferences-system",
                    title: "My Company",
                    side: "left",
                    onClick: function () { windowManager.open({ run: ["xdg-open", "/help"] }); }
                });
            });
        }]);

        // Trigger a notification from anywhere
        function notify() {
            window.dispatchEvent(new Event("my-app:notification"));
        }
    </script>
</body>
</html>

Troubleshooting

sgapps.io