@praxisjs/css 0.1.0
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/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/dist/__tests__/css-builder.test.d.ts +2 -0
- package/dist/__tests__/css-builder.test.d.ts.map +1 -0
- package/dist/__tests__/css-builder.test.js +198 -0
- package/dist/__tests__/css-builder.test.js.map +1 -0
- package/dist/__tests__/cx.test.d.ts +2 -0
- package/dist/__tests__/cx.test.d.ts.map +1 -0
- package/dist/__tests__/cx.test.js +47 -0
- package/dist/__tests__/cx.test.js.map +1 -0
- package/dist/__tests__/decorators.test.d.ts +2 -0
- package/dist/__tests__/decorators.test.d.ts.map +1 -0
- package/dist/__tests__/decorators.test.js +268 -0
- package/dist/__tests__/decorators.test.js.map +1 -0
- package/dist/__tests__/extract.test.d.ts +2 -0
- package/dist/__tests__/extract.test.d.ts.map +1 -0
- package/dist/__tests__/extract.test.js +420 -0
- package/dist/__tests__/extract.test.js.map +1 -0
- package/dist/__tests__/global.test.d.ts +2 -0
- package/dist/__tests__/global.test.d.ts.map +1 -0
- package/dist/__tests__/global.test.js +117 -0
- package/dist/__tests__/global.test.js.map +1 -0
- package/dist/__tests__/keyframes.test.d.ts +2 -0
- package/dist/__tests__/keyframes.test.d.ts.map +1 -0
- package/dist/__tests__/keyframes.test.js +90 -0
- package/dist/__tests__/keyframes.test.js.map +1 -0
- package/dist/__tests__/preflight.test.d.ts +2 -0
- package/dist/__tests__/preflight.test.d.ts.map +1 -0
- package/dist/__tests__/preflight.test.js +55 -0
- package/dist/__tests__/preflight.test.js.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.js +132 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/style.test.d.ts +2 -0
- package/dist/__tests__/style.test.d.ts.map +1 -0
- package/dist/__tests__/style.test.js +257 -0
- package/dist/__tests__/style.test.js.map +1 -0
- package/dist/__tests__/styled.test.d.ts +2 -0
- package/dist/__tests__/styled.test.d.ts.map +1 -0
- package/dist/__tests__/styled.test.js +951 -0
- package/dist/__tests__/styled.test.js.map +1 -0
- package/dist/__tests__/theme-instance.test.d.ts +2 -0
- package/dist/__tests__/theme-instance.test.d.ts.map +1 -0
- package/dist/__tests__/theme-instance.test.js +317 -0
- package/dist/__tests__/theme-instance.test.js.map +1 -0
- package/dist/__tests__/theme-uninit.test.d.ts +2 -0
- package/dist/__tests__/theme-uninit.test.d.ts.map +1 -0
- package/dist/__tests__/theme-uninit.test.js +10 -0
- package/dist/__tests__/theme-uninit.test.js.map +1 -0
- package/dist/__tests__/token-sheet.test.d.ts +2 -0
- package/dist/__tests__/token-sheet.test.d.ts.map +1 -0
- package/dist/__tests__/token-sheet.test.js +74 -0
- package/dist/__tests__/token-sheet.test.js.map +1 -0
- package/dist/base/stylesheet.d.ts +30 -0
- package/dist/base/stylesheet.d.ts.map +1 -0
- package/dist/base/stylesheet.js +29 -0
- package/dist/base/stylesheet.js.map +1 -0
- package/dist/builder/css-builder.d.ts +98 -0
- package/dist/builder/css-builder.d.ts.map +1 -0
- package/dist/builder/css-builder.js +79 -0
- package/dist/builder/css-builder.js.map +1 -0
- package/dist/builder/keyframes.d.ts +23 -0
- package/dist/builder/keyframes.d.ts.map +1 -0
- package/dist/builder/keyframes.js +54 -0
- package/dist/builder/keyframes.js.map +1 -0
- package/dist/cx.d.ts +3 -0
- package/dist/cx.d.ts.map +1 -0
- package/dist/cx.js +23 -0
- package/dist/cx.js.map +1 -0
- package/dist/decorators/param.d.ts +23 -0
- package/dist/decorators/param.d.ts.map +1 -0
- package/dist/decorators/param.js +39 -0
- package/dist/decorators/param.js.map +1 -0
- package/dist/decorators/style.d.ts +15 -0
- package/dist/decorators/style.d.ts.map +1 -0
- package/dist/decorators/style.js +53 -0
- package/dist/decorators/style.js.map +1 -0
- package/dist/decorators/styled.d.ts +37 -0
- package/dist/decorators/styled.d.ts.map +1 -0
- package/dist/decorators/styled.js +138 -0
- package/dist/decorators/styled.js.map +1 -0
- package/dist/extract.d.ts +14 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +124 -0
- package/dist/extract.js.map +1 -0
- package/dist/global.d.ts +27 -0
- package/dist/global.d.ts.map +1 -0
- package/dist/global.js +49 -0
- package/dist/global.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/collector.d.ts +15 -0
- package/dist/internal/collector.d.ts.map +1 -0
- package/dist/internal/collector.js +33 -0
- package/dist/internal/collector.js.map +1 -0
- package/dist/internal/hash.d.ts +5 -0
- package/dist/internal/hash.d.ts.map +1 -0
- package/dist/internal/hash.js +11 -0
- package/dist/internal/hash.js.map +1 -0
- package/dist/internal/registry.d.ts +3 -0
- package/dist/internal/registry.d.ts.map +1 -0
- package/dist/internal/registry.js +41 -0
- package/dist/internal/registry.js.map +1 -0
- package/dist/preflight.d.ts +18 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +191 -0
- package/dist/preflight.js.map +1 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +8 -0
- package/dist/server.js.map +1 -0
- package/dist/tokens/decorators.d.ts +41 -0
- package/dist/tokens/decorators.d.ts.map +1 -0
- package/dist/tokens/decorators.js +73 -0
- package/dist/tokens/decorators.js.map +1 -0
- package/dist/tokens/index.d.ts +4 -0
- package/dist/tokens/index.d.ts.map +1 -0
- package/dist/tokens/index.js +4 -0
- package/dist/tokens/index.js.map +1 -0
- package/dist/tokens/theme-instance.d.ts +33 -0
- package/dist/tokens/theme-instance.d.ts.map +1 -0
- package/dist/tokens/theme-instance.js +130 -0
- package/dist/tokens/theme-instance.js.map +1 -0
- package/dist/tokens/token-sheet.d.ts +46 -0
- package/dist/tokens/token-sheet.d.ts.map +1 -0
- package/dist/tokens/token-sheet.js +55 -0
- package/dist/tokens/token-sheet.js.map +1 -0
- package/package.json +33 -0
- package/src/__tests__/css-builder.test.ts +230 -0
- package/src/__tests__/cx.test.ts +63 -0
- package/src/__tests__/decorators.test.ts +127 -0
- package/src/__tests__/extract.test.ts +418 -0
- package/src/__tests__/global.test.ts +140 -0
- package/src/__tests__/keyframes.test.ts +103 -0
- package/src/__tests__/preflight.test.ts +63 -0
- package/src/__tests__/registry.test.ts +151 -0
- package/src/__tests__/style.test.ts +153 -0
- package/src/__tests__/styled.test.ts +427 -0
- package/src/__tests__/theme-instance.test.ts +374 -0
- package/src/__tests__/theme-uninit.test.ts +12 -0
- package/src/__tests__/token-sheet.test.ts +87 -0
- package/src/base/stylesheet.ts +35 -0
- package/src/builder/css-builder.ts +204 -0
- package/src/builder/keyframes.ts +66 -0
- package/src/cx.ts +26 -0
- package/src/decorators/param.ts +40 -0
- package/src/decorators/style.ts +55 -0
- package/src/decorators/styled.ts +167 -0
- package/src/extract.ts +133 -0
- package/src/global.ts +60 -0
- package/src/index.ts +10 -0
- package/src/internal/collector.ts +38 -0
- package/src/internal/hash.ts +10 -0
- package/src/internal/registry.ts +51 -0
- package/src/preflight.ts +192 -0
- package/src/server.ts +12 -0
- package/src/tokens/decorators.ts +108 -0
- package/src/tokens/index.ts +3 -0
- package/src/tokens/theme-instance.ts +149 -0
- package/src/tokens/token-sheet.ts +86 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { getCollector } from "../internal/collector.js";
|
|
2
|
+
|
|
3
|
+
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
function camelToKebab(s: string): string {
|
|
6
|
+
return s.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function instanceToVars(instance: Record<string, unknown>): Record<string, string> {
|
|
10
|
+
const result: Record<string, string> = {};
|
|
11
|
+
for (const [key, val] of Object.entries(instance)) {
|
|
12
|
+
if (typeof val === "string" || typeof val === "number") {
|
|
13
|
+
result[`--${camelToKebab(key)}`] = String(val);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function varsToCSS(vars: Record<string, string>): string {
|
|
20
|
+
const body = Object.entries(vars)
|
|
21
|
+
.map(([k, v]) => ` ${k}: ${v};`)
|
|
22
|
+
.join("\n");
|
|
23
|
+
return `:root {\n${body}\n}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function applyVarsToRoot(vars: Record<string, string>): void {
|
|
27
|
+
const col = getCollector();
|
|
28
|
+
if (col) { col.inject("theme", varsToCSS(vars)); return; }
|
|
29
|
+
|
|
30
|
+
if (typeof document === "undefined") return;
|
|
31
|
+
let el = document.querySelector<HTMLStyleElement>("style[data-praxis-theme]");
|
|
32
|
+
if (!el) {
|
|
33
|
+
el = document.createElement("style");
|
|
34
|
+
el.setAttribute("data-praxis-theme", "");
|
|
35
|
+
document.head.appendChild(el);
|
|
36
|
+
}
|
|
37
|
+
el.textContent = varsToCSS(vars);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export interface ThemedConfig {
|
|
43
|
+
persist: boolean;
|
|
44
|
+
syncTabs: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const STORAGE_KEY = "praxis-theme";
|
|
48
|
+
const BROADCAST_KEY = "praxis-theme";
|
|
49
|
+
|
|
50
|
+
// ─── ThemeInstance ────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Manages the active design token theme: injects CSS custom properties on
|
|
54
|
+
* `:root`, persists to `localStorage`, and synchronises across browser tabs
|
|
55
|
+
* via `BroadcastChannel`.
|
|
56
|
+
*
|
|
57
|
+
* Obtain the app-wide instance via {@link theme}. Use {@link ThemeInstance.switch}
|
|
58
|
+
* to swap to any class that extends the skeleton `TokenSheet`.
|
|
59
|
+
*/
|
|
60
|
+
export class ThemeInstance {
|
|
61
|
+
#current: Record<string, unknown>;
|
|
62
|
+
readonly #config: ThemedConfig;
|
|
63
|
+
#channel: BroadcastChannel | null = null;
|
|
64
|
+
|
|
65
|
+
constructor(DefaultTheme: new () => unknown, config: Partial<ThemedConfig> = {}) {
|
|
66
|
+
this.#config = { persist: false, syncTabs: false, ...config };
|
|
67
|
+
this.#current = new DefaultTheme() as Record<string, unknown>;
|
|
68
|
+
|
|
69
|
+
const saved = this.#config.persist ? this.#loadFromStorage() : null;
|
|
70
|
+
|
|
71
|
+
if (typeof document !== "undefined") {
|
|
72
|
+
applyVarsToRoot(saved ?? instanceToVars(this.#current));
|
|
73
|
+
if (this.#config.syncTabs) this.#setupBroadcast();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The currently active theme instance. */
|
|
78
|
+
get current(): unknown {
|
|
79
|
+
return this.#current;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Switches the active theme. Instantiates `ThemeClass`, updates `:root` CSS
|
|
84
|
+
* vars, persists to `localStorage` (if `persist` is enabled), and broadcasts
|
|
85
|
+
* to other tabs (if `syncTabs` is enabled).
|
|
86
|
+
*/
|
|
87
|
+
switch(ThemeClass: new () => unknown): void {
|
|
88
|
+
this.#current = new ThemeClass() as Record<string, unknown>;
|
|
89
|
+
const vars = instanceToVars(this.#current);
|
|
90
|
+
|
|
91
|
+
if (typeof document !== "undefined") applyVarsToRoot(vars);
|
|
92
|
+
if (this.#config.persist) this.#saveToStorage(vars);
|
|
93
|
+
if (this.#config.syncTabs) this.#channel?.postMessage({ vars });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Removes the injected `<style>` and closes the BroadcastChannel. */
|
|
97
|
+
destroy(): void {
|
|
98
|
+
this.#channel?.close();
|
|
99
|
+
this.#channel = null;
|
|
100
|
+
if (typeof document !== "undefined") {
|
|
101
|
+
document.querySelector("style[data-praxis-theme]")?.remove();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#saveToStorage(vars: Record<string, string>): void {
|
|
106
|
+
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(vars)); } catch { /* noop */ }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#loadFromStorage(): Record<string, string> | null {
|
|
110
|
+
try {
|
|
111
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
112
|
+
return raw ? (JSON.parse(raw) as Record<string, string>) : null;
|
|
113
|
+
} catch { return null; }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#setupBroadcast(): void {
|
|
117
|
+
if (typeof BroadcastChannel === "undefined") return;
|
|
118
|
+
this.#channel = new BroadcastChannel(BROADCAST_KEY);
|
|
119
|
+
this.#channel.addEventListener(
|
|
120
|
+
"message",
|
|
121
|
+
(e: MessageEvent<{ vars?: Record<string, string> }>) => {
|
|
122
|
+
if (e.data.vars) applyVarsToRoot(e.data.vars);
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Singleton ────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
let _instance: ThemeInstance | null = null;
|
|
131
|
+
|
|
132
|
+
export function createThemeInstance(
|
|
133
|
+
DefaultTheme: new () => unknown,
|
|
134
|
+
config?: Partial<ThemedConfig>,
|
|
135
|
+
): ThemeInstance {
|
|
136
|
+
_instance = new ThemeInstance(DefaultTheme, config);
|
|
137
|
+
return _instance;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns the app-wide {@link ThemeInstance}.
|
|
142
|
+
* Throws if `@Themed()` has not been applied to the root component.
|
|
143
|
+
*/
|
|
144
|
+
export function theme(): ThemeInstance {
|
|
145
|
+
if (!_instance) {
|
|
146
|
+
throw new Error("[PraxisJS] theme() called before @Themed() was applied to the root component.");
|
|
147
|
+
}
|
|
148
|
+
return _instance;
|
|
149
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// ─── camelCase → --kebab-case ─────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
function camelToKebab(s: string): string {
|
|
4
|
+
return s.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// ─── Static props that must never be intercepted ──────────────────────────────
|
|
8
|
+
|
|
9
|
+
const SKIP = new Set([
|
|
10
|
+
"length", "name", "prototype", "constructor",
|
|
11
|
+
"toString", "valueOf", "toLocaleString",
|
|
12
|
+
"hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable",
|
|
13
|
+
"call", "apply", "bind", "caller", "arguments",
|
|
14
|
+
"__esModule", "then", "catch", "finally",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
// ─── TokenSheet ───────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
20
|
+
const _Base = class {};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Base class for token skeletons and theme value classes.
|
|
24
|
+
*
|
|
25
|
+
* Extend to declare token names (skeleton) or token values (theme).
|
|
26
|
+
* Static property access returns CSS custom property references at any level
|
|
27
|
+
* of the inheritance chain: `AppTokens.colorPrimary` → `'var(--color-primary)'`.
|
|
28
|
+
*
|
|
29
|
+
* @example — skeleton (names only)
|
|
30
|
+
* class AppTokens extends TokenSheet {
|
|
31
|
+
* colorPrimary!: string
|
|
32
|
+
* spaceMd!: string
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* @example — theme (values)
|
|
36
|
+
* class LightTheme extends AppTokens {
|
|
37
|
+
* colorPrimary = '#3b82f6'
|
|
38
|
+
* spaceMd = '16px'
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* class DarkTheme extends LightTheme {
|
|
42
|
+
* colorPrimary = '#60a5fa' // only overrides what changes
|
|
43
|
+
* }
|
|
44
|
+
*/
|
|
45
|
+
export const TokenSheet = new Proxy(_Base, {
|
|
46
|
+
get(target, prop, receiver) {
|
|
47
|
+
if (
|
|
48
|
+
typeof prop === "string" &&
|
|
49
|
+
!SKIP.has(prop) &&
|
|
50
|
+
!prop.startsWith("_") &&
|
|
51
|
+
!prop.startsWith("$")
|
|
52
|
+
) {
|
|
53
|
+
return `var(--${camelToKebab(prop)})`;
|
|
54
|
+
}
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
56
|
+
return Reflect.get(target, prop, receiver);
|
|
57
|
+
},
|
|
58
|
+
}) as unknown as abstract new (...args: unknown[]) => object;
|
|
59
|
+
|
|
60
|
+
// ─── tokenVars ────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Returns a typed accessor for CSS custom property references derived from a
|
|
64
|
+
* {@link TokenSheet} subclass. Each key returns `'var(--token-name)'`.
|
|
65
|
+
*
|
|
66
|
+
* Use this in `Stylesheet` definitions to get full TypeScript autocomplete
|
|
67
|
+
* without casts.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* const t = tokenVars(AppTokens)
|
|
71
|
+
* // t.colorPrimary → 'var(--color-primary)'
|
|
72
|
+
* // t.spaceMd → 'var(--space-md)'
|
|
73
|
+
*
|
|
74
|
+
* class BtnStyles extends Stylesheet {
|
|
75
|
+
* $root = this.css({ background: t.colorPrimary, padding: t.spaceMd })
|
|
76
|
+
* }
|
|
77
|
+
*/
|
|
78
|
+
type TokenVarMap<T extends abstract new (...args: unknown[]) => unknown> = {
|
|
79
|
+
readonly [K in keyof InstanceType<T> as K extends string ? K : never]: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function tokenVars<T extends abstract new (...args: unknown[]) => unknown>(
|
|
83
|
+
cls: T,
|
|
84
|
+
): TokenVarMap<T> {
|
|
85
|
+
return cls as unknown as TokenVarMap<T>;
|
|
86
|
+
}
|