@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,204 @@
|
|
|
1
|
+
import type { Properties } from "csstype";
|
|
2
|
+
|
|
3
|
+
// ─── CSSProperties ────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Typed CSS property object — backed by `csstype`.
|
|
7
|
+
* All ~300 CSS properties with full value autocomplete.
|
|
8
|
+
* Used as the argument to `this.css({...})` and all nesting methods.
|
|
9
|
+
*/
|
|
10
|
+
export type CSSProperties = Properties<string | number> & Record<string, string | number | undefined>;
|
|
11
|
+
|
|
12
|
+
// ─── CSSBuilder ───────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fluent CSS builder returned by `this.css({...})`.
|
|
16
|
+
* TypeScript sees it as `string` — works in JSX `class=`, `cx()`, computed keys.
|
|
17
|
+
* Serialises to a CSS rule block via `toString()`.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* class BtnStyles extends Stylesheet {
|
|
21
|
+
* $base = this.css({ display: 'inline-flex', padding: '8px 16px' })
|
|
22
|
+
* .hover({ opacity: 0.9 })
|
|
23
|
+
* .disabled({ opacity: 0.4, pointerEvents: 'none' })
|
|
24
|
+
* .before({ content: '""', display: 'block' })
|
|
25
|
+
* .media('max-width: 640px', { width: '100%' })
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
export type CSSBuilder = string & {
|
|
29
|
+
// ── Pseudo-classes — interaction ──────────────────────────────────────────
|
|
30
|
+
/** `&:hover { ... }` */
|
|
31
|
+
hover(props: CSSProperties): CSSBuilder;
|
|
32
|
+
/** `&:focus { ... }` */
|
|
33
|
+
focus(props: CSSProperties): CSSBuilder;
|
|
34
|
+
/** `&:focus-within { ... }` */
|
|
35
|
+
focusWithin(props: CSSProperties): CSSBuilder;
|
|
36
|
+
/** `&:focus-visible { ... }` */
|
|
37
|
+
focusVisible(props: CSSProperties): CSSBuilder;
|
|
38
|
+
/** `&:active { ... }` */
|
|
39
|
+
active(props: CSSProperties): CSSBuilder;
|
|
40
|
+
/** `&:visited { ... }` */
|
|
41
|
+
visited(props: CSSProperties): CSSBuilder;
|
|
42
|
+
/** `&:target { ... }` */
|
|
43
|
+
target(props: CSSProperties): CSSBuilder;
|
|
44
|
+
|
|
45
|
+
// ── Pseudo-classes — form state ───────────────────────────────────────────
|
|
46
|
+
/** `&:disabled, &[disabled], &[aria-disabled="true"] { ... }` */
|
|
47
|
+
disabled(props: CSSProperties): CSSBuilder;
|
|
48
|
+
/** `&:enabled { ... }` */
|
|
49
|
+
enabled(props: CSSProperties): CSSBuilder;
|
|
50
|
+
/** `&:checked { ... }` */
|
|
51
|
+
checked(props: CSSProperties): CSSBuilder;
|
|
52
|
+
/** `&:indeterminate { ... }` */
|
|
53
|
+
indeterminate(props: CSSProperties): CSSBuilder;
|
|
54
|
+
/** `&:required { ... }` */
|
|
55
|
+
required(props: CSSProperties): CSSBuilder;
|
|
56
|
+
/** `&:optional { ... }` */
|
|
57
|
+
optional(props: CSSProperties): CSSBuilder;
|
|
58
|
+
/** `&:valid { ... }` */
|
|
59
|
+
valid(props: CSSProperties): CSSBuilder;
|
|
60
|
+
/** `&:invalid { ... }` */
|
|
61
|
+
invalid(props: CSSProperties): CSSBuilder;
|
|
62
|
+
/** `&:read-only { ... }` */
|
|
63
|
+
readOnly(props: CSSProperties): CSSBuilder;
|
|
64
|
+
|
|
65
|
+
// ── Pseudo-classes — structural ───────────────────────────────────────────
|
|
66
|
+
/** `&:first-child { ... }` */
|
|
67
|
+
first(props: CSSProperties): CSSBuilder;
|
|
68
|
+
/** `&:last-child { ... }` */
|
|
69
|
+
last(props: CSSProperties): CSSBuilder;
|
|
70
|
+
/** `&:nth-child(n) { ... }` */
|
|
71
|
+
nthChild(n: string | number, props: CSSProperties): CSSBuilder;
|
|
72
|
+
/** `&:empty { ... }` */
|
|
73
|
+
empty(props: CSSProperties): CSSBuilder;
|
|
74
|
+
|
|
75
|
+
// ── Pseudo-classes — relational (CSS Selectors Level 4) ──────────────────
|
|
76
|
+
/** `&:has(selector) { ... }` */
|
|
77
|
+
has(selector: string, props: CSSProperties): CSSBuilder;
|
|
78
|
+
/** `&:is(...) { ... }` */
|
|
79
|
+
is(selector: string, props: CSSProperties): CSSBuilder;
|
|
80
|
+
/** `&:where(...) { ... }` — zero specificity */
|
|
81
|
+
where(selector: string, props: CSSProperties): CSSBuilder;
|
|
82
|
+
/** `&:not(selector) { ... }` */
|
|
83
|
+
not(selector: string, props: CSSProperties): CSSBuilder;
|
|
84
|
+
|
|
85
|
+
// ── Pseudo-elements ───────────────────────────────────────────────────────
|
|
86
|
+
/** `&::before { ... }` */
|
|
87
|
+
before(props: CSSProperties): CSSBuilder;
|
|
88
|
+
/** `&::after { ... }` */
|
|
89
|
+
after(props: CSSProperties): CSSBuilder;
|
|
90
|
+
/** `&::placeholder { ... }` */
|
|
91
|
+
placeholder(props: CSSProperties): CSSBuilder;
|
|
92
|
+
/** `&::selection { ... }` */
|
|
93
|
+
selection(props: CSSProperties): CSSBuilder;
|
|
94
|
+
/** `&::first-line { ... }` */
|
|
95
|
+
firstLine(props: CSSProperties): CSSBuilder;
|
|
96
|
+
/** `&::first-letter { ... }` */
|
|
97
|
+
firstLetter(props: CSSProperties): CSSBuilder;
|
|
98
|
+
/** `&::marker { ... }` */
|
|
99
|
+
marker(props: CSSProperties): CSSBuilder;
|
|
100
|
+
/** `&::backdrop { ... }` */
|
|
101
|
+
backdrop(props: CSSProperties): CSSBuilder;
|
|
102
|
+
|
|
103
|
+
// ── At-rules ──────────────────────────────────────────────────────────────
|
|
104
|
+
/** `@media (query) { ... }` */
|
|
105
|
+
media(query: string, props: CSSProperties): CSSBuilder;
|
|
106
|
+
/** `@container (query) { ... }` */
|
|
107
|
+
container(query: string, props: CSSProperties): CSSBuilder;
|
|
108
|
+
/** `@supports (condition) { ... }` */
|
|
109
|
+
supports(condition: string, props: CSSProperties): CSSBuilder;
|
|
110
|
+
/** Any arbitrary nested selector or at-rule. */
|
|
111
|
+
on(selector: string, props: CSSProperties): CSSBuilder;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// ─── Runtime ──────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
const CSS_BUILDER = Symbol("css-builder");
|
|
117
|
+
|
|
118
|
+
export function isCSSBuilder(value: unknown): boolean {
|
|
119
|
+
return typeof value === "object" && value !== null &&
|
|
120
|
+
(value as Record<symbol, unknown>)[CSS_BUILDER] === true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function toKebab(camel: string): string {
|
|
124
|
+
return camel.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function propsToCSS(props: CSSProperties): string {
|
|
128
|
+
return Object.entries(props)
|
|
129
|
+
.filter(([, v]) => v != null)
|
|
130
|
+
.map(([k, v]) => `${toKebab(k)}: ${String(v)};`)
|
|
131
|
+
.join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createCSSBuilder(initialProps: CSSProperties): CSSBuilder {
|
|
135
|
+
const parts: string[] = [];
|
|
136
|
+
const rootCSS = propsToCSS(initialProps);
|
|
137
|
+
if (rootCSS) parts.push(rootCSS);
|
|
138
|
+
|
|
139
|
+
const addNested = (selector: string, props: CSSProperties): CSSBuilder => {
|
|
140
|
+
const rawCSS = propsToCSS(props);
|
|
141
|
+
if (!rawCSS) return builder;
|
|
142
|
+
const inner = rawCSS.split("\n").map((l) => ` ${l}`).join("\n");
|
|
143
|
+
parts.push(`${selector} {\n${inner}\n}`);
|
|
144
|
+
return builder;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const toCSS = (): string => parts.join("\n");
|
|
148
|
+
|
|
149
|
+
const builder = Object.assign(Object.create(null) as object, {
|
|
150
|
+
[CSS_BUILDER]: true,
|
|
151
|
+
toString: toCSS,
|
|
152
|
+
[Symbol.toPrimitive]: toCSS,
|
|
153
|
+
|
|
154
|
+
// Pseudo-classes — interaction
|
|
155
|
+
hover: (p: CSSProperties) => addNested("&:hover", p),
|
|
156
|
+
focus: (p: CSSProperties) => addNested("&:focus", p),
|
|
157
|
+
focusWithin: (p: CSSProperties) => addNested("&:focus-within", p),
|
|
158
|
+
focusVisible: (p: CSSProperties) => addNested("&:focus-visible", p),
|
|
159
|
+
active: (p: CSSProperties) => addNested("&:active", p),
|
|
160
|
+
visited: (p: CSSProperties) => addNested("&:visited", p),
|
|
161
|
+
target: (p: CSSProperties) => addNested("&:target", p),
|
|
162
|
+
|
|
163
|
+
// Pseudo-classes — form state
|
|
164
|
+
disabled: (p: CSSProperties) => addNested('&:disabled, &[disabled], &[aria-disabled="true"]', p),
|
|
165
|
+
enabled: (p: CSSProperties) => addNested("&:enabled", p),
|
|
166
|
+
checked: (p: CSSProperties) => addNested("&:checked", p),
|
|
167
|
+
indeterminate:(p: CSSProperties) => addNested("&:indeterminate", p),
|
|
168
|
+
required: (p: CSSProperties) => addNested("&:required", p),
|
|
169
|
+
optional: (p: CSSProperties) => addNested("&:optional", p),
|
|
170
|
+
valid: (p: CSSProperties) => addNested("&:valid", p),
|
|
171
|
+
invalid: (p: CSSProperties) => addNested("&:invalid", p),
|
|
172
|
+
readOnly: (p: CSSProperties) => addNested("&:read-only", p),
|
|
173
|
+
|
|
174
|
+
// Pseudo-classes — structural
|
|
175
|
+
first: (p: CSSProperties) => addNested("&:first-child", p),
|
|
176
|
+
last: (p: CSSProperties) => addNested("&:last-child", p),
|
|
177
|
+
nthChild: (n: string | number, p: CSSProperties) => addNested(`&:nth-child(${String(n)})`, p),
|
|
178
|
+
empty: (p: CSSProperties) => addNested("&:empty", p),
|
|
179
|
+
|
|
180
|
+
// Pseudo-classes — relational (Selectors Level 4)
|
|
181
|
+
has: (sel: string, p: CSSProperties) => addNested(`&:has(${sel})`, p),
|
|
182
|
+
is: (sel: string, p: CSSProperties) => addNested(`&:is(${sel})`, p),
|
|
183
|
+
where: (sel: string, p: CSSProperties) => addNested(`&:where(${sel})`, p),
|
|
184
|
+
not: (sel: string, p: CSSProperties) => addNested(`&:not(${sel})`, p),
|
|
185
|
+
|
|
186
|
+
// Pseudo-elements
|
|
187
|
+
before: (p: CSSProperties) => addNested("&::before", p),
|
|
188
|
+
after: (p: CSSProperties) => addNested("&::after", p),
|
|
189
|
+
placeholder: (p: CSSProperties) => addNested("&::placeholder", p),
|
|
190
|
+
selection: (p: CSSProperties) => addNested("&::selection", p),
|
|
191
|
+
firstLine: (p: CSSProperties) => addNested("&::first-line", p),
|
|
192
|
+
firstLetter: (p: CSSProperties) => addNested("&::first-letter", p),
|
|
193
|
+
marker: (p: CSSProperties) => addNested("&::marker", p),
|
|
194
|
+
backdrop: (p: CSSProperties) => addNested("&::backdrop", p),
|
|
195
|
+
|
|
196
|
+
// At-rules
|
|
197
|
+
media: (q: string, p: CSSProperties) => addNested(`@media (${q})`, p),
|
|
198
|
+
container: (q: string, p: CSSProperties) => addNested(`@container (${q})`, p),
|
|
199
|
+
supports: (cond: string, p: CSSProperties) => addNested(`@supports (${cond})`, p),
|
|
200
|
+
on: (sel: string, p: CSSProperties) => addNested(sel, p),
|
|
201
|
+
}) as unknown as CSSBuilder;
|
|
202
|
+
|
|
203
|
+
return builder;
|
|
204
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getCollector } from "../internal/collector.js";
|
|
2
|
+
import { hashCSS } from "../internal/hash.js";
|
|
3
|
+
|
|
4
|
+
import type { CSSProperties } from "./css-builder.js";
|
|
5
|
+
|
|
6
|
+
// Defined by the Vite CSS plugin — skip DOM injection when CSS is static.
|
|
7
|
+
declare const __PRAXIS_CSS_STATIC__: boolean | undefined;
|
|
8
|
+
const STATIC_MODE: boolean =
|
|
9
|
+
typeof __PRAXIS_CSS_STATIC__ !== "undefined" && __PRAXIS_CSS_STATIC__;
|
|
10
|
+
|
|
11
|
+
type KeyframeStops = Record<string, CSSProperties>;
|
|
12
|
+
|
|
13
|
+
const injected = new Set<string>();
|
|
14
|
+
|
|
15
|
+
function toKebab(camel: string): string {
|
|
16
|
+
return camel.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function stopToCSS(props: CSSProperties): string {
|
|
20
|
+
return Object.entries(props)
|
|
21
|
+
.filter(([, v]) => v != null)
|
|
22
|
+
.map(([k, v]) => ` ${toKebab(k)}: ${String(v)};`)
|
|
23
|
+
.join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Defines a scoped `@keyframes` animation and injects it into `document.head`.
|
|
28
|
+
* Returns the scoped animation name — use it in the `animation` or
|
|
29
|
+
* `animationName` property of a stylesheet field.
|
|
30
|
+
*
|
|
31
|
+
* The name is content-hashed so two identical animations share one injection.
|
|
32
|
+
* Safe to call at module level (outside components).
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const pulse = keyframes('pulse', {
|
|
36
|
+
* from: { opacity: 1 },
|
|
37
|
+
* to: { opacity: 0.4 },
|
|
38
|
+
* })
|
|
39
|
+
*
|
|
40
|
+
* class LoaderStyles extends Stylesheet {
|
|
41
|
+
* $spinner = this.css({ animation: `${pulse} 1.2s ease-in-out infinite` })
|
|
42
|
+
* }
|
|
43
|
+
*/
|
|
44
|
+
export function keyframes(name: string, stops: KeyframeStops): string {
|
|
45
|
+
const stopsCSS = Object.entries(stops)
|
|
46
|
+
.map(([stop, props]) => ` ${stop} {\n${stopToCSS(props)}\n }`)
|
|
47
|
+
.join("\n");
|
|
48
|
+
|
|
49
|
+
const scopedName = `prx-${name}-${hashCSS(stopsCSS)}`;
|
|
50
|
+
const kfCSS = `@keyframes ${scopedName} {\n${stopsCSS}\n}`;
|
|
51
|
+
|
|
52
|
+
if (STATIC_MODE) return scopedName;
|
|
53
|
+
|
|
54
|
+
const col = getCollector();
|
|
55
|
+
if (col) { col.addKeyframes(scopedName, kfCSS); return scopedName; }
|
|
56
|
+
|
|
57
|
+
if (!injected.has(scopedName) && typeof document !== "undefined") {
|
|
58
|
+
const el = document.createElement("style");
|
|
59
|
+
el.setAttribute("data-praxis-keyframes", scopedName);
|
|
60
|
+
el.textContent = kfCSS;
|
|
61
|
+
document.head.appendChild(el);
|
|
62
|
+
injected.add(scopedName);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return scopedName;
|
|
66
|
+
}
|
package/src/cx.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ClassValue =
|
|
2
|
+
| string
|
|
3
|
+
| number
|
|
4
|
+
| boolean
|
|
5
|
+
| null
|
|
6
|
+
| undefined
|
|
7
|
+
| ClassValue[]
|
|
8
|
+
| Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
export function cx(...args: ClassValue[]): string {
|
|
11
|
+
const classes: string[] = [];
|
|
12
|
+
for (const arg of args) {
|
|
13
|
+
if (!arg && arg !== 0) continue;
|
|
14
|
+
if (typeof arg === "string" || typeof arg === "number") {
|
|
15
|
+
classes.push(String(arg));
|
|
16
|
+
} else if (Array.isArray(arg)) {
|
|
17
|
+
const inner = cx(...(arg as ClassValue[]));
|
|
18
|
+
if (inner) classes.push(inner);
|
|
19
|
+
} else if (typeof arg === "object") {
|
|
20
|
+
for (const [key, val] of Object.entries(arg)) {
|
|
21
|
+
if (val) classes.push(key);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return classes.join(" ");
|
|
26
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createFieldDecorator } from "@praxisjs/decorators";
|
|
2
|
+
|
|
3
|
+
import type { ReactiveStylesheet } from "../base/stylesheet.js";
|
|
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
|
+
/**
|
|
12
|
+
* Marks a field in a {@link ReactiveStylesheet} subclass as a reactive CSS custom property.
|
|
13
|
+
* The field name becomes `--fieldName` on the component's container element.
|
|
14
|
+
* Setting the field updates the CSS var reactively — zero re-renders.
|
|
15
|
+
*
|
|
16
|
+
* Only valid inside a `ReactiveStylesheet` subclass. TypeScript enforces this at
|
|
17
|
+
* compile time via the `createFieldDecorator<ReactiveStylesheet>` constraint.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* class CardStyles extends ReactiveStylesheet {
|
|
21
|
+
* @Param() color = '#3b82f6' // → --color on the element
|
|
22
|
+
* @Param() radius = '8px' // → --radius on the element
|
|
23
|
+
*
|
|
24
|
+
* $root = `border-radius: var(--radius); background: var(--color);`
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // In the component:
|
|
28
|
+
* this.$card.color = '#ef4444' // updates --color reactively
|
|
29
|
+
*/
|
|
30
|
+
export function Param() {
|
|
31
|
+
return createFieldDecorator<ReactiveStylesheet>({
|
|
32
|
+
bind(instance, name) {
|
|
33
|
+
const ctor = (instance as object).constructor as object;
|
|
34
|
+
let meta = PARAM_META.get(ctor);
|
|
35
|
+
if (!meta) { meta = new Set(); PARAM_META.set(ctor, meta); }
|
|
36
|
+
meta.add(name);
|
|
37
|
+
return {};
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { effect, signal, type RootComponent } from "@praxisjs/core/internal";
|
|
2
|
+
import { createFieldDecorator } from "@praxisjs/decorators";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reactive field decorator that binds a signal value to a CSS custom property
|
|
6
|
+
* on the component's container element — with zero re-renders.
|
|
7
|
+
*
|
|
8
|
+
* Only CSS custom properties (variables) are supported: the property name must
|
|
9
|
+
* start with `--`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* class ThemeCard extends StatefulComponent {
|
|
13
|
+
* @Style('--accent') accent = '#3b82f6'
|
|
14
|
+
* @Style('--radius') radius = '8px'
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
export function Style(property: `--${string}`) {
|
|
18
|
+
return createFieldDecorator<object>({
|
|
19
|
+
bind(instance, _name, initialValue) {
|
|
20
|
+
const sig = signal(initialValue as string | number);
|
|
21
|
+
let stopEffect: (() => void) | null = null;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
descriptor: {
|
|
25
|
+
get() {
|
|
26
|
+
return sig();
|
|
27
|
+
},
|
|
28
|
+
set(v: string | number) {
|
|
29
|
+
sig.set(v);
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
onMount() {
|
|
34
|
+
// _anchor is the "[/ComponentName]" comment node inserted by the runtime.
|
|
35
|
+
// Its parentElement is the container where the component lives.
|
|
36
|
+
const el = (instance as unknown as RootComponent)._anchor
|
|
37
|
+
?.parentElement as HTMLElement | null;
|
|
38
|
+
if (!el) return;
|
|
39
|
+
|
|
40
|
+
stopEffect = effect(() => {
|
|
41
|
+
el.style.setProperty(property, String(sig()));
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
onUnmount() {
|
|
46
|
+
stopEffect?.();
|
|
47
|
+
stopEffect = null;
|
|
48
|
+
const el = (instance as unknown as RootComponent)._anchor
|
|
49
|
+
?.parentElement as HTMLElement | null;
|
|
50
|
+
if (el) el.style.removeProperty(property);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { StatefulComponent } from "@praxisjs/core";
|
|
2
|
+
import { effect, signal, type RootComponent } from "@praxisjs/core/internal";
|
|
3
|
+
import { createFieldDecorator } from "@praxisjs/decorators";
|
|
4
|
+
|
|
5
|
+
import { getParamNames } from "./param.js";
|
|
6
|
+
import { isCSSBuilder } from "../builder/css-builder.js";
|
|
7
|
+
import { hashCSS } from "../internal/hash.js";
|
|
8
|
+
import { injectStyle, releaseStyle } from "../internal/registry.js";
|
|
9
|
+
|
|
10
|
+
import type { ReactiveStylesheet, Stylesheet } from "../base/stylesheet.js";
|
|
11
|
+
|
|
12
|
+
// ─── Class data cache ─────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
interface StyleClassData {
|
|
15
|
+
classMap: Readonly<Record<string, string>>;
|
|
16
|
+
rawCSS: string;
|
|
17
|
+
registryKey: string;
|
|
18
|
+
paramDefaults: Readonly<Record<string, string | number>>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const styleClassCache = new Map<new () => Stylesheet, StyleClassData>();
|
|
22
|
+
|
|
23
|
+
function getStyleClassData(StyleClass: new () => Stylesheet): StyleClassData {
|
|
24
|
+
const cached = styleClassCache.get(StyleClass);
|
|
25
|
+
if (cached) return cached;
|
|
26
|
+
|
|
27
|
+
// Constructing the template fires @Param() addInitializers, populating PARAM_META.
|
|
28
|
+
const template = new StyleClass() as unknown as Record<string, unknown>;
|
|
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;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data: StyleClassData = {
|
|
51
|
+
classMap: Object.freeze(classMap),
|
|
52
|
+
rawCSS: blocks.join("\n"),
|
|
53
|
+
registryKey: blocks.length ? hashCSS(blocks.join("\n")) : "",
|
|
54
|
+
paramDefaults: Object.freeze(paramDefaults),
|
|
55
|
+
};
|
|
56
|
+
styleClassCache.set(StyleClass, data);
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── @Styled field decorator ──────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Scoped CSS field decorator. Receives a {@link Stylesheet} or
|
|
64
|
+
* {@link ReactiveStylesheet} subclass and builds a stylesheet object with:
|
|
65
|
+
*
|
|
66
|
+
* - `$xxx` fields → scoped class name strings (static, cached per class)
|
|
67
|
+
* - `@Param() xxx` fields → reactive CSS custom property setters
|
|
68
|
+
* (requires {@link ReactiveStylesheet} and a `StatefulComponent` field)
|
|
69
|
+
*
|
|
70
|
+
* **TypeScript enforcement:**
|
|
71
|
+
* - `@Styled(ReactiveStylesheetSubclass)` can only be applied to fields of
|
|
72
|
+
* classes that extend `StatefulComponent`.
|
|
73
|
+
* - `@Styled(StylesheetSubclass)` (no `@Param()`) can be applied to any component.
|
|
74
|
+
*
|
|
75
|
+
* @example — static CSS (any component)
|
|
76
|
+
* class IconStyles extends Stylesheet {
|
|
77
|
+
* $root = `display: inline-flex; align-items: center;`
|
|
78
|
+
* }
|
|
79
|
+
*
|
|
80
|
+
* class Icon extends StatelessComponent {
|
|
81
|
+
* @Styled(IconStyles) $s!: IconStyles
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* @example — reactive CSS vars (StatefulComponent only)
|
|
85
|
+
* class CardStyles extends ReactiveStylesheet {
|
|
86
|
+
* @Param() color = '#3b82f6'
|
|
87
|
+
* $root = `border: 2px solid var(--color);`
|
|
88
|
+
* }
|
|
89
|
+
*
|
|
90
|
+
* @Component()
|
|
91
|
+
* class Card extends StatefulComponent {
|
|
92
|
+
* @Styled(CardStyles) $card!: CardStyles
|
|
93
|
+
* }
|
|
94
|
+
*/
|
|
95
|
+
export function Styled<T extends Stylesheet>(
|
|
96
|
+
StyleClass: new () => T,
|
|
97
|
+
): [T] extends [ReactiveStylesheet]
|
|
98
|
+
? (value: undefined, context: ClassFieldDecoratorContext<StatefulComponent, T>) => void
|
|
99
|
+
: (value: undefined, context: ClassFieldDecoratorContext<object, T>) => void {
|
|
100
|
+
const data = getStyleClassData(StyleClass);
|
|
101
|
+
|
|
102
|
+
// Eagerly inject CSS at decoration time (module load) so it is in the DOM
|
|
103
|
+
// before any component mounts — avoids layout-measurement timing issues.
|
|
104
|
+
// The per-instance onMount/onUnmount still manage the ref count on top of
|
|
105
|
+
// this base reference, which intentionally stays for the page lifetime.
|
|
106
|
+
if (data.rawCSS) injectStyle(data.registryKey, data.rawCSS);
|
|
107
|
+
|
|
108
|
+
return createFieldDecorator<object>({
|
|
109
|
+
bind(instance) {
|
|
110
|
+
type ParamSig = ReturnType<typeof signal<string | number>>;
|
|
111
|
+
const paramSignals: Record<string, ParamSig> = {};
|
|
112
|
+
const hasParams = Object.keys(data.paramDefaults).length > 0;
|
|
113
|
+
|
|
114
|
+
const stylesheetObj: Record<string, unknown> = hasParams
|
|
115
|
+
? { ...data.classMap }
|
|
116
|
+
: (data.classMap as Record<string, unknown>);
|
|
117
|
+
|
|
118
|
+
for (const [paramName, defaultVal] of Object.entries(data.paramDefaults)) {
|
|
119
|
+
const sig = signal(defaultVal);
|
|
120
|
+
paramSignals[paramName] = sig;
|
|
121
|
+
Object.defineProperty(stylesheetObj, paramName, {
|
|
122
|
+
get() { return sig(); },
|
|
123
|
+
set(v: string | number) { sig.set(v); },
|
|
124
|
+
enumerable: true,
|
|
125
|
+
configurable: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let stopFns: Array<() => void> = [];
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
descriptor: { value: stylesheetObj, writable: false },
|
|
133
|
+
|
|
134
|
+
onMount() {
|
|
135
|
+
// Increment ref-count for this instance (base ref injected at decoration time).
|
|
136
|
+
if (data.rawCSS) injectStyle(data.registryKey, data.rawCSS);
|
|
137
|
+
|
|
138
|
+
if (hasParams) {
|
|
139
|
+
const el = (instance as unknown as RootComponent)._anchor?.parentElement;
|
|
140
|
+
if (el) {
|
|
141
|
+
for (const [paramName, sig] of Object.entries(paramSignals)) {
|
|
142
|
+
stopFns.push(effect(() => {
|
|
143
|
+
el.style.setProperty(`--${paramName}`, String(sig()));
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
onUnmount() {
|
|
151
|
+
if (data.rawCSS) releaseStyle(data.registryKey);
|
|
152
|
+
for (const stop of stopFns) stop();
|
|
153
|
+
stopFns = [];
|
|
154
|
+
|
|
155
|
+
if (hasParams) {
|
|
156
|
+
const el = (instance as unknown as RootComponent)._anchor?.parentElement;
|
|
157
|
+
if (el) {
|
|
158
|
+
for (const paramName of Object.keys(paramSignals)) {
|
|
159
|
+
el.style.removeProperty(`--${paramName}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|