@kouji-ui/themes 0.1.0 → 0.1.2
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 +3 -2
- package/src/density.css +135 -0
- package/src/density.spec.ts +144 -0
- package/src/index.css +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kouji-ui/themes",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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
|
-
"./
|
|
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/density.css
ADDED
|
@@ -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
|
+
});
|