@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/dist/__tests__/css-builder.test.d.ts +2 -0
  4. package/dist/__tests__/css-builder.test.d.ts.map +1 -0
  5. package/dist/__tests__/css-builder.test.js +198 -0
  6. package/dist/__tests__/css-builder.test.js.map +1 -0
  7. package/dist/__tests__/cx.test.d.ts +2 -0
  8. package/dist/__tests__/cx.test.d.ts.map +1 -0
  9. package/dist/__tests__/cx.test.js +47 -0
  10. package/dist/__tests__/cx.test.js.map +1 -0
  11. package/dist/__tests__/decorators.test.d.ts +2 -0
  12. package/dist/__tests__/decorators.test.d.ts.map +1 -0
  13. package/dist/__tests__/decorators.test.js +268 -0
  14. package/dist/__tests__/decorators.test.js.map +1 -0
  15. package/dist/__tests__/extract.test.d.ts +2 -0
  16. package/dist/__tests__/extract.test.d.ts.map +1 -0
  17. package/dist/__tests__/extract.test.js +420 -0
  18. package/dist/__tests__/extract.test.js.map +1 -0
  19. package/dist/__tests__/global.test.d.ts +2 -0
  20. package/dist/__tests__/global.test.d.ts.map +1 -0
  21. package/dist/__tests__/global.test.js +117 -0
  22. package/dist/__tests__/global.test.js.map +1 -0
  23. package/dist/__tests__/keyframes.test.d.ts +2 -0
  24. package/dist/__tests__/keyframes.test.d.ts.map +1 -0
  25. package/dist/__tests__/keyframes.test.js +90 -0
  26. package/dist/__tests__/keyframes.test.js.map +1 -0
  27. package/dist/__tests__/preflight.test.d.ts +2 -0
  28. package/dist/__tests__/preflight.test.d.ts.map +1 -0
  29. package/dist/__tests__/preflight.test.js +55 -0
  30. package/dist/__tests__/preflight.test.js.map +1 -0
  31. package/dist/__tests__/registry.test.d.ts +2 -0
  32. package/dist/__tests__/registry.test.d.ts.map +1 -0
  33. package/dist/__tests__/registry.test.js +132 -0
  34. package/dist/__tests__/registry.test.js.map +1 -0
  35. package/dist/__tests__/style.test.d.ts +2 -0
  36. package/dist/__tests__/style.test.d.ts.map +1 -0
  37. package/dist/__tests__/style.test.js +257 -0
  38. package/dist/__tests__/style.test.js.map +1 -0
  39. package/dist/__tests__/styled.test.d.ts +2 -0
  40. package/dist/__tests__/styled.test.d.ts.map +1 -0
  41. package/dist/__tests__/styled.test.js +951 -0
  42. package/dist/__tests__/styled.test.js.map +1 -0
  43. package/dist/__tests__/theme-instance.test.d.ts +2 -0
  44. package/dist/__tests__/theme-instance.test.d.ts.map +1 -0
  45. package/dist/__tests__/theme-instance.test.js +317 -0
  46. package/dist/__tests__/theme-instance.test.js.map +1 -0
  47. package/dist/__tests__/theme-uninit.test.d.ts +2 -0
  48. package/dist/__tests__/theme-uninit.test.d.ts.map +1 -0
  49. package/dist/__tests__/theme-uninit.test.js +10 -0
  50. package/dist/__tests__/theme-uninit.test.js.map +1 -0
  51. package/dist/__tests__/token-sheet.test.d.ts +2 -0
  52. package/dist/__tests__/token-sheet.test.d.ts.map +1 -0
  53. package/dist/__tests__/token-sheet.test.js +74 -0
  54. package/dist/__tests__/token-sheet.test.js.map +1 -0
  55. package/dist/base/stylesheet.d.ts +30 -0
  56. package/dist/base/stylesheet.d.ts.map +1 -0
  57. package/dist/base/stylesheet.js +29 -0
  58. package/dist/base/stylesheet.js.map +1 -0
  59. package/dist/builder/css-builder.d.ts +98 -0
  60. package/dist/builder/css-builder.d.ts.map +1 -0
  61. package/dist/builder/css-builder.js +79 -0
  62. package/dist/builder/css-builder.js.map +1 -0
  63. package/dist/builder/keyframes.d.ts +23 -0
  64. package/dist/builder/keyframes.d.ts.map +1 -0
  65. package/dist/builder/keyframes.js +54 -0
  66. package/dist/builder/keyframes.js.map +1 -0
  67. package/dist/cx.d.ts +3 -0
  68. package/dist/cx.d.ts.map +1 -0
  69. package/dist/cx.js +23 -0
  70. package/dist/cx.js.map +1 -0
  71. package/dist/decorators/param.d.ts +23 -0
  72. package/dist/decorators/param.d.ts.map +1 -0
  73. package/dist/decorators/param.js +39 -0
  74. package/dist/decorators/param.js.map +1 -0
  75. package/dist/decorators/style.d.ts +15 -0
  76. package/dist/decorators/style.d.ts.map +1 -0
  77. package/dist/decorators/style.js +53 -0
  78. package/dist/decorators/style.js.map +1 -0
  79. package/dist/decorators/styled.d.ts +37 -0
  80. package/dist/decorators/styled.d.ts.map +1 -0
  81. package/dist/decorators/styled.js +138 -0
  82. package/dist/decorators/styled.js.map +1 -0
  83. package/dist/extract.d.ts +14 -0
  84. package/dist/extract.d.ts.map +1 -0
  85. package/dist/extract.js +124 -0
  86. package/dist/extract.js.map +1 -0
  87. package/dist/global.d.ts +27 -0
  88. package/dist/global.d.ts.map +1 -0
  89. package/dist/global.js +49 -0
  90. package/dist/global.js.map +1 -0
  91. package/dist/index.d.ts +11 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +10 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/internal/collector.d.ts +15 -0
  96. package/dist/internal/collector.d.ts.map +1 -0
  97. package/dist/internal/collector.js +33 -0
  98. package/dist/internal/collector.js.map +1 -0
  99. package/dist/internal/hash.d.ts +5 -0
  100. package/dist/internal/hash.d.ts.map +1 -0
  101. package/dist/internal/hash.js +11 -0
  102. package/dist/internal/hash.js.map +1 -0
  103. package/dist/internal/registry.d.ts +3 -0
  104. package/dist/internal/registry.d.ts.map +1 -0
  105. package/dist/internal/registry.js +41 -0
  106. package/dist/internal/registry.js.map +1 -0
  107. package/dist/preflight.d.ts +18 -0
  108. package/dist/preflight.d.ts.map +1 -0
  109. package/dist/preflight.js +191 -0
  110. package/dist/preflight.js.map +1 -0
  111. package/dist/server.d.ts +8 -0
  112. package/dist/server.d.ts.map +1 -0
  113. package/dist/server.js +8 -0
  114. package/dist/server.js.map +1 -0
  115. package/dist/tokens/decorators.d.ts +41 -0
  116. package/dist/tokens/decorators.d.ts.map +1 -0
  117. package/dist/tokens/decorators.js +73 -0
  118. package/dist/tokens/decorators.js.map +1 -0
  119. package/dist/tokens/index.d.ts +4 -0
  120. package/dist/tokens/index.d.ts.map +1 -0
  121. package/dist/tokens/index.js +4 -0
  122. package/dist/tokens/index.js.map +1 -0
  123. package/dist/tokens/theme-instance.d.ts +33 -0
  124. package/dist/tokens/theme-instance.d.ts.map +1 -0
  125. package/dist/tokens/theme-instance.js +130 -0
  126. package/dist/tokens/theme-instance.js.map +1 -0
  127. package/dist/tokens/token-sheet.d.ts +46 -0
  128. package/dist/tokens/token-sheet.d.ts.map +1 -0
  129. package/dist/tokens/token-sheet.js +55 -0
  130. package/dist/tokens/token-sheet.js.map +1 -0
  131. package/package.json +33 -0
  132. package/src/__tests__/css-builder.test.ts +230 -0
  133. package/src/__tests__/cx.test.ts +63 -0
  134. package/src/__tests__/decorators.test.ts +127 -0
  135. package/src/__tests__/extract.test.ts +418 -0
  136. package/src/__tests__/global.test.ts +140 -0
  137. package/src/__tests__/keyframes.test.ts +103 -0
  138. package/src/__tests__/preflight.test.ts +63 -0
  139. package/src/__tests__/registry.test.ts +151 -0
  140. package/src/__tests__/style.test.ts +153 -0
  141. package/src/__tests__/styled.test.ts +427 -0
  142. package/src/__tests__/theme-instance.test.ts +374 -0
  143. package/src/__tests__/theme-uninit.test.ts +12 -0
  144. package/src/__tests__/token-sheet.test.ts +87 -0
  145. package/src/base/stylesheet.ts +35 -0
  146. package/src/builder/css-builder.ts +204 -0
  147. package/src/builder/keyframes.ts +66 -0
  148. package/src/cx.ts +26 -0
  149. package/src/decorators/param.ts +40 -0
  150. package/src/decorators/style.ts +55 -0
  151. package/src/decorators/styled.ts +167 -0
  152. package/src/extract.ts +133 -0
  153. package/src/global.ts +60 -0
  154. package/src/index.ts +10 -0
  155. package/src/internal/collector.ts +38 -0
  156. package/src/internal/hash.ts +10 -0
  157. package/src/internal/registry.ts +51 -0
  158. package/src/preflight.ts +192 -0
  159. package/src/server.ts +12 -0
  160. package/src/tokens/decorators.ts +108 -0
  161. package/src/tokens/index.ts +3 -0
  162. package/src/tokens/theme-instance.ts +149 -0
  163. package/src/tokens/token-sheet.ts +86 -0
  164. 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
+ }
@@ -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 };
@@ -0,0 +1,3 @@
1
+ export { TokenSheet, tokenVars } from "./token-sheet.js";
2
+ export { ThemeInstance, theme, type ThemedConfig } from "./theme-instance.js";
3
+ export { Themed, Theme } from "./decorators.js";