@pyreon/ui-core 0.11.1 → 0.11.3
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/lib/index.d.ts +12 -5
- package/lib/index.js +38 -22
- package/package.json +8 -7
- package/src/PyreonUI.tsx +142 -0
- package/src/__tests__/PyreonUI.test.tsx +97 -0
- package/src/__tests__/compose.test.ts +32 -0
- package/src/__tests__/config.test.ts +102 -0
- package/src/__tests__/context.test.tsx +70 -0
- package/src/__tests__/hoistNonReactStatics.test.tsx +166 -0
- package/src/__tests__/isEmpty.test.ts +53 -0
- package/src/__tests__/isEqual.test.ts +114 -0
- package/src/__tests__/render.test.tsx +72 -0
- package/src/__tests__/useStableValue.test.ts +113 -0
- package/src/__tests__/utils.test.ts +537 -0
- package/src/compose.ts +11 -0
- package/src/config.ts +57 -0
- package/src/context.tsx +40 -0
- package/src/hoistNonReactStatics.ts +59 -0
- package/src/html/htmlElementAttrs.ts +106 -0
- package/src/html/htmlTags.ts +151 -0
- package/src/html/index.ts +11 -0
- package/src/index.ts +55 -0
- package/src/isEmpty.ts +20 -0
- package/src/isEqual.ts +27 -0
- package/src/render.tsx +50 -0
- package/src/types.ts +5 -0
- package/src/useStableValue.ts +21 -0
- package/src/utils.ts +157 -0
package/lib/index.d.ts
CHANGED
|
@@ -225,9 +225,10 @@ interface PyreonUIProps {
|
|
|
225
225
|
theme: PyreonTheme;
|
|
226
226
|
/**
|
|
227
227
|
* Color mode: "light", "dark", or "system" (follows OS preference).
|
|
228
|
+
* Can be a signal or getter for reactive mode switching.
|
|
228
229
|
* @default "light"
|
|
229
230
|
*/
|
|
230
|
-
mode?: ThemeModeInput | undefined;
|
|
231
|
+
mode?: ThemeModeInput | (() => ThemeModeInput) | undefined;
|
|
231
232
|
/** Flip mode for a nested section (e.g. dark sidebar in light app). */
|
|
232
233
|
inversed?: boolean | undefined;
|
|
233
234
|
children?: VNodeChild;
|
|
@@ -247,11 +248,17 @@ declare function useMode(): ThemeMode;
|
|
|
247
248
|
* Replaces the need for separate UnistyleProvider, RocketstyleProvider,
|
|
248
249
|
* and ThemeProvider — one component, zero init.
|
|
249
250
|
*
|
|
250
|
-
*
|
|
251
|
+
* Mode can be a static string OR a signal/getter for reactive switching:
|
|
251
252
|
* ```tsx
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
253
|
+
* // Static
|
|
254
|
+
* <PyreonUI theme={theme} mode="dark">
|
|
255
|
+
*
|
|
256
|
+
* // Reactive signal
|
|
257
|
+
* const mode = signal<ThemeModeInput>("light")
|
|
258
|
+
* <PyreonUI theme={theme} mode={mode}>
|
|
259
|
+
*
|
|
260
|
+
* // System (follows OS preference)
|
|
261
|
+
* <PyreonUI theme={theme} mode="system">
|
|
255
262
|
* ```
|
|
256
263
|
*/
|
|
257
264
|
declare function PyreonUI({
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ThemeContext, css, keyframes, styled } from "@pyreon/styler";
|
|
2
2
|
import { createContext, h, provide, useContext } from "@pyreon/core";
|
|
3
|
-
import { signal } from "@pyreon/reactivity";
|
|
3
|
+
import { computed, signal } from "@pyreon/reactivity";
|
|
4
4
|
import { enrichTheme } from "@pyreon/unistyle";
|
|
5
5
|
|
|
6
6
|
//#region src/compose.ts
|
|
@@ -283,11 +283,12 @@ function getSystemMode() {
|
|
|
283
283
|
if (_systemMode) return _systemMode;
|
|
284
284
|
_systemMode = signal(_isBrowser && matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
|
285
285
|
if (_isBrowser) matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
|
286
|
-
_systemMode
|
|
286
|
+
_systemMode?.set(e.matches ? "dark" : "light");
|
|
287
287
|
});
|
|
288
288
|
return _systemMode;
|
|
289
289
|
}
|
|
290
|
-
|
|
290
|
+
/** Context value is a getter — consumers call it to read the current mode reactively. */
|
|
291
|
+
const ModeContext = createContext(() => "light");
|
|
291
292
|
const INVERSED = {
|
|
292
293
|
light: "dark",
|
|
293
294
|
dark: "light"
|
|
@@ -301,14 +302,9 @@ const INVERSED = {
|
|
|
301
302
|
* const mode = useMode() // "light" | "dark"
|
|
302
303
|
*/
|
|
303
304
|
function useMode() {
|
|
304
|
-
return useContext(ModeContext);
|
|
305
|
+
return useContext(ModeContext)();
|
|
305
306
|
}
|
|
306
307
|
let _autoInitDone = false;
|
|
307
|
-
/**
|
|
308
|
-
* Ensure the CSS engine is initialized. If init() was called manually,
|
|
309
|
-
* this is a no-op. Otherwise, imports @pyreon/styler defaults.
|
|
310
|
-
* Called once on first PyreonUI mount.
|
|
311
|
-
*/
|
|
312
308
|
function autoInit() {
|
|
313
309
|
if (_autoInitDone) return;
|
|
314
310
|
_autoInitDone = true;
|
|
@@ -319,28 +315,42 @@ function autoInit() {
|
|
|
319
315
|
* Replaces the need for separate UnistyleProvider, RocketstyleProvider,
|
|
320
316
|
* and ThemeProvider — one component, zero init.
|
|
321
317
|
*
|
|
322
|
-
*
|
|
318
|
+
* Mode can be a static string OR a signal/getter for reactive switching:
|
|
323
319
|
* ```tsx
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
320
|
+
* // Static
|
|
321
|
+
* <PyreonUI theme={theme} mode="dark">
|
|
322
|
+
*
|
|
323
|
+
* // Reactive signal
|
|
324
|
+
* const mode = signal<ThemeModeInput>("light")
|
|
325
|
+
* <PyreonUI theme={theme} mode={mode}>
|
|
326
|
+
*
|
|
327
|
+
* // System (follows OS preference)
|
|
328
|
+
* <PyreonUI theme={theme} mode="system">
|
|
327
329
|
* ```
|
|
328
330
|
*/
|
|
329
331
|
function PyreonUI({ theme, mode = "light", inversed, children }) {
|
|
330
332
|
autoInit();
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
333
|
+
const resolveMode = () => {
|
|
334
|
+
const raw = typeof mode === "function" ? mode() : mode;
|
|
335
|
+
const resolved = raw === "system" ? getSystemMode()() : raw;
|
|
336
|
+
return inversed ? INVERSED[resolved] : resolved;
|
|
337
|
+
};
|
|
338
|
+
const modeComputed = computed(resolveMode);
|
|
335
339
|
const enrichedTheme = enrichTheme(theme);
|
|
336
340
|
provide(ThemeContext, enrichedTheme);
|
|
337
341
|
provide(context, {
|
|
338
342
|
theme: enrichedTheme,
|
|
339
|
-
mode
|
|
340
|
-
|
|
341
|
-
|
|
343
|
+
get mode() {
|
|
344
|
+
return modeComputed();
|
|
345
|
+
},
|
|
346
|
+
get isDark() {
|
|
347
|
+
return modeComputed() === "dark";
|
|
348
|
+
},
|
|
349
|
+
get isLight() {
|
|
350
|
+
return modeComputed() === "light";
|
|
351
|
+
}
|
|
342
352
|
});
|
|
343
|
-
provide(ModeContext,
|
|
353
|
+
provide(ModeContext, () => modeComputed());
|
|
344
354
|
return children ?? null;
|
|
345
355
|
}
|
|
346
356
|
|
|
@@ -351,7 +361,13 @@ const render = (content, attachProps) => {
|
|
|
351
361
|
const t = typeof content;
|
|
352
362
|
if (t === "string" || t === "number" || t === "boolean" || t === "bigint") return content;
|
|
353
363
|
if (Array.isArray(content)) return content;
|
|
354
|
-
if (typeof content === "function")
|
|
364
|
+
if (typeof content === "function") {
|
|
365
|
+
if (attachProps && "key" in attachProps) {
|
|
366
|
+
const { key, ...rest } = attachProps;
|
|
367
|
+
return h(content, rest);
|
|
368
|
+
}
|
|
369
|
+
return h(content, attachProps ?? {});
|
|
370
|
+
}
|
|
355
371
|
if (typeof content === "object") return content;
|
|
356
372
|
return content;
|
|
357
373
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/ui-core",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/pyreon/pyreon",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"!lib/**/*.map",
|
|
25
25
|
"!lib/analysis",
|
|
26
26
|
"README.md",
|
|
27
|
-
"LICENSE"
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"src"
|
|
28
29
|
],
|
|
29
30
|
"engines": {
|
|
30
31
|
"node": ">= 22"
|
|
@@ -39,13 +40,13 @@
|
|
|
39
40
|
"typecheck": "tsc --noEmit"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
|
-
"@pyreon/core": "^0.11.
|
|
43
|
-
"@pyreon/styler": "^0.11.
|
|
44
|
-
"@pyreon/reactivity": "^0.11.
|
|
45
|
-
"@pyreon/unistyle": "^0.11.
|
|
43
|
+
"@pyreon/core": "^0.11.3",
|
|
44
|
+
"@pyreon/styler": "^0.11.3",
|
|
45
|
+
"@pyreon/reactivity": "^0.11.3",
|
|
46
|
+
"@pyreon/unistyle": "^0.11.3"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@vitus-labs/tools-rolldown": "^1.15.3",
|
|
49
|
-
"@pyreon/typescript": "^0.11.
|
|
50
|
+
"@pyreon/typescript": "^0.11.3"
|
|
50
51
|
}
|
|
51
52
|
}
|
package/src/PyreonUI.tsx
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { VNodeChild } from "@pyreon/core"
|
|
2
|
+
import { createContext, provide, useContext } from "@pyreon/core"
|
|
3
|
+
import { computed, signal } from "@pyreon/reactivity"
|
|
4
|
+
import { ThemeContext } from "@pyreon/styler"
|
|
5
|
+
import type { PyreonTheme } from "@pyreon/unistyle"
|
|
6
|
+
import { enrichTheme } from "@pyreon/unistyle"
|
|
7
|
+
import { context as coreContext } from "./context"
|
|
8
|
+
|
|
9
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export type ThemeMode = "light" | "dark"
|
|
12
|
+
export type ThemeModeInput = ThemeMode | "system"
|
|
13
|
+
|
|
14
|
+
export interface PyreonUIProps {
|
|
15
|
+
/** Theme object with breakpoints, rootSize, and custom keys. */
|
|
16
|
+
theme: PyreonTheme
|
|
17
|
+
/**
|
|
18
|
+
* Color mode: "light", "dark", or "system" (follows OS preference).
|
|
19
|
+
* Can be a signal or getter for reactive mode switching.
|
|
20
|
+
* @default "light"
|
|
21
|
+
*/
|
|
22
|
+
mode?: ThemeModeInput | (() => ThemeModeInput) | undefined
|
|
23
|
+
/** Flip mode for a nested section (e.g. dark sidebar in light app). */
|
|
24
|
+
inversed?: boolean | undefined
|
|
25
|
+
children?: VNodeChild
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── System mode detection ──────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const _isBrowser = typeof window !== "undefined" && typeof matchMedia === "function"
|
|
31
|
+
|
|
32
|
+
/** Reactive signal tracking the OS dark mode preference. Lazy-initialized on first use. */
|
|
33
|
+
let _systemMode: ReturnType<typeof signal<ThemeMode>> | undefined
|
|
34
|
+
|
|
35
|
+
function getSystemMode(): ReturnType<typeof signal<ThemeMode>> {
|
|
36
|
+
if (_systemMode) return _systemMode
|
|
37
|
+
|
|
38
|
+
const prefersDark = _isBrowser && matchMedia("(prefers-color-scheme: dark)").matches
|
|
39
|
+
_systemMode = signal<ThemeMode>(prefersDark ? "dark" : "light")
|
|
40
|
+
|
|
41
|
+
if (_isBrowser) {
|
|
42
|
+
matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
|
43
|
+
_systemMode?.set(e.matches ? "dark" : "light")
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return _systemMode
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Mode context ───────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/** Context value is a getter — consumers call it to read the current mode reactively. */
|
|
53
|
+
const ModeContext = createContext<() => ThemeMode>(() => "light")
|
|
54
|
+
|
|
55
|
+
const INVERSED: Record<ThemeMode, ThemeMode> = { light: "dark", dark: "light" }
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read the resolved color mode ("light" | "dark") from the nearest PyreonUI.
|
|
59
|
+
* Reactive — updates when the mode prop changes or when OS preference changes
|
|
60
|
+
* (if mode="system").
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* const mode = useMode() // "light" | "dark"
|
|
64
|
+
*/
|
|
65
|
+
export function useMode(): ThemeMode {
|
|
66
|
+
return useContext(ModeContext)()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Auto-init ──────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
let _autoInitDone = false
|
|
72
|
+
|
|
73
|
+
function autoInit(): void {
|
|
74
|
+
if (_autoInitDone) return
|
|
75
|
+
_autoInitDone = true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── PyreonUI ───────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Unified provider for the Pyreon UI system.
|
|
82
|
+
*
|
|
83
|
+
* Replaces the need for separate UnistyleProvider, RocketstyleProvider,
|
|
84
|
+
* and ThemeProvider — one component, zero init.
|
|
85
|
+
*
|
|
86
|
+
* Mode can be a static string OR a signal/getter for reactive switching:
|
|
87
|
+
* ```tsx
|
|
88
|
+
* // Static
|
|
89
|
+
* <PyreonUI theme={theme} mode="dark">
|
|
90
|
+
*
|
|
91
|
+
* // Reactive signal
|
|
92
|
+
* const mode = signal<ThemeModeInput>("light")
|
|
93
|
+
* <PyreonUI theme={theme} mode={mode}>
|
|
94
|
+
*
|
|
95
|
+
* // System (follows OS preference)
|
|
96
|
+
* <PyreonUI theme={theme} mode="system">
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export function PyreonUI({ theme, mode = "light", inversed, children }: PyreonUIProps): VNodeChild {
|
|
100
|
+
autoInit()
|
|
101
|
+
|
|
102
|
+
// Create a reactive mode getter that resolves "system" and applies inversion.
|
|
103
|
+
// This getter is provided via context — consumers read it lazily in their
|
|
104
|
+
// own reactive scopes, so mode changes propagate automatically.
|
|
105
|
+
const resolveMode = (): ThemeMode => {
|
|
106
|
+
const raw = typeof mode === "function" ? mode() : mode
|
|
107
|
+
const resolved = raw === "system" ? getSystemMode()() : raw
|
|
108
|
+
return inversed ? INVERSED[resolved] : resolved
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Wrap in computed for memoization
|
|
112
|
+
const modeComputed = computed(resolveMode)
|
|
113
|
+
|
|
114
|
+
// Enrich theme with responsive utilities (__PYREON__)
|
|
115
|
+
const enrichedTheme = enrichTheme(theme)
|
|
116
|
+
|
|
117
|
+
// Provide to all three context layers:
|
|
118
|
+
|
|
119
|
+
// 1. Styler ThemeContext — for styled() components and useTheme()
|
|
120
|
+
provide(ThemeContext, enrichedTheme)
|
|
121
|
+
|
|
122
|
+
// 2. Core context — provide a reactive object with getters.
|
|
123
|
+
// Rocketstyle reads mode/isDark/isLight from this context.
|
|
124
|
+
// By providing getters, the values update when modeComputed changes.
|
|
125
|
+
provide(coreContext, {
|
|
126
|
+
theme: enrichedTheme,
|
|
127
|
+
get mode() {
|
|
128
|
+
return modeComputed()
|
|
129
|
+
},
|
|
130
|
+
get isDark() {
|
|
131
|
+
return modeComputed() === "dark"
|
|
132
|
+
},
|
|
133
|
+
get isLight() {
|
|
134
|
+
return modeComputed() === "light"
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// 3. Mode context — getter function for useMode()
|
|
139
|
+
provide(ModeContext, () => modeComputed())
|
|
140
|
+
|
|
141
|
+
return children ?? null
|
|
142
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { h } from "@pyreon/core"
|
|
2
|
+
import { describe, expect, it, vi } from "vitest"
|
|
3
|
+
import { PyreonUI } from "../PyreonUI"
|
|
4
|
+
|
|
5
|
+
// Spy on provide to verify context provision
|
|
6
|
+
const provideSpy = vi.spyOn(await import("@pyreon/core"), "provide")
|
|
7
|
+
|
|
8
|
+
/** Get the value argument (2nd arg) from a provide() call by index. */
|
|
9
|
+
const getProvideValue = (callIndex: number): any => provideSpy.mock.calls[callIndex]![1]
|
|
10
|
+
|
|
11
|
+
describe("PyreonUI", () => {
|
|
12
|
+
const theme = {
|
|
13
|
+
rootSize: 16,
|
|
14
|
+
breakpoints: { xs: 0, sm: 576, md: 768 },
|
|
15
|
+
colors: { primary: "#228be6" },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
provideSpy.mockClear()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("renders children", () => {
|
|
23
|
+
const child = h("div", null, "hello")
|
|
24
|
+
const result = PyreonUI({ theme, children: child })
|
|
25
|
+
expect(result).toBe(child)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("returns null when no children", () => {
|
|
29
|
+
const result = PyreonUI({ theme })
|
|
30
|
+
expect(result).toBeNull()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("calls provide three times (ThemeContext, core context, mode context)", () => {
|
|
34
|
+
PyreonUI({ theme, children: null })
|
|
35
|
+
expect(provideSpy).toHaveBeenCalledTimes(3)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("defaults mode to light", () => {
|
|
39
|
+
PyreonUI({ theme, children: null })
|
|
40
|
+
|
|
41
|
+
// Core context (2nd call) — has getter properties for reactive mode
|
|
42
|
+
const coreCtx = getProvideValue(1)
|
|
43
|
+
expect(coreCtx.mode).toBe("light")
|
|
44
|
+
expect(coreCtx.isLight).toBe(true)
|
|
45
|
+
expect(coreCtx.isDark).toBe(false)
|
|
46
|
+
|
|
47
|
+
// Mode context (3rd call) — getter function
|
|
48
|
+
const modeGetter = getProvideValue(2)
|
|
49
|
+
expect(typeof modeGetter).toBe("function")
|
|
50
|
+
expect(modeGetter()).toBe("light")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("provides dark mode", () => {
|
|
54
|
+
PyreonUI({ theme, mode: "dark", children: null })
|
|
55
|
+
|
|
56
|
+
const coreCtx = getProvideValue(1)
|
|
57
|
+
expect(coreCtx.mode).toBe("dark")
|
|
58
|
+
expect(coreCtx.isDark).toBe(true)
|
|
59
|
+
expect(coreCtx.isLight).toBe(false)
|
|
60
|
+
|
|
61
|
+
const modeGetter = getProvideValue(2)
|
|
62
|
+
expect(modeGetter()).toBe("dark")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("inverts mode when inversed=true", () => {
|
|
66
|
+
PyreonUI({ theme, mode: "light", inversed: true, children: null })
|
|
67
|
+
expect(getProvideValue(2)()).toBe("dark")
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("inverts dark to light", () => {
|
|
71
|
+
PyreonUI({ theme, mode: "dark", inversed: true, children: null })
|
|
72
|
+
expect(getProvideValue(2)()).toBe("light")
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("enriches theme with __PYREON__ before providing", () => {
|
|
76
|
+
PyreonUI({ theme, children: null })
|
|
77
|
+
|
|
78
|
+
const providedTheme = getProvideValue(0)
|
|
79
|
+
expect(providedTheme.__PYREON__).toBeDefined()
|
|
80
|
+
expect(providedTheme.__PYREON__.sortedBreakpoints).toEqual(["xs", "sm", "md"])
|
|
81
|
+
expect(providedTheme.colors).toEqual({ primary: "#228be6" })
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("works with system mode (resolves to light in happy-dom)", () => {
|
|
85
|
+
PyreonUI({ theme, mode: "system", children: null })
|
|
86
|
+
expect(getProvideValue(2)()).toBe("light")
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("mode context is a getter function (reactive-ready)", () => {
|
|
90
|
+
PyreonUI({ theme, mode: "dark", children: null })
|
|
91
|
+
const modeGetter = getProvideValue(2)
|
|
92
|
+
// Mode context is a function, not a static value — consumers call it
|
|
93
|
+
// inside their own reactive scopes for reactive mode switching.
|
|
94
|
+
expect(typeof modeGetter).toBe("function")
|
|
95
|
+
expect(modeGetter()).toBe("dark")
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import compose from "../compose"
|
|
3
|
+
|
|
4
|
+
describe("compose", () => {
|
|
5
|
+
it("should compose two functions right-to-left", () => {
|
|
6
|
+
const double = (x: number) => x * 2
|
|
7
|
+
const addOne = (x: number) => x + 1
|
|
8
|
+
const composed = compose(addOne, double)
|
|
9
|
+
// double(3) = 6, then addOne(6) = 7
|
|
10
|
+
expect(composed(3)).toBe(7)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it("should compose three functions right-to-left", () => {
|
|
14
|
+
const add = (x: number) => x + 1
|
|
15
|
+
const mul = (x: number) => x * 3
|
|
16
|
+
const sub = (x: number) => x - 2
|
|
17
|
+
// sub(5) = 3, mul(3) = 9, add(9) = 10
|
|
18
|
+
expect(compose(add, mul, sub)(5)).toBe(10)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("should work with a single function", () => {
|
|
22
|
+
const identity = (x: number) => x
|
|
23
|
+
expect(compose(identity)(42)).toBe(42)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("should pass value through string transforms", () => {
|
|
27
|
+
const upper = (s: string) => s.toUpperCase()
|
|
28
|
+
const exclaim = (s: string) => `${s}!`
|
|
29
|
+
// exclaim('hello') = 'hello!', upper('hello!') = 'HELLO!'
|
|
30
|
+
expect(compose(upper, exclaim)("hello")).toBe("HELLO!")
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
2
|
+
import config, { init } from "../config"
|
|
3
|
+
|
|
4
|
+
describe("Configuration", () => {
|
|
5
|
+
it("has default component as div", () => {
|
|
6
|
+
expect(config.component).toBe("div")
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it("has default textComponent as span", () => {
|
|
10
|
+
expect(config.textComponent).toBe("span")
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it("has css function", () => {
|
|
14
|
+
expect(config.css).toBeDefined()
|
|
15
|
+
expect(typeof config.css).toBe("function")
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("has styled function", () => {
|
|
19
|
+
expect(config.styled).toBeDefined()
|
|
20
|
+
expect(typeof config.styled).toBe("function")
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("has keyframes function", () => {
|
|
24
|
+
expect(config.keyframes).toBeDefined()
|
|
25
|
+
expect(typeof config.keyframes).toBe("function")
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe("init", () => {
|
|
29
|
+
const originalCss = config.css
|
|
30
|
+
const originalStyled = config.styled
|
|
31
|
+
const originalKeyframes = config.keyframes
|
|
32
|
+
const originalComponent = config.component
|
|
33
|
+
const originalTextComponent = config.textComponent
|
|
34
|
+
const originalCreateMediaQueries = config.createMediaQueries
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
// restore defaults
|
|
38
|
+
config.css = originalCss
|
|
39
|
+
config.styled = originalStyled
|
|
40
|
+
config.keyframes = originalKeyframes
|
|
41
|
+
config.component = originalComponent
|
|
42
|
+
config.textComponent = originalTextComponent
|
|
43
|
+
config.createMediaQueries = originalCreateMediaQueries
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("updates css engine", () => {
|
|
47
|
+
const mockCss = (() => "") as any
|
|
48
|
+
init({ css: mockCss })
|
|
49
|
+
expect(config.css).toBe(mockCss)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("updates styled engine", () => {
|
|
53
|
+
const mockStyled = (() => "") as any
|
|
54
|
+
init({ styled: mockStyled })
|
|
55
|
+
expect(config.styled).toBe(mockStyled)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("updates component", () => {
|
|
59
|
+
init({ component: "section" })
|
|
60
|
+
expect(config.component).toBe("section")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("updates textComponent", () => {
|
|
64
|
+
init({ textComponent: "p" })
|
|
65
|
+
expect(config.textComponent).toBe("p")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("updates keyframes", () => {
|
|
69
|
+
const mockKeyframes = (() => "anim") as any
|
|
70
|
+
init({ keyframes: mockKeyframes })
|
|
71
|
+
expect(config.keyframes).toBe(mockKeyframes)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("updates createMediaQueries", () => {
|
|
75
|
+
const mockCreateMQ = (() => ({})) as any
|
|
76
|
+
init({ createMediaQueries: mockCreateMQ })
|
|
77
|
+
expect(config.createMediaQueries).toBe(mockCreateMQ)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("only updates provided fields", () => {
|
|
81
|
+
init({ component: "article" })
|
|
82
|
+
expect(config.component).toBe("article")
|
|
83
|
+
expect(config.textComponent).toBe("span")
|
|
84
|
+
expect(config.css).toBe(originalCss)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("does nothing with empty object", () => {
|
|
88
|
+
init({})
|
|
89
|
+
expect(config.component).toBe("div")
|
|
90
|
+
expect(config.textComponent).toBe("span")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("can be called multiple times to swap engine", () => {
|
|
94
|
+
const first = (() => "first") as any
|
|
95
|
+
const second = (() => "second") as any
|
|
96
|
+
init({ css: first })
|
|
97
|
+
expect(config.css).toBe(first)
|
|
98
|
+
init({ css: second })
|
|
99
|
+
expect(config.css).toBe(second)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { popContext, useContext } from "@pyreon/core"
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
3
|
+
import Provider, { context } from "../context"
|
|
4
|
+
|
|
5
|
+
describe("Provider", () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
// Clean up any pushed context frames
|
|
8
|
+
try {
|
|
9
|
+
popContext()
|
|
10
|
+
} catch {
|
|
11
|
+
// Ignore if no context was pushed
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("returns children when no theme is provided", () => {
|
|
16
|
+
const children = "Hello"
|
|
17
|
+
const result = Provider({ children })
|
|
18
|
+
expect(result).toBe("Hello")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("returns children with empty theme", () => {
|
|
22
|
+
const children = "Hello"
|
|
23
|
+
const result = Provider({ theme: {}, children })
|
|
24
|
+
expect(result).toBe("Hello")
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("returns children with null theme", () => {
|
|
28
|
+
const children = "Hello"
|
|
29
|
+
// @ts-expect-error testing null theme
|
|
30
|
+
const result = Provider({ theme: null, children })
|
|
31
|
+
expect(result).toBe("Hello")
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("returns children when theme is provided and pushes context", () => {
|
|
35
|
+
const theme = { rootSize: 16, breakpoints: { xs: 0 } }
|
|
36
|
+
const children = "Styled"
|
|
37
|
+
const result = Provider({ theme, children })
|
|
38
|
+
expect(result).toBe("Styled")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("pushes context with theme and extra props", () => {
|
|
42
|
+
const theme = { rootSize: 16 }
|
|
43
|
+
const children = "Content"
|
|
44
|
+
Provider({ theme, children, custom: "value" })
|
|
45
|
+
// After Provider runs, context should have been pushed
|
|
46
|
+
// We can verify by reading the context value
|
|
47
|
+
const ctx = useContext(context)
|
|
48
|
+
expect(ctx.theme).toEqual({ rootSize: 16 })
|
|
49
|
+
expect(ctx.custom).toBe("value")
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("returns null when no children and no theme", () => {
|
|
53
|
+
const result = Provider({})
|
|
54
|
+
expect(result).toBeNull()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("returns null when theme is provided but no children", () => {
|
|
58
|
+
const theme = { rootSize: 16 }
|
|
59
|
+
const result = Provider({ theme })
|
|
60
|
+
expect(result).toBeNull()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe("context", () => {
|
|
65
|
+
it("exports context object with an id", () => {
|
|
66
|
+
expect(context).toBeDefined()
|
|
67
|
+
expect(context.id).toBeDefined()
|
|
68
|
+
expect(typeof context.id).toBe("symbol")
|
|
69
|
+
})
|
|
70
|
+
})
|