Configurable Vue 3 dashboard: drag-and-resize tiles on a grid, persist layout in the browser, and extend with new tile types. Built with Vite, TypeScript, vue-ts-responsive-grid-layout, and AJV for JSON Schema validation on API tiles.
npm install
npm run devnpm run build
npm run previewThe dashboard opens in Display mode: tiles cannot be moved or resized, and tile chrome has no Configure/Remove actions. Use Edit dashboard to change layout and settings; Done returns to Display mode. The active mode is stored in localStorage under dashly-dashboard-mode (display or edit).
TV mode (toolbar): hides tile chrome (title / configure / remove) and Refresh on API, API + Markdown, and API-number tiles for a cleaner wall display; text tiles hide their header only (body stays). Switches to Display mode automatically. Persisted under dashly-tv-mode (1 / 0). Use Exit TV mode to restore.
Dark mode: Toolbar icons — Moon switches to dark theme, Sun switches back to light. Preference is stored under dashly-theme (dark / light).
- Grid: In Edit mode, drag tiles by the header (
.tile-drag-handle) and resize from the corner handle. Display mode locks layout and hides tile edit actions. TV mode (toolbar) hides tile chrome on all tiles and the refresh bar on API / API + Markdown / API-number tiles only. Layout and tile settings are stored underlocalStoragekeydashly-dashboard-v1. - Export / Import: In Edit mode, use the toolbar JSON dropdown (Export JSON, Import JSON). Export JSON is also in that menu on Display mode for backups. Writes or reads a JSON document (
version,colNum,rowHeight,margin,tiles). - API tile:
GET/POST/… to a URL with optional headers/body, optional polling, optional JSON Schema (draft 2020-12) validation (AJV +ajv-formats), and optional property display: newline-separated JSON Pointers (e.g./id,/title) in configure — leave empty to show the full JSON body. - API + Markdown tile: Same HTTP options and optional JSON Schema validation as the API tile. Markdown is first rendered as a Nunjucks template (
jsoncontains the full response, top-level object keys are exposed directly), then converted with marked and sanitized with DOMPurify beforev-html. Legacy JSON Pointer placeholders{{/path}}(RFC 6901) still work and are interpolated before Nunjucks. - API number tile: Same HTTP options as the API tile, but the JSON response must yield one finite number — either the root value (
42) or a field addressed by an optional JSON Pointer (e.g./id). The value is shown large in the tile body with an optional label above the number (valueTitle); the chrome title is separate static text.
Assume your API response has fields like:
{
"title": "Deploy completed",
"publishedAt": "2026-05-07T14:36:22Z",
"createdAtUnix": 1778164582,
"meta": {
"tz": "Europe/Berlin"
}
}Basic fields and conditionals:
## {{ title }}
{% if json.meta %}
Has metadata.
{% else %}
No metadata.
{% endif %}Legacy JSON Pointer placeholder still works:
### Same title via pointer
{{ /title }}Date parsing and formatting:
Published (ISO): {{ publishedAt | dateISO }}
Published (US short): {{ publishedAt | dateFormat("en-US", "datetime", "short,short") }}
Published (German long): {{ publishedAt | dateFormat("de-DE", "datetime", "long,short") }}
Published (Berlin TZ): {{ publishedAt | dateFormat("de-DE", "datetime", "long,short", "Europe/Berlin") }}Unix timestamp support (fromUnix):
Created (unix seconds): {{ createdAtUnix | fromUnix("s") | dateFormat("en-GB", "datetime", "medium,short") }}The available date helpers are:
dateISO: parse value and output ISO stringdateFormat(locale, preset, styles, timeZone?): locale-aware formattingfromUnix(unit): convert Unix timestamp to Date ("s"or"ms")
Browsers only allow cross-origin fetch when the target API sends appropriate CORS headers. For development, this project includes a Vite proxy: URLs starting with /api-proxy/... are forwarded to https://jsonplaceholder.typicode.com (see vite.config.ts). Point an API tile at e.g. /api-proxy/posts/1 while running npm run dev.
For production, serve the app and APIs from the same origin or put a gateway in front.
- Create a Vue component under
src/tiles/types/<name>/that acceptsconfig: Record<string, unknown>and emitsconfigureandremove(seeTextTile.vue). - Add a
defaultConfig()factory insrc/tiles/tileConfigs.tsif you want typed defaults. - Register the type in
src/tiles/registry.ts. - Teach
TileConfigureDialog.vuehow to edit that tile’sconfig(forms or JSON fields).
The grid renders getTileDefinition(tile.type).component dynamically, so new types show up in Add tile automatically after registration.
{
"version": 1,
"colNum": 12,
"rowHeight": 36,
"margin": [10, 10],
"tiles": [
{
"id": "uuid",
"type": "text",
"layout": { "x": 0, "y": 0, "w": 4, "h": 4 },
"config": { "title": "…", "body": "…" }
},
{
"id": "uuid",
"type": "api",
"layout": { "x": 0, "y": 0, "w": 4, "h": 4 },
"config": {
"title": "…",
"url": "https://…",
"displayPathsText": "/id\n/title",
"responseSchemaText": "{ … }"
}
},
{
"id": "uuid",
"type": "apiMarkdown",
"layout": { "x": 0, "y": 0, "w": 4, "h": 4 },
"config": {
"title": "…",
"url": "https://…",
"markdownTemplate": "## {{ /title }}\n\n{% if body %}{{ body }}{% else %}_No body_{% endif %}",
"responseSchemaText": "{ … }"
}
}
]
}- The grid package lists
style.cssinexportsbut shipsvue-ts-responsive-grid-layout.css; the app imports the CSS file fromnode_modulesdirectly insrc/main.ts. - Upstream typings use a misspelled
"typeings"field inpackage.json, sosrc/env.d.tsdeclares the module forvue-tsc.