@kouji-ui/themes 0.0.6

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.
@@ -0,0 +1,86 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ terminal theme — dark
3
+ Pure black with phosphor green. Monospace everywhere, sharp
4
+ geometry, no rounding. Hacker terminal / CRT aesthetic.
5
+ ──────────────────────────────────────────────────────────── */
6
+
7
+ @layer kj.shared {
8
+ [data-theme="terminal"] {
9
+ /* ── shape — sharp, hard-edged ── */
10
+ --kj-radius-box: var(--kj-base-radius-0);
11
+ --kj-radius-field: var(--kj-base-radius-0);
12
+ --kj-radius-selector: var(--kj-base-radius-0);
13
+ --kj-border: var(--kj-base-border-1);
14
+ --kj-depth: 0;
15
+
16
+ /* ── type — mono everywhere. Phosphor terminals don't kern; keep
17
+ letter-spacing at 0 and weight medium for that CRT readout look. */
18
+ --kj-font-sans: var(--kj-base-font-mono);
19
+ --kj-font-display: var(--kj-base-font-mono);
20
+ --kj-display-weight: 500;
21
+ --kj-letter-spacing: 0;
22
+ /* ── motion ── */
23
+ --kj-transition: var(--kj-base-transition-fast);
24
+
25
+ --kj-bg-body: #000000;
26
+ --kj-bg-surface: #050505;
27
+ --kj-bg-field: #0a0a0a;
28
+ --kj-bg-elevated: #0f0f0f;
29
+ --kj-bg-overlay: rgba(0, 0, 0, 0.75);
30
+ --kj-bg-inverse: #b6ffce;
31
+ --kj-bg-disabled: #1a1a1a;
32
+
33
+ --kj-bg-primary: #00ff66;
34
+ --kj-bg-primary-subtle: #0a2010;
35
+ --kj-bg-accent: #00ff66;
36
+ --kj-bg-accent-subtle: #0a2010;
37
+
38
+ --kj-bg-info: #00d4ff;
39
+ --kj-bg-info-subtle: #0a1820;
40
+ --kj-bg-success: #00ff66;
41
+ --kj-bg-success-subtle: #0a2010;
42
+ --kj-bg-warning: #ffd000;
43
+ --kj-bg-warning-subtle: #1f1a05;
44
+ --kj-bg-danger: #ff5050;
45
+ --kj-bg-danger-subtle: #2a0a0a;
46
+
47
+ --kj-fg-default: #b6ffce;
48
+ --kj-fg-muted: #86cc9a;
49
+ --kj-fg-subtle: #5a9b6c;
50
+ --kj-fg-disabled: #2a3a30;
51
+
52
+ /* Bright fills + black text — phosphor-on-CRT inversion. */
53
+ --kj-fg-on-primary: #000000;
54
+ --kj-fg-on-accent: #000000;
55
+ --kj-fg-on-info: #000000;
56
+ --kj-fg-on-success: #000000;
57
+ --kj-fg-on-warning: #000000;
58
+ --kj-fg-on-danger: #000000;
59
+ --kj-fg-on-inverse: #000000;
60
+
61
+ --kj-fg-primary: #00ff66;
62
+ --kj-fg-accent: #00ff66;
63
+ --kj-fg-info: #00d4ff;
64
+ --kj-fg-success: #00ff66;
65
+ --kj-fg-warning: #ffd000;
66
+ --kj-fg-danger: #ff7575;
67
+
68
+ --kj-border-default: #1a3325;
69
+ --kj-border-muted: #0a1f10;
70
+ --kj-border-strong: #2a5535;
71
+ --kj-border-focus: #00ff66;
72
+ --kj-border-disabled: #1a1a1a;
73
+ --kj-border-primary: #00ff66;
74
+ --kj-border-danger: #ff5050;
75
+
76
+ /* Green phosphor glow instead of soft drop-shadow. */
77
+ --kj-shadow-sm: 0 0 4px rgba(0, 255, 102, 0.20);
78
+ --kj-shadow-md: 0 0 12px rgba(0, 255, 102, 0.28);
79
+ --kj-shadow-lg: 0 0 32px rgba(0, 255, 102, 0.40);
80
+ --kj-shadow-focus: 0 0 0 2px #00ff66;
81
+
82
+ /* Dim the phosphor green on hover (slight green-down) instead of
83
+ black-mixing — keeps the CRT look. */
84
+ --kj-button-bg-hover: #00cc52;
85
+ }
86
+ }
@@ -0,0 +1,87 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import postcss from 'postcss';
4
+ import { describe, expect, test } from 'vitest';
5
+
6
+ /**
7
+ * The shared-layer token contract. Every theme MUST define every token in this list.
8
+ * Update this list when §3.2 of the design spec changes.
9
+ */
10
+ const REQUIRED_SHARED_TOKENS = [
11
+ // Neutral surfaces
12
+ '--kj-bg-body', '--kj-bg-surface', '--kj-bg-field',
13
+ '--kj-bg-elevated', '--kj-bg-overlay', '--kj-bg-inverse', '--kj-bg-disabled',
14
+ // Intent surfaces
15
+ '--kj-bg-primary', '--kj-bg-primary-subtle',
16
+ '--kj-bg-accent', '--kj-bg-accent-subtle',
17
+ '--kj-bg-info', '--kj-bg-info-subtle',
18
+ '--kj-bg-success', '--kj-bg-success-subtle',
19
+ '--kj-bg-warning', '--kj-bg-warning-subtle',
20
+ '--kj-bg-danger', '--kj-bg-danger-subtle',
21
+ // FG Class A
22
+ '--kj-fg-default', '--kj-fg-muted', '--kj-fg-subtle', '--kj-fg-disabled',
23
+ // FG Class B
24
+ '--kj-fg-on-primary', '--kj-fg-on-accent',
25
+ '--kj-fg-on-info', '--kj-fg-on-success', '--kj-fg-on-warning', '--kj-fg-on-danger',
26
+ '--kj-fg-on-inverse',
27
+ // FG Class C
28
+ '--kj-fg-primary', '--kj-fg-accent',
29
+ '--kj-fg-info', '--kj-fg-success', '--kj-fg-warning', '--kj-fg-danger',
30
+ // Borders
31
+ '--kj-border-default', '--kj-border-muted', '--kj-border-strong',
32
+ '--kj-border-focus', '--kj-border-disabled',
33
+ '--kj-border-primary', '--kj-border-danger',
34
+ // Shadows
35
+ '--kj-shadow-sm', '--kj-shadow-md', '--kj-shadow-lg', '--kj-shadow-focus',
36
+ // shape
37
+ '--kj-radius-box', '--kj-radius-field', '--kj-radius-selector',
38
+ '--kj-border', '--kj-depth',
39
+ // motion
40
+ '--kj-transition',
41
+ // Note: font tokens (--kj-font-sans/mono/display), text-* scale, and
42
+ // space-* scale are NOT required per-theme. They cascade from `:root`
43
+ // defaults declared in base.css and are overridden by themes only when
44
+ // they need to differ (e.g. kouji sans=mono; corporate display=sans).
45
+ ] as const;
46
+
47
+ const themesDir = resolve(import.meta.dirname, 'themes');
48
+
49
+ function discoverThemes(): { name: string; cssPath: string }[] {
50
+ return readdirSync(themesDir)
51
+ .filter(f => f.endsWith('.css'))
52
+ .map(f => ({ name: f.replace(/\.css$/, ''), cssPath: resolve(themesDir, f) }));
53
+ }
54
+
55
+ function tokensDefinedInThemeBlock(cssText: string, themeName: string): Set<string> {
56
+ const root = postcss.parse(cssText);
57
+ const tokens = new Set<string>();
58
+ root.walkRules(rule => {
59
+ // Match either bare [data-theme="X"] or layered :where([data-theme="X"]).
60
+ if (!rule.selector.includes(`[data-theme="${themeName}"]`)) return;
61
+ rule.walkDecls(decl => {
62
+ if (decl.prop.startsWith('--kj-')) tokens.add(decl.prop);
63
+ });
64
+ });
65
+ return tokens;
66
+ }
67
+
68
+ describe('theme contract', () => {
69
+ const themes = discoverThemes();
70
+
71
+ test('at least one theme is present', () => {
72
+ expect(themes.length).toBeGreaterThan(0);
73
+ });
74
+
75
+ for (const theme of themes) {
76
+ describe(theme.name, () => {
77
+ const css = readFileSync(theme.cssPath, 'utf-8');
78
+ const defined = tokensDefinedInThemeBlock(css, theme.name);
79
+
80
+ for (const token of REQUIRED_SHARED_TOKENS) {
81
+ test(`defines ${token}`, () => {
82
+ expect(defined).toContain(token);
83
+ });
84
+ }
85
+ });
86
+ }
87
+ });