@pyreon/ui-core 0.22.0 → 0.23.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/README.md +113 -77
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,150 +1,186 @@
|
|
|
1
1
|
# @pyreon/ui-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Foundation layer for Pyreon's UI system — `PyreonUI` provider, config singleton, utilities.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@pyreon/ui-core` is the cross-cutting layer that ties Pyreon's UI packages (`styler`, `unistyle`, `elements`, `rocketstyle`, `coolgrid`, …) together. It ships the unified `<PyreonUI>` provider (theme + light/dark/system mode in one component), the `config` styling-engine singleton (a thin facade over `@pyreon/styler`), helper utilities (`omit` / `pick` / `merge` / `compose` / `throttle` / `set` / `get` / `isEmpty` / `isEqual` / `hoistNonReactStatics`), and the canonical HTML tag arrays + types. No external utility deps — every helper is built-in with prototype-pollution protection where it matters.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
bun add @pyreon/ui-core
|
|
10
|
+
bun add @pyreon/ui-core @pyreon/core @pyreon/styler
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Quick start
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
```tsx
|
|
16
|
+
import { PyreonUI, useMode } from '@pyreon/ui-core'
|
|
17
|
+
import { theme } from '@pyreon/ui-theme'
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
<PyreonUI theme={theme} mode="system">
|
|
20
|
+
<App />
|
|
21
|
+
</PyreonUI>
|
|
22
|
+
|
|
23
|
+
// In any descendant:
|
|
24
|
+
function ModeAware() {
|
|
25
|
+
const mode = useMode() // "light" | "dark" — reactive
|
|
26
|
+
return <span>Current mode: {mode}</span>
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## `<PyreonUI>` — unified provider
|
|
31
|
+
|
|
32
|
+
Single provider that replaces the previous trio of theme / mode / config providers.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
<PyreonUI
|
|
36
|
+
theme={theme} // Theme object (breakpoints, rootSize, custom keys)
|
|
37
|
+
mode="light" // "light" | "dark" | "system" — also accepts () => mode
|
|
38
|
+
inversed // Flip mode for a nested section (dark sidebar in light app)
|
|
39
|
+
>
|
|
40
|
+
<App />
|
|
41
|
+
</PyreonUI>
|
|
19
42
|
```
|
|
20
43
|
|
|
21
|
-
|
|
44
|
+
- `mode="system"` — auto-detects OS dark mode via `matchMedia('(prefers-color-scheme: dark)')` and updates reactively when the user changes their OS preference.
|
|
45
|
+
- `inversed` — flips the resolved mode for the subtree (e.g. a dark hero on a light page).
|
|
46
|
+
- `mode` accepts a signal accessor — `<PyreonUI mode={() => userPref()}>` re-renders the entire subtree's CSS on change WITHOUT remounting (the theme is wrapped in `computed()`).
|
|
47
|
+
- `PyreonUI` calls `init()` internally — you don't normally need to call it yourself.
|
|
48
|
+
|
|
49
|
+
`PyreonUI` is marked `nativeCompat` so it works correctly inside compat-mode apps (`@pyreon/{react,preact,vue,solid,svelte}-compat`) — its `provide()` calls land in Pyreon's setup frame, not inside the compat wrapper.
|
|
50
|
+
|
|
51
|
+
## `useMode()`
|
|
52
|
+
|
|
53
|
+
Reactive — returns `'light'` or `'dark'`, updates on `mode` prop changes AND OS preference changes (when `mode="system"`).
|
|
22
54
|
|
|
23
55
|
```ts
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Provider({
|
|
27
|
-
theme: { rootSize: 16, breakpoints: { xs: 0, md: 768 } },
|
|
28
|
-
children: [
|
|
29
|
-
/* your app */
|
|
30
|
-
],
|
|
31
|
-
})
|
|
56
|
+
const mode = useMode()
|
|
32
57
|
```
|
|
33
58
|
|
|
34
|
-
|
|
59
|
+
## `config` — styling engine singleton
|
|
35
60
|
|
|
36
61
|
```ts
|
|
37
62
|
import { config } from '@pyreon/ui-core'
|
|
38
63
|
|
|
39
|
-
// Access the engine from anywhere
|
|
40
64
|
const { styled, css, keyframes } = config
|
|
41
65
|
```
|
|
42
66
|
|
|
43
|
-
|
|
67
|
+
Pyreon uses `@pyreon/styler` directly — `config` is a thin facade exposed for symmetry across the UI packages. `init()` is the (idempotent) bootstrap call `PyreonUI` invokes internally.
|
|
44
68
|
|
|
45
|
-
|
|
69
|
+
## Utility helpers
|
|
46
70
|
|
|
47
|
-
|
|
71
|
+
### `compose(...fns)` — right-to-left function composition
|
|
48
72
|
|
|
49
73
|
```ts
|
|
50
|
-
import { compose } from '@pyreon/ui-core'
|
|
51
|
-
|
|
52
74
|
const transform = compose(toUpperCase, trim, normalize)
|
|
53
|
-
transform(' hello ')
|
|
75
|
+
transform(' hello ') // 'HELLO'
|
|
54
76
|
```
|
|
55
77
|
|
|
56
|
-
|
|
78
|
+
### `render(value)` — flexible value/element renderer
|
|
57
79
|
|
|
58
|
-
|
|
80
|
+
Handles components, primitives, arrays, null. Useful when authoring helper components that accept a polymorphic content prop.
|
|
59
81
|
|
|
60
82
|
```ts
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
render(
|
|
64
|
-
render(MyComponent) // => MyComponent({})
|
|
65
|
-
render(null) // => null
|
|
83
|
+
render('hello') // 'hello'
|
|
84
|
+
render(MyComponent) // MyComponent({})
|
|
85
|
+
render(null) // null
|
|
66
86
|
```
|
|
67
87
|
|
|
68
|
-
|
|
88
|
+
### `isEmpty(value)` / `isEqual(a, b)`
|
|
69
89
|
|
|
70
|
-
Type-safe
|
|
90
|
+
Type-safe checks. `isEmpty` returns `true` for `null`, `undefined`, `{}`, `[]`, and non-object primitives. `isEqual` performs a deep structural compare.
|
|
71
91
|
|
|
72
92
|
```ts
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
isEmpty({})
|
|
76
|
-
|
|
77
|
-
isEmpty(null) // => true
|
|
78
|
-
isEmpty({ a: 1 }) // => false
|
|
93
|
+
isEmpty({}) // true
|
|
94
|
+
isEmpty([]) // true
|
|
95
|
+
isEmpty({ a: 1 }) // false
|
|
96
|
+
isEqual({a:1}, {a:1}) // true
|
|
79
97
|
```
|
|
80
98
|
|
|
81
|
-
|
|
99
|
+
### `omit(obj, keys)` / `pick(obj, keys)`
|
|
82
100
|
|
|
83
|
-
|
|
101
|
+
Object key filtering. Accept nullable inputs. **`omit()` also accepts a pre-built `Set<string>`** for hot paths where the same key list is reused across many calls (used by rocketstyle to avoid per-mount Set allocation):
|
|
84
102
|
|
|
85
103
|
```ts
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
omit({ a: 1, b: 2, c: 3 }, ['b']) // => { a: 1, c: 3 }
|
|
89
|
-
pick({ a: 1, b: 2, c: 3 }, ['a', 'b']) // => { a: 1, b: 2 }
|
|
104
|
+
omit({ a:1, b:2, c:3 }, ['b']) // { a:1, c:3 }
|
|
105
|
+
pick({ a:1, b:2, c:3 }, ['a','b']) // { a:1, b:2 }
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
omit({ a: 1, b: 2, c: 3 }, omitSet) // => { a: 1 }
|
|
107
|
+
const omitSet = new Set(['b','c'])
|
|
108
|
+
omit({ a:1, b:2, c:3 }, omitSet) // { a:1 }
|
|
94
109
|
```
|
|
95
110
|
|
|
96
|
-
|
|
111
|
+
### `set(obj, path, value)` / `get(obj, path, default?)`
|
|
97
112
|
|
|
98
|
-
Nested property access
|
|
113
|
+
Nested property access by dot/bracket path. **`set` blocks prototype pollution** — `__proto__`, `constructor`, `prototype` keys are silently ignored.
|
|
99
114
|
|
|
100
115
|
```ts
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
get(obj, 'a.b.c') // => 42
|
|
106
|
-
get(obj, 'a.x', 'default') // => 'default'
|
|
116
|
+
const o = {}
|
|
117
|
+
set(o, 'a.b.c', 42) // { a: { b: { c: 42 } } }
|
|
118
|
+
get(o, 'a.b.c') // 42
|
|
119
|
+
get(o, 'a.x', 'default') // 'default'
|
|
107
120
|
```
|
|
108
121
|
|
|
109
|
-
|
|
122
|
+
### `merge(...objects)` — left-to-right deep merge
|
|
110
123
|
|
|
111
|
-
|
|
124
|
+
Only plain objects are recursed; arrays are replaced wholesale. Prototype-pollution keys are blocked.
|
|
112
125
|
|
|
113
126
|
```ts
|
|
114
|
-
|
|
127
|
+
merge({ a: { x: 1 } }, { a: { y: 2 } }) // { a: { x: 1, y: 2 } }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `throttle(fn, ms)`
|
|
131
|
+
|
|
132
|
+
Limits execution to at most once per window. Returns a function with `.cancel()`.
|
|
115
133
|
|
|
116
|
-
|
|
134
|
+
```ts
|
|
135
|
+
const onResize = throttle(handleResize, 200)
|
|
136
|
+
window.addEventListener('resize', onResize)
|
|
137
|
+
// onResize.cancel()
|
|
117
138
|
```
|
|
118
139
|
|
|
119
|
-
|
|
140
|
+
### `useStableValue(value)`
|
|
141
|
+
|
|
142
|
+
Returns a stable accessor that emits only when the input changes by `isEqual`. Useful for deriving signal-shaped values from props that may re-construct identical objects every render.
|
|
143
|
+
|
|
144
|
+
### `hoistNonReactStatics(target, source)`
|
|
120
145
|
|
|
121
|
-
|
|
146
|
+
Standard "hoist non-React statics" copy — used by HOC factories so wrapped components retain their static methods + display names.
|
|
147
|
+
|
|
148
|
+
## HTML tag constants
|
|
122
149
|
|
|
123
150
|
```ts
|
|
124
|
-
import {
|
|
151
|
+
import { HTML_TAGS, HTML_TEXT_TAGS, HTMLTags, HTMLTextTags } from '@pyreon/ui-core'
|
|
125
152
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
//
|
|
153
|
+
HTML_TAGS // array of 100+ valid HTML tag names
|
|
154
|
+
HTML_TEXT_TAGS // text-content tags: h1-h6, p, span, strong, em, ...
|
|
155
|
+
type Tag = HTMLTags // narrowed union type
|
|
156
|
+
type TextTag = HTMLTextTags
|
|
129
157
|
```
|
|
130
158
|
|
|
131
|
-
|
|
159
|
+
Used by `@pyreon/elements` to constrain the `tag` prop and by the styler's `as` polymorphism.
|
|
160
|
+
|
|
161
|
+
## Types
|
|
132
162
|
|
|
133
163
|
```ts
|
|
134
|
-
import {
|
|
164
|
+
import type {
|
|
165
|
+
BreakpointKeys, Breakpoints,
|
|
166
|
+
CoreContextValue,
|
|
167
|
+
HTMLElementAttrs, HTMLTagAttrsByTag, HTMLTags, HTMLTextTags,
|
|
168
|
+
IsEmpty,
|
|
169
|
+
PyreonUIProps, ThemeMode, ThemeModeInput,
|
|
170
|
+
Render,
|
|
171
|
+
} from '@pyreon/ui-core'
|
|
135
172
|
```
|
|
136
173
|
|
|
137
|
-
|
|
138
|
-
- **HTML_TEXT_TAGS** — array of text-content tags (h1–h6, p, span, strong, em, etc.)
|
|
174
|
+
## Gotchas
|
|
139
175
|
|
|
140
|
-
|
|
176
|
+
- **`<PyreonUI>` is the canonical app-root provider.** Internal sub-providers (`CoreProvider` / `UnistyleProvider` / `ThemeProvider` / `Provider` from rocketstyle / `OverlayContextProvider`) are marked `nativeCompat` even though they're internal — never mount a sub-provider manually if `<PyreonUI>` is already in the tree.
|
|
177
|
+
- **`set` / `merge` silently drop prototype-pollution keys.** If a user-supplied key matches `__proto__` / `constructor` / `prototype` it is ignored. Don't rely on these utilities for arbitrary JSON merging that might intentionally need those keys.
|
|
178
|
+
- **`omit` with a Set is faster than with an array** only at scale. For one-off calls the array form is fine.
|
|
179
|
+
- **`useMode()` is reactive — call inside a tracking scope** (JSX expression, effect, computed). Reading at component setup top-level captures the initial value.
|
|
141
180
|
|
|
142
|
-
##
|
|
181
|
+
## Documentation
|
|
143
182
|
|
|
144
|
-
|
|
145
|
-
| -------------- | -------- |
|
|
146
|
-
| @pyreon/core | >= 0.0.1 |
|
|
147
|
-
| @pyreon/styler | >= 0.0.1 |
|
|
183
|
+
Full docs: [docs.pyreon.dev/docs/ui-core](https://docs.pyreon.dev/docs/ui-core) (or `docs/docs/ui-core.md` in this repo).
|
|
148
184
|
|
|
149
185
|
## License
|
|
150
186
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/ui-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Core utilities, config, and context for Pyreon UI System",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@pyreon/manifest": "0.13.1",
|
|
41
|
-
"@pyreon/typescript": "^0.
|
|
42
|
-
"@vitus-labs/tools-rolldown": "^2.
|
|
41
|
+
"@pyreon/typescript": "^0.23.0",
|
|
42
|
+
"@vitus-labs/tools-rolldown": "^2.4.0"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">= 22"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@pyreon/core": "^0.
|
|
49
|
-
"@pyreon/reactivity": "^0.
|
|
50
|
-
"@pyreon/styler": "^0.
|
|
51
|
-
"@pyreon/unistyle": "^0.
|
|
48
|
+
"@pyreon/core": "^0.23.0",
|
|
49
|
+
"@pyreon/reactivity": "^0.23.0",
|
|
50
|
+
"@pyreon/styler": "^0.23.0",
|
|
51
|
+
"@pyreon/unistyle": "^0.23.0"
|
|
52
52
|
}
|
|
53
53
|
}
|