@tasker-systems/temper-design 0.0.0-stage → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +330 -2
- package/brand/mark.svg +4 -0
- package/brand/wordmark.svg +5 -0
- package/contract/base.css +221 -0
- package/contract/graph.css +53 -0
- package/contract/markdown.css +279 -0
- package/contract/region.css +98 -0
- package/contract/shadcn-bridge.css +86 -0
- package/contract/tailwind-roles.css +67 -0
- package/contract/tailwind.css +70 -0
- package/contract/theme.schema.json +444 -0
- package/dist/graph.d.ts +191 -0
- package/dist/graph.js +293 -0
- package/dist/handoff.d.ts +22 -0
- package/dist/handoff.js +70 -0
- package/dist/highlight.d.ts +2 -0
- package/dist/highlight.js +48 -0
- package/dist/links.d.ts +32 -0
- package/dist/links.js +65 -0
- package/dist/markdown.d.ts +23 -0
- package/dist/markdown.js +61 -0
- package/dist/omission.d.ts +81 -0
- package/dist/omission.js +84 -0
- package/dist/region.d.ts +16 -0
- package/dist/region.js +59 -0
- package/dist/sanitize.d.ts +22 -0
- package/dist/sanitize.js +205 -0
- package/dist/theme.d.ts +29 -0
- package/dist/theme.js +37 -0
- package/dist/vocabulary.d.ts +35 -0
- package/dist/vocabulary.js +74 -0
- package/dist/ways-in.d.ts +36 -0
- package/dist/ways-in.js +35 -0
- package/guard/colours.mjs +347 -0
- package/package.json +97 -4
- package/quiet-instrument/theme.css +66 -0
- package/quiet-instrument/theme.json +85 -0
- package/quiet-instrument-paper/theme.css +66 -0
- package/quiet-instrument-paper/theme.json +85 -0
- package/themes.json +14 -0
- package/toolchain/check.mjs +76 -0
- package/toolchain/majors.json +18 -0
package/README.md
CHANGED
|
@@ -1,3 +1,331 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tasker-systems/temper-design
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
temper's theme contract, its themes, and the pure modules every temper surface renders
|
|
4
|
+
through — the one source every temper surface paints from.
|
|
5
|
+
The web UI (`packages/temper-ui`) and the desktop (`tasker-systems/temper-contrib`) both
|
|
6
|
+
consume it, so the same role, the same term and the same state read alike on each.
|
|
7
|
+
|
|
8
|
+
A theme is **data**: a `theme.json` that gives a value to every role in the contract.
|
|
9
|
+
Components consume roles, never values, so any theme that satisfies the contract restyles
|
|
10
|
+
everything a surface paints — its chrome, shadcn-svelte primitives, and anything rendered
|
|
11
|
+
from a json-render spec — with no per-theme code.
|
|
12
|
+
|
|
13
|
+
A closed JSON Schema is the source of truth, the files are validated by a self-check, and a
|
|
14
|
+
hand-broken fixture must be refused for the check to count.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
packages/temper-design/
|
|
18
|
+
├── themes.json # the theme index — name, title, appearance, counterpart (generated)
|
|
19
|
+
├── contract/
|
|
20
|
+
│ ├── theme.schema.json # the contract — every role, closed (draft 2020-12)
|
|
21
|
+
│ ├── tailwind.css # roles → Tailwind v4 utilities (generated from the schema)
|
|
22
|
+
│ ├── tailwind-roles.css # the same utilities without the palette reset (generated)
|
|
23
|
+
│ ├── shadcn-bridge.css # shadcn-svelte's variable names answered from roles
|
|
24
|
+
│ ├── base.css # base styles + t-*/ed-* recipes, roles only
|
|
25
|
+
│ ├── region.css # the four region states' recipe, roles only
|
|
26
|
+
│ ├── graph.css # the graph's edge recipe: colour per stroke role, roles only
|
|
27
|
+
│ └── markdown.css # the rendered document's recipe (`.md-body`), roles only
|
|
28
|
+
├── quiet-instrument/ # dark — temper's house theme
|
|
29
|
+
│ ├── theme.json # source of truth
|
|
30
|
+
│ └── theme.css # generated — never edit
|
|
31
|
+
├── quiet-instrument-paper/ # light counterpart
|
|
32
|
+
├── brand/ # the threaded-t mark and wordmark, in currentColor
|
|
33
|
+
├── preview/index.html # every role and the foundational components, both themes
|
|
34
|
+
├── scripts/themes.sh # build · check · contrast · --self-check
|
|
35
|
+
├── src/ # the shared modules: markdown, sanitize, links, highlight, region, omission, handoff, ways-in, theme, vocabulary, graph
|
|
36
|
+
├── dist/ # their compiled output (committed; see "The shared modules")
|
|
37
|
+
├── toolchain/ # the toolchain majors every surface builds on, and their check
|
|
38
|
+
└── tests/fixtures/broken/ # themes the check must refuse
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Installing
|
|
42
|
+
|
|
43
|
+
Published to npm in lockstep with temper's releases. Inside this repository, consume it as a
|
|
44
|
+
`file:` dependency, as `packages/temper-ui` does with `clients/temper-telemetry-ts`; outside
|
|
45
|
+
it, depend on the published version:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{ "dependencies": { "@tasker-systems/temper-design": "<temper version>" } }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
bun **copies** a `file:` dependency into the consumer's install; it does not link it. An edit
|
|
52
|
+
here reaches a consumer only after that consumer runs `bun install` again — until then it paints
|
|
53
|
+
from the copy it took, and stale styling will look like a bug in your change. Reinstall after
|
|
54
|
+
every edit you want to see in a consuming surface.
|
|
55
|
+
|
|
56
|
+
What a consumer imports:
|
|
57
|
+
|
|
58
|
+
| Path | What it is |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `@tasker-systems/temper-design/contract/tailwind.css` | the roles as `tp-` Tailwind v4 utilities; resets the default palette |
|
|
61
|
+
| `@tasker-systems/temper-design/contract/tailwind-roles.css` | the same utilities without the reset — for a surface still moving off the default palette one area at a time; it switches to `tailwind.css` once nothing reads the palette |
|
|
62
|
+
| `@tasker-systems/temper-design/contract/shadcn-bridge.css` | optional: shadcn-svelte's variable names answered from roles |
|
|
63
|
+
| `@tasker-systems/temper-design/contract/base.css` | base styles and the `t-*`/`ed-*` recipes |
|
|
64
|
+
| `@tasker-systems/temper-design/<theme>/theme.css` | one theme's values, under `[data-theme="<theme>"]` |
|
|
65
|
+
| `@tasker-systems/temper-design/<theme>/theme.json` | one theme as data, for applying it without a build |
|
|
66
|
+
| `@tasker-systems/temper-design/themes.json` | every theme's name, title, appearance and counterpart — list and pair themes from this, never by globbing the package's directories |
|
|
67
|
+
| `@tasker-systems/temper-design/contract/theme.schema.json` | the contract itself |
|
|
68
|
+
| `@tasker-systems/temper-design/brand/*.svg` | the mark and wordmark, in `currentColor` |
|
|
69
|
+
| `@tasker-systems/temper-design/toolchain/majors.json` | the toolchain majors every surface builds on |
|
|
70
|
+
| `@tasker-systems/temper-design/toolchain/check.mjs` | the check a surface runs against them (see below) |
|
|
71
|
+
| `@tasker-systems/temper-design/guard/colours.mjs` | the colour guard a surface runs over the components it has moved onto roles (see below) |
|
|
72
|
+
| `@tasker-systems/temper-design/contract/region.css` | the `region-state` recipe: the four region states' colour and border |
|
|
73
|
+
| `@tasker-systems/temper-design/contract/graph.css` | the `graph-edge` recipe: a graph line, its heads and its cross, coloured by the stroke role `./graph` decides |
|
|
74
|
+
| `@tasker-systems/temper-design/contract/markdown.css` | the `md-body` recipe: how a rendered document reads — headings, links, lists, code, tables, refusals |
|
|
75
|
+
| `@tasker-systems/temper-design/markdown` | markdown → HTML with highlighted fences, bounded; DOM-free |
|
|
76
|
+
| `@tasker-systems/temper-design/sanitize` | the one sanitizer pass for that HTML; needs a DOM. Given a body's link answers, it rewrites every `./<uuid>` link: the held title when temper resolved it, inert when temper answered there is nothing you can see, and marked but still followable when nothing was verified |
|
|
77
|
+
| `@tasker-systems/temper-design/links` | body links: the exact `./<uuid>` matcher, `extractVaultLinks` (bounded, DOM-free, total — run before render to resolve the ids), the answer types, the cap and the hover words |
|
|
78
|
+
| `@tasker-systems/temper-design/highlight` | the one highlight.js registration, for direct code viewers |
|
|
79
|
+
| `@tasker-systems/temper-design/region` | the four region states' words, markers and ARIA roles |
|
|
80
|
+
| `@tasker-systems/temper-design/omission` | the omission sentence a bounded view ends with — counted (`omissionSentence`), uncounted (`uncountedSentence`: a read that reports only whether more exist, a cursor page, or nothing), drawn (`drawnSentence`: a read ranked from a bounded draw, which says what it drew from and never "all"), or a graph read (`graphSentence`) |
|
|
81
|
+
| `@tasker-systems/temper-design/handoff` | the latest handoff: its name, and `nextSection`, which quotes the next steps a session recorded as written, bounded, and `null` when it recorded none — never a summary |
|
|
82
|
+
| `@tasker-systems/temper-design/ways-in` | the standing ways into the work both surfaces offer on arrival (active goals, in-progress tasks, sessions): each one's name, scope and filter, their bound, and the order they are read in |
|
|
83
|
+
| `@tasker-systems/temper-design/theme` | the theme model: a reader's preference (follow the system within a family, or one theme) resolved against `themes.json` |
|
|
84
|
+
| `@tasker-systems/temper-design/vocabulary` | bound vocabularies: core's doc type → slot binding, `tintFor` (a doc type's slot, or the neutral), and `bindingsFrom`, which refuses a binding to anything but a slot |
|
|
85
|
+
| `@tasker-systems/temper-design/graph` | the graph's geometry and edge grammar: `settle` (deterministic; unconnected nodes listed, not placed; one line per pair; radial only when the input holds both core and ring), `placeCaptions`, `edgeGeometry` (lines end at the marks' rims, heads as shapes), and `stroke`/`pairStroke`/`pairEnds` (role, dash, width over weights in (0, 1], heads, the contradicts cross). A surface supplies its plane and its caption room |
|
|
86
|
+
|
|
87
|
+
The theme checker (`scripts/`, `tests/`, `pyproject.toml`) is not published: it runs in this
|
|
88
|
+
repository's CI, and the package carries only what it proves. The toolchain check is
|
|
89
|
+
published, because each surface runs it itself; its harness is not.
|
|
90
|
+
|
|
91
|
+
## The toolchain
|
|
92
|
+
|
|
93
|
+
Every surface builds on the same majors of one named set: `typescript`, `svelte`,
|
|
94
|
+
`@sveltejs/kit`, `@sveltejs/vite-plugin-svelte`, `svelte-check`, `vite`, `vitest`,
|
|
95
|
+
`tailwindcss`, `@tailwindcss/vite` and `@biomejs/biome`, plus each surface's own SvelteKit
|
|
96
|
+
adapter. A major upgrade of any of them reaches every surface together. `toolchain/majors.json`
|
|
97
|
+
declares the majors, and each surface checks what it resolves against them:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
node <package>/toolchain/check.mjs --adapter @sveltejs/adapter-static
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The check fails when the surface resolves a different major of a named tool, does not
|
|
104
|
+
resolve one, or names an adapter the declaration lacks. It looks a tool up the way Node
|
|
105
|
+
resolves a bare import, from the surface's directory upward; it reads no lockfile.
|
|
106
|
+
|
|
107
|
+
- **The web** (`packages/temper-ui`, `bun run toolchain`) reads the declaration from this
|
|
108
|
+
working tree. A web major cannot move unless `majors.json` moves in the same change.
|
|
109
|
+
- **The desktop** reads it at the version it pins. The pin bump that carries a new major
|
|
110
|
+
fails until the desktop has taken it too.
|
|
111
|
+
|
|
112
|
+
So between a release that moves a major and the desktop's next pin bump, the two surfaces
|
|
113
|
+
can differ and nothing fails. That gap is where the check stops, not something it covers.
|
|
114
|
+
|
|
115
|
+
Both repositories' `dependabot.yml` hold semver-majors of this set, so a major arrives as a
|
|
116
|
+
deliberate change rather than an automatic one. `node --test toolchain/check.test.mjs` is
|
|
117
|
+
the check's harness.
|
|
118
|
+
|
|
119
|
+
## The shared modules
|
|
120
|
+
|
|
121
|
+
`src/` holds what both surfaces render through, so a rendered body, a region's state and a
|
|
122
|
+
bounded view's omission read the same on each: one sanitizer policy (what one surface strips
|
|
123
|
+
or refuses, the other does not render), one set of region words, one omission sentence, one graph geometry, one set of ways in. They
|
|
124
|
+
compile with `tsc` into `dist/`, which is committed — bun copies a `file:` dependency rather
|
|
125
|
+
than building it — and CI rebuilds it and fails when it is stale.
|
|
126
|
+
|
|
127
|
+
A shared module never binds a surface's runtime: no Tauri, no SvelteKit, no `svelte`, no
|
|
128
|
+
`$lib`/`$app`/`$env`, no Node. The DOM is common ground (the sanitizer needs one; `markdown`
|
|
129
|
+
deliberately does not, so a surface can prepare a body wherever it renders).
|
|
130
|
+
`node scripts/no-runtime.mjs` refuses such an import, and `node --test
|
|
131
|
+
scripts/no-runtime.test.mjs` proves each one is refused. Why a given surface's CSP backs the
|
|
132
|
+
sanitizer up is that surface's comment to keep, next to its CSP — not this package's.
|
|
133
|
+
|
|
134
|
+
## The contract
|
|
135
|
+
|
|
136
|
+
`contract/theme.schema.json` names every role a component may read. A theme must supply
|
|
137
|
+
all of them and may not invent more (`additionalProperties: false`): a component that
|
|
138
|
+
needs a colour the contract lacks is a change to the contract, reviewed as one, not a
|
|
139
|
+
theme quietly growing a private name.
|
|
140
|
+
|
|
141
|
+
| Group | Roles | Used for |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| Ground | `ground` `surface` `surface-raised` `overlay` | window, panels, popovers, modal scrim |
|
|
144
|
+
| Text | `text` `text-muted` `text-subtle` `text-faint` | reading, secondary, labels, disabled-only |
|
|
145
|
+
| Rule | `rule` `rule-strong` | hairlines, input outlines |
|
|
146
|
+
| Accent | `accent` `accent-muted` `accent-line` `accent-line-soft` `accent-wash` `accent-hover` `on-accent` `focus` | the single accent and its tints; the rail; keyboard focus |
|
|
147
|
+
| Conditions | `notice` `success` `danger` `pending` (+ `-wash` each) | attention, confirmation, destructive, awaiting-you |
|
|
148
|
+
| Region states | `region-arriving` `region-empty` `region-gave-up` `region-failed` (+ `-wash` each) | the four states a read can present as |
|
|
149
|
+
| Authorship | `author-human` `author-agent` | gutter and attribution marks |
|
|
150
|
+
| Categorical | `cat-1` … `cat-8` `cat-neutral` | bound vocabularies — core's doc types and plugins' (see below) |
|
|
151
|
+
| Code | `code-key` `code-string` `code-number` `code-comment` | syntax highlighting |
|
|
152
|
+
| Font | `reading` `doing` `ui` | serif prose, mono technical, dense UI |
|
|
153
|
+
| Radius, motion, tracking | `chip` `panel` · `quick` `settle` `easing` · `label` `strip` `eyebrow` `ui` `mark` | shape and rhythm |
|
|
154
|
+
|
|
155
|
+
Every role becomes a CSS custom property `--tp-<role>` (`--tp-text-muted`,
|
|
156
|
+
`--tp-font-reading`, `--tp-motion-quick`) and a Tailwind utility with a `tp-` prefix
|
|
157
|
+
(`bg-tp-ground`, `text-tp-text-muted`, `border-tp-rule`, `font-tp-doing`). The prefix keeps
|
|
158
|
+
temper's roles apart from shadcn's names, where `accent` and `muted` mean other things.
|
|
159
|
+
|
|
160
|
+
### What the check enforces
|
|
161
|
+
|
|
162
|
+
`scripts/themes.sh --self-check` runs in CI. For every theme it checks:
|
|
163
|
+
|
|
164
|
+
- **The contract.** Every role present, none invented, every value `#rrggbb` or `rgba()`.
|
|
165
|
+
- **Legibility floors.** Contrast is measured on the colours as they paint — alpha
|
|
166
|
+
composited over `ground`, washes composited before the text on them is measured.
|
|
167
|
+
`text` 7:1; secondary and label text, the accent, the categorical slots, conditions,
|
|
168
|
+
region states and code 4.5:1, the slots on every ground text sits on (`ground`, `surface`,
|
|
169
|
+
`surface-raised`); focus rings, authorship marks 3:1; `text-faint` 2.5:1, which is why it
|
|
170
|
+
is reserved for disabled and decorative marks and never carries information. `themes.sh contrast <theme.json>` prints every pair.
|
|
171
|
+
- **Distinctness.** The four region states, the nine categorical marks (eight slots and
|
|
172
|
+
the neutral), the two authorship marks and the conditions must stay apart; and no
|
|
173
|
+
slot may read as a chrome or state role (`accent`, `author-*`, `notice`, `region-*`)
|
|
174
|
+
— a vocabulary bound to a slot that paints like the accent would dress its terms in
|
|
175
|
+
meaning they do not carry (CIE76 ΔE on the painted colour).
|
|
176
|
+
`themes.sh slots <theme.json>` prints the slot canvas. Colour is only one of the
|
|
177
|
+
channels that separates a failed read from a refused one, but a theme that collapses
|
|
178
|
+
two of them has removed a channel, and that is checkable.
|
|
179
|
+
- **Generated files agree.** `theme.css` matches its `theme.json`; `contract/tailwind.css` and
|
|
180
|
+
`contract/tailwind-roles.css` cover every role in the schema.
|
|
181
|
+
- **Counterparts pair.** A theme's `counterpart` exists, points back, and has the
|
|
182
|
+
opposite `appearance`.
|
|
183
|
+
|
|
184
|
+
And across the tree: the contract CSS carries no literal colour and references no role the
|
|
185
|
+
contract lacks; the brand marks paint in `currentColor`; every fixture in
|
|
186
|
+
`tests/fixtures/broken/` is refused.
|
|
187
|
+
|
|
188
|
+
## Using a theme
|
|
189
|
+
|
|
190
|
+
```css
|
|
191
|
+
@import "tailwindcss";
|
|
192
|
+
@import "<themes>/contract/tailwind.css"; /* roles as utilities; default palette reset */
|
|
193
|
+
@import "<themes>/contract/shadcn-bridge.css"; /* optional: shadcn-svelte components in-theme */
|
|
194
|
+
@import "<themes>/quiet-instrument/theme.css";
|
|
195
|
+
@import "<themes>/quiet-instrument-paper/theme.css";
|
|
196
|
+
@import "<themes>/contract/base.css";
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Then choose the theme with one attribute: `<html data-theme="quiet-instrument">`.
|
|
200
|
+
|
|
201
|
+
`contract/tailwind.css` resets Tailwind's default palette. A component cannot reach for
|
|
202
|
+
`zinc-800` or `white`; if it needs a colour, it names a role. A surface still painting from
|
|
203
|
+
the default palette imports `contract/tailwind-roles.css` instead until nothing reads it —
|
|
204
|
+
the reset would otherwise turn every unmoved `zinc-*` into a class that paints nothing.
|
|
205
|
+
|
|
206
|
+
A theme can also be applied without a build: a surface can read a `theme.json` and set
|
|
207
|
+
each `--tp-*` property on the root element. That is how a theme installed after the app
|
|
208
|
+
was built — or one shipped by a plugin — takes effect.
|
|
209
|
+
|
|
210
|
+
## The colour guard
|
|
211
|
+
|
|
212
|
+
The reset makes a stock colour paint nothing; it does not say so. `guard/colours.mjs` does: it
|
|
213
|
+
fails a surface's build when a component it guards paints anything but a role.
|
|
214
|
+
|
|
215
|
+
- **A literal colour**: hex, a colour function, or a named colour.
|
|
216
|
+
- **A Tailwind colour utility** naming the stock palette, `white` or `black`, or a colour
|
|
217
|
+
name the surface declares for itself (`--color-<name>` in the theme files it lists).
|
|
218
|
+
- **An opacity modifier on a role** (`bg-tp-accent/15`): a strength no theme chose. Use the
|
|
219
|
+
role's `-wash` or `-hover` sibling.
|
|
220
|
+
- **A colour read from a variable that is not a role**, in a declaration
|
|
221
|
+
(`color: var(--parchment)`), an SVG attribute or Svelte style directive
|
|
222
|
+
(`fill="var(--obsidian)"`, `style:color=…`), or an arbitrary utility (`bg-[var(--obsidian)]`).
|
|
223
|
+
A declared carrier may be read (below).
|
|
224
|
+
- **A role mixed down** (`color-mix(in srgb, var(--tp-accent) 15%, transparent)`): the opacity
|
|
225
|
+
modifier spelled in CSS, and the same strength no theme chose. A bridged name mixed down is a
|
|
226
|
+
role mixed down one hop removed, and is refused the same way.
|
|
227
|
+
- **A custom property declared from a variable that is not a role**
|
|
228
|
+
(`--wx-color-font: var(--color-quiet-fg)`): a private name laundered through a property the
|
|
229
|
+
colour-declaration rule does not read by name. Fonts are held too: `var(--tp-font-ui)`, not
|
|
230
|
+
`var(--font-sans)`. A bridge stays legal: a name declared within the guarded paths whose every
|
|
231
|
+
declaration there resolves to roles (`--background: var(--tp-ground)`, then
|
|
232
|
+
`--color-background: var(--background)`) may be read. A declaration that reads no variable
|
|
233
|
+
(`style="--hue: {hue}"`, a colour from script) unbridges its name. Svelte's `style:--x=`
|
|
234
|
+
directive is a declaration like any other. The guard sees only the paths: a name also declared
|
|
235
|
+
elsewhere (a vendor stylesheet, a selector scope it is not bridged in) is not seen there, and a
|
|
236
|
+
declaration wrapped across lines is not read. The rule cannot tell what a custom property
|
|
237
|
+
holds, so it over-reaches: a spacing alias (`--gap: var(--space-column)`) is refused as though
|
|
238
|
+
it were a colour. Read the variable where it is used (`gap: var(--space-column)`) instead.
|
|
239
|
+
- **A colour read from a bridge that is not a declared carrier.** A bridge may be declared from,
|
|
240
|
+
but painting with it needs the surface to vouch for it in `"carriers"`: a bridge proves only
|
|
241
|
+
the declarations inside the paths, and a vendor stylesheet may set the same name to a literal
|
|
242
|
+
(SVAR's `--wx-color-danger: #fe6158`). A carrier (`--hue` on a view's root, carrying a doc
|
|
243
|
+
type's slot) must be declared by writing one role out — `var(--tp-cat-2)` or
|
|
244
|
+
`var(--tp-{tint})`, no fallback, no other expression — must resolve to roles, and must be
|
|
245
|
+
read. A declaration inside `<script>` declares nothing.
|
|
246
|
+
|
|
247
|
+
A surface commits a config naming what it guards, relative to the config's own directory,
|
|
248
|
+
and runs the guard against it:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{ "paths": ["src/lib/components/Sidebar.svelte", "src/routes/(app)"], "themes": ["src/app.css"] }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```sh
|
|
255
|
+
node node_modules/@tasker-systems/temper-design/guard/colours.mjs colour-guard.json
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
A surface moving off the stock palette lists the areas it has moved, and each area that
|
|
259
|
+
moves adds its paths. An empty list, a listed path that does not exist, and a listed
|
|
260
|
+
directory with no components are refused rather than passed. `node --test
|
|
261
|
+
guard/colours.test.mjs` plants each class and requires its refusal.
|
|
262
|
+
|
|
263
|
+
A variable that is not a role yet, but that a named task will make one, is declared rather than
|
|
264
|
+
exempted:
|
|
265
|
+
|
|
266
|
+
```json
|
|
267
|
+
"pending": [{ "var": "--tint", "until": "T12", "why": "plugin tint; the plugin binds its doc types to slots" }]
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Its reads pass, and so does any `color-mix()` reading it, a role inside that mix included,
|
|
271
|
+
since the result is part of the hole. Every other variable beside it is still held. Every run
|
|
272
|
+
prints how many reads there are and what closes them. An entry missing `var`, `until` or
|
|
273
|
+
`why`, or naming a role, is refused, and so is an entry nothing reads any more: its hole has
|
|
274
|
+
closed, and the entry would outlive it.
|
|
275
|
+
|
|
276
|
+
## Bound vocabularies and categorical slots
|
|
277
|
+
|
|
278
|
+
Colour is bound, never named. The contract owns eight categorical slots and a neutral;
|
|
279
|
+
**vocabularies own the bindings to them** — core's doc types through core's declared
|
|
280
|
+
default binding, a plugin's doc types through its own definition. A doc type bound
|
|
281
|
+
nowhere renders in the neutral role. Many doc types may share a slot; a binding to
|
|
282
|
+
anything but a slot is refused.
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
{ "doctype": "story", "tint": "cat-1" }
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The binding is part of the vocabulary that declares the doc type — core's default for
|
|
289
|
+
core's kinds, the package's entry for a plugin's — so the same term keeps the same slot
|
|
290
|
+
in every theme and on every surface that honours it. Nothing in any app names a
|
|
291
|
+
`doctype-*` role, because no such role exists: a view tints by **one vocabulary per
|
|
292
|
+
colour channel**, so one slot never means two words in the same view. Where a hue marks
|
|
293
|
+
a doc type, the doc type's word is shown beside it — hue never carries identity alone.
|
|
294
|
+
|
|
295
|
+
Core's default binding is `./vocabulary`'s `CORE_BINDINGS`, grouped by what the doc types are
|
|
296
|
+
for: `task` cat-2; `goal`, `commitment` cat-5; `session`, `memory` cat-3; `research`, `question`
|
|
297
|
+
cat-7; `decision`, `principle` cat-4; `concept`, `domain`, `theme` cat-6; `fact` cat-8; `concern`
|
|
298
|
+
cat-1. A surface paints the slot as written role text (`var(--tp-{tint})`), so the colour guard
|
|
299
|
+
reads it. `bindingsFrom` checks a plugin's binding and resolves it alone; a surface honouring
|
|
300
|
+
several vocabularies merges them before resolving.
|
|
301
|
+
|
|
302
|
+
## Adding a theme
|
|
303
|
+
|
|
304
|
+
1. Copy a theme directory and rename it; set `name` to the directory name.
|
|
305
|
+
2. Change values in `theme.json` — only values; the roles are fixed.
|
|
306
|
+
3. `scripts/themes.sh build` to regenerate `theme.css`.
|
|
307
|
+
4. `scripts/themes.sh check <name>/theme.json`, and `contrast` to see where you stand.
|
|
308
|
+
5. If it has a light/dark partner, set `counterpart` on both.
|
|
309
|
+
|
|
310
|
+
## The house theme
|
|
311
|
+
|
|
312
|
+
**Quiet Instrument** carries temper's visual voice to every surface: obsidian ground,
|
|
313
|
+
parchment text, one steel-blue accent; Source Serif 4 for reading, JetBrains Mono for
|
|
314
|
+
doing, Inter only for dense UI. Semantic colour is a whisper — the conditions and the
|
|
315
|
+
categorical slots exist for meaning, not decoration. Hover shifts colour and nothing
|
|
316
|
+
else; there is no bounce, no shadow, no gradient. The rail — a 2px accent-soft left
|
|
317
|
+
border — is the markdown blockquote marker made structural.
|
|
318
|
+
|
|
319
|
+
**Quiet Instrument — Paper** keeps the same restraint on a warm paper ground with ink
|
|
320
|
+
text and a deepened accent; every colour was re-chosen to clear the floors, not inverted.
|
|
321
|
+
|
|
322
|
+
Fonts are bundled by each surface (`@fontsource-variable/*`), never fetched by the contract:
|
|
323
|
+
a surface must be able to render offline. `preview/index.html` loads them from Google Fonts for convenience only.
|
|
324
|
+
|
|
325
|
+
## Setup
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
cd packages/temper-design
|
|
329
|
+
uv sync
|
|
330
|
+
scripts/themes.sh --self-check
|
|
331
|
+
```
|
package/brand/mark.svg
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="temper">
|
|
2
|
+
<path d="M 12 7 L 12 25" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" fill="none"/>
|
|
3
|
+
<path d="M 6 13 L 18 13 Q 23 13 25 16.5 Q 27 20 25 24" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" fill="none"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 40" role="img" aria-label="temper">
|
|
2
|
+
<path d="M 12 6 L 12 34" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" fill="none"/>
|
|
3
|
+
<path d="M 4 16 L 20 16 Q 27 16 30 21 Q 33 26 31 32" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" fill="none"/>
|
|
4
|
+
<text x="46" y="27" font-family="'JetBrains Mono', 'Fira Code', monospace" font-size="18" fill="currentColor" letter-spacing="0.12em">temper</text>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/* Theme-independent base and recipes. Consumes contract roles only — no literal
|
|
2
|
+
colour appears in this file, so every recipe restyles with the active theme.
|
|
3
|
+
|
|
4
|
+
The `t-*` type recipes and `ed-*` editorial classes are carried over from temper's
|
|
5
|
+
web UI by name, so a component ported from there keeps its class list. They are plain
|
|
6
|
+
CSS rather than Tailwind utilities: they are recipes a component composes, and they
|
|
7
|
+
must read the same whether or not the consumer uses Tailwind.
|
|
8
|
+
|
|
9
|
+
Import after tailwindcss (when used), contract/tailwind.css, and a theme.css. */
|
|
10
|
+
|
|
11
|
+
/* ─── Base ──────────────────────────────────────────────────────────────── */
|
|
12
|
+
|
|
13
|
+
html {
|
|
14
|
+
background: var(--tp-ground);
|
|
15
|
+
color: var(--tp-text);
|
|
16
|
+
font-family: var(--tp-font-ui);
|
|
17
|
+
-webkit-font-smoothing: antialiased;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
::selection {
|
|
21
|
+
background: var(--tp-accent-hover);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
:focus-visible {
|
|
25
|
+
outline: 2px solid var(--tp-focus);
|
|
26
|
+
outline-offset: 2px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@media (prefers-reduced-motion: reduce) {
|
|
30
|
+
*,
|
|
31
|
+
*::before,
|
|
32
|
+
*::after {
|
|
33
|
+
animation-duration: 0.01ms !important;
|
|
34
|
+
animation-iteration-count: 1 !important;
|
|
35
|
+
transition-duration: 0.01ms !important;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* ─── Type recipes ──────────────────────────────────────────────────────── */
|
|
40
|
+
|
|
41
|
+
.t-hero-title {
|
|
42
|
+
font-family: var(--tp-font-reading);
|
|
43
|
+
font-size: clamp(2.4rem, 5vw, 3.8rem);
|
|
44
|
+
font-weight: 300;
|
|
45
|
+
line-height: 1.15;
|
|
46
|
+
letter-spacing: 0.01em;
|
|
47
|
+
color: var(--tp-text);
|
|
48
|
+
}
|
|
49
|
+
.t-hero-title em { color: var(--tp-accent); font-style: italic; }
|
|
50
|
+
|
|
51
|
+
.t-ed-hero-name {
|
|
52
|
+
font-family: var(--tp-font-reading);
|
|
53
|
+
font-size: clamp(2.5rem, 6vw, 4.5rem);
|
|
54
|
+
font-weight: 400;
|
|
55
|
+
letter-spacing: -0.025em;
|
|
56
|
+
line-height: 0.95;
|
|
57
|
+
color: var(--tp-text);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.t-h2 {
|
|
61
|
+
font-family: var(--tp-font-reading);
|
|
62
|
+
font-size: 1.6rem;
|
|
63
|
+
font-weight: 300;
|
|
64
|
+
line-height: 1.3;
|
|
65
|
+
color: var(--tp-text);
|
|
66
|
+
}
|
|
67
|
+
.t-h2 em { color: var(--tp-accent); font-style: italic; }
|
|
68
|
+
|
|
69
|
+
.t-label {
|
|
70
|
+
font-family: var(--tp-font-doing);
|
|
71
|
+
font-size: 0.65rem;
|
|
72
|
+
letter-spacing: var(--tp-tracking-label);
|
|
73
|
+
text-transform: uppercase;
|
|
74
|
+
color: var(--tp-accent);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.t-body {
|
|
78
|
+
font-family: var(--tp-font-reading);
|
|
79
|
+
font-size: 1rem;
|
|
80
|
+
line-height: 1.8;
|
|
81
|
+
color: var(--tp-text-muted);
|
|
82
|
+
}
|
|
83
|
+
.t-body strong { color: var(--tp-text); font-weight: 400; }
|
|
84
|
+
|
|
85
|
+
.t-tagline {
|
|
86
|
+
font-family: var(--tp-font-reading);
|
|
87
|
+
font-size: 1.1rem;
|
|
88
|
+
font-style: italic;
|
|
89
|
+
line-height: 1.7;
|
|
90
|
+
color: var(--tp-text-subtle);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.t-ed-deck {
|
|
94
|
+
font-family: var(--tp-font-reading);
|
|
95
|
+
font-size: 1.05rem;
|
|
96
|
+
font-style: italic;
|
|
97
|
+
line-height: 1.55;
|
|
98
|
+
color: var(--tp-text-muted);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.t-code {
|
|
102
|
+
font-family: var(--tp-font-doing);
|
|
103
|
+
font-size: 0.8rem;
|
|
104
|
+
color: var(--tp-text-muted);
|
|
105
|
+
}
|
|
106
|
+
.t-code .cmd { color: var(--tp-accent); }
|
|
107
|
+
.t-code .flag { color: var(--tp-text-subtle); }
|
|
108
|
+
|
|
109
|
+
.t-wordmark {
|
|
110
|
+
font-family: var(--tp-font-doing);
|
|
111
|
+
font-weight: 500;
|
|
112
|
+
letter-spacing: var(--tp-tracking-mark);
|
|
113
|
+
color: var(--tp-accent);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.t-strip {
|
|
117
|
+
font-family: var(--tp-font-doing);
|
|
118
|
+
font-size: 0.62rem;
|
|
119
|
+
letter-spacing: var(--tp-tracking-strip);
|
|
120
|
+
text-transform: uppercase;
|
|
121
|
+
color: var(--tp-text-subtle);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.t-action {
|
|
125
|
+
font-family: var(--tp-font-doing);
|
|
126
|
+
font-size: 0.62rem;
|
|
127
|
+
letter-spacing: 0.18em;
|
|
128
|
+
text-transform: uppercase;
|
|
129
|
+
color: var(--tp-accent);
|
|
130
|
+
background: none;
|
|
131
|
+
border: none;
|
|
132
|
+
cursor: pointer;
|
|
133
|
+
transition: color var(--tp-motion-quick) var(--tp-motion-easing);
|
|
134
|
+
}
|
|
135
|
+
.t-action:hover { color: var(--tp-text); }
|
|
136
|
+
|
|
137
|
+
/* ─── Editorial structure ───────────────────────────────────────────────── */
|
|
138
|
+
|
|
139
|
+
/* The rail: the markdown blockquote marker made structural. */
|
|
140
|
+
.ed-rail {
|
|
141
|
+
padding-left: 1.8rem;
|
|
142
|
+
border-left: 2px solid var(--tp-accent-line-soft);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.ed-strip {
|
|
146
|
+
display: flex;
|
|
147
|
+
align-items: baseline;
|
|
148
|
+
gap: 0.6rem;
|
|
149
|
+
padding-bottom: 1rem;
|
|
150
|
+
border-bottom: 1px solid var(--tp-rule);
|
|
151
|
+
font-family: var(--tp-font-doing);
|
|
152
|
+
font-size: 0.62rem;
|
|
153
|
+
letter-spacing: var(--tp-tracking-strip);
|
|
154
|
+
text-transform: uppercase;
|
|
155
|
+
color: var(--tp-text-subtle);
|
|
156
|
+
}
|
|
157
|
+
.ed-strip-em { color: var(--tp-text-muted); }
|
|
158
|
+
.ed-strip-accent { color: var(--tp-accent); }
|
|
159
|
+
.ed-strip-sep { opacity: 0.5; }
|
|
160
|
+
.ed-strip-spacer { flex: 1; }
|
|
161
|
+
|
|
162
|
+
.ed-notice {
|
|
163
|
+
border-left: 2px solid var(--tp-notice);
|
|
164
|
+
padding: 0.7rem 1.2rem;
|
|
165
|
+
background: var(--tp-notice-wash);
|
|
166
|
+
color: var(--tp-notice);
|
|
167
|
+
font-family: var(--tp-font-reading);
|
|
168
|
+
font-style: italic;
|
|
169
|
+
font-size: 0.9rem;
|
|
170
|
+
line-height: 1.55;
|
|
171
|
+
}
|
|
172
|
+
.ed-notice--success {
|
|
173
|
+
border-left-color: var(--tp-success);
|
|
174
|
+
background: var(--tp-success-wash);
|
|
175
|
+
color: var(--tp-success);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.ed-action {
|
|
179
|
+
font-family: var(--tp-font-doing);
|
|
180
|
+
font-size: 0.62rem;
|
|
181
|
+
text-transform: uppercase;
|
|
182
|
+
letter-spacing: 0.18em;
|
|
183
|
+
background: none;
|
|
184
|
+
border: none;
|
|
185
|
+
cursor: pointer;
|
|
186
|
+
padding: 0.4rem 0;
|
|
187
|
+
transition: color var(--tp-motion-quick) var(--tp-motion-easing);
|
|
188
|
+
}
|
|
189
|
+
.ed-action--primary { color: var(--tp-accent); }
|
|
190
|
+
.ed-action--primary:hover { color: var(--tp-text); }
|
|
191
|
+
.ed-action--ghost { color: var(--tp-text-subtle); }
|
|
192
|
+
.ed-action--ghost:hover { color: var(--tp-text-muted); }
|
|
193
|
+
.ed-action--danger { color: var(--tp-danger); }
|
|
194
|
+
.ed-action:disabled { color: var(--tp-text-faint); cursor: not-allowed; }
|
|
195
|
+
.ed-action-sep { display: inline-block; margin: 0 0.6rem; color: var(--tp-text-faint); }
|
|
196
|
+
|
|
197
|
+
/* ─── Code ──────────────────────────────────────────────────────────────── */
|
|
198
|
+
|
|
199
|
+
.hljs .hljs-comment,
|
|
200
|
+
.hljs .hljs-quote { color: var(--tp-code-comment); font-style: italic; }
|
|
201
|
+
.hljs .hljs-attr,
|
|
202
|
+
.hljs .hljs-attribute,
|
|
203
|
+
.hljs .hljs-property,
|
|
204
|
+
.hljs .hljs-selector-tag,
|
|
205
|
+
.hljs .hljs-name,
|
|
206
|
+
.hljs .hljs-meta,
|
|
207
|
+
.hljs .hljs-symbol,
|
|
208
|
+
.hljs .hljs-bullet,
|
|
209
|
+
.hljs .hljs-addition { color: var(--tp-code-key); }
|
|
210
|
+
.hljs .hljs-string,
|
|
211
|
+
.hljs .hljs-regexp { color: var(--tp-code-string); }
|
|
212
|
+
.hljs .hljs-number,
|
|
213
|
+
.hljs .hljs-literal,
|
|
214
|
+
.hljs .hljs-keyword,
|
|
215
|
+
.hljs .hljs-built_in,
|
|
216
|
+
.hljs .hljs-type { color: var(--tp-code-number); }
|
|
217
|
+
.hljs .hljs-title,
|
|
218
|
+
.hljs .hljs-title.function_ { color: var(--tp-text); }
|
|
219
|
+
.hljs .hljs-deletion { color: var(--tp-danger); }
|
|
220
|
+
.hljs .hljs-emphasis { font-style: italic; }
|
|
221
|
+
.hljs .hljs-strong { font-weight: 600; }
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* The graph's edge recipe: how a line is painted for its role, on every surface.
|
|
2
|
+
|
|
3
|
+
The package's `graph` module decides a pair's role, dash, width, heads and cross; this recipe
|
|
4
|
+
decides only colour, so a relation reads the same on every surface that draws it:
|
|
5
|
+
|
|
6
|
+
structural text-muted the ordinary relation
|
|
7
|
+
derived text-subtle, and always dashed the derivation bridge, quieter than structure
|
|
8
|
+
contradicts danger, with a cross at the mid never told by colour alone
|
|
9
|
+
|
|
10
|
+
The rule roles are hairlines, too faint for a line 1–2.5 wide, so edges take text roles.
|
|
11
|
+
|
|
12
|
+
A component composes it as
|
|
13
|
+
|
|
14
|
+
<g class="graph-edge" data-role="<role>">
|
|
15
|
+
<line class="graph-line" stroke-width=… stroke-dasharray=… />
|
|
16
|
+
<path class="graph-head" d=… /> one per head the pair carries
|
|
17
|
+
<g class="graph-cross"><line …/><line …/></g> when the pair contradicts
|
|
18
|
+
</g>
|
|
19
|
+
|
|
20
|
+
and paints no edge colour of its own. Roles only — no literal colour appears here. */
|
|
21
|
+
|
|
22
|
+
/* Each role spelled out per rule rather than through a variable of the recipe's own, so a colour
|
|
23
|
+
guard reading a surface sees a role wherever a colour is painted. */
|
|
24
|
+
|
|
25
|
+
.graph-line {
|
|
26
|
+
stroke: var(--tp-text-muted);
|
|
27
|
+
fill: none;
|
|
28
|
+
}
|
|
29
|
+
.graph-head {
|
|
30
|
+
fill: var(--tp-text-muted);
|
|
31
|
+
stroke: none;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.graph-edge[data-role="derived"] .graph-line {
|
|
35
|
+
stroke: var(--tp-text-subtle);
|
|
36
|
+
}
|
|
37
|
+
.graph-edge[data-role="derived"] .graph-head {
|
|
38
|
+
fill: var(--tp-text-subtle);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.graph-edge[data-role="contradicts"] .graph-line,
|
|
42
|
+
.graph-edge[data-role="contradicts"] .graph-cross {
|
|
43
|
+
stroke: var(--tp-danger);
|
|
44
|
+
}
|
|
45
|
+
.graph-edge[data-role="contradicts"] .graph-head {
|
|
46
|
+
fill: var(--tp-danger);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.graph-cross {
|
|
50
|
+
stroke-width: 1.6;
|
|
51
|
+
stroke-linecap: round;
|
|
52
|
+
fill: none;
|
|
53
|
+
}
|