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.
taskbarsgapps/applications/window-manager/taskbarwindowManager.open(...) window)<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.
<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" });| 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({ icon, title, side, onClick }) — 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 |
// 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"]
});| 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 |
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.
| Preset | Context menu | Persistence | Widgets |
|---|---|---|---|
full | enabled | localStorage | launcher / show-desktop / network / battery / clock |
no-config | disabled | none | none |
minimal | enabled | none | clock |
--preset=<name> on launch, or swap at runtime:taskbarWin.emit("api-request::ready", [function (p) {
p.then(function (app) { app.preset("no-config"); });
}]);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.
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.
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.
┌──────────────┐
│ 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).
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 sessionnone — no persistenceFor server-persisted configuration or a custom backing store, pass an object implementing { get(), set(state) }:
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 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 | — |
--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" } }, ... ]Clicking the launcher opens a frosted popup with:
category field if the app descriptor defines one).windowManager.applicationsLibrary(), so any runtime-registered app shows up automatically.A widget is any factory returning { node, destroy? }. Register it once with any taskbar instance, then add it to a slot. It automatically:
sgapps-taskbar--widget and sgapps-taskbar--widget-<type>.{ type, side, options } and re-instantiates through your factory).// 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; }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
});
}]);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 */ });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?, callback) signature: pass a value to set, omit it to read.
taskbarWin.emit("api-request::instance:<event>", [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:socket.fire("webapp::instance::request", "<event>", arg1, arg2, …);Every Try button below uses the iframe form against the demo above.
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
}]);config:mode -- Switch Layout Modesocket.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 Edgesocket.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 Stickysocket.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 ModeWhen mode is "auto", resolvedMode reports what the bar is actuallyrendering as right now ("taskbar" or "mobile").
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 -- Togglessocket.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:add -- Append a Built-in Widgetsocket.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 Widgetssocket.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 Addedsocket.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 WidgetThe 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:apply -- One-Shot ConfigurationResets the widget set, context menu state, and storage adapter in a singlecall. 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 |
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 Presetssocket.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>
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 Iconsocket.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 theonClickhandler can't cross the postMessage boundary, so the Try buttons below add icon-only items. In your own pages, pass an inlinefunction () { … }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 Itemssocket.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>
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 Trackingsocket.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 WindowsCloses 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:serialize -- Snapshot the Current ConfigurationReturns 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 Persistencesocket.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 Runtimesocket.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>
Higher-level scenarios that chain several events together — handy for showingoff the bar's behavior in one click.
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>
// 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>
// 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>
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>
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.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("webapp::instance::request", "<event>", …). See the
Window-event API for the equivalent
Try buttons.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::readywaits for the full render pipeline (state load + initial widgets).api-request::instancereturns the raw app immediately and is preferred when you want to subscribe to events before render finishes (so you don't misswidget:addedfor the initial widget set).
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");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.
A widget is a factory function (options, tbApp) -> { node, destroy? }. 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.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 } });
});
}]);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.
Replace the built-in localStorage adapter with anything implementing{ get(), set(state) }. 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 —stateStorageis applied after the initial render, so if you need server state to be the source of truth on first paint, also pass--state-storage=noneat launch and callapp.loadState()yourself once the backend handler is wired.
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();
});
}]);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);
});
});
}]);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.
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);
}]);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.
<!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>run: ["taskbar"]) instead of importing the module manually — the loader guarantees exports resolve before the window opens.document.body has the has-sgapps-taskbar-* classes applied, and that taskbar.css is actually loaded (DevTools → Network, filter taskbar.css).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.app.addWidget explicitly after register.nav.clientWidth − leftSlot.offsetWidth − rightSlot.offsetWidth − 24..52.