@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
package/src/extract.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Stylesheet, ReactiveStylesheet } from "./base/stylesheet.js";
|
|
2
|
+
import { isCSSBuilder, createCSSBuilder, type CSSProperties } from "./builder/css-builder.js";
|
|
3
|
+
import { hashCSS } from "./internal/hash.js";
|
|
4
|
+
|
|
5
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export type EmitFn = (css: string, prepend?: boolean) => void;
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
type Ctor = new (...args: any[]) => unknown;
|
|
11
|
+
|
|
12
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function camelToKebab(s: string): string {
|
|
15
|
+
return s.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function themeToRootCSS(ThemeClass: Ctor): string | null {
|
|
19
|
+
try {
|
|
20
|
+
const inst = new ThemeClass() as Record<string, unknown>;
|
|
21
|
+
const vars = Object.entries(inst)
|
|
22
|
+
.filter(([, v]) => typeof v === "string" || typeof v === "number")
|
|
23
|
+
.map(([k, v]) => ` --${camelToKebab(k)}: ${String(v)};`)
|
|
24
|
+
.join("\n");
|
|
25
|
+
return vars ? `:root {\n${vars}\n}` : null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function collectSheetCSS(SheetClass: Ctor, emit: EmitFn): void {
|
|
32
|
+
try {
|
|
33
|
+
const inst = new SheetClass() as Record<string, unknown>;
|
|
34
|
+
const blocks: string[] = [];
|
|
35
|
+
for (const [k, v] of Object.entries(inst)) {
|
|
36
|
+
if (!k.startsWith("$")) continue;
|
|
37
|
+
const css = typeof v === "string" ? v : (isCSSBuilder(v) ? String(v) : null);
|
|
38
|
+
if (!css) continue;
|
|
39
|
+
const name = k.slice(1).replace(/[^\w-]/g, "-");
|
|
40
|
+
blocks.push(`.prx-${name}-${hashCSS(css)} { ${css.trim()} }`);
|
|
41
|
+
}
|
|
42
|
+
if (blocks.length) emit(blocks.join("\n"));
|
|
43
|
+
} catch { /* skip sheets that can't be instantiated at build time */ }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Module factory ───────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates the `@praxisjs/css` module API surface for build-time CSS extraction.
|
|
50
|
+
*
|
|
51
|
+
* All CSS-generating calls route through `emit` instead of the DOM.
|
|
52
|
+
* Pass the returned object as the `@praxisjs/css` entry in a vm sandbox's
|
|
53
|
+
* `require()` handler.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* const mod = extractionModule((css, prepend) => { ... })
|
|
57
|
+
* sandbox.require = id => id === '@praxisjs/css' ? mod : {}
|
|
58
|
+
*/
|
|
59
|
+
export function extractionModule(emit: EmitFn): Record<string, unknown> {
|
|
60
|
+
const seen = new Set<string>();
|
|
61
|
+
|
|
62
|
+
const dedupEmit: EmitFn = (css, prepend) => {
|
|
63
|
+
const key = hashCSS(css);
|
|
64
|
+
if (seen.has(key)) return;
|
|
65
|
+
seen.add(key);
|
|
66
|
+
emit(css, prepend);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
class ExtractionThemeInstance {
|
|
70
|
+
constructor(DefaultTheme: Ctor) {
|
|
71
|
+
const css = themeToRootCSS(DefaultTheme);
|
|
72
|
+
if (css) dedupEmit(css, true);
|
|
73
|
+
}
|
|
74
|
+
get current() { return {}; }
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
76
|
+
switch(_T: Ctor) {}
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
78
|
+
destroy() {}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
__esModule: true,
|
|
83
|
+
|
|
84
|
+
// Base classes — real implementations, used by user's `extends Stylesheet`
|
|
85
|
+
Stylesheet,
|
|
86
|
+
ReactiveStylesheet,
|
|
87
|
+
createCSSBuilder,
|
|
88
|
+
|
|
89
|
+
// Token system — extraction stubs
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
91
|
+
TokenSheet: class TokenSheet {},
|
|
92
|
+
tokenVars: (cls: unknown) => cls,
|
|
93
|
+
ThemeInstance: ExtractionThemeInstance,
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
95
|
+
theme: () => new ExtractionThemeInstance(class {}),
|
|
96
|
+
createThemeInstance: (T: Ctor) => new ExtractionThemeInstance(T),
|
|
97
|
+
Themed: (_skeleton: unknown, DefaultTheme: Ctor) => {
|
|
98
|
+
const css = themeToRootCSS(DefaultTheme);
|
|
99
|
+
if (css) dedupEmit(css, true);
|
|
100
|
+
return (cls: unknown) => cls;
|
|
101
|
+
},
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
103
|
+
Theme: () => () => {},
|
|
104
|
+
|
|
105
|
+
// CSS API — collect instead of inject
|
|
106
|
+
Styled: (SheetClass: Ctor) => {
|
|
107
|
+
collectSheetCSS(SheetClass, dedupEmit);
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
109
|
+
return () => {};
|
|
110
|
+
},
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
112
|
+
Param: () => () => {},
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
114
|
+
Style: () => () => {},
|
|
115
|
+
cx: () => "",
|
|
116
|
+
keyframes: (name: string, stops: Record<string, CSSProperties>) => {
|
|
117
|
+
const stopToCSS = (p: CSSProperties) =>
|
|
118
|
+
Object.entries(p)
|
|
119
|
+
.filter(([, v]) => v != null)
|
|
120
|
+
.map(([k, v]) => ` ${camelToKebab(k)}: ${String(v)};`)
|
|
121
|
+
.join("\n");
|
|
122
|
+
const body = Object.entries(stops)
|
|
123
|
+
.map(([s, p]) => ` ${s} {\n${stopToCSS(p)}\n }`)
|
|
124
|
+
.join("\n");
|
|
125
|
+
const sn = `prx-${name}-${hashCSS(body)}`;
|
|
126
|
+
dedupEmit(`@keyframes ${sn} {\n${body}\n}`);
|
|
127
|
+
return sn;
|
|
128
|
+
},
|
|
129
|
+
globalStyle: (factory: (css: typeof createCSSBuilder) => unknown) => {
|
|
130
|
+
dedupEmit(String(factory(createCSSBuilder)));
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
package/src/global.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createCSSBuilder, type CSSProperties, type CSSBuilder } from "./builder/css-builder.js";
|
|
2
|
+
import { getCollector } from "./internal/collector.js";
|
|
3
|
+
import { hashCSS } from "./internal/hash.js";
|
|
4
|
+
|
|
5
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
/** Factory function that receives `css` (= `createCSSBuilder`) and returns a `CSSBuilder` or raw CSS string. */
|
|
8
|
+
export type GlobalStyleFactory = (css: (props: CSSProperties) => CSSBuilder) => CSSBuilder | string;
|
|
9
|
+
|
|
10
|
+
// ─── Static mode ──────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
declare const __PRAXIS_CSS_STATIC__: boolean | undefined;
|
|
13
|
+
const STATIC_MODE: boolean =
|
|
14
|
+
typeof __PRAXIS_CSS_STATIC__ !== "undefined" && __PRAXIS_CSS_STATIC__;
|
|
15
|
+
|
|
16
|
+
const injected = new Set<string>();
|
|
17
|
+
|
|
18
|
+
// ─── globalStyle ─────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Injects unscoped CSS into `<head>` exactly once.
|
|
22
|
+
*
|
|
23
|
+
* Receives a factory function that is passed `css` — the same builder
|
|
24
|
+
* function as `this.css()` in a `Stylesheet` class — and returns either a
|
|
25
|
+
* {@link CSSBuilder} or a raw CSS string.
|
|
26
|
+
*
|
|
27
|
+
* Use `.on(selector, props)` to attach rules to specific elements.
|
|
28
|
+
* Chain multiple `.on()` calls to produce multiple selectors in one injection.
|
|
29
|
+
*
|
|
30
|
+
* Idempotent — duplicate content is ignored (content-hashed).
|
|
31
|
+
* Safe to call at module level. With the `praxisjsCSS()` Vite plugin the
|
|
32
|
+
* CSS is extracted at build time.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* globalStyle(css =>
|
|
36
|
+
* css({})
|
|
37
|
+
* .on('*, *::before, *::after', { boxSizing: 'border-box', margin: 0, padding: 0 })
|
|
38
|
+
* .on('body', { fontFamily: 'system-ui', lineHeight: 1.5 })
|
|
39
|
+
* .on('img, video', { display: 'block', maxWidth: '100%' })
|
|
40
|
+
* )
|
|
41
|
+
*/
|
|
42
|
+
export function globalStyle(factory: GlobalStyleFactory): void {
|
|
43
|
+
const css = String(factory(createCSSBuilder));
|
|
44
|
+
|
|
45
|
+
const key = hashCSS(css);
|
|
46
|
+
if (injected.has(key)) return;
|
|
47
|
+
injected.add(key);
|
|
48
|
+
|
|
49
|
+
if (STATIC_MODE) return;
|
|
50
|
+
|
|
51
|
+
const col = getCollector();
|
|
52
|
+
if (col) { col.inject(key, css); return; }
|
|
53
|
+
|
|
54
|
+
if (typeof document === "undefined") return;
|
|
55
|
+
|
|
56
|
+
const el = document.createElement("style");
|
|
57
|
+
el.setAttribute("data-praxis-global", key);
|
|
58
|
+
el.textContent = css;
|
|
59
|
+
document.head.appendChild(el);
|
|
60
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { cx, type ClassValue } from "./cx.js";
|
|
2
|
+
export { preflight } from "./preflight.js";
|
|
3
|
+
export { globalStyle, type GlobalStyleFactory } from "./global.js";
|
|
4
|
+
export { type CSSBuilder, type CSSProperties } from "./builder/css-builder.js";
|
|
5
|
+
export { keyframes } from "./builder/keyframes.js";
|
|
6
|
+
export { Param } from "./decorators/param.js";
|
|
7
|
+
export { ReactiveStylesheet, Stylesheet } from "./base/stylesheet.js";
|
|
8
|
+
export { Styled } from "./decorators/styled.js";
|
|
9
|
+
export { Style } from "./decorators/style.js";
|
|
10
|
+
export { TokenSheet, tokenVars, ThemeInstance, theme, Themed, Theme, type ThemedConfig } from "./tokens/index.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS collector — used by the Vite CSS plugin during build-time extraction.
|
|
3
|
+
* When a collector is active, `injectStyle` and `keyframes` push CSS to it
|
|
4
|
+
* instead of injecting `<style>` elements into the DOM.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface CSSCollector {
|
|
8
|
+
inject(key: string, css: string): void;
|
|
9
|
+
addKeyframes(name: string, css: string): void;
|
|
10
|
+
getCSS(): string;
|
|
11
|
+
clear(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let _active: CSSCollector | null = null;
|
|
15
|
+
|
|
16
|
+
export function setCollector(c: CSSCollector | null): void {
|
|
17
|
+
_active = c;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getCollector(): CSSCollector | null {
|
|
21
|
+
return _active;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createCollector(): CSSCollector {
|
|
25
|
+
const seen = new Set<string>();
|
|
26
|
+
const blocks: string[] = [];
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
inject(key, css) {
|
|
30
|
+
if (!seen.has(key)) { seen.add(key); blocks.push(css); }
|
|
31
|
+
},
|
|
32
|
+
addKeyframes(name, css) {
|
|
33
|
+
if (!seen.has(name)) { seen.add(name); blocks.push(css); }
|
|
34
|
+
},
|
|
35
|
+
getCSS() { return blocks.join("\n"); },
|
|
36
|
+
clear() { seen.clear(); blocks.length = 0; },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* djb2-variant hash of a string, returning a 6-char base-36 identifier.
|
|
3
|
+
*/
|
|
4
|
+
export function hashCSS(str: string): string {
|
|
5
|
+
let h = 5381;
|
|
6
|
+
for (let i = 0; i < str.length; i++) {
|
|
7
|
+
h = (h * 33) ^ str.charCodeAt(i);
|
|
8
|
+
}
|
|
9
|
+
return (h >>> 0).toString(36).slice(0, 6);
|
|
10
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getCollector } from "./collector.js";
|
|
2
|
+
|
|
3
|
+
// ─── Static mode ──────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
// Defined by the Vite CSS plugin via `define: { __PRAXIS_CSS_STATIC__: true }`.
|
|
6
|
+
// When true, CSS is already bundled via virtual:praxisjs/styles — skip DOM injection.
|
|
7
|
+
declare const __PRAXIS_CSS_STATIC__: boolean | undefined;
|
|
8
|
+
|
|
9
|
+
const STATIC_MODE: boolean =
|
|
10
|
+
typeof __PRAXIS_CSS_STATIC__ !== "undefined" && __PRAXIS_CSS_STATIC__;
|
|
11
|
+
|
|
12
|
+
// ─── DOM registry ─────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
// WeakRef lets the GC reclaim style elements that were removed externally
|
|
15
|
+
// without going through releaseStyle (e.g. framework teardown, test cleanup).
|
|
16
|
+
const cssRegistry = new Map<string, { ref: WeakRef<HTMLStyleElement>; count: number }>();
|
|
17
|
+
|
|
18
|
+
export function injectStyle(key: string, css: string): void {
|
|
19
|
+
if (STATIC_MODE) return;
|
|
20
|
+
|
|
21
|
+
const col = getCollector();
|
|
22
|
+
if (col) { col.inject(key, css); return; }
|
|
23
|
+
|
|
24
|
+
if (typeof document === "undefined") return;
|
|
25
|
+
|
|
26
|
+
const existing = cssRegistry.get(key);
|
|
27
|
+
const existingEl = existing?.ref.deref();
|
|
28
|
+
|
|
29
|
+
if (existing && existingEl && document.head.contains(existingEl)) {
|
|
30
|
+
existing.count++;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const el = document.createElement("style");
|
|
35
|
+
el.setAttribute("data-praxis-hash", key);
|
|
36
|
+
el.textContent = css;
|
|
37
|
+
document.head.appendChild(el);
|
|
38
|
+
cssRegistry.set(key, { ref: new WeakRef(el), count: 1 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function releaseStyle(key: string): void {
|
|
42
|
+
if (STATIC_MODE) return;
|
|
43
|
+
|
|
44
|
+
const entry = cssRegistry.get(key);
|
|
45
|
+
if (!entry) return;
|
|
46
|
+
entry.count--;
|
|
47
|
+
if (entry.count <= 0) {
|
|
48
|
+
entry.ref.deref()?.remove();
|
|
49
|
+
cssRegistry.delete(key);
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/preflight.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { globalStyle } from "./global.js";
|
|
2
|
+
|
|
3
|
+
// Tailwind CSS preflight — https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/preflight.css
|
|
4
|
+
// --theme() references stripped and replaced with their fallback values.
|
|
5
|
+
const PREFLIGHT = `
|
|
6
|
+
*,
|
|
7
|
+
::after,
|
|
8
|
+
::before,
|
|
9
|
+
::backdrop,
|
|
10
|
+
::file-selector-button {
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 0;
|
|
14
|
+
border: 0 solid;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
html,
|
|
18
|
+
:host {
|
|
19
|
+
line-height: 1.5;
|
|
20
|
+
-webkit-text-size-adjust: 100%;
|
|
21
|
+
tab-size: 4;
|
|
22
|
+
font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
|
23
|
+
font-feature-settings: normal;
|
|
24
|
+
font-variation-settings: normal;
|
|
25
|
+
-webkit-tap-highlight-color: transparent;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
hr {
|
|
29
|
+
height: 0;
|
|
30
|
+
color: inherit;
|
|
31
|
+
border-top-width: 1px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
abbr:where([title]) {
|
|
35
|
+
-webkit-text-decoration: underline dotted;
|
|
36
|
+
text-decoration: underline dotted;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
h1, h2, h3, h4, h5, h6 {
|
|
40
|
+
font-size: inherit;
|
|
41
|
+
font-weight: inherit;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
a {
|
|
45
|
+
color: inherit;
|
|
46
|
+
-webkit-text-decoration: inherit;
|
|
47
|
+
text-decoration: inherit;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
b, strong {
|
|
51
|
+
font-weight: bolder;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
code, kbd, samp, pre {
|
|
55
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
|
56
|
+
font-feature-settings: normal;
|
|
57
|
+
font-variation-settings: normal;
|
|
58
|
+
font-size: 1em;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
small {
|
|
62
|
+
font-size: 80%;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
sub, sup {
|
|
66
|
+
font-size: 75%;
|
|
67
|
+
line-height: 0;
|
|
68
|
+
position: relative;
|
|
69
|
+
vertical-align: baseline;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
sub { bottom: -0.25em; }
|
|
73
|
+
sup { top: -0.5em; }
|
|
74
|
+
|
|
75
|
+
table {
|
|
76
|
+
text-indent: 0;
|
|
77
|
+
border-color: inherit;
|
|
78
|
+
border-collapse: collapse;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
:-moz-focusring { outline: auto; }
|
|
82
|
+
|
|
83
|
+
progress { vertical-align: baseline; }
|
|
84
|
+
|
|
85
|
+
summary { display: list-item; }
|
|
86
|
+
|
|
87
|
+
ol, ul, menu { list-style: none; }
|
|
88
|
+
|
|
89
|
+
img, svg, video, canvas, audio, iframe, embed, object {
|
|
90
|
+
display: block;
|
|
91
|
+
vertical-align: middle;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
img, video {
|
|
95
|
+
max-width: 100%;
|
|
96
|
+
height: auto;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
button,
|
|
100
|
+
input,
|
|
101
|
+
select,
|
|
102
|
+
optgroup,
|
|
103
|
+
textarea,
|
|
104
|
+
::file-selector-button {
|
|
105
|
+
font: inherit;
|
|
106
|
+
font-feature-settings: inherit;
|
|
107
|
+
font-variation-settings: inherit;
|
|
108
|
+
letter-spacing: inherit;
|
|
109
|
+
color: inherit;
|
|
110
|
+
border-radius: 0;
|
|
111
|
+
background-color: transparent;
|
|
112
|
+
opacity: 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
:where(select:is([multiple], [size])) optgroup { font-weight: bolder; }
|
|
116
|
+
|
|
117
|
+
:where(select:is([multiple], [size])) optgroup option { padding-inline-start: 20px; }
|
|
118
|
+
|
|
119
|
+
::file-selector-button { margin-inline-end: 4px; }
|
|
120
|
+
|
|
121
|
+
::placeholder { opacity: 1; }
|
|
122
|
+
|
|
123
|
+
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
|
|
124
|
+
::placeholder {
|
|
125
|
+
color: color-mix(in oklab, currentcolor 50%, transparent);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
textarea { resize: vertical; }
|
|
130
|
+
|
|
131
|
+
::-webkit-search-decoration { -webkit-appearance: none; }
|
|
132
|
+
|
|
133
|
+
::-webkit-date-and-time-value {
|
|
134
|
+
min-height: 1lh;
|
|
135
|
+
text-align: inherit;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
::-webkit-datetime-edit { display: inline-flex; }
|
|
139
|
+
|
|
140
|
+
::-webkit-datetime-edit-fields-wrapper { padding: 0; }
|
|
141
|
+
|
|
142
|
+
::-webkit-datetime-edit,
|
|
143
|
+
::-webkit-datetime-edit-year-field,
|
|
144
|
+
::-webkit-datetime-edit-month-field,
|
|
145
|
+
::-webkit-datetime-edit-day-field,
|
|
146
|
+
::-webkit-datetime-edit-hour-field,
|
|
147
|
+
::-webkit-datetime-edit-minute-field,
|
|
148
|
+
::-webkit-datetime-edit-second-field,
|
|
149
|
+
::-webkit-datetime-edit-millisecond-field,
|
|
150
|
+
::-webkit-datetime-edit-meridiem-field {
|
|
151
|
+
padding-block: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
::-webkit-calendar-picker-indicator { line-height: 1; }
|
|
155
|
+
|
|
156
|
+
:-moz-ui-invalid { box-shadow: none; }
|
|
157
|
+
|
|
158
|
+
button,
|
|
159
|
+
input:where([type='button'], [type='reset'], [type='submit']),
|
|
160
|
+
::file-selector-button {
|
|
161
|
+
appearance: button;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
::-webkit-inner-spin-button,
|
|
165
|
+
::-webkit-outer-spin-button {
|
|
166
|
+
height: auto;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
[hidden]:where(:not([hidden='until-found'])) {
|
|
170
|
+
display: none !important;
|
|
171
|
+
}
|
|
172
|
+
`;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Injects an opinionated browser reset into `document.head` exactly once.
|
|
176
|
+
*
|
|
177
|
+
* Inspired by the Tailwind CSS preflight reset — normalises browser defaults,
|
|
178
|
+
* removes margins and padding, resets borders, and makes replaced elements
|
|
179
|
+
* block-level. Tailwind-specific `--theme()` references are not included;
|
|
180
|
+
* font stacks use standard system defaults instead.
|
|
181
|
+
*
|
|
182
|
+
* Safe to call at module level. Idempotent — multiple calls inject the CSS
|
|
183
|
+
* only once. Works with the `praxisjsCSS()` Vite plugin for static extraction.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* // src/main.ts
|
|
187
|
+
* import { preflight } from '@praxisjs/css'
|
|
188
|
+
* preflight()
|
|
189
|
+
*/
|
|
190
|
+
export function preflight(): void {
|
|
191
|
+
globalStyle(_css => PREFLIGHT);
|
|
192
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side / build-time API for the `@praxisjs/vite-plugin` CSS extractor.
|
|
3
|
+
*
|
|
4
|
+
* Import this at `@praxisjs/css/server` — it is NOT part of the browser bundle.
|
|
5
|
+
* Using it in browser code has no effect and the tree-shaker will remove it.
|
|
6
|
+
*/
|
|
7
|
+
export {
|
|
8
|
+
setCollector,
|
|
9
|
+
getCollector,
|
|
10
|
+
createCollector,
|
|
11
|
+
type CSSCollector,
|
|
12
|
+
} from "./internal/collector.js";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { RootComponent } from "@praxisjs/core/internal";
|
|
2
|
+
import {
|
|
3
|
+
createClassDecorator,
|
|
4
|
+
ClassBehavior,
|
|
5
|
+
type ClassEnhancement,
|
|
6
|
+
createFieldDecorator,
|
|
7
|
+
type FieldBinding,
|
|
8
|
+
} from "@praxisjs/decorators";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createThemeInstance,
|
|
12
|
+
theme,
|
|
13
|
+
ThemeInstance,
|
|
14
|
+
type ThemedConfig,
|
|
15
|
+
} from "./theme-instance.js";
|
|
16
|
+
|
|
17
|
+
// ─── @Themed ─────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
class ThemedBehavior extends ClassBehavior {
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly DefaultTheme: new () => unknown,
|
|
22
|
+
private readonly config: Partial<ThemedConfig>,
|
|
23
|
+
) {
|
|
24
|
+
super();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
create(_instance: RootComponent): ClassEnhancement {
|
|
28
|
+
createThemeInstance(this.DefaultTheme, this.config);
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
initialize(
|
|
33
|
+
_Enhanced: new (...args: unknown[]) => unknown,
|
|
34
|
+
_original: new (...args: unknown[]) => unknown,
|
|
35
|
+
): void {
|
|
36
|
+
createThemeInstance(this.DefaultTheme, this.config);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Class decorator that installs the design token system on the root component.
|
|
42
|
+
*
|
|
43
|
+
* - `skeleton` — `TokenSheet` subclass that declares token names. Its static
|
|
44
|
+
* properties (`AppTokens.colorPrimary`) return CSS var references.
|
|
45
|
+
* - `DefaultTheme` — concrete class (extends skeleton) that provides the
|
|
46
|
+
* initial CSS custom property values injected on `:root`.
|
|
47
|
+
* - `config.persist` — save the active theme to `localStorage` and restore it
|
|
48
|
+
* on the next page load.
|
|
49
|
+
* - `config.syncTabs` — broadcast theme changes to other browser tabs via
|
|
50
|
+
* `BroadcastChannel`.
|
|
51
|
+
*
|
|
52
|
+
* Must appear above `@Component()` in the decorator stack.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* @Themed(AppTokens, LightTheme, { persist: true, syncTabs: true })
|
|
56
|
+
* @Component()
|
|
57
|
+
* class App extends StatefulComponent { ... }
|
|
58
|
+
*/
|
|
59
|
+
export function Themed(
|
|
60
|
+
_skeleton: abstract new (...args: unknown[]) => unknown,
|
|
61
|
+
DefaultTheme: new () => unknown,
|
|
62
|
+
config?: Partial<ThemedConfig>,
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
): any {
|
|
65
|
+
return createClassDecorator(
|
|
66
|
+
new ThemedBehavior(DefaultTheme, config ?? {}),
|
|
67
|
+
) as unknown as (
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
cls: new (...args: any[]) => any,
|
|
70
|
+
ctx: ClassDecoratorContext,
|
|
71
|
+
) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── @Theme ───────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Field decorator that injects the active {@link ThemeInstance} into a
|
|
78
|
+
* component field. Use `.switch(ThemeClass)` to change the theme.
|
|
79
|
+
*
|
|
80
|
+
* Equivalent to the programmatic {@link theme} function — choose whichever
|
|
81
|
+
* fits the component's style.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* @Component()
|
|
85
|
+
* class Header extends StatefulComponent {
|
|
86
|
+
* @Theme() theme!: ThemeInstance
|
|
87
|
+
*
|
|
88
|
+
* render() {
|
|
89
|
+
* return <button onClick={() => this.theme.switch(DarkTheme)}>Dark</button>
|
|
90
|
+
* }
|
|
91
|
+
* }
|
|
92
|
+
*/
|
|
93
|
+
export function Theme() {
|
|
94
|
+
return createFieldDecorator({
|
|
95
|
+
bind(_instance, _name, _initialValue): FieldBinding {
|
|
96
|
+
return {
|
|
97
|
+
descriptor: {
|
|
98
|
+
get(): ThemeInstance { return theme(); },
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
100
|
+
set(): void {},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
}) as unknown as (_value: undefined, context: ClassFieldDecoratorContext<any>) => void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { theme, ThemeInstance, type ThemedConfig };
|