@praxisjs/css 0.3.4 → 0.3.5
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 +22 -0
- package/dist/builder/css-builder.d.ts.map +1 -1
- package/dist/builder/css-builder.js +68 -74
- package/dist/builder/css-builder.js.map +1 -1
- package/dist/builder/keyframes.d.ts +5 -0
- package/dist/builder/keyframes.d.ts.map +1 -1
- package/dist/builder/keyframes.js +12 -14
- package/dist/builder/keyframes.js.map +1 -1
- package/dist/cx.d.ts.map +1 -1
- package/dist/cx.js +23 -18
- package/dist/cx.js.map +1 -1
- package/dist/decorators/param.d.ts +1 -2
- package/dist/decorators/param.d.ts.map +1 -1
- package/dist/decorators/param.js +9 -17
- package/dist/decorators/param.js.map +1 -1
- package/dist/decorators/style.d.ts.map +1 -1
- package/dist/decorators/style.js +61 -33
- package/dist/decorators/style.js.map +1 -1
- package/dist/decorators/styled.d.ts.map +1 -1
- package/dist/decorators/styled.js +112 -95
- package/dist/decorators/styled.js.map +1 -1
- package/dist/extract.d.ts.map +1 -1
- package/dist/extract.js +19 -35
- package/dist/extract.js.map +1 -1
- package/dist/global.d.ts +3 -0
- package/dist/global.d.ts.map +1 -1
- package/dist/global.js +9 -4
- package/dist/global.js.map +1 -1
- package/dist/internal/kebab.d.ts +5 -0
- package/dist/internal/kebab.d.ts.map +1 -0
- package/dist/internal/kebab.js +23 -0
- package/dist/internal/kebab.js.map +1 -0
- package/dist/internal/lifecycle.d.ts +15 -0
- package/dist/internal/lifecycle.d.ts.map +1 -0
- package/dist/internal/lifecycle.js +16 -0
- package/dist/internal/lifecycle.js.map +1 -0
- package/dist/internal/registry.d.ts +5 -1
- package/dist/internal/registry.d.ts.map +1 -1
- package/dist/internal/registry.js +9 -23
- package/dist/internal/registry.js.map +1 -1
- package/dist/internal/sheet.d.ts +20 -0
- package/dist/internal/sheet.d.ts.map +1 -0
- package/dist/internal/sheet.js +56 -0
- package/dist/internal/sheet.js.map +1 -0
- package/dist/internal/static-mode.d.ts +2 -0
- package/dist/internal/static-mode.d.ts.map +1 -0
- package/dist/internal/static-mode.js +2 -0
- package/dist/internal/static-mode.js.map +1 -0
- package/dist/preflight.d.ts +3 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +7 -3
- package/dist/preflight.js.map +1 -1
- package/dist/tokens/decorators.d.ts.map +1 -1
- package/dist/tokens/decorators.js +13 -4
- package/dist/tokens/decorators.js.map +1 -1
- package/dist/tokens/theme-instance.d.ts +5 -0
- package/dist/tokens/theme-instance.d.ts.map +1 -1
- package/dist/tokens/theme-instance.js +25 -17
- package/dist/tokens/theme-instance.js.map +1 -1
- package/dist/tokens/token-sheet.d.ts.map +1 -1
- package/dist/tokens/token-sheet.js +12 -9
- package/dist/tokens/token-sheet.js.map +1 -1
- package/package.json +13 -5
- package/src/builder/css-builder.ts +79 -83
- package/src/builder/keyframes.ts +12 -20
- package/src/cx.ts +20 -13
- package/src/decorators/param.ts +12 -16
- package/src/decorators/style.ts +79 -35
- package/src/decorators/styled.ts +127 -111
- package/src/extract.ts +18 -34
- package/src/global.ts +10 -9
- package/src/internal/kebab.ts +23 -0
- package/src/internal/lifecycle.ts +21 -0
- package/src/internal/registry.ts +9 -35
- package/src/internal/sheet.ts +71 -0
- package/src/internal/static-mode.ts +6 -0
- package/src/preflight.ts +8 -3
- package/src/tokens/decorators.ts +15 -3
- package/src/tokens/theme-instance.ts +26 -16
- package/src/tokens/token-sheet.ts +12 -12
- package/dist/__tests__/css-builder.test.d.ts +0 -2
- package/dist/__tests__/css-builder.test.d.ts.map +0 -1
- package/dist/__tests__/css-builder.test.js +0 -198
- package/dist/__tests__/css-builder.test.js.map +0 -1
- package/dist/__tests__/cx.test.d.ts +0 -2
- package/dist/__tests__/cx.test.d.ts.map +0 -1
- package/dist/__tests__/cx.test.js +0 -47
- package/dist/__tests__/cx.test.js.map +0 -1
- package/dist/__tests__/decorators.test.d.ts +0 -2
- package/dist/__tests__/decorators.test.d.ts.map +0 -1
- package/dist/__tests__/decorators.test.js +0 -264
- package/dist/__tests__/decorators.test.js.map +0 -1
- package/dist/__tests__/extract.test.d.ts +0 -2
- package/dist/__tests__/extract.test.d.ts.map +0 -1
- package/dist/__tests__/extract.test.js +0 -416
- package/dist/__tests__/extract.test.js.map +0 -1
- package/dist/__tests__/global.test.d.ts +0 -2
- package/dist/__tests__/global.test.d.ts.map +0 -1
- package/dist/__tests__/global.test.js +0 -136
- package/dist/__tests__/global.test.js.map +0 -1
- package/dist/__tests__/keyframes.test.d.ts +0 -2
- package/dist/__tests__/keyframes.test.d.ts.map +0 -1
- package/dist/__tests__/keyframes.test.js +0 -90
- package/dist/__tests__/keyframes.test.js.map +0 -1
- package/dist/__tests__/preflight.test.d.ts +0 -2
- package/dist/__tests__/preflight.test.d.ts.map +0 -1
- package/dist/__tests__/preflight.test.js +0 -74
- package/dist/__tests__/preflight.test.js.map +0 -1
- package/dist/__tests__/registry.test.d.ts +0 -2
- package/dist/__tests__/registry.test.d.ts.map +0 -1
- package/dist/__tests__/registry.test.js +0 -132
- package/dist/__tests__/registry.test.js.map +0 -1
- package/dist/__tests__/style.test.d.ts +0 -2
- package/dist/__tests__/style.test.d.ts.map +0 -1
- package/dist/__tests__/style.test.js +0 -281
- package/dist/__tests__/style.test.js.map +0 -1
- package/dist/__tests__/styled.test.d.ts +0 -2
- package/dist/__tests__/styled.test.d.ts.map +0 -1
- package/dist/__tests__/styled.test.js +0 -904
- package/dist/__tests__/styled.test.js.map +0 -1
- package/dist/__tests__/theme-instance.test.d.ts +0 -2
- package/dist/__tests__/theme-instance.test.d.ts.map +0 -1
- package/dist/__tests__/theme-instance.test.js +0 -309
- package/dist/__tests__/theme-instance.test.js.map +0 -1
- package/dist/__tests__/theme-uninit.test.d.ts +0 -2
- package/dist/__tests__/theme-uninit.test.d.ts.map +0 -1
- package/dist/__tests__/theme-uninit.test.js +0 -10
- package/dist/__tests__/theme-uninit.test.js.map +0 -1
- package/dist/__tests__/token-sheet.test.d.ts +0 -2
- package/dist/__tests__/token-sheet.test.d.ts.map +0 -1
- package/dist/__tests__/token-sheet.test.js +0 -74
- package/dist/__tests__/token-sheet.test.js.map +0 -1
- package/src/__tests__/css-builder.test.ts +0 -230
- package/src/__tests__/cx.test.ts +0 -63
- package/src/__tests__/decorators.test.ts +0 -127
- package/src/__tests__/extract.test.ts +0 -459
- package/src/__tests__/global.test.ts +0 -163
- package/src/__tests__/keyframes.test.ts +0 -103
- package/src/__tests__/preflight.test.ts +0 -85
- package/src/__tests__/registry.test.ts +0 -151
- package/src/__tests__/style.test.ts +0 -170
- package/src/__tests__/styled.test.ts +0 -443
- package/src/__tests__/theme-instance.test.ts +0 -374
- package/src/__tests__/theme-uninit.test.ts +0 -12
- package/src/__tests__/token-sheet.test.ts +0 -87
- package/tsconfig.json +0 -8
package/src/cx.ts
CHANGED
|
@@ -8,19 +8,26 @@ export type ClassValue =
|
|
|
8
8
|
| Record<string, unknown>;
|
|
9
9
|
|
|
10
10
|
export function cx(...args: ClassValue[]): string {
|
|
11
|
-
|
|
12
|
-
for (const arg of args)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
11
|
+
let out = "";
|
|
12
|
+
for (const arg of args) out = append(out, arg);
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Builds the string directly instead of collecting parts in an array: cx() runs on every render of
|
|
17
|
+
// every styled component.
|
|
18
|
+
function append(out: string, arg: ClassValue): string {
|
|
19
|
+
if (!arg && arg !== 0) return out;
|
|
20
|
+
if (typeof arg === "string" || typeof arg === "number") {
|
|
21
|
+
return out === "" ? String(arg) : `${out} ${String(arg)}`;
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(arg)) {
|
|
24
|
+
for (const item of arg) out = append(out, item);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
if (typeof arg === "object") {
|
|
28
|
+
for (const key in arg) {
|
|
29
|
+
if (Object.hasOwn(arg, key) && arg[key]) out = out === "" ? key : `${out} ${key}`;
|
|
23
30
|
}
|
|
24
31
|
}
|
|
25
|
-
return
|
|
32
|
+
return out;
|
|
26
33
|
}
|
package/src/decorators/param.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { addParamName } from "../internal/sheet.js";
|
|
2
2
|
|
|
3
3
|
import type { ReactiveStylesheet } from "../base/stylesheet.js";
|
|
4
4
|
|
|
5
|
-
const PARAM_META = new WeakMap<object, Set<string>>();
|
|
6
|
-
|
|
7
|
-
export function getParamNames(ctor: object): Set<string> {
|
|
8
|
-
return PARAM_META.get(ctor) ?? new Set();
|
|
9
|
-
}
|
|
10
|
-
|
|
11
5
|
/**
|
|
12
6
|
* Marks a field in a {@link ReactiveStylesheet} subclass as a reactive CSS custom property.
|
|
13
7
|
* The field name becomes `--fieldName` on the component's container element.
|
|
@@ -28,13 +22,15 @@ export function getParamNames(ctor: object): Set<string> {
|
|
|
28
22
|
* this.$card.color = '#ef4444' // updates --color reactively
|
|
29
23
|
*/
|
|
30
24
|
export function Param() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
// Only records the field name: a stylesheet is instantiated once per class, as the template
|
|
26
|
+
// `@Styled` compiles, and every component instance then gets its own signals from there.
|
|
27
|
+
return function <This extends ReactiveStylesheet, Value>(
|
|
28
|
+
_value: undefined,
|
|
29
|
+
context: ClassFieldDecoratorContext<This, Value>,
|
|
30
|
+
): void {
|
|
31
|
+
const name = context.name as string;
|
|
32
|
+
context.addInitializer(function (this: This) {
|
|
33
|
+
addParamName(this.constructor, name);
|
|
34
|
+
});
|
|
35
|
+
};
|
|
40
36
|
}
|
package/src/decorators/style.ts
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createEffect,
|
|
3
|
+
disposeEffect,
|
|
4
|
+
getComponentAnchor,
|
|
5
|
+
readSignal,
|
|
6
|
+
SignalNode,
|
|
7
|
+
writeSignal,
|
|
8
|
+
type EffectNode,
|
|
9
|
+
} from "@praxisjs/core/internal";
|
|
10
|
+
import { addClaimableInitializer, assertFieldContext } from "@praxisjs/decorators";
|
|
11
|
+
|
|
12
|
+
import { HookedField, type LifecycleHost } from "../internal/lifecycle.js";
|
|
13
|
+
|
|
14
|
+
class StyleField extends HookedField {
|
|
15
|
+
readonly node: SignalNode<string | number>;
|
|
16
|
+
effect: EffectNode | undefined = undefined;
|
|
17
|
+
el: HTMLElement | undefined = undefined;
|
|
18
|
+
|
|
19
|
+
constructor(instance: LifecycleHost, value: string | number) {
|
|
20
|
+
super(instance);
|
|
21
|
+
this.node = new SignalNode(value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
3
24
|
|
|
4
25
|
/**
|
|
5
26
|
* Reactive field decorator that binds a signal value to a CSS custom property
|
|
@@ -15,37 +36,60 @@ import { createFieldDecorator } from "@praxisjs/decorators";
|
|
|
15
36
|
* }
|
|
16
37
|
*/
|
|
17
38
|
export function Style(property: `--${string}`) {
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
return function <This extends object, Value = unknown>(
|
|
40
|
+
_value: undefined,
|
|
41
|
+
context: ClassFieldDecoratorContext<This, Value>,
|
|
42
|
+
): void {
|
|
43
|
+
assertFieldContext(context);
|
|
44
|
+
const name = context.name;
|
|
45
|
+
// Accessors and hooks are shared by every instance of the decorated field; the instance's
|
|
46
|
+
// own state lives in this slot.
|
|
47
|
+
const slot = Symbol(`praxis.style.${String(name)}`);
|
|
48
|
+
type Host = LifecycleHost & Record<typeof slot, StyleField>;
|
|
49
|
+
|
|
50
|
+
const descriptor: PropertyDescriptor = {
|
|
51
|
+
enumerable: true,
|
|
52
|
+
configurable: true,
|
|
53
|
+
get(this: Host) {
|
|
54
|
+
return readSignal(this[slot].node);
|
|
55
|
+
},
|
|
56
|
+
set(this: Host, value: string | number) {
|
|
57
|
+
writeSignal(this[slot].node, value);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function onMount(this: Host): void {
|
|
62
|
+
const field = this[slot];
|
|
63
|
+
field.prevMount?.call(this);
|
|
64
|
+
const el = getComponentAnchor(this)?.parentElement;
|
|
65
|
+
if (!el) return;
|
|
66
|
+
const node = field.node;
|
|
67
|
+
field.el = el;
|
|
68
|
+
field.effect = createEffect(() => {
|
|
69
|
+
el.style.setProperty(property, String(readSignal(node)));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function onUnmount(this: Host): void {
|
|
74
|
+
const field = this[slot];
|
|
75
|
+
field.prevUnmount?.call(this);
|
|
76
|
+
if (field.effect) {
|
|
77
|
+
disposeEffect(field.effect);
|
|
78
|
+
field.effect = undefined;
|
|
79
|
+
}
|
|
80
|
+
// The element it was set on, even if the anchor has been detached by now.
|
|
81
|
+
if (field.el) {
|
|
82
|
+
field.el.style.removeProperty(property);
|
|
83
|
+
field.el = undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
addClaimableInitializer(context, function (value) {
|
|
88
|
+
const host = this as unknown as Host & Record<PropertyKey, unknown>;
|
|
89
|
+
host[slot] = new StyleField(host, value as string | number);
|
|
90
|
+
Object.defineProperty(host, name, descriptor);
|
|
91
|
+
host.onMount = onMount;
|
|
92
|
+
host.onUnmount = onUnmount;
|
|
93
|
+
});
|
|
94
|
+
};
|
|
51
95
|
}
|
package/src/decorators/styled.ts
CHANGED
|
@@ -1,63 +1,72 @@
|
|
|
1
1
|
import type { StatefulComponent } from "@praxisjs/core";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import {
|
|
3
|
+
createEffect,
|
|
4
|
+
disposeEffect,
|
|
5
|
+
getComponentAnchor,
|
|
6
|
+
readSignal,
|
|
7
|
+
SignalNode,
|
|
8
|
+
writeSignal,
|
|
9
|
+
type EffectNode,
|
|
10
|
+
} from "@praxisjs/core/internal";
|
|
11
|
+
import { assertFieldContext } from "@praxisjs/decorators";
|
|
12
|
+
|
|
13
|
+
import { HookedField, type LifecycleHost } from "../internal/lifecycle.js";
|
|
14
|
+
import { injectStyle } from "../internal/registry.js";
|
|
15
|
+
import { compileSheet, type CompiledSheet } from "../internal/sheet.js";
|
|
9
16
|
|
|
10
17
|
import type { ReactiveStylesheet, Stylesheet } from "../base/stylesheet.js";
|
|
11
18
|
|
|
12
|
-
|
|
19
|
+
type ParamValue = string | number;
|
|
20
|
+
|
|
21
|
+
const PARAMS = Symbol("praxis.styled.params");
|
|
13
22
|
|
|
14
|
-
interface
|
|
15
|
-
|
|
16
|
-
rawCSS: string;
|
|
17
|
-
registryKey: string;
|
|
18
|
-
paramDefaults: Readonly<Record<string, string | number>>;
|
|
23
|
+
interface ParamSheet {
|
|
24
|
+
readonly [PARAMS]: Array<SignalNode<ParamValue>>;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const paramNames = getParamNames(StyleClass);
|
|
30
|
-
|
|
31
|
-
const classMap: Record<string, string> = {};
|
|
32
|
-
const blocks: string[] = [];
|
|
33
|
-
const paramDefaults: Record<string, string | number> = {};
|
|
34
|
-
|
|
35
|
-
for (const [key, value] of Object.entries(template)) {
|
|
36
|
-
if (key.startsWith("$")) {
|
|
37
|
-
// Accept plain strings or fluent CSSBuilder values.
|
|
38
|
-
const css = typeof value === "string" ? value : isCSSBuilder(value) ? String(value) : null;
|
|
39
|
-
if (css !== null) {
|
|
40
|
-
const safeName = key.slice(1).replace(/[^\w-]/g, "-");
|
|
41
|
-
const className = `prx-${safeName}-${hashCSS(css)}`;
|
|
42
|
-
classMap[key] = className;
|
|
43
|
-
blocks.push(`.${className} { ${css.trim()} }`);
|
|
44
|
-
}
|
|
45
|
-
} else if (paramNames.has(key) && (typeof value === "string" || typeof value === "number")) {
|
|
46
|
-
paramDefaults[key] = value;
|
|
27
|
+
// One class per stylesheet: the class names and the @Param() accessors live on its prototype,
|
|
28
|
+
// so an instance is a single slot holding its signal nodes.
|
|
29
|
+
function createParamSheetClass(sheet: CompiledSheet): new (nodes: Array<SignalNode<ParamValue>>) => ParamSheet {
|
|
30
|
+
class Sheet implements ParamSheet {
|
|
31
|
+
readonly [PARAMS]: Array<SignalNode<ParamValue>>;
|
|
32
|
+
|
|
33
|
+
constructor(nodes: Array<SignalNode<ParamValue>>) {
|
|
34
|
+
this[PARAMS] = nodes;
|
|
47
35
|
}
|
|
48
36
|
}
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
37
|
+
const proto = Sheet.prototype;
|
|
38
|
+
for (const key in sheet.classMap) {
|
|
39
|
+
Object.defineProperty(proto, key, { value: sheet.classMap[key], writable: true, enumerable: true, configurable: true });
|
|
40
|
+
}
|
|
41
|
+
sheet.params.forEach((param, i) => {
|
|
42
|
+
Object.defineProperty(proto, param, {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
get(this: ParamSheet) {
|
|
46
|
+
return readSignal(this[PARAMS][i]);
|
|
47
|
+
},
|
|
48
|
+
set(this: ParamSheet, value: ParamValue) {
|
|
49
|
+
writeSignal(this[PARAMS][i], value);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
return Sheet;
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
|
|
56
|
+
class ParamField extends HookedField {
|
|
57
|
+
readonly nodes: Array<SignalNode<ParamValue>>;
|
|
58
|
+
readonly sheet: ParamSheet;
|
|
59
|
+
effects: EffectNode[] | undefined = undefined;
|
|
60
|
+
el: HTMLElement | undefined = undefined;
|
|
61
|
+
|
|
62
|
+
constructor(instance: LifecycleHost, Sheet: new (nodes: Array<SignalNode<ParamValue>>) => ParamSheet, defaults: readonly ParamValue[]) {
|
|
63
|
+
super(instance);
|
|
64
|
+
const nodes = new Array<SignalNode<ParamValue>>(defaults.length);
|
|
65
|
+
for (let i = 0; i < defaults.length; i++) nodes[i] = new SignalNode(defaults[i]);
|
|
66
|
+
this.nodes = nodes;
|
|
67
|
+
this.sheet = new Sheet(nodes);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
61
70
|
|
|
62
71
|
/**
|
|
63
72
|
* Scoped CSS field decorator. Receives a {@link Stylesheet} or
|
|
@@ -97,71 +106,78 @@ export function Styled<T extends Stylesheet>(
|
|
|
97
106
|
): [T] extends [ReactiveStylesheet]
|
|
98
107
|
? (value: undefined, context: ClassFieldDecoratorContext<StatefulComponent, T>) => void
|
|
99
108
|
: (value: undefined, context: ClassFieldDecoratorContext<object, T>) => void {
|
|
100
|
-
const
|
|
109
|
+
const sheet = compileSheet(StyleClass);
|
|
110
|
+
const { css, key } = sheet;
|
|
101
111
|
|
|
102
112
|
// Eagerly inject CSS at decoration time (module load) so it is in the DOM
|
|
103
113
|
// before any component mounts — avoids layout-measurement timing issues.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
114
|
+
if (css) injectStyle(key, css);
|
|
115
|
+
|
|
116
|
+
// Both paths return the field's value from its initializer, which is much cheaper than redefining
|
|
117
|
+
// the field with Object.defineProperty on every new instance. Each instance also re-checks the
|
|
118
|
+
// stylesheet (cheap when present): it is collected again during a server render, and put back
|
|
119
|
+
// if its <style> was removed.
|
|
120
|
+
if (sheet.params.length === 0) {
|
|
121
|
+
// The same frozen class map for every instance: no per-instance object, signals or hooks.
|
|
122
|
+
const { classMap } = sheet;
|
|
123
|
+
return ((_value: undefined, context: ClassFieldDecoratorContext) => {
|
|
124
|
+
assertFieldContext(context);
|
|
125
|
+
return () => {
|
|
126
|
+
if (css) injectStyle(key, css);
|
|
127
|
+
return classMap;
|
|
128
|
+
};
|
|
129
|
+
}) as never;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const Sheet = createParamSheetClass(sheet);
|
|
133
|
+
const { defaults } = sheet;
|
|
134
|
+
const vars = sheet.params.map((param) => `--${param}`);
|
|
135
|
+
|
|
136
|
+
return ((_value: undefined, context: ClassFieldDecoratorContext) => {
|
|
137
|
+
assertFieldContext(context);
|
|
138
|
+
const name = context.name;
|
|
139
|
+
const slot = Symbol(`praxis.styled.${String(name)}`);
|
|
140
|
+
type Host = LifecycleHost & Record<typeof slot, ParamField>;
|
|
141
|
+
|
|
142
|
+
function onMount(this: Host): void {
|
|
143
|
+
const field = this[slot];
|
|
144
|
+
field.prevMount?.call(this);
|
|
145
|
+
const el = getComponentAnchor(this)?.parentElement;
|
|
146
|
+
if (!el) return;
|
|
147
|
+
const effects: EffectNode[] = [];
|
|
148
|
+
for (let i = 0; i < vars.length; i++) {
|
|
149
|
+
const node = field.nodes[i];
|
|
150
|
+
const prop = vars[i];
|
|
151
|
+
effects.push(createEffect(() => {
|
|
152
|
+
el.style.setProperty(prop, String(readSignal(node)));
|
|
153
|
+
}));
|
|
127
154
|
}
|
|
155
|
+
field.el = el;
|
|
156
|
+
field.effects = effects;
|
|
157
|
+
}
|
|
128
158
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
stopFns = [];
|
|
154
|
-
|
|
155
|
-
if (hasParams) {
|
|
156
|
-
const el = getComponentAnchor(instance)?.parentElement;
|
|
157
|
-
if (el) {
|
|
158
|
-
for (const paramName of Object.keys(paramSignals)) {
|
|
159
|
-
el.style.removeProperty(`--${paramName}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
},
|
|
166
|
-
});
|
|
159
|
+
function onUnmount(this: Host): void {
|
|
160
|
+
const field = this[slot];
|
|
161
|
+
field.prevUnmount?.call(this);
|
|
162
|
+
const { effects, el } = field;
|
|
163
|
+
if (effects) {
|
|
164
|
+
for (const effect of effects) disposeEffect(effect);
|
|
165
|
+
field.effects = undefined;
|
|
166
|
+
}
|
|
167
|
+
// The element the vars were set on, even if the anchor has been detached by now.
|
|
168
|
+
if (el) {
|
|
169
|
+
for (const prop of vars) el.style.removeProperty(prop);
|
|
170
|
+
field.el = undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return function (this: Host): ParamSheet {
|
|
175
|
+
if (css) injectStyle(key, css);
|
|
176
|
+
const field = new ParamField(this, Sheet, defaults);
|
|
177
|
+
this[slot] = field;
|
|
178
|
+
this.onMount = onMount;
|
|
179
|
+
this.onUnmount = onUnmount;
|
|
180
|
+
return field.sheet;
|
|
181
|
+
};
|
|
182
|
+
}) as never;
|
|
167
183
|
}
|
package/src/extract.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Stylesheet, ReactiveStylesheet } from "./base/stylesheet.js";
|
|
2
|
-
import {
|
|
2
|
+
import { createCSSBuilder, type CSSProperties } from "./builder/css-builder.js";
|
|
3
|
+
import { keyframesRule } from "./builder/keyframes.js";
|
|
4
|
+
import { globalCSS, type GlobalStyleFactory, type GlobalStyleOptions } from "./global.js";
|
|
3
5
|
import { hashCSS } from "./internal/hash.js";
|
|
6
|
+
import { compileSheet } from "./internal/sheet.js";
|
|
7
|
+
import { preflightCSS, type PreflightOptions } from "./preflight.js";
|
|
8
|
+
import { instanceToVars, varsToCSS } from "./tokens/theme-instance.js";
|
|
4
9
|
import { TokenSheet, tokenVars } from "./tokens/token-sheet.js";
|
|
5
10
|
|
|
6
11
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
@@ -12,18 +17,10 @@ type Ctor = new (...args: any[]) => unknown;
|
|
|
12
17
|
|
|
13
18
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
14
19
|
|
|
15
|
-
function camelToKebab(s: string): string {
|
|
16
|
-
return s.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
20
|
function themeToRootCSS(ThemeClass: Ctor): string | null {
|
|
20
21
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
.filter(([, v]) => typeof v === "string" || typeof v === "number")
|
|
24
|
-
.map(([k, v]) => ` --${camelToKebab(k)}: ${String(v)};`)
|
|
25
|
-
.join("\n");
|
|
26
|
-
return vars ? `:root {\n${vars}\n}` : null;
|
|
22
|
+
const vars = instanceToVars(new ThemeClass() as Record<string, unknown>);
|
|
23
|
+
return Object.keys(vars).length > 0 ? varsToCSS(vars) : null;
|
|
27
24
|
} catch {
|
|
28
25
|
return null;
|
|
29
26
|
}
|
|
@@ -31,16 +28,8 @@ function themeToRootCSS(ThemeClass: Ctor): string | null {
|
|
|
31
28
|
|
|
32
29
|
function collectSheetCSS(SheetClass: Ctor, emit: EmitFn): void {
|
|
33
30
|
try {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
for (const [k, v] of Object.entries(inst)) {
|
|
37
|
-
if (!k.startsWith("$")) continue;
|
|
38
|
-
const css = typeof v === "string" ? v : (isCSSBuilder(v) ? String(v) : null);
|
|
39
|
-
if (!css) continue;
|
|
40
|
-
const name = k.slice(1).replace(/[^\w-]/g, "-");
|
|
41
|
-
blocks.push(`.prx-${name}-${hashCSS(css)} { ${css.trim()} }`);
|
|
42
|
-
}
|
|
43
|
-
if (blocks.length) emit(blocks.join("\n"));
|
|
31
|
+
const { css } = compileSheet(SheetClass);
|
|
32
|
+
if (css) emit(css);
|
|
44
33
|
} catch { /* skip sheets that can't be instantiated at build time */ }
|
|
45
34
|
}
|
|
46
35
|
|
|
@@ -117,20 +106,15 @@ export function extractionModule(emit: EmitFn): Record<string, unknown> {
|
|
|
117
106
|
Style: () => () => {},
|
|
118
107
|
cx: () => "",
|
|
119
108
|
keyframes: (name: string, stops: Record<string, CSSProperties>) => {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
.map(([s, p]) => ` ${s} {\n${stopToCSS(p)}\n }`)
|
|
127
|
-
.join("\n");
|
|
128
|
-
const sn = `prx-${name}-${hashCSS(body)}`;
|
|
129
|
-
dedupEmit(`@keyframes ${sn} {\n${body}\n}`);
|
|
130
|
-
return sn;
|
|
109
|
+
const rule = keyframesRule(name, stops);
|
|
110
|
+
dedupEmit(rule.css);
|
|
111
|
+
return rule.name;
|
|
112
|
+
},
|
|
113
|
+
globalStyle: (factory: GlobalStyleFactory, options?: GlobalStyleOptions) => {
|
|
114
|
+
dedupEmit(globalCSS(factory, options));
|
|
131
115
|
},
|
|
132
|
-
|
|
133
|
-
dedupEmit(
|
|
116
|
+
preflight: (options?: PreflightOptions) => {
|
|
117
|
+
dedupEmit(preflightCSS(options));
|
|
134
118
|
},
|
|
135
119
|
};
|
|
136
120
|
}
|
package/src/global.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createCSSBuilder, type CSSProperties, type CSSBuilder } from "./builder/css-builder.js";
|
|
2
2
|
import { getCollector } from "./internal/collector.js";
|
|
3
3
|
import { hashCSS } from "./internal/hash.js";
|
|
4
|
+
import { STATIC_MODE } from "./internal/static-mode.js";
|
|
4
5
|
|
|
5
6
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
6
7
|
|
|
@@ -17,14 +18,14 @@ export interface GlobalStyleOptions {
|
|
|
17
18
|
layer?: string | false;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
// ─── Static mode ──────────────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
declare const __PRAXIS_CSS_STATIC__: boolean | undefined;
|
|
23
|
-
const STATIC_MODE: boolean =
|
|
24
|
-
typeof __PRAXIS_CSS_STATIC__ !== "undefined" && __PRAXIS_CSS_STATIC__;
|
|
25
|
-
|
|
26
21
|
const injected = new Set<string>();
|
|
27
22
|
|
|
23
|
+
/** The CSS a `globalStyle()` call injects — shared with build-time extraction. */
|
|
24
|
+
export function globalCSS(factory: GlobalStyleFactory, { layer }: GlobalStyleOptions = {}): string {
|
|
25
|
+
const rawCSS = String(factory(createCSSBuilder));
|
|
26
|
+
return layer ? `@layer ${layer} {\n${rawCSS}\n}` : rawCSS;
|
|
27
|
+
}
|
|
28
|
+
|
|
28
29
|
// ─── globalStyle ─────────────────────────────────────────────────────────────
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -54,10 +55,10 @@ const injected = new Set<string>();
|
|
|
54
55
|
* globalStyle(_css => `body { font-family: system-ui; }`, { layer: 'base' })
|
|
55
56
|
*/
|
|
56
57
|
export function globalStyle(factory: GlobalStyleFactory, options: GlobalStyleOptions = {}): void {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const css = layer ? `@layer ${layer} {\n${rawCSS}\n}` : rawCSS;
|
|
58
|
+
injectGlobalCSS(globalCSS(factory, options));
|
|
59
|
+
}
|
|
60
60
|
|
|
61
|
+
export function injectGlobalCSS(css: string): void {
|
|
61
62
|
const key = hashCSS(css);
|
|
62
63
|
if (injected.has(key)) return;
|
|
63
64
|
injected.add(key);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const cache = new Map<string, string>();
|
|
2
|
+
|
|
3
|
+
/** `backgroundColor` → `background-color`. Memoized: the same few hundred property and token names repeat. */
|
|
4
|
+
export function toKebab(camel: string): string {
|
|
5
|
+
let kebab = cache.get(camel);
|
|
6
|
+
if (kebab === undefined) {
|
|
7
|
+
kebab = camel.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
8
|
+
cache.set(camel, kebab);
|
|
9
|
+
}
|
|
10
|
+
return kebab;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** One `prop: value;` line per defined entry, each prefixed with `indent`. */
|
|
14
|
+
export function declarations(props: Readonly<Record<string, string | number | null | undefined>>, indent: string): string {
|
|
15
|
+
let out = "";
|
|
16
|
+
for (const key in props) {
|
|
17
|
+
const value = props[key];
|
|
18
|
+
if (value == null) continue;
|
|
19
|
+
const line = `${indent}${toKebab(key)}: ${String(value)};`;
|
|
20
|
+
out = out === "" ? line : `${out}\n${line}`;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface LifecycleHost {
|
|
2
|
+
onMount?(): void;
|
|
3
|
+
onUnmount?(): void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Per-instance state of a decorated field that needs mount/unmount hooks. The hooks themselves
|
|
8
|
+
* are shared per decorated field and find this state through a symbol slot, so chaining onto
|
|
9
|
+
* the instance's own `onMount`/`onUnmount` allocates no closures.
|
|
10
|
+
*/
|
|
11
|
+
export class HookedField {
|
|
12
|
+
readonly prevMount: (() => void) | undefined;
|
|
13
|
+
readonly prevUnmount: (() => void) | undefined;
|
|
14
|
+
|
|
15
|
+
constructor(instance: LifecycleHost) {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method -- called back with `.call(instance)`
|
|
17
|
+
this.prevMount = instance.onMount;
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method -- called back with `.call(instance)`
|
|
19
|
+
this.prevUnmount = instance.onUnmount;
|
|
20
|
+
}
|
|
21
|
+
}
|