@kouji-ui/themes 0.0.6 → 0.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kouji-ui/themes",
3
- "version": "0.0.6",
3
+ "version": "0.1.1",
4
4
  "description": "Theme tokens and theme stylesheets for kouji-ui — pure CSS, swappable at runtime via data-theme.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,7 +32,8 @@
32
32
  "exports": {
33
33
  ".": "./src/index.css",
34
34
  "./base.css": "./src/base.css",
35
- "./themes/dark.css": "./src/themes/dark.css"
35
+ "./density.css": "./src/density.css",
36
+ "./themes/*.css": "./src/themes/*.css"
36
37
  },
37
38
  "scripts": {
38
39
  "build": "echo 'no build for css-only package'",
package/src/base.css CHANGED
@@ -128,6 +128,26 @@
128
128
  --kj-base-transition-fast: 0.12s ease;
129
129
  --kj-base-transition-base: 0.2s ease;
130
130
 
131
+ /* Motion primitives — durations + easings for the motion preset
132
+ library (@kouji-ui/core motion.css). Semantic --kj-motion-* tokens
133
+ below resolve from these; motion.css also carries inline fallbacks
134
+ so it works when themes aren't loaded. Themes may retune any of
135
+ these for a distinct motion identity. */
136
+ --kj-base-duration-fast: 150ms;
137
+ --kj-base-duration-base: 250ms;
138
+ --kj-base-duration-slow: 400ms;
139
+ --kj-base-ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1);
140
+ --kj-base-ease-in: cubic-bezier(0.7, 0, 0.84, 0);
141
+ --kj-base-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
142
+
143
+ --kj-motion-duration-sm: var(--kj-base-duration-fast);
144
+ --kj-motion-duration-md: var(--kj-base-duration-base);
145
+ --kj-motion-duration-lg: var(--kj-base-duration-slow);
146
+ --kj-motion-ease: var(--kj-base-ease-emphasized);
147
+ --kj-motion-ease-in: var(--kj-base-ease-in);
148
+ --kj-motion-ease-spring: var(--kj-base-ease-spring);
149
+ --kj-motion-distance: 0.5rem;
150
+
131
151
  /* ── Display-tone primitives (per-theme overridable, sane defaults)
132
152
  Themes only redeclare when their identity calls for it (e.g. retro
133
153
  wants italic display; bauhaus wants extra-heavy weight; sakura
@@ -0,0 +1,135 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ @kouji-ui/themes — density layer
3
+ App-wide density. Before this, density existed only on the table
4
+ (--kj-table-row-height); everything else was fixed.
5
+
6
+ Mechanism: two inherited scalars, not per-component selectors.
7
+ Components use ViewEncapsulation.None, so a [data-density] selector
8
+ WOULD reach them — but it would need one block per component per level,
9
+ and the ratio would live in dozens of places. Two inherited numbers put
10
+ the ratio in one place and, because custom properties inherit, a dense
11
+ region nests correctly inside a comfortable app with no extra rules.
12
+
13
+ Every derived value is round()ed to a whole pixel. Fractional heights
14
+ blur hairlines and desync rows from any JS/SVG surface that mirrors them.
15
+
16
+ Layer note: these rules join the EXISTING kj.base layer. Do not add a
17
+ kj.density sublayer — that would mean editing the @layer statement in
18
+ base.css, which desyncs consumers who re-declare the layer order.
19
+ Sitting in kj.base keeps themes (kj.shared) and consumer CSS above it.
20
+ ──────────────────────────────────────────────────────────── */
21
+
22
+ /* Registered so JS can read a resolved px value. An UNregistered custom
23
+ property has no computed value: getComputedStyle() returns the raw token
24
+ stream ("round(calc(...))"), and parseFloat gives NaN.
25
+
26
+ Only the scalars and the JS-read ladders are registered. The existing
27
+ --kj-space-* / --kj-text-* deliberately are NOT: registration makes
28
+ invalid-at-computed-value-time *apply*, which would silently discard a
29
+ legitimate consumer override and is a breaking change. */
30
+ @property --kj-density { syntax: "<number>"; inherits: true; initial-value: 1; }
31
+ @property --kj-type-scale { syntax: "<number>"; inherits: true; initial-value: 1; }
32
+
33
+ @property --kj-ctl-h-xs { syntax: "<length>"; inherits: true; initial-value: 28px; }
34
+ @property --kj-ctl-h-sm { syntax: "<length>"; inherits: true; initial-value: 32px; }
35
+ @property --kj-ctl-h-md { syntax: "<length>"; inherits: true; initial-value: 36px; }
36
+ @property --kj-ctl-h-lg { syntax: "<length>"; inherits: true; initial-value: 44px; }
37
+ @property --kj-ctl-h-xl { syntax: "<length>"; inherits: true; initial-value: 52px; }
38
+ @property --kj-row-h { syntax: "<length>"; inherits: true; initial-value: 36px; }
39
+
40
+ @layer kj.base {
41
+ :root {
42
+ --kj-density: 1;
43
+ --kj-type-scale: 1;
44
+
45
+ /* ── numeric spacing ladder ──
46
+ The t-shirt names below re-point at these. Values are kept in rem so
47
+ spacing still tracks the root font size, exactly as before. At
48
+ density 1 every t-shirt token resolves to its previous value to the
49
+ byte — this is a re-pointing, not a re-scaling. */
50
+ --kj-space-1: round(calc(0.125rem * var(--kj-density)), 1px); /* 2px */
51
+ --kj-space-2: round(calc(0.25rem * var(--kj-density)), 1px); /* 4px */
52
+ --kj-space-3: round(calc(0.375rem * var(--kj-density)), 1px); /* 6px */
53
+ --kj-space-4: round(calc(0.5rem * var(--kj-density)), 1px); /* 8px */
54
+ --kj-space-5: round(calc(0.625rem * var(--kj-density)), 1px); /* 10px */
55
+ --kj-space-6: round(calc(0.75rem * var(--kj-density)), 1px); /* 12px */
56
+ --kj-space-7: round(calc(1rem * var(--kj-density)), 1px); /* 16px */
57
+ --kj-space-8: round(calc(1.25rem * var(--kj-density)), 1px); /* 20px */
58
+ --kj-space-9: round(calc(1.5rem * var(--kj-density)), 1px); /* 24px */
59
+ --kj-space-10: round(calc(1.75rem * var(--kj-density)), 1px); /* 28px */
60
+ --kj-space-11: round(calc(2rem * var(--kj-density)), 1px); /* 32px */
61
+
62
+ /* t-shirt names stay the documented API; numeric is the dense-UI
63
+ escape hatch. These land on the same values they always had. */
64
+ --kj-space-xs: var(--kj-space-2); /* 4px */
65
+ --kj-space-sm: var(--kj-space-4); /* 8px */
66
+ --kj-space-md: var(--kj-space-6); /* 12px */
67
+ --kj-space-lg: var(--kj-space-7); /* 16px */
68
+ --kj-space-xl: var(--kj-space-9); /* 24px */
69
+ --kj-space-2xl: var(--kj-space-11); /* 32px */
70
+ --kj-space-3xl: round(calc(3rem * var(--kj-density)), 1px); /* 48px */
71
+ --kj-space-4xl: round(calc(4rem * var(--kj-density)), 1px); /* 64px */
72
+ --kj-space-5xl: round(calc(6rem * var(--kj-density)), 1px); /* 96px */
73
+ --kj-space-6xl: round(calc(8rem * var(--kj-density)), 1px); /* 128px */
74
+
75
+ /* ── type ──
76
+ xs..2xl keep their existing values at scale 1. 3xs/2xs and 3xl/4xl/
77
+ display are ADDITIVE: dense UIs need badge- and meta-sized steps that
78
+ a 6-step scale cannot express, and new names mean no reflow for
79
+ existing consumers. Type is snapped to a half pixel, not a whole one —
80
+ fonts render fine on halves and it keeps the ramp from collapsing. */
81
+ --kj-text-3xs: round(calc(0.625rem * var(--kj-type-scale)), 0.5px); /* 10px */
82
+ --kj-text-2xs: round(calc(0.6875rem * var(--kj-type-scale)), 0.5px); /* 11px */
83
+ --kj-text-xs: round(calc(0.75rem * var(--kj-type-scale)), 0.5px); /* 12px */
84
+ --kj-text-sm: round(calc(0.875rem * var(--kj-type-scale)), 0.5px); /* 14px */
85
+ --kj-text-base: round(calc(1rem * var(--kj-type-scale)), 0.5px); /* 16px */
86
+ --kj-text-lg: round(calc(1.125rem * var(--kj-type-scale)), 0.5px); /* 18px */
87
+ --kj-text-xl: round(calc(1.25rem * var(--kj-type-scale)), 0.5px); /* 20px */
88
+ --kj-text-2xl: round(calc(1.5rem * var(--kj-type-scale)), 0.5px); /* 24px */
89
+ --kj-text-3xl: round(calc(2rem * var(--kj-type-scale)), 0.5px); /* 32px */
90
+ --kj-text-4xl: round(calc(2.5rem * var(--kj-type-scale)), 0.5px); /* 40px */
91
+ --kj-text-display: round(calc(3rem * var(--kj-type-scale)), 0.5px); /* 48px */
92
+
93
+ /* Code surfaces (Monaco, terminals) take metrics as JS numbers — they
94
+ cannot inherit font-size — so they read these two directly. */
95
+ --kj-text-code: round(calc(0.8125rem * var(--kj-type-scale)), 0.5px); /* 13px */
96
+ --kj-leading-code: round(calc(1.125rem * var(--kj-type-scale)), 1px); /* 18px */
97
+
98
+ /* ── control heights ──
99
+ sm/md/lg stay each component's semantic API; this shared ladder is
100
+ what those variants resolve to, so density scales every variant
101
+ uniformly instead of each component hardcoding a rem value. Values
102
+ match the current button heights exactly at density 1. */
103
+ --kj-ctl-h-xs: round(calc(1.75rem * var(--kj-density)), 1px); /* 28px */
104
+ --kj-ctl-h-sm: round(calc(2rem * var(--kj-density)), 1px); /* 32px */
105
+ --kj-ctl-h-md: round(calc(2.25rem * var(--kj-density)), 1px); /* 36px */
106
+ --kj-ctl-h-lg: round(calc(2.75rem * var(--kj-density)), 1px); /* 44px */
107
+ --kj-ctl-h-xl: round(calc(3.25rem * var(--kj-density)), 1px); /* 52px */
108
+
109
+ /* Row height for list/table/tree surfaces. */
110
+ --kj-row-h: var(--kj-ctl-h-md);
111
+ }
112
+
113
+ /* ── presets ──
114
+ Ratios validated in a dense IDE: type breathes more gently than boxes,
115
+ because an 0.85 squeeze that flatters padding is punishing on 12px text.
116
+
117
+ "standard" is declared as an explicit no-op so KjTableDensity's existing
118
+ three values all remain valid — renaming or dropping one would be an API
119
+ break. "comfy" is accepted as an alias; "comfortable" stays canonical. */
120
+ [data-density="compact"] {
121
+ --kj-density: 0.85;
122
+ --kj-type-scale: 0.875;
123
+ }
124
+
125
+ [data-density="standard"] {
126
+ --kj-density: 1;
127
+ --kj-type-scale: 1;
128
+ }
129
+
130
+ [data-density="comfortable"],
131
+ [data-density="comfy"] {
132
+ --kj-density: 1.18;
133
+ --kj-type-scale: 1.125;
134
+ }
135
+ }
@@ -0,0 +1,144 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import postcss from 'postcss';
4
+ import { describe, expect, test } from 'vitest';
5
+
6
+ const css = readFileSync(resolve(import.meta.dirname, 'density.css'), 'utf-8');
7
+ const baseCss = readFileSync(resolve(import.meta.dirname, 'base.css'), 'utf-8');
8
+ const indexCss = readFileSync(resolve(import.meta.dirname, 'index.css'), 'utf-8');
9
+
10
+ /** Custom properties registered via `@property` in density.css. */
11
+ function registeredProps(text: string): Set<string> {
12
+ const out = new Set<string>();
13
+ postcss.parse(text).walkAtRules('property', rule => out.add(rule.params.trim()));
14
+ return out;
15
+ }
16
+
17
+ /** Declarations inside a rule whose selector matches `selector`. */
18
+ function declsFor(text: string, selector: string): Map<string, string> {
19
+ const out = new Map<string, string>();
20
+ postcss.parse(text).walkRules(rule => {
21
+ if (!rule.selector.includes(selector)) return;
22
+ rule.walkDecls(decl => out.set(decl.prop, decl.value));
23
+ });
24
+ return out;
25
+ }
26
+
27
+ describe('density layer', () => {
28
+ test('is exported from the package entry', () => {
29
+ expect(indexCss).toContain("@import './density.css';");
30
+ });
31
+
32
+ test('joins the existing kj.base layer without redeclaring layer order', () => {
33
+ // Editing the `@layer kj.reset, kj.base, kj.shared, kj.component` statement
34
+ // desyncs consumers who re-declare it, so density.css must not carry one.
35
+ expect(css).toMatch(/@layer\s+kj\.base\s*\{/);
36
+ expect(css).not.toMatch(/@layer\s+[\w.]+\s*,/);
37
+ expect(baseCss).toMatch(/@layer kj\.reset, kj\.base, kj\.shared, kj\.component;/);
38
+ });
39
+
40
+ describe('@property registration', () => {
41
+ const registered = registeredProps(css);
42
+
43
+ test('registers the two scalars', () => {
44
+ expect(registered).toContain('--kj-density');
45
+ expect(registered).toContain('--kj-type-scale');
46
+ });
47
+
48
+ test('registers the height ladder that JS reads', () => {
49
+ for (const t of ['xs', 'sm', 'md', 'lg', 'xl']) {
50
+ expect(registered).toContain(`--kj-ctl-h-${t}`);
51
+ }
52
+ expect(registered).toContain('--kj-row-h');
53
+ });
54
+
55
+ test('does NOT register the pre-existing space/text tokens', () => {
56
+ // Registering them would make invalid-at-computed-value-time APPLY,
57
+ // silently discarding a legitimate consumer override such as
58
+ // `--kj-space-md: 0`. That is a breaking change, so it must not happen.
59
+ for (const prop of registered) {
60
+ expect(prop, `${prop} must not be registered`).not.toMatch(
61
+ /^--kj-(space|text)-/,
62
+ );
63
+ }
64
+ });
65
+ });
66
+
67
+ describe('scale', () => {
68
+ const root = declsFor(css, ':root');
69
+
70
+ test('every t-shirt spacing name re-points at the numeric ladder', () => {
71
+ const expected: Record<string, string> = {
72
+ '--kj-space-xs': 'var(--kj-space-2)',
73
+ '--kj-space-sm': 'var(--kj-space-4)',
74
+ '--kj-space-md': 'var(--kj-space-6)',
75
+ '--kj-space-lg': 'var(--kj-space-7)',
76
+ '--kj-space-xl': 'var(--kj-space-9)',
77
+ '--kj-space-2xl': 'var(--kj-space-11)',
78
+ };
79
+ for (const [name, target] of Object.entries(expected)) {
80
+ expect(root.get(name), `${name} must re-point at ${target}`).toBe(target);
81
+ }
82
+ });
83
+
84
+ test('the numeric ladder and heights derive from --kj-density', () => {
85
+ for (const n of [1, 4, 7, 11]) {
86
+ expect(root.get(`--kj-space-${n}`)).toMatch(
87
+ /round\(calc\(.*var\(--kj-density\).*\),\s*1px\)/,
88
+ );
89
+ }
90
+ for (const t of ['xs', 'sm', 'md', 'lg', 'xl']) {
91
+ expect(root.get(`--kj-ctl-h-${t}`)).toMatch(
92
+ /round\(calc\(.*var\(--kj-density\).*\),\s*1px\)/,
93
+ );
94
+ }
95
+ });
96
+
97
+ test('the type ramp derives from --kj-type-scale, snapped to a half px', () => {
98
+ for (const t of ['3xs', '2xs', 'xs', 'sm', 'base', 'lg', 'xl', '2xl']) {
99
+ expect(root.get(`--kj-text-${t}`)).toMatch(
100
+ /round\(calc\(.*var\(--kj-type-scale\).*\),\s*0\.5px\)/,
101
+ );
102
+ }
103
+ });
104
+
105
+ test('exposes code metrics for editor and terminal surfaces', () => {
106
+ expect(root.get('--kj-text-code')).toBeDefined();
107
+ expect(root.get('--kj-leading-code')).toBeDefined();
108
+ });
109
+ });
110
+
111
+ describe('presets', () => {
112
+ test('all three KjTableDensity values remain valid selectors', () => {
113
+ // Renaming or dropping one would break the existing table API.
114
+ for (const level of ['compact', 'standard', 'comfortable']) {
115
+ expect(css).toContain(`[data-density="${level}"]`);
116
+ }
117
+ });
118
+
119
+ test('standard is an explicit no-op', () => {
120
+ const standard = declsFor(css, '[data-density="standard"]');
121
+ expect(standard.get('--kj-density')).toBe('1');
122
+ expect(standard.get('--kj-type-scale')).toBe('1');
123
+ });
124
+
125
+ test('comfy is accepted as an alias of comfortable', () => {
126
+ expect(css).toMatch(/\[data-density="comfortable"\],\s*\[data-density="comfy"\]/);
127
+ });
128
+
129
+ test('type breathes more gently than boxes at both ends', () => {
130
+ const compact = declsFor(css, '[data-density="compact"]');
131
+ const comfortable = declsFor(css, '[data-density="comfortable"]');
132
+
133
+ const cDensity = Number(compact.get('--kj-density'));
134
+ const cType = Number(compact.get('--kj-type-scale'));
135
+ const fDensity = Number(comfortable.get('--kj-density'));
136
+ const fType = Number(comfortable.get('--kj-type-scale'));
137
+
138
+ // An 0.85 squeeze that flatters padding is punishing on 12px text, so the
139
+ // type scalar must always sit closer to 1 than the box scalar.
140
+ expect(1 - cType).toBeLessThan(1 - cDensity);
141
+ expect(fType - 1).toBeLessThan(fDensity - 1);
142
+ });
143
+ });
144
+ });
package/src/index.css CHANGED
@@ -1,4 +1,5 @@
1
1
  @import './base.css';
2
+ @import './density.css';
2
3
  @import './themes/dark.css';
3
4
  @import './themes/light.css';
4
5
  @import './themes/kouji.css';
@@ -12,3 +13,5 @@
12
13
  @import './themes/forest.css';
13
14
  @import './themes/nord.css';
14
15
  @import './themes/terminal.css';
16
+ @import './themes/orrery.css';
17
+ @import './themes/orrery-light.css';
@@ -84,5 +84,12 @@
84
84
  --kj-shadow-focus: 0 0 0 3px #1d3aff;
85
85
 
86
86
  --kj-button-bg-hover: #8e0f1d;
87
+ /* ── chart palette ── */
88
+ --kj-chart-1: var(--kj-bg-primary);
89
+ --kj-chart-2: var(--kj-bg-accent);
90
+ --kj-chart-3: var(--kj-bg-success);
91
+ --kj-chart-4: var(--kj-bg-warning);
92
+ --kj-chart-5: var(--kj-bg-info, var(--kj-bg-accent-subtle));
93
+ --kj-chart-6: var(--kj-bg-danger);
87
94
  }
88
95
  }
@@ -87,5 +87,12 @@
87
87
  /* 8% darken on #0a2540 is invisible; lift to #15355a so the hover
88
88
  reads against the deep navy primary. */
89
89
  --kj-button-bg-hover: #15355a;
90
+ /* ── chart palette ── */
91
+ --kj-chart-1: var(--kj-bg-primary);
92
+ --kj-chart-2: var(--kj-bg-accent);
93
+ --kj-chart-3: var(--kj-bg-success);
94
+ --kj-chart-4: var(--kj-bg-warning);
95
+ --kj-chart-5: var(--kj-bg-info, var(--kj-bg-accent-subtle));
96
+ --kj-chart-6: var(--kj-bg-danger);
90
97
  }
91
98
  }
@@ -78,5 +78,12 @@
78
78
  --kj-shadow-md: 4px 4px 0 #0a0a0a;
79
79
  --kj-shadow-lg: 8px 8px 0 #0a0a0a;
80
80
  --kj-shadow-focus: 0 0 0 3px #ff007f;
81
+ /* ── chart palette ── */
82
+ --kj-chart-1: var(--kj-bg-primary);
83
+ --kj-chart-2: var(--kj-bg-accent);
84
+ --kj-chart-3: var(--kj-bg-success);
85
+ --kj-chart-4: var(--kj-bg-warning);
86
+ --kj-chart-5: var(--kj-bg-info, var(--kj-bg-accent-subtle));
87
+ --kj-chart-6: var(--kj-bg-danger);
81
88
  }
82
89
  }
@@ -87,5 +87,12 @@
87
87
  --kj-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
88
88
  --kj-shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.6);
89
89
  --kj-shadow-focus: 0 0 0 3px rgba(43, 98, 219, 0.5);
90
+ /* ── chart palette ── */
91
+ --kj-chart-1: var(--kj-bg-primary);
92
+ --kj-chart-2: var(--kj-bg-accent);
93
+ --kj-chart-3: var(--kj-bg-success);
94
+ --kj-chart-4: var(--kj-bg-warning);
95
+ --kj-chart-5: var(--kj-bg-info, var(--kj-bg-accent-subtle));
96
+ --kj-chart-6: var(--kj-bg-danger);
90
97
  }
91
98
  }
@@ -81,5 +81,12 @@
81
81
  /* Indigo deepens on hover; fg stays cream to match the desert tone
82
82
  (already the default fg-on-primary, so no fg-hover needed). */
83
83
  --kj-button-bg-hover: #281a48;
84
+ /* ── chart palette ── */
85
+ --kj-chart-1: var(--kj-bg-primary);
86
+ --kj-chart-2: var(--kj-bg-accent);
87
+ --kj-chart-3: var(--kj-bg-success);
88
+ --kj-chart-4: var(--kj-bg-warning);
89
+ --kj-chart-5: var(--kj-bg-info, var(--kj-bg-accent-subtle));
90
+ --kj-chart-6: var(--kj-bg-danger);
84
91
  }
85
92
  }
@@ -83,5 +83,12 @@
83
83
  /* Mustard gold brightens on hover (8% black-mix would push it duller
84
84
  on a dark theme — we want it more luminous against the moss bg). */
85
85
  --kj-button-bg-hover: #e8b22a;
86
+ /* ── chart palette ── */
87
+ --kj-chart-1: var(--kj-bg-primary);
88
+ --kj-chart-2: var(--kj-bg-accent);
89
+ --kj-chart-3: var(--kj-bg-success);
90
+ --kj-chart-4: var(--kj-bg-warning);
91
+ --kj-chart-5: var(--kj-bg-info, var(--kj-bg-accent-subtle));
92
+ --kj-chart-6: var(--kj-bg-danger);
86
93
  }
87
94
  }
@@ -98,6 +98,13 @@
98
98
  --kj-shadow-md: 0 0 0 1px #1a1a1a;
99
99
  --kj-shadow-lg: 0 0 0 2px #1a1a1a;
100
100
  --kj-shadow-focus: 0 0 0 2px #c4ff3d;
101
+ /* ── chart palette ── */
102
+ --kj-chart-1: var(--kj-bg-primary);
103
+ --kj-chart-2: var(--kj-bg-accent);
104
+ --kj-chart-3: var(--kj-bg-success);
105
+ --kj-chart-4: var(--kj-bg-warning);
106
+ --kj-chart-5: var(--kj-bg-info, var(--kj-bg-accent-subtle));
107
+ --kj-chart-6: var(--kj-bg-danger);
101
108
  }
102
109
  }
103
110
 
@@ -93,5 +93,12 @@
93
93
  /* 8% darken against #1a1a1a is nearly imperceptible; lift toward
94
94
  #3a3a3a so the hover reads as a deliberate state change. */
95
95
  --kj-button-bg-hover: #3a3a3a;
96
+ /* ── chart palette ── */
97
+ --kj-chart-1: var(--kj-bg-primary);
98
+ --kj-chart-2: var(--kj-bg-accent);
99
+ --kj-chart-3: var(--kj-bg-success);
100
+ --kj-chart-4: var(--kj-bg-warning);
101
+ --kj-chart-5: var(--kj-bg-info, var(--kj-bg-accent-subtle));
102
+ --kj-chart-6: var(--kj-bg-danger);
96
103
  }
97
104
  }
@@ -95,5 +95,12 @@
95
95
  /* Deepen the spearmint on hover (~10% darker) so the interaction
96
96
  reads on the bright body. */
97
97
  --kj-button-bg-hover: #035a42;
98
+ /* ── chart palette ── */
99
+ --kj-chart-1: var(--kj-bg-primary);
100
+ --kj-chart-2: var(--kj-bg-accent);
101
+ --kj-chart-3: var(--kj-bg-success);
102
+ --kj-chart-4: var(--kj-bg-warning);
103
+ --kj-chart-5: var(--kj-bg-info, var(--kj-bg-accent-subtle));
104
+ --kj-chart-6: var(--kj-bg-danger);
98
105
  }
99
106
  }
@@ -81,5 +81,12 @@
81
81
  /* Brighten frost teal on hover (dark themes need luminosity lift,
82
82
  not the default 8% black-mix darken). */
83
83
  --kj-button-bg-hover: #a3d4e2;
84
+ /* ── chart palette ── */
85
+ --kj-chart-1: var(--kj-bg-primary);
86
+ --kj-chart-2: var(--kj-bg-accent);
87
+ --kj-chart-3: var(--kj-bg-success);
88
+ --kj-chart-4: var(--kj-bg-warning);
89
+ --kj-chart-5: var(--kj-bg-info, var(--kj-bg-accent-subtle));
90
+ --kj-chart-6: var(--kj-bg-danger);
84
91
  }
85
92
  }
@@ -0,0 +1,103 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ orrery-light theme — shared layer
3
+ "Paper": One Light's structure and soft ink, shifted down into real grey.
4
+ Pure neutral (chroma 0.0000). Elevation goes DOWN, not up — the editor is
5
+ the single brightest surface and everything else recedes, and `elevated`
6
+ is capped short of white so popovers never glare.
7
+ ──────────────────────────────────────────────────────────── */
8
+
9
+ @layer kj.shared {
10
+ [data-theme="orrery-light"] {
11
+ /* ── shape ── */
12
+ --kj-radius-box: 0.5rem;
13
+ --kj-radius-box-lg: 0.75rem;
14
+ --kj-radius-field: 0.3125rem;
15
+ --kj-radius-selector: 0.3125rem;
16
+ --kj-border: var(--kj-base-border-1);
17
+ --kj-depth: 1;
18
+
19
+ /* ── type ── */
20
+ --kj-font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
21
+ --kj-font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
22
+ --kj-font-display: var(--kj-font-sans);
23
+ --kj-display-weight: 600;
24
+ --kj-letter-spacing: -0.01em;
25
+
26
+ /* ── motion ── */
27
+ --kj-transition: var(--kj-base-transition-fast);
28
+
29
+ /* ── neutral surfaces ── */
30
+ --kj-bg-body: #dedede;
31
+ --kj-bg-surface: #e8e8e8;
32
+ --kj-bg-field: #e4e4e4;
33
+ --kj-bg-elevated: #f7f7f7;
34
+ --kj-bg-overlay: rgba(30, 30, 30, 0.34);
35
+ --kj-bg-inverse: #1d1e20;
36
+ --kj-bg-disabled: #e4e4e4;
37
+
38
+ /* ── intent surfaces ──
39
+ One Light's own syntax palette fails AA on light (#50a14f = 2.86:1,
40
+ #4078f2 = 3.62:1), so these keep its muted character while clearing
41
+ 4.6:1. They are dark enough to carry white ink. */
42
+ --kj-bg-primary: #752cb3;
43
+ --kj-bg-primary-subtle: color-mix(in oklch, #752cb3 12%, #dedede);
44
+ --kj-bg-accent: #752cb3;
45
+ --kj-bg-accent-subtle: color-mix(in oklch, #752cb3 12%, #dedede);
46
+
47
+ --kj-bg-info: #1c6dc8;
48
+ --kj-bg-info-subtle: color-mix(in oklch, #1c6dc8 12%, #dedede);
49
+ --kj-bg-success: #1e7d3e;
50
+ --kj-bg-success-subtle: color-mix(in oklch, #1e7d3e 12%, #dedede);
51
+ --kj-bg-warning: #996100;
52
+ --kj-bg-warning-subtle: color-mix(in oklch, #996100 12%, #dedede);
53
+ --kj-bg-danger: #bf4240;
54
+ --kj-bg-danger-subtle: color-mix(in oklch, #bf4240 12%, #dedede);
55
+
56
+ /* ── fg class A — soft foreground, not near-black (~10:1, not 16:1) ── */
57
+ --kj-fg-default: #3b3b3b; /* 10.0:1 */
58
+ --kj-fg-muted: #515151; /* 7.1:1 */
59
+ --kj-fg-subtle: #6e6e6e; /* 4.6:1 */
60
+ --kj-fg-disabled: #8c8c8c; /* 3.0:1 — non-text / disabled only */
61
+
62
+ /* ── fg class B — every intent fill here is dark enough for white ink,
63
+ which is the inverse of the dark theme's pastel fills. ── */
64
+ --kj-fg-on-primary: #ffffff;
65
+ --kj-fg-on-accent: #ffffff;
66
+ --kj-fg-on-info: #ffffff;
67
+ --kj-fg-on-success: #ffffff;
68
+ --kj-fg-on-warning: #ffffff;
69
+ --kj-fg-on-danger: #ffffff;
70
+ --kj-fg-on-inverse: #dee0e2;
71
+
72
+ /* ── fg class C — intent as text on the body ── */
73
+ --kj-fg-primary: #8d4dcc; /* 4.6:1 */
74
+ --kj-fg-accent: #8d4dcc;
75
+ --kj-fg-info: #1c6dc8;
76
+ --kj-fg-success: #1e7d3e;
77
+ --kj-fg-warning: #996100;
78
+ --kj-fg-danger: #bf4240;
79
+
80
+ /* ── borders ── */
81
+ --kj-border-default: #d1d1d1;
82
+ --kj-border-muted: #e4e4e4;
83
+ --kj-border-strong: #bababa;
84
+ --kj-border-focus: #8d4dcc;
85
+ --kj-border-disabled: #d1d1d1;
86
+ --kj-border-primary: #8d4dcc;
87
+ --kj-border-danger: #bf4240;
88
+
89
+ /* ── shadows — shorter and weaker than dark; light elevation recedes ── */
90
+ --kj-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.12);
91
+ --kj-shadow-md: 0 6px 18px -10px rgba(0, 0, 0, 0.28);
92
+ --kj-shadow-lg: 0 14px 32px -12px rgba(0, 0, 0, 0.34);
93
+ --kj-shadow-focus: 0 0 0 3px color-mix(in oklch, #8d4dcc, transparent 80%);
94
+
95
+ /* ── chart palette — lane identity, darkened for the paper ground ── */
96
+ --kj-chart-1: #6b53b8;
97
+ --kj-chart-2: #3f6d96;
98
+ --kj-chart-3: #3f8664;
99
+ --kj-chart-4: #9a7530;
100
+ --kj-chart-5: #96566c;
101
+ --kj-chart-6: #4d6da2;
102
+ }
103
+ }
@@ -0,0 +1,108 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ orrery theme — shared layer
3
+ Graphite: a dense IDE palette. Chroma is held flat at ~0.005 OKLCH
4
+ across the whole neutral ramp — elevation is a lightness step plus a
5
+ hairline, never a hue shift. The single violet accent carries meaning;
6
+ everything else stays grey or semantic.
7
+ ──────────────────────────────────────────────────────────── */
8
+
9
+ @layer kj.shared {
10
+ [data-theme="orrery"] {
11
+ /* ── shape ── */
12
+ --kj-radius-box: 0.5rem;
13
+ --kj-radius-box-lg: 0.75rem;
14
+ --kj-radius-field: 0.3125rem;
15
+ --kj-radius-selector: 0.3125rem;
16
+ --kj-border: var(--kj-base-border-1);
17
+ --kj-depth: 1;
18
+
19
+ /* ── type — Inter for chrome, JetBrains Mono for code, paths, shas and
20
+ figures. Display is sans: an IDE has no headline face. ── */
21
+ --kj-font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
22
+ --kj-font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
23
+ --kj-font-display: var(--kj-font-sans);
24
+ --kj-display-weight: 600;
25
+ --kj-letter-spacing: -0.01em;
26
+
27
+ /* ── motion ── */
28
+ --kj-transition: var(--kj-base-transition-fast);
29
+
30
+ /* ── neutral surfaces — the graphite ramp ── */
31
+ --kj-bg-body: #121315;
32
+ --kj-bg-surface: #17181a;
33
+ --kj-bg-field: #232427;
34
+ --kj-bg-elevated: #2c2d30;
35
+ --kj-bg-overlay: rgba(4, 5, 9, 0.58);
36
+ --kj-bg-inverse: #f2f2f2;
37
+ --kj-bg-disabled: #232427;
38
+
39
+ /* ── intent surfaces ──
40
+ Accent aliases primary: graphite is a single-hue system. The `-subtle`
41
+ fills are mixed INTO the body rather than made translucent, so a subtle
42
+ surface stays opaque over any parent. */
43
+ --kj-bg-primary: #b082f7;
44
+ --kj-bg-primary-subtle: color-mix(in oklch, #b082f7 14%, #121315);
45
+ --kj-bg-accent: #b082f7;
46
+ --kj-bg-accent-subtle: color-mix(in oklch, #b082f7 14%, #121315);
47
+
48
+ --kj-bg-info: #5e9cf5;
49
+ --kj-bg-info-subtle: color-mix(in oklch, #5e9cf5 14%, #121315);
50
+ --kj-bg-success: #57c98a;
51
+ --kj-bg-success-subtle: color-mix(in oklch, #57c98a 14%, #121315);
52
+ --kj-bg-warning: #d9a441;
53
+ --kj-bg-warning-subtle: color-mix(in oklch, #d9a441 14%, #121315);
54
+ --kj-bg-danger: #e06c7a;
55
+ --kj-bg-danger-subtle: color-mix(in oklch, #e06c7a 14%, #121315);
56
+
57
+ /* ── fg class A — the ink ramp, contrast-checked against bg-field ── */
58
+ --kj-fg-default: #dee0e2; /* 12.6:1 */
59
+ --kj-fg-muted: #b0b1b3; /* 7.8:1 */
60
+ --kj-fg-subtle: #8a8b8c; /* 4.9:1 */
61
+ --kj-fg-disabled: #68696b; /* 3.0:1 — non-text / disabled only */
62
+
63
+ /* ── fg class B — ink ON a solid intent fill.
64
+ Every intent hue here is a light pastel (they double as ink on the dark
65
+ body), so near-black is the only choice that clears AA on the fill —
66
+ white would land near 2:1 on all of them. */
67
+ --kj-fg-on-primary: #0d0d0f;
68
+ --kj-fg-on-accent: #0d0d0f;
69
+ --kj-fg-on-info: #0d0d0f;
70
+ --kj-fg-on-success: #0d0d0f;
71
+ --kj-fg-on-warning: #0d0d0f;
72
+ --kj-fg-on-danger: #0d0d0f;
73
+ --kj-fg-on-inverse: #3b3b3b;
74
+
75
+ /* ── fg class C — intent as text on the body ── */
76
+ --kj-fg-primary: #c39afc;
77
+ --kj-fg-accent: #c39afc;
78
+ --kj-fg-info: #5e9cf5;
79
+ --kj-fg-success: #57c98a;
80
+ --kj-fg-warning: #d9a441;
81
+ --kj-fg-danger: #e06c7a;
82
+
83
+ /* ── borders — hairlines, not boxes ── */
84
+ --kj-border-default: #353739;
85
+ --kj-border-muted: #232427;
86
+ --kj-border-strong: #444548;
87
+ --kj-border-focus: #a855f7;
88
+ --kj-border-disabled: #353739;
89
+ --kj-border-primary: #a855f7;
90
+ --kj-border-danger: #e06c7a;
91
+
92
+ /* ── shadows — long and soft, lifted off pure black ── */
93
+ --kj-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
94
+ --kj-shadow-md: 0 8px 24px -10px rgba(0, 0, 0, 0.65);
95
+ --kj-shadow-lg: 0 16px 40px -12px rgba(0, 0, 0, 0.72);
96
+ --kj-shadow-focus: 0 0 0 3px color-mix(in oklch, #a855f7, transparent 84%);
97
+
98
+ /* ── chart palette — the lane identity colours.
99
+ Series need identity, not intent: six distinguishable hues at matched
100
+ lightness beat six semantic colours that all mean something else. */
101
+ --kj-chart-1: #8f7bd6;
102
+ --kj-chart-2: #5e8fb8;
103
+ --kj-chart-3: #67a888;
104
+ --kj-chart-4: #c09a54;
105
+ --kj-chart-5: #b8798f;
106
+ --kj-chart-6: #6f8fc4;
107
+ }
108
+ }
@@ -85,5 +85,13 @@
85
85
  reads better than the default ink on the deepened terracotta. */
86
86
  --kj-button-bg-hover: #d05c57;
87
87
  --kj-button-fg-hover: #fff5e4;
88
+
89
+ /* ── chart palette ── */
90
+ --kj-chart-1: var(--kj-bg-primary);
91
+ --kj-chart-2: var(--kj-bg-accent);
92
+ --kj-chart-3: var(--kj-bg-success);
93
+ --kj-chart-4: var(--kj-bg-warning);
94
+ --kj-chart-5: var(--kj-bg-info, var(--kj-bg-accent-subtle));
95
+ --kj-chart-6: var(--kj-bg-danger);
88
96
  }
89
97
  }
@@ -81,5 +81,12 @@
81
81
  /* Lift the cherry magenta to a deeper rose so the hover reads
82
82
  against the warm body. */
83
83
  --kj-button-bg-hover: #a01548;
84
+ /* ── chart palette ── */
85
+ --kj-chart-1: var(--kj-bg-primary);
86
+ --kj-chart-2: var(--kj-bg-accent);
87
+ --kj-chart-3: var(--kj-bg-success);
88
+ --kj-chart-4: var(--kj-bg-warning);
89
+ --kj-chart-5: var(--kj-bg-info, var(--kj-bg-accent-subtle));
90
+ --kj-chart-6: var(--kj-bg-danger);
84
91
  }
85
92
  }
@@ -82,5 +82,12 @@
82
82
  /* Dim the phosphor green on hover (slight green-down) instead of
83
83
  black-mixing — keeps the CRT look. */
84
84
  --kj-button-bg-hover: #00cc52;
85
+ /* ── chart palette ── */
86
+ --kj-chart-1: var(--kj-bg-primary);
87
+ --kj-chart-2: var(--kj-bg-accent);
88
+ --kj-chart-3: var(--kj-bg-success);
89
+ --kj-chart-4: var(--kj-bg-warning);
90
+ --kj-chart-5: var(--kj-bg-info, var(--kj-bg-accent-subtle));
91
+ --kj-chart-6: var(--kj-bg-danger);
85
92
  }
86
93
  }