@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.
- package/package.json +42 -0
- package/src/base.css +165 -0
- package/src/index.css +14 -0
- package/src/themes/bauhaus.css +88 -0
- package/src/themes/corporate.css +91 -0
- package/src/themes/cyberpunk.css +82 -0
- package/src/themes/dark.css +91 -0
- package/src/themes/dune.css +85 -0
- package/src/themes/forest.css +87 -0
- package/src/themes/kouji.css +128 -0
- package/src/themes/light.css +97 -0
- package/src/themes/mint.css +99 -0
- package/src/themes/nord.css +85 -0
- package/src/themes/retro.css +89 -0
- package/src/themes/sakura.css +85 -0
- package/src/themes/terminal.css +86 -0
- package/src/themes.spec.ts +87 -0
|
@@ -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
|
+
});
|