@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 CHANGED
@@ -1,3 +1,331 @@
1
- # Temporary Holding Version
1
+ # @tasker-systems/temper-design
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ }