@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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The standing ways into the work: the lists both surfaces offer a reader on arrival, declared
|
|
3
|
+
* once so each is offered under the same name, answering the same question, bounded the same way
|
|
4
|
+
* (`the-same-ways-in-on-both-surfaces`). The set is the one ruled for both — active goals,
|
|
5
|
+
* in-progress tasks and sessions — with the latest handoff beside them (`./handoff`).
|
|
6
|
+
*
|
|
7
|
+
* The desktop contributes these through its `temper-workflows` plugin manifest; the web has no
|
|
8
|
+
* plugin model and serves them as its own defaults. Each surface maps `filter` onto its own read.
|
|
9
|
+
* Every list is read newest update first, and says so (`WAY_IN_ORDER`): a list that does not
|
|
10
|
+
* state its order leaves the reader to guess what "first" means.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* What a way in reads: the reader's own work of one doc type, narrowed by its workflow state where
|
|
14
|
+
* one applies. `owner: '@me'` is ownership, which is the nearest read that exists of "yours": work
|
|
15
|
+
* the reader does on what someone else owns is not in it, and the scope words say "of yours" rather
|
|
16
|
+
* than claiming more. When a read of what the reader changed exists, this is where it moves.
|
|
17
|
+
*/
|
|
18
|
+
export type WayInFilter = {
|
|
19
|
+
docType: string;
|
|
20
|
+
owner: '@me';
|
|
21
|
+
status?: string;
|
|
22
|
+
stage?: string;
|
|
23
|
+
};
|
|
24
|
+
export type WayIn = {
|
|
25
|
+
id: string;
|
|
26
|
+
/** The name both surfaces show. */
|
|
27
|
+
label: string;
|
|
28
|
+
/** What the list holds, as its omission sentence names it. */
|
|
29
|
+
scope: string;
|
|
30
|
+
filter: WayInFilter;
|
|
31
|
+
};
|
|
32
|
+
/** How many rows a way in shows before the reader asks for more. */
|
|
33
|
+
export declare const WAY_IN_BOUND = 8;
|
|
34
|
+
/** The order every way in is read in, in the words both surfaces state it. */
|
|
35
|
+
export declare const WAY_IN_ORDER = "most recently updated first";
|
|
36
|
+
export declare const WAYS_IN: readonly WayIn[];
|
package/dist/ways-in.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The standing ways into the work: the lists both surfaces offer a reader on arrival, declared
|
|
3
|
+
* once so each is offered under the same name, answering the same question, bounded the same way
|
|
4
|
+
* (`the-same-ways-in-on-both-surfaces`). The set is the one ruled for both — active goals,
|
|
5
|
+
* in-progress tasks and sessions — with the latest handoff beside them (`./handoff`).
|
|
6
|
+
*
|
|
7
|
+
* The desktop contributes these through its `temper-workflows` plugin manifest; the web has no
|
|
8
|
+
* plugin model and serves them as its own defaults. Each surface maps `filter` onto its own read.
|
|
9
|
+
* Every list is read newest update first, and says so (`WAY_IN_ORDER`): a list that does not
|
|
10
|
+
* state its order leaves the reader to guess what "first" means.
|
|
11
|
+
*/
|
|
12
|
+
/** How many rows a way in shows before the reader asks for more. */
|
|
13
|
+
export const WAY_IN_BOUND = 8;
|
|
14
|
+
/** The order every way in is read in, in the words both surfaces state it. */
|
|
15
|
+
export const WAY_IN_ORDER = 'most recently updated first';
|
|
16
|
+
export const WAYS_IN = [
|
|
17
|
+
{
|
|
18
|
+
id: 'goals',
|
|
19
|
+
label: 'goals',
|
|
20
|
+
scope: 'active goals of yours',
|
|
21
|
+
filter: { docType: 'goal', owner: '@me', status: 'active' },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'tasks',
|
|
25
|
+
label: 'in progress',
|
|
26
|
+
scope: 'tasks in progress of yours',
|
|
27
|
+
filter: { docType: 'task', owner: '@me', stage: 'in-progress' },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'sessions',
|
|
31
|
+
label: 'sessions',
|
|
32
|
+
scope: 'sessions of yours',
|
|
33
|
+
filter: { docType: 'session', owner: '@me' },
|
|
34
|
+
},
|
|
35
|
+
];
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `web-paints-only-roles` / `no-private-colour-names`: a component paints colour from the
|
|
3
|
+
// contract's roles (`var(--tp-*)`, `tp-` utilities) and nothing else, so every theme restyles
|
|
4
|
+
// everything. Within the paths a surface lists, this refuses:
|
|
5
|
+
//
|
|
6
|
+
// (a) a literal colour: hex, a colour function, or a named colour;
|
|
7
|
+
// (b) a Tailwind colour utility naming the stock palette, white or black, or a colour name the
|
|
8
|
+
// surface declares for itself (`--color-<name>` in the theme files it lists);
|
|
9
|
+
// (c) an opacity modifier on a role utility: a role at 15% is a value no theme chose;
|
|
10
|
+
// (d) a colour declaration, SVG attribute, style directive or arbitrary colour utility reading
|
|
11
|
+
// a variable that is not a role or a declared carrier (below);
|
|
12
|
+
// (e) a role mixed down: a `color-mix()` reading a role, or a bridged name (below), is (c)
|
|
13
|
+
// spelled in CSS;
|
|
14
|
+
// (f) a custom property declared from a variable that is not a role: `--wx-color-font:
|
|
15
|
+
// var(--color-quiet-fg)` launders a private name past (d), which reads only colour
|
|
16
|
+
// properties by name. A custom property may read roles (fonts included), a pending hole,
|
|
17
|
+
// or a bridged name: one declared within the guarded paths whose every declaration there
|
|
18
|
+
// reads a variable and resolves to roles (`--color-background: var(--background)`, where
|
|
19
|
+
// `--background: var(--tp-ground)`). A declaration outside the paths — a vendor stylesheet,
|
|
20
|
+
// a selector scope the name is not bridged in — is not seen.
|
|
21
|
+
//
|
|
22
|
+
// Every rule reads one line at a time: a declaration whose value wraps onto the next line is not
|
|
23
|
+
// seen. A colour set from script (`el.style.setProperty`, a JS style object) is out of reach.
|
|
24
|
+
// (f) cannot tell what a custom property holds, so it over-reaches: a spacing or tracking alias
|
|
25
|
+
// (`--gap: var(--space-column)`) is refused as though it were a colour. Read the variable in the
|
|
26
|
+
// property that uses it instead (`gap: var(--space-column)`), which no rule refuses.
|
|
27
|
+
//
|
|
28
|
+
// A surface commits a config naming what it guards, relative to the config's own directory:
|
|
29
|
+
//
|
|
30
|
+
// { "paths": ["src/lib/components/Sidebar.svelte", "src/routes/(app)"],
|
|
31
|
+
// "themes": ["src/app.css"],
|
|
32
|
+
// "carriers": ["--hue"],
|
|
33
|
+
// "pending": [{ "var": "--tint", "until": "T12", "why": "plugin tint, not yet bound" }] }
|
|
34
|
+
//
|
|
35
|
+
// A carrier is a name the surface vouches for as carrying a role to its marks: `--hue` set on a
|
|
36
|
+
// view's root as `style="--hue: var(--tp-{tint})"`, read as `color: var(--hue)`. (d) admits a
|
|
37
|
+
// carrier only; a bridge alone is not enough to paint with, because it proves only the
|
|
38
|
+
// declarations inside the paths, and a vendor stylesheet outside them may set the same name to a
|
|
39
|
+
// literal. Every declaration of a carrier must write one role out — `var(--tp-cat-2)`, or with
|
|
40
|
+
// its suffix interpolated, `var(--tp-{tint})` — with no fallback and no other expression; it must
|
|
41
|
+
// resolve to roles; and something must read it. Declarations inside a `<script>` block declare
|
|
42
|
+
// nothing.
|
|
43
|
+
//
|
|
44
|
+
// A `pending` entry is a declared hole: a variable that is not a role yet, the task that makes it
|
|
45
|
+
// one, and why it waits. Its reads — and any `color-mix()` reading it — are counted and reported
|
|
46
|
+
// on every run rather than refused, so the hole stays stated. A literal beside it is still (a).
|
|
47
|
+
//
|
|
48
|
+
// A listed directory is walked for .svelte and .css files; a listed file is read whatever its
|
|
49
|
+
// extension. A surface still moving off the stock palette lists the areas it has moved, and
|
|
50
|
+
// each area that moves adds its paths. Comments are ignored.
|
|
51
|
+
//
|
|
52
|
+
// node guard/colours.mjs <colour-guard.json>
|
|
53
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
54
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
55
|
+
|
|
56
|
+
const USAGE = 'usage: colours.mjs <colour-guard.json>';
|
|
57
|
+
|
|
58
|
+
function fail(message) {
|
|
59
|
+
console.error(`colours: ${message}`);
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const args = process.argv.slice(2);
|
|
64
|
+
if (args.length !== 1) fail(USAGE);
|
|
65
|
+
const configPath = resolve(args[0]);
|
|
66
|
+
if (!existsSync(configPath)) fail(`no config at ${configPath}`);
|
|
67
|
+
let config;
|
|
68
|
+
try {
|
|
69
|
+
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
70
|
+
} catch (e) {
|
|
71
|
+
fail(`${configPath} is not JSON: ${e.message}`);
|
|
72
|
+
}
|
|
73
|
+
const base = dirname(configPath);
|
|
74
|
+
const at = (p) => (isAbsolute(p) ? p : join(base, p));
|
|
75
|
+
const paths = config.paths ?? [];
|
|
76
|
+
const themes = config.themes ?? [];
|
|
77
|
+
if (!Array.isArray(paths) || paths.length === 0)
|
|
78
|
+
fail('"paths" is empty: an empty scope guards nothing');
|
|
79
|
+
if (!Array.isArray(themes)) fail('"themes" must be a list');
|
|
80
|
+
const pendingEntries = config.pending ?? [];
|
|
81
|
+
if (!Array.isArray(pendingEntries)) fail('"pending" must be a list');
|
|
82
|
+
const pending = new Map();
|
|
83
|
+
for (const entry of pendingEntries) {
|
|
84
|
+
const { var: name, until, why } = entry ?? {};
|
|
85
|
+
if (![name, until, why].every((v) => typeof v === 'string' && v.trim()))
|
|
86
|
+
fail(`pending entry ${JSON.stringify(entry)} needs "var", "until" and "why": a hole is named`);
|
|
87
|
+
if (!/^--[a-zA-Z0-9-]+$/.test(name)) fail(`pending "var" ${name} is not a custom property`);
|
|
88
|
+
if (name.startsWith('--tp-')) fail(`pending "var" ${name} is a role: a role needs no excuse`);
|
|
89
|
+
pending.set(name, { until, reads: 0 });
|
|
90
|
+
}
|
|
91
|
+
const carrierNames = config.carriers ?? [];
|
|
92
|
+
if (!Array.isArray(carrierNames)) fail('"carriers" must be a list');
|
|
93
|
+
const carriers = new Map();
|
|
94
|
+
for (const name of carrierNames) {
|
|
95
|
+
if (typeof name !== 'string' || !/^--[a-zA-Z0-9-]+$/.test(name))
|
|
96
|
+
fail(`carrier ${JSON.stringify(name)} is not a custom property`);
|
|
97
|
+
if (name.startsWith('--tp-')) fail(`carrier ${name} is a role: a role needs no carrier`);
|
|
98
|
+
if (pending.has(name)) fail(`carrier ${name} is also pending: a name is a hole or a carrier`);
|
|
99
|
+
carriers.set(name, { reads: 0 });
|
|
100
|
+
}
|
|
101
|
+
// A carrier's declaration names one role, written out: `var(--tp-cat-2)`, or with its suffix
|
|
102
|
+
// interpolated, `var(--tp-{tint})`. No fallback, no other expression.
|
|
103
|
+
// The value a declaration rule captures stops at `}`, so an interpolated suffix is matched
|
|
104
|
+
// against the line it sits on.
|
|
105
|
+
const STRICT_CARRIER = /^\s*var\(--tp-[a-z0-9-]+\)\s*$/;
|
|
106
|
+
const strictCarrier = (name, value, line) =>
|
|
107
|
+
STRICT_CARRIER.test(value) ||
|
|
108
|
+
new RegExp(
|
|
109
|
+
`(?<![\\w-])${name}\\s*:\\s*var\\(--tp-[a-z0-9-]*\\{[\\w.]+\\}[a-z0-9-]*\\)\\s*(?:[;"'}]|$)`,
|
|
110
|
+
).test(line);
|
|
111
|
+
|
|
112
|
+
// The desktop's literal rule: hex, the colour functions, and named colours where a value ends.
|
|
113
|
+
// An id or anchor like `#r0` is not a colour; only hex-shaped runs of 3/4/6/8 count. Nor is an
|
|
114
|
+
// HTML numeric entity (`⌘`, the ⌘ glyph), which the desktop's rule would misread.
|
|
115
|
+
const LITERAL =
|
|
116
|
+
/(?<!&)#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(|\b(?:white|black|red|green|blue|gray|grey|orange|yellow|purple|pink)\b(?=\s*[;})'"])/g;
|
|
117
|
+
|
|
118
|
+
const PREFIX =
|
|
119
|
+
'(?:bg|text-shadow|text|border-[xytrbl]|border-[se]|border|ring-offset|ring|inset-ring|inset-shadow|divide|outline|placeholder|from|via|to|fill|stroke|drop-shadow|shadow|decoration|accent|caret)';
|
|
120
|
+
const VARIANTS = '(?:[a-z0-9-]+:)*';
|
|
121
|
+
// A colour utility with a plain value: `hover:bg-zinc-800/50`, `class:border-zinc-700`.
|
|
122
|
+
const UTILITY = new RegExp(
|
|
123
|
+
`(?<![\\w-])${VARIANTS}${PREFIX}-([a-z0-9-]+)(/(?:\\d+|\\[[^\\]\\s]+\\]))?(?![\\w-])`,
|
|
124
|
+
'g',
|
|
125
|
+
);
|
|
126
|
+
// A colour utility with an arbitrary value: `bg-[var(--obsidian)]`, `text-(--chalk)`,
|
|
127
|
+
// `bg-[color-mix(in_srgb,var(--hue)_15%,var(--obsidian))]`. Every variable in its body outside
|
|
128
|
+
// the contract is refused, not only the first.
|
|
129
|
+
const ARBITRARY = new RegExp(
|
|
130
|
+
`(?<![\\w-])${VARIANTS}${PREFIX}-(?:\\[([^\\]\\s]+)\\]|\\(([^\\)\\s]+)\\))`,
|
|
131
|
+
'g',
|
|
132
|
+
);
|
|
133
|
+
const NON_ROLE_NAME = /--(?!tp-)[a-zA-Z0-9-]+/g;
|
|
134
|
+
const STOCK =
|
|
135
|
+
/^(?:(?:slate|gray|zinc|neutral|stone|mauve|olive|mist|taupe|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d{2,3}|white|black)$/;
|
|
136
|
+
// A declaration that paints a colour, and the value it paints.
|
|
137
|
+
const DECLARATION =
|
|
138
|
+
/(?<![\w-])(color|background(?:-color)?|border(?:-(?:top|right|bottom|left|inline|block)(?:-(?:start|end))?)?(?:-color)?|outline(?:-color)?|fill|stroke|box-shadow|text-decoration(?:-color)?|caret-color|accent-color|stop-color|column-rule(?:-color)?)\s*:\s*([^;}"'\n]*)/g;
|
|
139
|
+
// The same, painted through markup: an SVG presentation attribute (`fill="var(--x)"`) or a
|
|
140
|
+
// Svelte style directive (`style:color="var(--x)"`).
|
|
141
|
+
const ATTRIBUTE =
|
|
142
|
+
/(?<![\w-])(?:style:)?(fill|stroke|stop-color|flood-color|lighting-color|color|background(?:-color)?|border(?:-[a-z]+)*-color|outline-color|caret-color|accent-color|text-decoration-color)\s*=\s*["'{]([^"'}\n]*)/g;
|
|
143
|
+
// A custom property's declaration and its value: `--wx-color-font: var(--x)`, or Svelte's
|
|
144
|
+
// directive for one, `style:--wx-color-font="var(--x)"` / `style:--wx-color-font={'var(--x)'}`.
|
|
145
|
+
const CUSTOM_PROPERTY = /(?<![\w-])(--[a-zA-Z0-9-]+)\s*:\s*([^;}"'\n]*)/g;
|
|
146
|
+
const CUSTOM_DIRECTIVE =
|
|
147
|
+
/(?<![\w-])style:(--[a-zA-Z0-9-]+)\s*=\s*(?:"([^"\n]*)"|'([^'\n]*)'|\{([^}\n]*)\})/g;
|
|
148
|
+
// Each custom-property declaration in `text`, as [name, value].
|
|
149
|
+
function* customProperties(text) {
|
|
150
|
+
for (const m of text.matchAll(CUSTOM_PROPERTY)) yield [m[1], m[2]];
|
|
151
|
+
for (const m of text.matchAll(CUSTOM_DIRECTIVE)) yield [m[1], m[2] ?? m[3] ?? m[4]];
|
|
152
|
+
}
|
|
153
|
+
const PRIVATE_VAR = /var\(\s*(--(?!tp-)[a-zA-Z0-9-]+)/g;
|
|
154
|
+
const ROLE_VAR = /var\(\s*--tp-/;
|
|
155
|
+
const PENDING_VAR = /var\(\s*(--[a-zA-Z0-9-]+)/g;
|
|
156
|
+
|
|
157
|
+
// Each outermost `color-mix(…)` on a line, with its balanced argument list. Like every rule here
|
|
158
|
+
// it reads one line at a time: a mix whose role sits on a later line is not seen. Tailwind's arbitrary values
|
|
159
|
+
// spell spaces as `_`, which changes nothing here.
|
|
160
|
+
function* mixes(line) {
|
|
161
|
+
for (let at = line.indexOf('color-mix('); at >= 0; ) {
|
|
162
|
+
let depth = 0;
|
|
163
|
+
let end = at + 'color-mix'.length;
|
|
164
|
+
for (; end < line.length; end++) {
|
|
165
|
+
if (line[end] === '(') depth++;
|
|
166
|
+
else if (line[end] === ')' && --depth === 0) break;
|
|
167
|
+
}
|
|
168
|
+
yield line.slice(at, end + 1);
|
|
169
|
+
// A mix nested inside this one is part of it, not a second refusal.
|
|
170
|
+
at = line.indexOf('color-mix(', end + 1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Blank out every `open … close` span, keeping its newlines so reported line numbers stay true.
|
|
175
|
+
// A scan rather than a regex replace: an unterminated comment blanks the rest of the file instead
|
|
176
|
+
// of leaving a fragment behind.
|
|
177
|
+
function blank(text, open, close) {
|
|
178
|
+
let out = '';
|
|
179
|
+
let i = 0;
|
|
180
|
+
for (;;) {
|
|
181
|
+
const start = text.indexOf(open, i);
|
|
182
|
+
if (start < 0) return out + text.slice(i);
|
|
183
|
+
out += text.slice(i, start);
|
|
184
|
+
const end = text.indexOf(close, start + open.length);
|
|
185
|
+
const stop = end < 0 ? text.length : end + close.length;
|
|
186
|
+
out += text.slice(start, stop).replace(/[^\n]/g, '');
|
|
187
|
+
if (end < 0) return out;
|
|
188
|
+
i = stop;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const stripComments = (text) =>
|
|
193
|
+
blank(blank(text, '/*', '*/'), '<!--', '-->').replace(/^[ \t]*\/\/.*$/gm, '');
|
|
194
|
+
|
|
195
|
+
// The colour names a surface declares for itself: every `--color-<name>` in its theme files that
|
|
196
|
+
// is not a role. These are what `no-private-colour-names` refuses.
|
|
197
|
+
const privateNames = new Set();
|
|
198
|
+
for (const theme of themes) {
|
|
199
|
+
const path = at(theme);
|
|
200
|
+
if (!existsSync(path)) fail(`listed theme ${theme} does not exist`);
|
|
201
|
+
const text = stripComments(readFileSync(path, 'utf8'));
|
|
202
|
+
for (const m of text.matchAll(/--color-([a-z0-9-]+)\s*:/g)) {
|
|
203
|
+
if (!m[1].startsWith('tp-')) privateNames.add(m[1]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function* walk(dir) {
|
|
208
|
+
for (const name of readdirSync(dir)) {
|
|
209
|
+
const path = join(dir, name);
|
|
210
|
+
if (statSync(path).isDirectory()) yield* walk(path);
|
|
211
|
+
else if (/\.(svelte|css)$/.test(name)) yield path;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const files = [];
|
|
216
|
+
for (const listed of paths) {
|
|
217
|
+
const path = at(listed);
|
|
218
|
+
if (!existsSync(path))
|
|
219
|
+
fail(`listed path ${listed} does not exist: a scope that names nothing guards nothing`);
|
|
220
|
+
if (statSync(path).isDirectory()) {
|
|
221
|
+
const found = [...walk(path)];
|
|
222
|
+
if (found.length === 0) fail(`listed directory ${listed} holds no .svelte or .css file`);
|
|
223
|
+
files.push(...found);
|
|
224
|
+
} else files.push(path);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Bridged names: custom properties declared within the paths whose every declaration reads only
|
|
228
|
+
// roles or other bridged names, to a fixed point. A name with any other read is not bridged, and
|
|
229
|
+
// nor is one with a declaration reading no variable at all: `style="--hue: {hue}"` is a colour
|
|
230
|
+
// supplied from script, which no theme chose, not a value that resolves to roles.
|
|
231
|
+
const UNRESOLVED = '(no variable)';
|
|
232
|
+
const declared = new Map();
|
|
233
|
+
for (const path of files) {
|
|
234
|
+
const markup = blank(stripComments(readFileSync(path, 'utf8')), '<script', '</script>');
|
|
235
|
+
for (const [name, value] of customProperties(markup)) {
|
|
236
|
+
const reads = [...value.matchAll(PRIVATE_VAR)].map((v) => v[1]);
|
|
237
|
+
if (!/var\(/.test(value)) reads.push(UNRESOLVED);
|
|
238
|
+
declared.set(name, [...(declared.get(name) ?? []), ...reads]);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const bridged = new Set();
|
|
242
|
+
for (let grew = true; grew; ) {
|
|
243
|
+
grew = false;
|
|
244
|
+
for (const [name, reads] of declared) {
|
|
245
|
+
if (!bridged.has(name) && reads.every((r) => bridged.has(r))) {
|
|
246
|
+
bridged.add(name);
|
|
247
|
+
grew = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// A carrier may be painted with only while every declaration of it the guard sees is a role.
|
|
253
|
+
const carries = (name) => carriers.has(name) && bridged.has(name);
|
|
254
|
+
const readsPending = (text) => [...text.matchAll(PENDING_VAR)].some((m) => pending.has(m[1]));
|
|
255
|
+
// A bridged name resolves to roles, so mixing it is mixing a role one hop removed.
|
|
256
|
+
const readsRole = (text) =>
|
|
257
|
+
ROLE_VAR.test(text) || [...text.matchAll(PENDING_VAR)].some((m) => bridged.has(m[1]));
|
|
258
|
+
|
|
259
|
+
function refusals(line) {
|
|
260
|
+
const out = [];
|
|
261
|
+
for (const mix of mixes(line)) {
|
|
262
|
+
if (readsRole(mix) && !readsPending(mix))
|
|
263
|
+
out.push(
|
|
264
|
+
`${mix} mixes a role to a strength no theme chose — use a role (*-wash, accent-hover, …)`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
for (const m of line.matchAll(PENDING_VAR)) {
|
|
268
|
+
if (pending.has(m[1])) pending.get(m[1]).reads++;
|
|
269
|
+
if (carriers.has(m[1])) carriers.get(m[1]).reads++;
|
|
270
|
+
}
|
|
271
|
+
for (const m of line.matchAll(LITERAL)) {
|
|
272
|
+
if (m[0].startsWith('#') && ![4, 5, 7, 9].includes(m[0].length)) continue;
|
|
273
|
+
out.push(`literal colour ${m[0]} — use a theme role`);
|
|
274
|
+
}
|
|
275
|
+
for (const m of line.matchAll(UTILITY)) {
|
|
276
|
+
const [utility, value, modifier] = m;
|
|
277
|
+
if (value.startsWith('tp-')) {
|
|
278
|
+
if (modifier) out.push(`${utility} sets a role's strength — use its -wash or -hover role`);
|
|
279
|
+
} else if (STOCK.test(value)) {
|
|
280
|
+
out.push(`${utility} paints from Tailwind's palette — use a tp- role`);
|
|
281
|
+
} else if (privateNames.has(value)) {
|
|
282
|
+
out.push(`${utility} paints a colour name the contract lacks — use a tp- role`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const m of line.matchAll(ARBITRARY)) {
|
|
286
|
+
for (const [name] of (m[1] ?? m[2]).matchAll(NON_ROLE_NAME)) {
|
|
287
|
+
if (!pending.has(name) && !carries(name))
|
|
288
|
+
out.push(`${m[0]} reads ${name}, which is not a role`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
for (const m of [...line.matchAll(DECLARATION), ...line.matchAll(ATTRIBUTE)]) {
|
|
292
|
+
for (const v of m[2].matchAll(PRIVATE_VAR)) {
|
|
293
|
+
if (pending.has(v[1]) || carries(v[1])) continue;
|
|
294
|
+
out.push(`${m[1]} reads var(${v[1]}), which is not a role`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
for (const [name, value] of customProperties(line)) {
|
|
298
|
+
if (carriers.has(name) && !strictCarrier(name, value, line))
|
|
299
|
+
out.push(
|
|
300
|
+
`carrier ${name} is declared from ${value.trim()} — write the role out: var(--tp-…)`,
|
|
301
|
+
);
|
|
302
|
+
for (const v of value.matchAll(PRIVATE_VAR)) {
|
|
303
|
+
if (pending.has(v[1]) || bridged.has(v[1])) continue;
|
|
304
|
+
out.push(`${name} is declared from var(${v[1]}), which is not a role`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let failures = 0;
|
|
311
|
+
for (const path of files) {
|
|
312
|
+
const lines = stripComments(readFileSync(path, 'utf8')).split('\n');
|
|
313
|
+
lines.forEach((line, i) => {
|
|
314
|
+
for (const why of refusals(line)) {
|
|
315
|
+
console.error(`${relative(base, path)}:${i + 1}: ${why}`);
|
|
316
|
+
failures++;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
for (const [name, { reads }] of carriers) {
|
|
321
|
+
if (!bridged.has(name)) {
|
|
322
|
+
console.error(
|
|
323
|
+
`carrier ${name} does not resolve to roles within the paths — declare it from a role`,
|
|
324
|
+
);
|
|
325
|
+
failures++;
|
|
326
|
+
} else if (reads === 0) {
|
|
327
|
+
console.error(`carrier: nothing reads ${name} — remove the entry`);
|
|
328
|
+
failures++;
|
|
329
|
+
} else console.log(`carrier: ${name} read ${reads} time(s)`);
|
|
330
|
+
}
|
|
331
|
+
let pendingReads = 0;
|
|
332
|
+
for (const [name, { until, reads }] of pending) {
|
|
333
|
+
pendingReads += reads;
|
|
334
|
+
if (reads === 0) {
|
|
335
|
+
console.error(`pending: nothing reads ${name} any more — remove the entry (${until})`);
|
|
336
|
+
failures++;
|
|
337
|
+
} else console.log(`pending: ${name} read ${reads} time(s) until ${until} — not a role yet`);
|
|
338
|
+
}
|
|
339
|
+
if (failures) {
|
|
340
|
+
console.error(
|
|
341
|
+
`\n${failures} colour(s) outside the contract. Components paint --tp-* roles only (@tasker-systems/temper-design).`,
|
|
342
|
+
);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
console.log(
|
|
346
|
+
`ok: ${files.length} file(s) paint only contract roles${pendingReads ? ', apart from the pending reads above' : ''}`,
|
|
347
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,99 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tasker-systems/temper-design",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.6.3",
|
|
4
|
+
"description": "temper's theme contract, themes and shared rendering modules: the one source every temper surface paints from",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/tasker-systems/temper.git",
|
|
10
|
+
"directory": "packages/temper-design"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"registry": "https://registry.npmjs.org"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"contract/*.css",
|
|
17
|
+
"contract/theme.schema.json",
|
|
18
|
+
"*/theme.css",
|
|
19
|
+
"*/theme.json",
|
|
20
|
+
"themes.json",
|
|
21
|
+
"brand/*.svg",
|
|
22
|
+
"guard/colours.mjs",
|
|
23
|
+
"toolchain/check.mjs",
|
|
24
|
+
"toolchain/majors.json",
|
|
25
|
+
"dist/*.js",
|
|
26
|
+
"dist/*.d.ts"
|
|
27
|
+
],
|
|
28
|
+
"exports": {
|
|
29
|
+
"./markdown": {
|
|
30
|
+
"types": "./dist/markdown.d.ts",
|
|
31
|
+
"default": "./dist/markdown.js"
|
|
32
|
+
},
|
|
33
|
+
"./sanitize": {
|
|
34
|
+
"types": "./dist/sanitize.d.ts",
|
|
35
|
+
"default": "./dist/sanitize.js"
|
|
36
|
+
},
|
|
37
|
+
"./highlight": {
|
|
38
|
+
"types": "./dist/highlight.d.ts",
|
|
39
|
+
"default": "./dist/highlight.js"
|
|
40
|
+
},
|
|
41
|
+
"./handoff": {
|
|
42
|
+
"types": "./dist/handoff.d.ts",
|
|
43
|
+
"default": "./dist/handoff.js"
|
|
44
|
+
},
|
|
45
|
+
"./ways-in": {
|
|
46
|
+
"types": "./dist/ways-in.d.ts",
|
|
47
|
+
"default": "./dist/ways-in.js"
|
|
48
|
+
},
|
|
49
|
+
"./links": {
|
|
50
|
+
"types": "./dist/links.d.ts",
|
|
51
|
+
"default": "./dist/links.js"
|
|
52
|
+
},
|
|
53
|
+
"./region": {
|
|
54
|
+
"types": "./dist/region.d.ts",
|
|
55
|
+
"default": "./dist/region.js"
|
|
56
|
+
},
|
|
57
|
+
"./omission": {
|
|
58
|
+
"types": "./dist/omission.d.ts",
|
|
59
|
+
"default": "./dist/omission.js"
|
|
60
|
+
},
|
|
61
|
+
"./theme": {
|
|
62
|
+
"types": "./dist/theme.d.ts",
|
|
63
|
+
"default": "./dist/theme.js"
|
|
64
|
+
},
|
|
65
|
+
"./vocabulary": {
|
|
66
|
+
"types": "./dist/vocabulary.d.ts",
|
|
67
|
+
"default": "./dist/vocabulary.js"
|
|
68
|
+
},
|
|
69
|
+
"./graph": {
|
|
70
|
+
"types": "./dist/graph.d.ts",
|
|
71
|
+
"default": "./dist/graph.js"
|
|
72
|
+
},
|
|
73
|
+
"./themes.json": "./themes.json",
|
|
74
|
+
"./contract/*": "./contract/*",
|
|
75
|
+
"./brand/*": "./brand/*",
|
|
76
|
+
"./*/theme.css": "./*/theme.css",
|
|
77
|
+
"./*/theme.json": "./*/theme.json"
|
|
78
|
+
},
|
|
79
|
+
"scripts": {
|
|
80
|
+
"build": "tsc",
|
|
81
|
+
"typecheck": "tsc --noEmit",
|
|
82
|
+
"test": "vitest run",
|
|
83
|
+
"lint": "biome check"
|
|
84
|
+
},
|
|
85
|
+
"dependencies": {
|
|
86
|
+
"d3-force": "^3.0.0",
|
|
87
|
+
"dompurify": "^3.4.17",
|
|
88
|
+
"highlight.js": "^11.12.0",
|
|
89
|
+
"marked": "^18.1.1",
|
|
90
|
+
"marked-highlight": "^2.2.4"
|
|
91
|
+
},
|
|
92
|
+
"devDependencies": {
|
|
93
|
+
"@biomejs/biome": "^2.5.15",
|
|
94
|
+
"@types/d3-force": "^3.0.10",
|
|
95
|
+
"jsdom": "^30",
|
|
96
|
+
"typescript": "^6.0.3",
|
|
97
|
+
"vitest": "^5.0.3"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* Generated from quiet-instrument/theme.json by themes/scripts/themes.sh build. Do not edit. */
|
|
2
|
+
[data-theme="quiet-instrument"] {
|
|
3
|
+
color-scheme: dark;
|
|
4
|
+
--tp-ground: #0a0a0f;
|
|
5
|
+
--tp-surface: #0c0c11;
|
|
6
|
+
--tp-surface-raised: #12121a;
|
|
7
|
+
--tp-overlay: rgba(10, 10, 15, 0.72);
|
|
8
|
+
--tp-text: #e8e4df;
|
|
9
|
+
--tp-text-muted: rgba(255, 255, 255, 0.65);
|
|
10
|
+
--tp-text-subtle: rgba(255, 255, 255, 0.45);
|
|
11
|
+
--tp-text-faint: rgba(255, 255, 255, 0.30);
|
|
12
|
+
--tp-rule: rgba(255, 255, 255, 0.06);
|
|
13
|
+
--tp-rule-strong: rgba(255, 255, 255, 0.10);
|
|
14
|
+
--tp-accent: #7eb8da;
|
|
15
|
+
--tp-accent-muted: rgba(126, 184, 218, 0.40);
|
|
16
|
+
--tp-accent-line: rgba(126, 184, 218, 0.50);
|
|
17
|
+
--tp-accent-line-soft: rgba(126, 184, 218, 0.25);
|
|
18
|
+
--tp-accent-wash: rgba(126, 184, 218, 0.08);
|
|
19
|
+
--tp-accent-hover: rgba(126, 184, 218, 0.12);
|
|
20
|
+
--tp-on-accent: #0a0a0f;
|
|
21
|
+
--tp-focus: #8ecbff;
|
|
22
|
+
--tp-notice: #fcd34d;
|
|
23
|
+
--tp-notice-wash: rgba(245, 158, 11, 0.05);
|
|
24
|
+
--tp-success: #a7f3d0;
|
|
25
|
+
--tp-success-wash: rgba(110, 231, 183, 0.05);
|
|
26
|
+
--tp-danger: #e0755f;
|
|
27
|
+
--tp-danger-wash: rgba(224, 117, 95, 0.08);
|
|
28
|
+
--tp-pending: #a9d2ea;
|
|
29
|
+
--tp-pending-wash: rgba(126, 184, 218, 0.06);
|
|
30
|
+
--tp-region-arriving: #8fb6e8;
|
|
31
|
+
--tp-region-arriving-wash: rgba(143, 182, 232, 0.05);
|
|
32
|
+
--tp-region-empty: #79828f;
|
|
33
|
+
--tp-region-empty-wash: rgba(255, 255, 255, 0.02);
|
|
34
|
+
--tp-region-gave-up: #b9a6de;
|
|
35
|
+
--tp-region-gave-up-wash: rgba(143, 124, 192, 0.07);
|
|
36
|
+
--tp-region-failed: #e0a377;
|
|
37
|
+
--tp-region-failed-wash: rgba(192, 122, 74, 0.08);
|
|
38
|
+
--tp-author-human: #e8e4df;
|
|
39
|
+
--tp-author-agent: #8e9fc7;
|
|
40
|
+
--tp-cat-1: #de8fa5;
|
|
41
|
+
--tp-cat-2: #67d9a4;
|
|
42
|
+
--tp-cat-3: #4fc9c9;
|
|
43
|
+
--tp-cat-4: #e08ad2;
|
|
44
|
+
--tp-cat-5: #5f87e8;
|
|
45
|
+
--tp-cat-6: #d9a83f;
|
|
46
|
+
--tp-cat-7: #8f74d9;
|
|
47
|
+
--tp-cat-8: #a8c95c;
|
|
48
|
+
--tp-cat-neutral: #a89e93;
|
|
49
|
+
--tp-code-key: #8fd8a8;
|
|
50
|
+
--tp-code-string: #fcd34d;
|
|
51
|
+
--tp-code-number: #93c5fd;
|
|
52
|
+
--tp-code-comment: rgba(255, 255, 255, 0.45);
|
|
53
|
+
--tp-font-reading: "Source Serif 4 Variable", "Source Serif 4", Georgia, "Times New Roman", serif;
|
|
54
|
+
--tp-font-doing: "JetBrains Mono Variable", "JetBrains Mono", "Fira Code", ui-monospace, monospace;
|
|
55
|
+
--tp-font-ui: "Inter Variable", Inter, system-ui, -apple-system, sans-serif;
|
|
56
|
+
--tp-radius-chip: 3px;
|
|
57
|
+
--tp-radius-panel: 0;
|
|
58
|
+
--tp-motion-quick: 150ms;
|
|
59
|
+
--tp-motion-settle: 240ms;
|
|
60
|
+
--tp-motion-easing: ease;
|
|
61
|
+
--tp-tracking-label: 0.20em;
|
|
62
|
+
--tp-tracking-strip: 0.22em;
|
|
63
|
+
--tp-tracking-eyebrow: 0.28em;
|
|
64
|
+
--tp-tracking-ui: 0.05em;
|
|
65
|
+
--tp-tracking-mark: 0.15em;
|
|
66
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../contract/theme.schema.json",
|
|
3
|
+
"kind": "theme",
|
|
4
|
+
"name": "quiet-instrument",
|
|
5
|
+
"title": "Quiet Instrument",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"description": "Obsidian ground, parchment text, one steel-blue accent. Reading in serif, doing in mono. Semantic colour is a whisper.",
|
|
8
|
+
"appearance": "dark",
|
|
9
|
+
"counterpart": "quiet-instrument-paper",
|
|
10
|
+
"provenance": "Ported from temper's design-system/ and packages/temper-ui/src/app.css (Quiet Instrument), including the region vocabulary from RegionState.svelte and the hljs palette.",
|
|
11
|
+
"tokens": {
|
|
12
|
+
"color": {
|
|
13
|
+
"ground": "#0a0a0f",
|
|
14
|
+
"surface": "#0c0c11",
|
|
15
|
+
"surface-raised": "#12121a",
|
|
16
|
+
"overlay": "rgba(10, 10, 15, 0.72)",
|
|
17
|
+
"text": "#e8e4df",
|
|
18
|
+
"text-muted": "rgba(255, 255, 255, 0.65)",
|
|
19
|
+
"text-subtle": "rgba(255, 255, 255, 0.45)",
|
|
20
|
+
"text-faint": "rgba(255, 255, 255, 0.30)",
|
|
21
|
+
"rule": "rgba(255, 255, 255, 0.06)",
|
|
22
|
+
"rule-strong": "rgba(255, 255, 255, 0.10)",
|
|
23
|
+
"accent": "#7eb8da",
|
|
24
|
+
"accent-muted": "rgba(126, 184, 218, 0.40)",
|
|
25
|
+
"accent-line": "rgba(126, 184, 218, 0.50)",
|
|
26
|
+
"accent-line-soft": "rgba(126, 184, 218, 0.25)",
|
|
27
|
+
"accent-wash": "rgba(126, 184, 218, 0.08)",
|
|
28
|
+
"accent-hover": "rgba(126, 184, 218, 0.12)",
|
|
29
|
+
"on-accent": "#0a0a0f",
|
|
30
|
+
"focus": "#8ecbff",
|
|
31
|
+
"notice": "#fcd34d",
|
|
32
|
+
"notice-wash": "rgba(245, 158, 11, 0.05)",
|
|
33
|
+
"success": "#a7f3d0",
|
|
34
|
+
"success-wash": "rgba(110, 231, 183, 0.05)",
|
|
35
|
+
"danger": "#e0755f",
|
|
36
|
+
"danger-wash": "rgba(224, 117, 95, 0.08)",
|
|
37
|
+
"pending": "#a9d2ea",
|
|
38
|
+
"pending-wash": "rgba(126, 184, 218, 0.06)",
|
|
39
|
+
"region-arriving": "#8fb6e8",
|
|
40
|
+
"region-arriving-wash": "rgba(143, 182, 232, 0.05)",
|
|
41
|
+
"region-empty": "#79828f",
|
|
42
|
+
"region-empty-wash": "rgba(255, 255, 255, 0.02)",
|
|
43
|
+
"region-gave-up": "#b9a6de",
|
|
44
|
+
"region-gave-up-wash": "rgba(143, 124, 192, 0.07)",
|
|
45
|
+
"region-failed": "#e0a377",
|
|
46
|
+
"region-failed-wash": "rgba(192, 122, 74, 0.08)",
|
|
47
|
+
"author-human": "#e8e4df",
|
|
48
|
+
"author-agent": "#8e9fc7",
|
|
49
|
+
"cat-1": "#de8fa5",
|
|
50
|
+
"cat-2": "#67d9a4",
|
|
51
|
+
"cat-3": "#4fc9c9",
|
|
52
|
+
"cat-4": "#e08ad2",
|
|
53
|
+
"cat-5": "#5f87e8",
|
|
54
|
+
"cat-6": "#d9a83f",
|
|
55
|
+
"cat-7": "#8f74d9",
|
|
56
|
+
"cat-8": "#a8c95c",
|
|
57
|
+
"cat-neutral": "#a89e93",
|
|
58
|
+
"code-key": "#8fd8a8",
|
|
59
|
+
"code-string": "#fcd34d",
|
|
60
|
+
"code-number": "#93c5fd",
|
|
61
|
+
"code-comment": "rgba(255, 255, 255, 0.45)"
|
|
62
|
+
},
|
|
63
|
+
"font": {
|
|
64
|
+
"reading": "\"Source Serif 4 Variable\", \"Source Serif 4\", Georgia, \"Times New Roman\", serif",
|
|
65
|
+
"doing": "\"JetBrains Mono Variable\", \"JetBrains Mono\", \"Fira Code\", ui-monospace, monospace",
|
|
66
|
+
"ui": "\"Inter Variable\", Inter, system-ui, -apple-system, sans-serif"
|
|
67
|
+
},
|
|
68
|
+
"radius": {
|
|
69
|
+
"chip": "3px",
|
|
70
|
+
"panel": "0"
|
|
71
|
+
},
|
|
72
|
+
"motion": {
|
|
73
|
+
"quick": "150ms",
|
|
74
|
+
"settle": "240ms",
|
|
75
|
+
"easing": "ease"
|
|
76
|
+
},
|
|
77
|
+
"tracking": {
|
|
78
|
+
"label": "0.20em",
|
|
79
|
+
"strip": "0.22em",
|
|
80
|
+
"eyebrow": "0.28em",
|
|
81
|
+
"ui": "0.05em",
|
|
82
|
+
"mark": "0.15em"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|