Manifest reference
Every manifest.json field: identity, versions, entry, sizing, permissions, settings, popout, and bundled fonts.
manifest.json sits at the root of the widget folder and declares everything
about the widget up front: identity, what it may touch, its settings, and its
size on the grid. The loader is strict: unknown fields are rejected, and an
invalid manifest quarantines the widget with a reason instead of loading it.
A minimal manifest:
{
"$schema": "./.deskdash/manifest.schema.json",
"id": "clock",
"name": "Clock",
"version": "0.1.0",
"apiVersion": 1,
"permissions": { "time": true },
"defaultSize": { "w": 4, "h": 2 }
}Top-level fields
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
$schema | string | no | none | Editor tooling metadata (the dev kit writes ./.deskdash/manifest.schema.json). Accepted and ignored by the app. |
id | string | yes | The widget id. Lowercase letters, digits, . and -, starting with a letter or digit. Must equal the folder name. | |
name | string | yes | Display name. | |
version | string | yes | The widget version. The app treats it as opaque; the marketplace requires dotted numbers (like 1.2.3) and a strictly higher version on every update. | |
apiVersion | number | yes | The bridge API contract the widget was written against (currently 1). See errors and versioning. | |
minAppVersion | string | no | none | "This widget needs DeskDash x.y.z or newer." A feature floor, checked before anything else, so a too-old app shows a friendly message instead of a parse error. |
entry | string | no | index.html | The widget document, a file inside the widget folder. Relative paths only: no leading /, no .., no \. |
description | string | no | "" | Short description. |
author | string | no | "" | Author name. Marketplace installs get this stamped by the store. |
permissions | object | no | all off | What the widget may touch. See the permissions reference. |
interactive | boolean | no | false | The widget wants real mouse input: while the cursor is over it, the window captures the mouse. Non-interactive widgets can never be clicked. |
contextMenu | boolean | no | true | false opts the widget out of the right-click widget menu: in view mode right-clicks pass through it (to a widget beneath, or to the desktop's own menu), so a decorative widget is fully inert. Edit mode ignores the flag, so the widget stays configurable and removable there. Needs "minAppVersion": "0.2.3". |
settings | array | no | [] | Setting fields and preset names, mixed. The marketplace accepts up to 32 entries; a preset name counts as one. See below. |
defaultSize | { w, h } | no | { "w": 4, "h": 2 } | Size in grid cells when the widget is added. Each dimension 1..64. |
minSize | { w, h } | no | { "w": 1, "h": 1 } | The smallest the user can resize it to. Each dimension 1..64. |
popout | object | no | none | Declares the popout capability. See below. |
fonts | object | no | none | Bundled font pins. Needs "minAppVersion": "0.2.0". See below. |
Settings
Each settings entry is either a full field object or a preset name string;
the array accepts both, mixed:
"settings": [
{ "key": "title", "type": "text", "label": "Title", "default": "Tasks" },
"panelStyle"
]The marketplace accepts up to 32 entries in settings; a preset name counts
as one entry, whatever it expands to.
Field objects
| Key | Type | Required | Meaning |
|---|---|---|---|
key | string | yes | The name your code reads the value under. |
type | string | yes | One of the field types below. |
label | string | yes | Shown in the settings dialog. |
default | any | no | The value before the user edits anything. Ignored for secretRef, filePath, and instances (see below). |
options | string[] | no | The choices for a select. A select with no default falls back to its first option. |
min, max | number | no | Bounds for a number, and max caps the rows of a list. They are numbers, so date and time take no bounds. |
placeholder | string | no | Hint text for empty text and filePath inputs, and for an empty list row. Needs "minAppVersion": "0.1.10". |
pick | "file" or "folder" | no | Adds a Browse button beside a filePath input and controls what it picks. Needs "minAppVersion": "0.1.10". |
showIf | { "key", "equals" } | no | Renders the field only while the sibling setting named key has the value equals (its default counts while unset). Use primitive values; a key that names no sibling fails open and the field shows. Needs "minAppVersion": "0.2.6". |
The date and time types need "minAppVersion": "0.3.4", and list needs
"minAppVersion": "0.3.7". Older apps do not know them and quarantine the widget.
Field types
| Type | The user gets | Your code gets |
|---|---|---|
text | a text input | string |
number | a number input (min/max bounds) | number |
boolean | a toggle | boolean |
select | a dropdown of options | string |
color | a color picker | string |
date | a calendar picker | a "YYYY-MM-DD" string, or "" while unset; dd.dates.fromDayKey makes it a local Date |
time | a clock picker | an "HH:MM" string, or "" while unset |
list | text rows they add, reorder and remove | string[], always an array; [] when they added nothing and the manifest declares no default |
secretRef | a picker over their stored secrets | a name usable in dd.http.fetch secrets; never the value |
filePath | a path input (plus Browse with pick) | nothing directly; used as settingKey in dd.files, dd.folders, dd.dialogs, dd.claudeUsage |
instances | a picker over other widget instances | nothing directly; feeds dd.widgets |
The three consent-bearing types (secretRef, filePath, instances) never
inherit a manifest default. Their value grants access to something the user
owns (a credential, a file, other widgets), so only a value the user actively
picked counts; a published widget cannot ship pre-pointed at anyone's
private data.
Setting presets
A preset bundles a recurring field (or field pair) with the code that applies it, declared by name:
| Preset | Expands to | Applied by |
|---|---|---|
panelStyle | a Panel style select (none / solid / transparent / image, default solid, key panel), plus from app 0.2.4 a Background image file picker (key panelImage) and an Image fit select (key panelImageFit), both shown in the settings dialog only while the style is image | <dd-panel settings="panelStyle"> toggles its transparent / bare attributes and paints the image; zero widget code |
accentColor | a Use theme accent color toggle (key useAccent, default on) plus a Custom color picker (key color) | your code, via dd.theme.accent(settings) or dd.theme.bindAccent(cb); it has no component-side auto-apply |
systray | a Show the system tray toggle (key showTray, default on) | <dd-tray-caret settings="systray"> shows or hides the caret; zero widget code |
pins | a Show pinned apps toggle (key showPins, default on) | <dd-apps settings="pins"> shows or hides the launchers (and never asks the host for pins while off); zero widget code. Bar widgets only, app 0.3.0+ |
weekStart | a Week starts on select (monday / sunday, key weekStart); app 0.3.4+ | your code, via dd.dates.startOfWeek(d, settings.weekStart), which takes the string as stored |
textScale | a Text size select (tiny / small / medium / large / huge, key textSize); app 0.3.4+ | your code mirrors it onto the body (document.body.dataset.textScale = value); the base stylesheet turns it into the --dd-text-scale multiplier, which the autoscale skeleton and any calc(... * var(--dd-text-scale, 1)) of your own ride |
An unknown preset name quarantines the manifest. To keep a preset's behavior
but change its default, declare the field inline with the same key and your
default, and still put the preset name on the component; it keys off the
setting value regardless of how the field was declared. Declare all of the
preset's fields when you do, and drop the preset name from the manifest:
expansion appends without deduplicating, so keeping both lists a field twice.
Published manifests carry presets already expanded (the store renders the field list from them), so a preset gaining a field reaches installed widgets only through a new widget version.
Bar
"kind": "bar",
"bar": {
"thickness": 64, "reserve": 64, "edges": ["left", "right"], "allowOverlay": true,
"autoHide": { "hideDelay": 150, "revealBand": 2, "slide": 140 }
}A kind: "bar" manifest renders as the taskbar instead of a grid widget
and must carry a bar block. thickness (logical px, 24 to 128) is the
window's extent away from its edge, and the user may scale it (the Size
slider in the bar's settings dialog, 50% to 200%), so treat it as the design
size and lay out against the viewport; reserve (0 to thickness, default =
thickness) is the strip the OS reserves, and a smaller value leaves
transparent headroom for hover animations; edges (default ["bottom"];
"left" and "right" make a vertical bar) lists the edges the bar can
dock to, the first being the default and several giving the user a Side
choice; allowOverlay (default true) lets the user drop the reservation;
autoHide (optional, every field optional) tunes the user's auto-hide:
hideDelay ms before the bar hides (0 to 2000, default 150), revealBand
logical px on the screen edge that reveals it (1 to 8, default 2), slide
ms for the slide (0 to 600, default 140). Side, auto-hide and overlay are
picked in the bar's own settings dialog. Bars need
"minAppVersion": "0.3.0". The bar widgets
reference covers the surface.
A bar declares no explicit filePath, secretRef or instances field: the
bar's settings dialog never offers those types, so the user could never set
one, and the loader, the upload validator and the CI check all refuse the
manifest (app 0.3.7 or newer; older apps loaded it and never showed the
field). Preset names are exempt: panelStyle keeps its image field on a bar,
and its image option stays unavailable there (solid and transparent
work).
Popout
"popout": { "max": { "w": 8, "h": 6 }, "entry": "pane.html" }Presence declares that the widget may temporarily extend past its grid rect
via dd.popout.open. It
is a geometry capability like interactive, not a privacy permission: the
pane is a second sandboxed document with the same grants. A kind: "bar"
widget declares it without max (its pane renders beside the strip, capped
to the display).
| Key | Meaning |
|---|---|
max | Caps the pane size, in base grid cells like defaultSize. Each dimension 1..64. |
entry | The pane document, a file in the widget folder (same path rules as entry). |
Fonts
Optional, and it needs "minAppVersion": "0.2.0" (older apps do not know the
key). Pins a font onto one or more of the three face tokens, in this
widget's documents only:
"fonts": { "use": { "display": "audiowide" } }use takes any of sans, display, mono. Values are bundled catalog ids
(audiowide, nunito, nunito-sans, and from app 0.2.4 the pixel set
pixelify-sans, press-start-2p, silkscreen; an unknown id quarantines
the manifest, so the pixel ids also need "minAppVersion": "0.2.4") or a
self: reference to a family the widget ships itself:
"fonts": {
"bundle": [{ "id": "heading", "files": [{ "src": "fonts/mine.woff2", "weight": "400" }] }],
"use": { "display": "self:heading" }
}| Key | Meaning |
|---|---|
bundle | Families shipped in the widget folder. Each has an id (for self: references) and 1..6 files. |
bundle[].files[].src | Path inside the widget folder, charset A-Za-z0-9._/-. |
bundle[].files[].weight | One value or a variable range ("200 1000"), each within 1..1000. Default "400". |
bundle[].files[].style | "normal" (default) or "italic". |
use | Optional pins: sans, display, mono, each a catalog id or self:<bundle id>. |
Rules, enforced at manifest load and again by the marketplace validators: woff2 only (sniffed by magic number, never by extension); at most 4 families, 6 files per family, 2 MB per file, and 4 MB of fonts per widget. The host generates all resulting CSS itself and names families by index, so nothing a manifest says ever reaches a stylesheet.
The pin wins over themes and user overrides inside your documents. Your CSS
keeps using the token (font-family: var(--dd-font-display)). Two notes:
dd.theme.tokens() does not see pins, dd.ui.token() does, so canvas
drawing must resolve faces through
dd.ui.token(). Every bundled catalog face
also exists as a convenience var (--dd-font-audiowide, --dd-font-nunito,
--dd-font-nunito-sans, --dd-font-pixelify-sans,
--dd-font-press-start-2p, --dd-font-silkscreen), so a select setting
can offer a font choice without a literal family name in widget code.
Publishing note: the marketplace requires every font-shaped file in the
folder to be declared in fonts.bundle, holds font-carrying submissions for
licensing review, and requires the font's license to permit embedding and
redistribution, with the license text included in the widget.
Validation
What the loader enforces, in order:
minAppVersion, when present, must be a string that parses asx.y.z, and the running app must satisfy it. A too-old app reports "needs DeskDash x.y.z or newer".apiVersionnewer than the host reports "requires a newer dashboard".- The strict parse: unknown fields anywhere are rejected.
idmust match^[a-z0-9][a-z0-9.-]*$and equal the folder name.entry(andpopout.entry) must be a relative path inside the widget folder: no leading/, no.., no\.- Sizes (
defaultSize,minSize,popout.max) must each be 1..64 cells per dimension;popout.maxis required for grid widgets and absent on bars.bar.edgesis a non-empty list of distinct edges;bar.autoHidefields must sit in their ranges (hideDelay0 to 2000,revealBand1 to 8,slide0 to 600). - A
kind: "bar"manifest may not declare an explicitfilePath,secretReforinstancesfield (app 0.3.7 or newer); preset names are exempt. fonts, when present, is validated file by file (existence, format, caps) before the widget loads. Fail closed: a bad font spec quarantines the manifest.
A quarantined widget shows up with its reason in the widget list; it never takes the rest of the dashboard down.