Compare commits

..
Author SHA1 Message Date
Waters ChurchandClaude Opus 5 f72170b4e1 The folding section reads as a heading first
Theme's title was coming out at the normal weight — <summary> does not
inherit an <h1>'s boldness, so sitting it under .admin-h1 got the size,
colour and spacing but not the type. It now matches the other section
headings exactly.

The caret moved to the right of the words. A marker in front pushes the
heading out of line with every other one on the page; behind it, the section
still reads as "Theme" first and the caret only says there is more.

Written as the pattern rather than as Theme's own styling: any section that
folds is a <details class="admin-fold"> headed by a
<summary class="admin-h1 admin-fold-title">, and the whole row is the target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 13:22:11 -04:00
Waters ChurchandClaude Opus 5 ea1eac1c28 Twelve themes, and the Theme section folds away
Seven more palettes: Midnight (indigo, violet), Moss (forest floor, lime),
Plum (aubergine, purple), Sepia (warm brown with a cool teal — Ember's
arrangement reversed), Blackout (true black for a dark room, where any glow
off the screen carries), Paper (the warm light one, next to Daylight's cool
white) and Iron, which has no colour of its own: its accent is near-white, so
the only hues on screen are a module's own and the red and amber that mean
something.

Each holds the line the first five set. Text on panel lands between 14.8:1
and 19.8:1, muted between 5.8:1 and 7.4:1, and every accent clears its own
theme's danger red and warning amber by more than Ember already does — the
narrowest of the new ones is Moss at 54 degrees of hue, against Ember's 18.

The Theme section now starts folded, wearing the same disclosure triangle as
a module's config groups. A building picks its palette once and then wants
the rest of the page, which was getting further down the screen with every
swatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 13:16:47 -04:00
14 changed files with 258 additions and 432 deletions
-6
View File
@@ -32,12 +32,6 @@ title-bar controls). The rules that matter most:
and bump the module's `version` on every change. and bump the module's `version` on every change.
- Browsers never call upstream services directly; the module's server routes - Browsers never call upstream services directly; the module's server routes
proxy everything (no CORS, secrets stay server-side). proxy everything (no CORS, secrets stay server-side).
- **Keep the interface clean** (guide rules 5-11): no `help` and no label
over three words in a tile's gear; server-side settings in `configSchema`
only; a tile setting changes that tile alone; hide-toggles ungrouped when
there are four or fewer, in a folded **Show** group when there are five or
more; title-bar controls are icons with tooltips, never labels, and never
repeat something the gear already offers.
- Style with the shell's CSS variables only, scoped under - Style with the shell's CSS variables only, scoped under
`[data-module="<id>"]`; survive upstream loss (reconnect + `setStatus`). `[data-module="<id>"]`; survive upstream loss (reconnect + `setStatus`).
- Verify against the guide's testing section and resilience checklist. Mocks - Verify against the guide's testing section and resilience checklist. Mocks
+21 -7
View File
@@ -102,7 +102,7 @@ with this machine's shell settings — the same keys as the checked-in
| `adminPin` | The admin PIN, as a scrypt hash. **Set it in the admin page's Admin PIN section**, not by hand — 6 digits, and only the hash is stored. Absent means admin is open to anyone on the network. | | `adminPin` | The admin PIN, as a scrypt hash. **Set it in the admin page's Admin PIN section**, not by hand — 6 digits, and only the hash is stored. Absent means admin is open to anyone on the network. |
| `adminPasscode` | An older free-text passcode. Still honoured, so an existing install keeps working; new installs should use `adminPin`. | | `adminPasscode` | An older free-text passcode. Still honoured, so an existing install keeps working; new installs should use `adminPin`. |
| `forge` | Where updates and module installs are fetched from. Absent or `"github"` uses GitHub; a Gitea or Forgejo base URL such as `"http://10.3.8.90:3000"` uses a server of your own. Set it per machine in the data directory, alongside `repo`. | | `forge` | Where updates and module installs are fetched from. Absent or `"github"` uses GitHub; a Gitea or Forgejo base URL such as `"http://10.3.8.90:3000"` uses a server of your own. Set it per machine in the data directory, alongside `repo`. |
| `theme` | The dashboard palette: `booth` (default), `harbor`, `graphite`, `ember` or `daylight`. The admin page's **Theme** section writes this one — see [Themes](#themes). | | `theme` | The dashboard palette: `booth` (default), `harbor`, `graphite`, `ember`, `daylight`, `midnight`, `moss`, `plum`, `sepia`, `blackout`, `paper` or `iron`. The admin page's **Theme** section writes this one — see [Themes](#themes). |
**Locked out?** Delete `adminPin` from the data directory's `proddash.json` **Locked out?** Delete `adminPin` from the data directory's `proddash.json`
and restart, or start the server with `PRODDASH_PASSCODE` set — that passcode and restart, or start the server with `PRODDASH_PASSCODE` set — that passcode
@@ -140,12 +140,26 @@ ignored from then on.
The whole dashboard — every tile, on every browser — draws with one palette, The whole dashboard — every tile, on every browser — draws with one palette,
chosen in the admin page's **Theme** section and switched live: pick a swatch chosen in the admin page's **Theme** section and switched live: pick a swatch
and open dashboards change without a reload. Five presets ship: **Booth** and open dashboards change without a reload. The section starts folded —
(the default dark, mint accent), **Harbor** (deep navy, cyan), **Graphite** a building picks its palette once — and holds twelve presets:
(neutral charcoal, electric blue), **Ember** (warm broadcast black, orange)
and **Daylight**, a light palette for a front-of-house desk in daytime. Each | | |
keeps the status colours apart — the accent is never near the danger red or |---|---|
the warning amber — and muted text readable in a dark booth. The choice is | **Booth** | the default dark, mint accent |
| **Harbor** | deep navy, cyan |
| **Graphite** | neutral charcoal, electric blue |
| **Ember** | warm broadcast black, orange |
| **Daylight** | a cool light palette, for a front-of-house desk in daytime |
| **Midnight** | indigo night, violet |
| **Moss** | forest floor, lime |
| **Plum** | aubergine, purple |
| **Sepia** | warm brown room, cool teal — Ember's arrangement reversed |
| **Blackout** | true black, cyan, for a dark room where any glow carries |
| **Paper** | the warm light one, indigo accent |
| **Iron** | no colour of its own: a near-white accent, so the only hues on screen are a module's own and the red and amber that mean something |
Each keeps the status colours apart — the accent is never near the danger red
or the warning amber — and muted text readable in a dark booth. The choice is
stored as `theme` in the data directory's `proddash.json` (table above), so stored as `theme` in the data directory's `proddash.json` (table above), so
it holds across restarts and updates; a browser remembers the last theme it it holds across restarts and updates; a browser remembers the last theme it
saw and applies it before the server answers, so nothing flashes. Modules saw and applies it before the server answers, so nothing flashes. Modules
+16 -62
View File
@@ -34,46 +34,6 @@ people running the booth depend on them.
The admin page refuses to install a module whose requirement the running The admin page refuses to install a module whose requirement the running
shell doesn't meet, and it offers updates by comparing versions. shell doesn't meet, and it offers updates by comparing versions.
### Keeping the interface clean
A dashboard is read across a booth, at a glance, by someone doing three
other things. These rules are what keep every tile looking like one product
instead of six. They are as binding as the four above.
5. **No explanatory text in a tile's gear.** No `help` on an
`instanceSchema` field or an `instanceGroups` entry. Labels are **three
words at most, one or two preferred** — "Slide count", "Item tags",
"Running clock". A control that cannot be understood in three words is
not a display choice; it belongs in `configSchema`, where `help` is
welcome and expected. *One exception:* a `template` field may keep a
single `help` line naming its placeholders, which nobody can guess.
6. **Server-side settings are admin-only.** Anything the module's server
acts on — a host, a credential, what to fetch, how often to poll — is
`configSchema`. `instanceSchema` is display choices and nothing else:
what this tile shows, and how big.
7. **A tile setting changes that tile and nothing else.** Never another
tile, never the module as a whole, never the server. If two tiles must
agree on something, it is either admin config or page state the shell
owns — not a setting each tile keeps its own copy of.
8. **Offer toggles for the elements you draw.** Every tile should let the
operator switch off the parts it does not need. **One to four toggles:**
give them no `group` at all, so they sit unlabelled at the top of the
gear. **Five or more:** one group named **Show**, `collapsed: true`,
`columns: 2`. Nothing else goes in that group — colours, formats and
text belong in groups of their own.
9. **Title-bar controls are icons.** No `label` on `addButton` or
`addMenu`; the tooltip carries the words, and `docs` above covers drawing
the icon. A small state indicator may ride alongside an icon — ProdCom's
channel button shows a count while a filter is on — but a control is
never named in the bar.
10. **If a control needs words to explain it, it is not a title-bar
control.** Put it in the gear. The bar is for actions whose icon and
tooltip say everything: refresh, clear, jump to latest, bigger text.
11. **One control per action.** Whatever has a dedicated title-bar button is
not offered again in the gear, and the reverse. Text size is A− / A+ up
there, never a "Text size" field; a timestamps button in the bar means
no timestamps switch below it.
## The shape of a module ## The shape of a module
A module is one folder dropped into `modules/`: A module is one folder dropped into `modules/`:
@@ -217,18 +177,14 @@ the group name:
`help` on a field puts one line of guidance under its control — allowed `help` on a field puts one line of guidance under its control — allowed
placeholders, where a value comes from. Keep labels short and put the placeholders, where a value comes from. Keep labels short and put the
explanation there. **This is for `configSchema` only**: a tile's gear explanation there.
carries no explanations at all (rule 5), so a setting that needs one is
telling you it belongs in admin.
`group` works on `instanceSchema` fields too, and a manifest `group` and `help` work on `instanceSchema` fields too, and a manifest
`instanceGroups` map plays the `configGroups` role for the tile's gear `instanceGroups` map plays the `configGroups` role for the tile's gear
popover (`collapsed: true | false`, `columns`, `toggle` — but not `help`, popover (`collapsed: true | false`, `columns`, `help`, `toggle`). That is how
per rule 5). That is how a module with a long show/hide checklist keeps the a module with a long show/hide checklist keeps the popover usable — see
popover usable: five or more hide-toggles go in one **Show** group, folded `pco-plan`. Display choices belong there, per tile; admin config is for
by default, and fewer than five go in no group at all — see `pco-plan` for connections. The popover itself is a small window beside the tile — sized to
the long form and `slack` for the short one. Display choices belong there,
per tile; admin config is for connections. The popover itself is a small window beside the tile — sized to
its content, moved by the slim bar on top, closed by clicking anywhere else its content, moved by the slim bar on top, closed by clicking anywhere else
or pressing Escape — so a tile never has to be big enough to hold its own or pressing Escape — so a tile never has to be big enough to hold its own
settings, and a change can be watched as it lands. Under everything you settings, and a change can be watched as it lands. Under everything you
@@ -398,19 +354,17 @@ menu.setLabel('Channels (2/4) ▾'); // update the button text any time
``` ```
Everything you add is removed automatically when your instance stops, so a Everything you add is removed automatically when your instance stops, so a
remount never duplicates controls. **Icons only, no labels** (rule 9): the remount never duplicates controls. Keep them compact (icon buttons, short
title bar is 32 px tall and shared with the tile's name, and the tooltip labels): the title bar is 32 px tall and shared with the tile's name. Draw
says what the icon means. Draw icons as inline SVG rather than characters — icons as inline SVG rather than characters — a glyph such as ✕ or + takes
a glyph such as ✕ or + takes its size from whatever font each browser its size from whatever font each browser picks; text-size buttons are a
picks; text-size buttons are a drawn − and +. A small state indicator may drawn − and +.
ride alongside an icon (ProdCom's channel button carries a count while a
filter is on); a name may not.
**One control per action** (rule 11). Whatever has a dedicated header button **One control per action.** Whatever has a dedicated header button is not
is not offered again in the gear popover: text size is − / + up here offered again in the gear popover: text size is A− / A+ up here (persisted
(persisted through `saveInstanceSettings({ textSize })`, read back with a through `saveInstanceSettings({ textSize })`, read back with a sane
sane fallback), never a "Text size" field in `instanceSchema`; a timestamps fallback), never a "Text size" field in `instanceSchema`; a timestamps or
or newest-first toggle in the header means no such switch in the gear. The newest-first toggle in the header means no such switch in the gear. The
gear is for display choices that have no button of their own. gear is for display choices that have no button of their own.
One more thing to know: the whole title bar folds away when the dashboard One more thing to know: the whole title bar folds away when the dashboard
+49 -245
View File
@@ -1,23 +1,15 @@
{ {
"id": "pco-plan", "id": "pco-plan",
"name": "PCO Plan", "name": "PCO Plan",
"version": "1.5.0", "version": "1.4.0",
"proddash": ">=1.4.0", "proddash": ">=1.4.0",
"description": "Order of service from Planning Center Services", "description": "Order of service from Planning Center Services",
"provides": [ "provides": ["timers"],
"timers"
],
"client": "client.js", "client": "client.js",
"server": "server.js", "server": "server.js",
"style": "style.css", "style": "style.css",
"minSize": { "minSize": { "w": 3, "h": 3 },
"w": 3, "defaultSize": { "w": 5, "h": 6 },
"h": 3
},
"defaultSize": {
"w": 5,
"h": 6
},
"configGroups": { "configGroups": {
"Planning Center credentials": { "Planning Center credentials": {
"collapsed": "whenSet", "collapsed": "whenSet",
@@ -56,12 +48,7 @@
"type": "select", "type": "select",
"label": "Plan", "label": "Plan",
"default": "", "default": "",
"options": [ "options": [{ "value": "", "label": "Automatic — today's plan, else the next upcoming" }],
{
"value": "",
"label": "Automatic — today's plan, else the next upcoming"
}
],
"optionsRoute": "/plans", "optionsRoute": "/plans",
"optionsDependsOn": "serviceTypeId" "optionsDependsOn": "serviceTypeId"
}, },
@@ -95,14 +82,8 @@
"group": "Service timing", "group": "Service timing",
"showWhen": "useDocumentTriggers", "showWhen": "useDocumentTriggers",
"options": [ "options": [
{ { "value": "schedule", "label": "Scheduled service time" },
"value": "schedule", { "value": "document", "label": "ProPresenter document" }
"label": "Scheduled service time"
},
{
"value": "document",
"label": "ProPresenter document"
}
] ]
}, },
"serviceStartDocument": { "serviceStartDocument": {
@@ -143,94 +124,37 @@
"Item notes": { "Item notes": {
"toggle": "showNotes" "toggle": "showNotes"
}, },
"Show": { "Show / hide": {
"collapsed": true, "collapsed": true,
"columns": 2 "columns": 3,
"help": "Switch off anything this tile should not show."
} }
}, },
"instanceSchema": { "instanceSchema": {
"autoScroll": { "autoScroll": { "type": "boolean", "label": "Keep the current item in view", "default": true },
"type": "boolean",
"label": "Auto-scroll",
"default": true
},
"headerTemplate": { "headerTemplate": {
"type": "template", "type": "template",
"label": "Top bar text", "label": "Top bar text",
"default": "{series} • {part} | {serviceType} • {date}", "default": "{series} • {part} | {serviceType} • {date}",
"group": "Top bar", "group": "Top bar",
"variables": [ "variables": [
{ { "value": "{series}", "label": "Series" },
"value": "{series}", { "value": "{part}", "label": "Part / plan title" },
"label": "Series" { "value": "{serviceType}", "label": "Service type" },
}, { "value": "{date}", "label": "Date" },
{ { "value": "{shortDate}", "label": "Short date" },
"value": "{part}", { "value": "{time}", "label": "Service time" },
"label": "Part / plan title" { "value": "{length}", "label": "Planned length" },
}, { "value": "{items}", "label": "Item count" }
{
"value": "{serviceType}",
"label": "Service type"
},
{
"value": "{date}",
"label": "Date"
},
{
"value": "{shortDate}",
"label": "Short date"
},
{
"value": "{time}",
"label": "Service time"
},
{
"value": "{length}",
"label": "Planned length"
},
{
"value": "{items}",
"label": "Item count"
}
], ],
"help": "Use • between items and | to start a second line." "help": "Use • between items and | to start a second line."
}, },
"keyColor": { "keyColor": { "type": "color", "label": "Song key", "default": "#4ea1ff", "group": "Colors" },
"type": "color", "headerColor": { "type": "color", "label": "Section headers", "default": "#2ee59a", "group": "Colors" },
"label": "Song key", "currentColor": { "type": "color", "label": "Current item", "default": "#2ee59a", "group": "Colors" },
"default": "#4ea1ff", "headerGradient": { "type": "boolean", "label": "Fade header color to the right", "default": true, "group": "Colors" },
"group": "Colors" "tintByType": { "type": "boolean", "label": "Tint items by type (song / media / other)", "default": false, "group": "Colors" },
}, "showNotes": { "type": "switch", "label": "Item notes", "default": true, "group": "Item notes" },
"headerColor": {
"type": "color",
"label": "Section headers",
"default": "#2ee59a",
"group": "Colors"
},
"currentColor": {
"type": "color",
"label": "Current item",
"default": "#2ee59a",
"group": "Colors"
},
"headerGradient": {
"type": "boolean",
"label": "Header fade",
"default": true,
"group": "Colors"
},
"tintByType": {
"type": "boolean",
"label": "Tint by type",
"default": false,
"group": "Colors"
},
"showNotes": {
"type": "switch",
"label": "Item notes",
"default": true,
"group": "Item notes"
},
"noteCategories": { "noteCategories": {
"type": "multiselect", "type": "multiselect",
"label": "Categories", "label": "Categories",
@@ -239,149 +163,29 @@
"columns": 3, "columns": 3,
"allByDefault": true "allByDefault": true
}, },
"showTopBar": { "showTopBar": { "type": "boolean", "label": "Top bar", "default": true, "group": "Show / hide" },
"type": "boolean", "showSeries": { "type": "boolean", "label": "Series name", "default": true, "group": "Show / hide" },
"label": "Top bar", "showPart": { "type": "boolean", "label": "Part # / plan title", "default": true, "group": "Show / hide" },
"default": true, "showServiceType": { "type": "boolean", "label": "Service type", "default": true, "group": "Show / hide" },
"group": "Show" "showDate": { "type": "boolean", "label": "Plan date", "default": true, "group": "Show / hide" },
}, "showServiceTime": { "type": "boolean", "label": "Service time", "default": true, "group": "Show / hide" },
"showSeries": { "showTotalLength": { "type": "boolean", "label": "Planned total length", "default": true, "group": "Show / hide" },
"type": "boolean", "showRunningClock": { "type": "boolean", "label": "Running clock (ahead / behind)", "default": true, "group": "Show / hide" },
"label": "Series name", "showCountdown": { "type": "boolean", "label": "Countdown to service start", "default": true, "group": "Show / hide" },
"default": true, "showHeaders": { "type": "boolean", "label": "Section headers", "default": true, "group": "Show / hide" },
"group": "Show" "showItemNumbers": { "type": "boolean", "label": "Item numbers", "default": false, "group": "Show / hide" },
}, "showStartTimes": { "type": "boolean", "label": "Scheduled start times", "default": true, "group": "Show / hide" },
"showPart": { "showLength": { "type": "boolean", "label": "Item length", "default": true, "group": "Show / hide" },
"type": "boolean", "showItemType": { "type": "boolean", "label": "Item type badge", "default": true, "group": "Show / hide" },
"label": "Part / title", "showKey": { "type": "boolean", "label": "Song key", "default": true, "group": "Show / hide" },
"default": true, "showArrangement": { "type": "boolean", "label": "Song arrangement", "default": false, "group": "Show / hide" },
"group": "Show" "showSongAuthor": { "type": "boolean", "label": "Song author / CCLI", "default": false, "group": "Show / hide" },
}, "showDescription": { "type": "boolean", "label": "Item description", "default": true, "group": "Show / hide" },
"showServiceType": { "showPreService": { "type": "boolean", "label": "Pre-service items", "default": true, "group": "Show / hide" },
"type": "boolean", "showPostService": { "type": "boolean", "label": "Post-service items", "default": true, "group": "Show / hide" },
"label": "Service type", "showNowIndicator": { "type": "boolean", "label": "Now indicator", "default": true, "group": "Show / hide" },
"default": true, "showItemRuntime": { "type": "boolean", "label": "Item runtime (elapsed vs planned)", "default": true, "group": "Show / hide" },
"group": "Show" "dimCompleted": { "type": "boolean", "label": "Dim completed items", "default": true, "group": "Show / hide" },
}, "showLiveFooter": { "type": "boolean", "label": "ProPresenter match line", "default": true, "group": "Show / hide" }
"showDate": {
"type": "boolean",
"label": "Plan date",
"default": true,
"group": "Show"
},
"showServiceTime": {
"type": "boolean",
"label": "Service time",
"default": true,
"group": "Show"
},
"showTotalLength": {
"type": "boolean",
"label": "Total length",
"default": true,
"group": "Show"
},
"showRunningClock": {
"type": "boolean",
"label": "Running clock",
"default": true,
"group": "Show"
},
"showCountdown": {
"type": "boolean",
"label": "Countdown",
"default": true,
"group": "Show"
},
"showHeaders": {
"type": "boolean",
"label": "Section headers",
"default": true,
"group": "Show"
},
"showItemNumbers": {
"type": "boolean",
"label": "Item numbers",
"default": false,
"group": "Show"
},
"showStartTimes": {
"type": "boolean",
"label": "Start times",
"default": true,
"group": "Show"
},
"showLength": {
"type": "boolean",
"label": "Item length",
"default": true,
"group": "Show"
},
"showItemType": {
"type": "boolean",
"label": "Type badge",
"default": true,
"group": "Show"
},
"showKey": {
"type": "boolean",
"label": "Song key",
"default": true,
"group": "Show"
},
"showArrangement": {
"type": "boolean",
"label": "Arrangement",
"default": false,
"group": "Show"
},
"showSongAuthor": {
"type": "boolean",
"label": "Author / CCLI",
"default": false,
"group": "Show"
},
"showDescription": {
"type": "boolean",
"label": "Item description",
"default": true,
"group": "Show"
},
"showPreService": {
"type": "boolean",
"label": "Pre-service",
"default": true,
"group": "Show"
},
"showPostService": {
"type": "boolean",
"label": "Post-service",
"default": true,
"group": "Show"
},
"showNowIndicator": {
"type": "boolean",
"label": "Now indicator",
"default": true,
"group": "Show"
},
"showItemRuntime": {
"type": "boolean",
"label": "Item runtime",
"default": true,
"group": "Show"
},
"dimCompleted": {
"type": "boolean",
"label": "Dim completed",
"default": true,
"group": "Show"
},
"showLiveFooter": {
"type": "boolean",
"label": "Match line",
"default": true,
"group": "Show"
}
} }
} }
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"id": "prodcom-transcript", "id": "prodcom-transcript",
"name": "ProdCom", "name": "ProdCom",
"version": "1.3.2", "version": "1.3.1",
"proddash": ">=1.1.0", "proddash": ">=1.1.0",
"description": "Live ProdCom transcript", "description": "Live ProdCom transcript",
"permissions": [ "permissions": [
@@ -40,7 +40,7 @@
"instanceSchema": { "instanceSchema": {
"showAvatars": { "showAvatars": {
"type": "boolean", "type": "boolean",
"label": "Channel icons", "label": "Show channel icons",
"default": true "default": true
} }
} }
+15 -74
View File
@@ -1,34 +1,22 @@
{ {
"id": "propresenter-now-next", "id": "propresenter-now-next",
"name": "ProPresenter", "name": "ProPresenter",
"version": "1.1.1", "version": "1.1.0",
"proddash": ">=1.5.0", "proddash": ">=1.5.0",
"description": "What is live in ProPresenter and what comes next", "description": "What is live in ProPresenter and what comes next",
"permissions": [ "permissions": [
{ { "kind": "local-network", "reason": "Reaching ProPresenter on this network" }
"kind": "local-network",
"reason": "Reaching ProPresenter on this network"
}
], ],
"client": "client.js", "client": "client.js",
"server": "server.js", "server": "server.js",
"style": "style.css", "style": "style.css",
"minSize": { "minSize": { "w": 3, "h": 3 },
"w": 3, "defaultSize": { "w": 4, "h": 5 },
"h": 3
},
"defaultSize": {
"w": 4,
"h": 5
},
"configSchema": { "configSchema": {
"propresenter": { "propresenter": {
"type": "endpoint", "type": "endpoint",
"label": "ProPresenter", "label": "ProPresenter",
"default": { "default": { "host": "", "port": 1025 }
"host": "",
"port": 1025
}
}, },
"password": { "password": {
"type": "password", "type": "password",
@@ -37,64 +25,17 @@
} }
}, },
"instanceSchema": { "instanceSchema": {
"cleanTitles": { "cleanTitles": { "type": "switch", "label": "Tidy titles", "default": true },
"type": "switch", "showPlaylist": { "type": "switch", "label": "Playlist", "default": true, "group": "Show" },
"label": "Tidy titles", "showLive": { "type": "switch", "label": "Live status", "default": true, "group": "Show" },
"default": true "showNowLabel": { "type": "switch", "label": "Now label", "default": true, "group": "Show" },
}, "showSlideCount": { "type": "switch", "label": "Slide count", "default": true, "group": "Show" },
"showPlaylist": { "showProgress": { "type": "switch", "label": "Progress bar", "default": true, "group": "Show" },
"type": "switch", "showNext": { "type": "switch", "label": "Next list", "default": true, "group": "Show" },
"label": "Playlist", "showNextLabel": { "type": "switch", "label": "Next label", "default": true, "group": "Show" },
"default": true, "showTags": { "type": "switch", "label": "Item tags", "default": true, "group": "Show" }
"group": "Show"
},
"showLive": {
"type": "switch",
"label": "Live status",
"default": true,
"group": "Show"
},
"showNowLabel": {
"type": "switch",
"label": "Now label",
"default": true,
"group": "Show"
},
"showSlideCount": {
"type": "switch",
"label": "Slide count",
"default": true,
"group": "Show"
},
"showProgress": {
"type": "switch",
"label": "Progress bar",
"default": true,
"group": "Show"
},
"showNext": {
"type": "switch",
"label": "Next list",
"default": true,
"group": "Show"
},
"showNextLabel": {
"type": "switch",
"label": "Next label",
"default": true,
"group": "Show"
},
"showTags": {
"type": "switch",
"label": "Item tags",
"default": true,
"group": "Show"
}
}, },
"instanceGroups": { "instanceGroups": {
"Show": { "Show": { "collapsed": false, "columns": 2 }
"collapsed": true,
"columns": 2
}
} }
} }
+1 -1
View File
@@ -140,7 +140,7 @@ module.exports = {
showSlideCount: { type: 'switch', label: 'Slide count', default: true, group: 'Show' }, showSlideCount: { type: 'switch', label: 'Slide count', default: true, group: 'Show' },
showProgress: { type: 'switch', label: 'Progress bar', default: true, group: 'Show' }, showProgress: { type: 'switch', label: 'Progress bar', default: true, group: 'Show' },
}, },
instanceGroups: { Show: { collapsed: true, columns: 2 } }, instanceGroups: { Show: { collapsed: false, columns: 2 } },
}, },
]; ];
}, },
+6 -18
View File
@@ -15,7 +15,7 @@
Per-tile choices: the gear popover's static switches (whole sources, Per-tile choices: the gear popover's static switches (whole sources,
headings, status lines, the clock's format, the state colours) and the headings, status lines, the clock's format, the state colours) and the
timers header menu, which hides any single timer — keyed `source:id` "Timers ▾" header menu, which hides any single timer — keyed `source:id`
and saved with the layout via saveInstanceSettings. The header can be and saved with the layout via saveInstanceSettings. The header can be
hidden by the user, so the static switches stay the primary controls. hidden by the user, so the static switches stay the primary controls.
A solo tile's gear carries only the settings its one card can use (the A solo tile's gear carries only the settings its one card can use (the
@@ -196,10 +196,6 @@ function defaultStatus(timer, value, tone) {
/* ── the tile ─────────────────────────────────────────────────────────── */ /* ── the tile ─────────────────────────────────────────────────────────── */
/* The header menu's icon: a stopwatch, for the list of timers behind it.
Title-bar controls carry no words (module guide, rule 9). */
const TIMERS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 2h4"/><path d="M12 14V9"/><circle cx="12" cy="14" r="8"/></svg>';
export default function create({ root, moduleApi }) { export default function create({ root, moduleApi }) {
let state = null; let state = null;
let feedOffline = false; let feedOffline = false;
@@ -231,7 +227,7 @@ export default function create({ root, moduleApi }) {
} }
/* Which picker entry this tile was added as (the module's tiles() list): /* Which picker entry this tile was added as (the module's tiles() list):
'' or 'all' = every timer, selectable via the timers menu; '' or 'all' = every timer, selectable via the Timers ▾ menu;
'solo:<source>:<id>' = exactly one card, no menu, no headings. The 'solo:<source>:<id>' = exactly one card, no menu, no headings. The
earlier entries 'timer:<uuid>' and 'ltc' map onto the new keys. */ earlier entries 'timer:<uuid>' and 'ltc' map onto the new keys. */
const variant = String(moduleApi.variant || ''); const variant = String(moduleApi.variant || '');
@@ -287,8 +283,8 @@ export default function create({ root, moduleApi }) {
let menuEl = null; // the open menu's element (filled on each open) let menuEl = null; // the open menu's element (filled on each open)
const itemsMenu = solo ? null : moduleApi.header.addMenu({ const itemsMenu = solo ? null : moduleApi.header.addMenu({
icon: TIMERS_SVG, label: 'Timers ▾',
title: 'Timers', title: 'Choose timers',
build(menu) { build(menu) {
// the shell hands over an emptied element on every open // the shell hands over an emptied element on every open
menu.classList.add('tm-menu'); menu.classList.add('tm-menu');
@@ -296,12 +292,6 @@ export default function create({ root, moduleApi }) {
rebuildMenu(true); rebuildMenu(true);
}, },
}); });
// The icon carries the name; a count rides alongside it while some timers
// are hidden, so the bar says "filtered" without saying it in words.
const itemsCountEl = document.createElement('span');
itemsCountEl.className = 'tm-count';
itemsCountEl.hidden = true;
if (itemsMenu) itemsMenu.button.appendChild(itemsCountEl);
function menuRows() { function menuRows() {
const rows = []; const rows = [];
@@ -329,9 +319,7 @@ export default function create({ root, moduleApi }) {
if (!itemsMenu) return; if (!itemsMenu) return;
const rows = menuRows(); const rows = menuRows();
const visible = rows.filter((r) => !hidden.has(r.key)).length; const visible = rows.filter((r) => !hidden.has(r.key)).length;
const filtering = visible !== rows.length; itemsMenu.setLabel(visible === rows.length ? 'Timers ▾' : `Timers (${visible}/${rows.length}) ▾`);
itemsCountEl.textContent = filtering ? `${visible}/${rows.length}` : '';
itemsCountEl.hidden = !filtering;
} }
let menuSignature = ''; let menuSignature = '';
@@ -791,7 +779,7 @@ export default function create({ root, moduleApi }) {
let hint = ''; let hint = '';
const groups = visibleGroups(); const groups = visibleGroups();
if (state && !state.sources.length) hint = 'Every source is switched off — turn one on in Admin.'; if (state && !state.sources.length) hint = 'Every source is switched off — turn one on in Admin.';
else if (state && !groups.length) hint = 'Nothing selected — switch a source on in this tile’s settings, or pick timers in the title bar’s timers menu.'; else if (state && !groups.length) hint = 'Nothing selected — switch a source on in this tile’s settings, or pick timers in the Timers ▾ menu.';
hintEl.hidden = !hint; hintEl.hidden = !hint;
hintEl.textContent = hint; hintEl.textContent = hint;
groupsEl.hidden = Boolean(hint); groupsEl.hidden = Boolean(hint);
+4 -3
View File
@@ -1,7 +1,7 @@
{ {
"id": "propresenter-timers", "id": "propresenter-timers",
"name": "Timers", "name": "Timers",
"version": "1.4.0", "version": "1.3.0",
"proddash": ">=1.4.0", "proddash": ">=1.4.0",
"description": "Timers, Clocks, and LTC From configured Modules", "description": "Timers, Clocks, and LTC From configured Modules",
"provides": [ "provides": [
@@ -112,7 +112,7 @@
}, },
"instanceGroups": { "instanceGroups": {
"Show": { "Show": {
"collapsed": true, "collapsed": false,
"columns": 2 "columns": 2
}, },
"Clock": { "Clock": {
@@ -121,7 +121,8 @@
}, },
"Colours": { "Colours": {
"collapsed": true, "collapsed": true,
"columns": 2 "columns": 2,
"help": "The time's colour per state. Left at the default, a colour follows the dashboard theme; one you change here wins over it."
} }
}, },
"instanceSchema": { "instanceSchema": {
+1 -9
View File
@@ -321,7 +321,7 @@
@keyframes tm-nosignal-blink { 50% { opacity: 0.35; } } @keyframes tm-nosignal-blink { 50% { opacity: 0.35; } }
/* ── the timers menu (container styled/positioned by the shell) ── */ /* ── the "Timers ▾" menu (container styled/positioned by the shell) ── */
[data-module="propresenter-timers"] .tm-menu-item { [data-module="propresenter-timers"] .tm-menu-item {
display: flex; display: flex;
@@ -383,11 +383,3 @@
font-size: 12px; font-size: 12px;
white-space: nowrap; white-space: nowrap;
} }
/* the count beside the title-bar timers icon, while some are hidden */
[data-module="propresenter-timers"] .tm-count {
font-size: 10px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
[data-module="propresenter-timers"] .tm-count[hidden] { display: none; }
+30
View File
@@ -451,6 +451,36 @@ a.btn { text-decoration: none; }
/* one swatch per preset: a miniature dashboard (page, a tile on it, the /* one swatch per preset: a miniature dashboard (page, a tile on it, the
accent as a status dot) and the name; the current one wears the accent */ accent as a status dot) and the name; the current one wears the accent */
/* ── a collapsible admin section ─────────────────────────────────────
The pattern for any section of this page that folds. The markup is a
<details class="admin-fold"> holding the section's contents, headed by a
<summary class="admin-h1 admin-fold-title">: the title keeps the same
type as every other section heading — a <summary> would otherwise come
out at the normal weight — and the caret follows the words, so a folded
section still reads as a heading first. The whole row is the hit target. */
details.admin-fold > summary.admin-fold-title {
cursor: pointer;
list-style: none;
display: flex;
align-items: center;
gap: 8px;
font-weight: 700; /* matches h1.admin-h1; summary defaults to normal */
user-select: none;
}
details.admin-fold > summary.admin-fold-title::-webkit-details-marker { display: none; }
/* after the title, not before it — the heading reads first, the caret says
there is more behind it */
details.admin-fold > summary.admin-fold-title::after {
content: "";
width: 0; height: 0;
border-left: 6px solid var(--muted);
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
transition: transform 0.15s;
}
details.admin-fold[open] > summary.admin-fold-title::after { transform: rotate(90deg); }
details.admin-fold > summary.admin-fold-title:hover { color: var(--accent); }
.theme-swatches { .theme-swatches {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(116px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
+5 -3
View File
@@ -41,9 +41,11 @@
</section> </section>
<section id="theme-section" hidden> <section id="theme-section" hidden>
<h1 class="admin-h1">Theme</h1> <details class="admin-fold">
<div class="theme-swatches" id="theme-swatches" role="radiogroup" aria-label="Theme"></div> <summary class="admin-h1 admin-fold-title">Theme</summary>
<div class="admin-status" id="theme-status"></div> <div class="theme-swatches" id="theme-swatches" role="radiogroup" aria-label="Theme"></div>
<div class="admin-status" id="theme-status"></div>
</details>
</section> </section>
<section id="modules-section" hidden> <section id="modules-section" hidden>
+101 -2
View File
@@ -941,8 +941,10 @@ body.fullscreen #grid { padding-bottom: 0; }
every shell element follows, and a colour a tile is configured with (set every shell element follows, and a colour a tile is configured with (set
on the tile itself) still wins. The :root block at the top of this file is on the tile itself) still wins. The :root block at the top of this file is
"booth", the default; the list of ids and names lives in server.js "booth", the default; the list of ids and names lives in server.js
(GET /api/theme). All four are booth-grade: high contrast, the accent (GET /api/theme). Every one is booth-grade: high contrast, the accent
nowhere near the danger red or the warning amber, muted still readable. */ nowhere near the danger red or the warning amber, muted still readable.
Each sets --accent-text too when the default (a near-black green, for
booth's mint) would look wrong against its own accent. */
/* Harbor — deep navy, cyan accent */ /* Harbor — deep navy, cyan accent */
html[data-theme="harbor"] { html[data-theme="harbor"] {
@@ -993,3 +995,100 @@ html[data-theme="daylight"] {
--danger: #d1242f; --danger: #d1242f;
--warn: #b26a00; --warn: #b26a00;
} }
/* Midnight — indigo night, violet accent */
html[data-theme="midnight"] {
--bg: #0a0b18;
--panel: #14162b;
--border: #272a4a;
--text: #e8e9fb;
--muted: #9a9cc8;
--accent: #9d8cff;
--accent-text: #12103a;
--danger: #ff5a72;
--warn: #f5b93a;
}
/* Moss — forest floor, lime accent (warn pushed orange so the two greens
and the amber never read as one another) */
html[data-theme="moss"] {
--bg: #08120c;
--panel: #101e15;
--border: #1f3728;
--text: #e9f4ea;
--muted: #8fb09a;
--accent: #8fdb3f;
--accent-text: #15250a;
--danger: #ff5a52;
--warn: #ff9f1c;
}
/* Plum — aubergine, purple accent */
html[data-theme="plum"] {
--bg: #110a17;
--panel: #1d1228;
--border: #332042;
--text: #f2e9f8;
--muted: #b195c2;
--accent: #c084fc;
--accent-text: #260a37;
--danger: #ff4d4f;
--warn: #f7b53b;
}
/* Sepia — warm brown room, cool teal accent: the opposite arrangement to
Ember, which is warm throughout */
html[data-theme="sepia"] {
--bg: #12100b;
--panel: #1d1913;
--border: #33291d;
--text: #f4ede0;
--muted: #b3a48b;
--accent: #5eead4;
--accent-text: #06231e;
--danger: #ff5a52;
--warn: #ffc247;
}
/* Blackout — true black for a dark room, where any glow off the screen
carries; everything else is as bright as it can be against it */
html[data-theme="blackout"] {
--bg: #000000;
--panel: #0a0a0a;
--border: #1f1f1f;
--text: #ffffff;
--muted: #9e9e9e;
--accent: #00e5ff;
--accent-text: #00212a;
--danger: #ff3b30;
--warn: #ffcc00;
}
/* Paper — the warm light one, next to Daylight's cool white. Danger and warn
darken, as they must on a light background, and the indigo accent takes
white text. */
html[data-theme="paper"] {
--bg: #f5f1e8;
--panel: #fffdf8;
--border: #ddd5c4;
--text: #1c1a15;
--muted: #6b6455;
--accent: #4f46e5;
--accent-text: #ffffff;
--danger: #c8102e;
--warn: #9a6700;
}
/* Iron — no colour of its own: the accent is near-white, so the only hues on
screen are a module's own and the red/amber that mean something */
html[data-theme="iron"] {
--bg: #0c0c0d;
--panel: #161617;
--border: #2b2b2d;
--text: #f2f2f3;
--muted: #9a9a9d;
--accent: #d8d8dc;
--accent-text: #131314;
--danger: #ff4d4f;
--warn: #ffb224;
}
+7
View File
@@ -712,6 +712,13 @@ const THEMES = [
{ id: 'graphite', name: 'Graphite', colors: { bg: '#0f1012', panel: '#17191d', accent: '#6cb4ff' } }, { id: 'graphite', name: 'Graphite', colors: { bg: '#0f1012', panel: '#17191d', accent: '#6cb4ff' } },
{ id: 'ember', name: 'Ember', colors: { bg: '#120c0a', panel: '#1d1411', accent: '#ff8f3a' } }, { id: 'ember', name: 'Ember', colors: { bg: '#120c0a', panel: '#1d1411', accent: '#ff8f3a' } },
{ id: 'daylight', name: 'Daylight', colors: { bg: '#eef1f5', panel: '#ffffff', accent: '#0b8a5f' } }, { id: 'daylight', name: 'Daylight', colors: { bg: '#eef1f5', panel: '#ffffff', accent: '#0b8a5f' } },
{ id: 'midnight', name: 'Midnight', colors: { bg: '#0a0b18', panel: '#14162b', accent: '#9d8cff' } },
{ id: 'moss', name: 'Moss', colors: { bg: '#08120c', panel: '#101e15', accent: '#8fdb3f' } },
{ id: 'plum', name: 'Plum', colors: { bg: '#110a17', panel: '#1d1228', accent: '#c084fc' } },
{ id: 'sepia', name: 'Sepia', colors: { bg: '#12100b', panel: '#1d1913', accent: '#5eead4' } },
{ id: 'blackout', name: 'Blackout', colors: { bg: '#000000', panel: '#0a0a0a', accent: '#00e5ff' } },
{ id: 'paper', name: 'Paper', colors: { bg: '#f5f1e8', panel: '#fffdf8', accent: '#4f46e5' } },
{ id: 'iron', name: 'Iron', colors: { bg: '#0c0c0d', panel: '#161617', accent: '#d8d8dc' } },
]; ];
const DEFAULT_THEME = THEMES[0].id; const DEFAULT_THEME = THEMES[0].id;