@pyreon/ui-core 0.11.0 → 0.11.2

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.
@@ -0,0 +1,106 @@
1
+ type Base = HTMLElement
2
+
3
+ export interface HTMLElementAttrs {
4
+ a: HTMLAnchorElement
5
+ abbr: Base
6
+ address: Base
7
+ area: HTMLAreaElement
8
+ article: Base
9
+ aside: Base
10
+ audio: HTMLAudioElement
11
+ b: Base
12
+ bdi: Base
13
+ bdo: Base
14
+ big: Base
15
+ blockquote: HTMLQuoteElement
16
+ body: HTMLBodyElement
17
+ br: HTMLBRElement
18
+ button: HTMLButtonElement
19
+ canvas: HTMLCanvasElement
20
+ caption: Base
21
+ cite: HTMLQuoteElement
22
+ code: Base
23
+ col: HTMLTableColElement
24
+ colgroup: HTMLTableColElement
25
+ data: HTMLDataElement
26
+ datalist: HTMLDataListElement
27
+ dd: Base
28
+ del: HTMLModElement
29
+ details: HTMLDetailsElement
30
+ dfn: Base
31
+ dialog: HTMLDialogElement
32
+ div: HTMLDivElement
33
+ dl: HTMLDListElement
34
+ dt: Base
35
+ em: Base
36
+ embed: HTMLEmbedElement
37
+ fieldset: HTMLFieldSetElement
38
+ figcaption: Base
39
+ figure: Base
40
+ footer: Base
41
+ form: HTMLFormElement
42
+ h1: HTMLHeadingElement
43
+ h2: HTMLHeadingElement
44
+ h3: HTMLHeadingElement
45
+ h4: HTMLHeadingElement
46
+ h5: HTMLHeadingElement
47
+ h6: HTMLHeadingElement
48
+ header: Base
49
+ hr: HTMLHRElement
50
+ html: HTMLHtmlElement
51
+ i: Base
52
+ iframe: HTMLIFrameElement
53
+ img: HTMLImageElement
54
+ input: HTMLInputElement
55
+ ins: HTMLModElement
56
+ kbd: Base
57
+ label: HTMLLabelElement
58
+ legend: HTMLLegendElement
59
+ li: HTMLLIElement
60
+ main: Base
61
+ map: HTMLMapElement
62
+ mark: Base
63
+ meter: HTMLMeterElement
64
+ nav: Base
65
+ object: HTMLObjectElement
66
+ ol: HTMLOListElement
67
+ optgroup: HTMLOptGroupElement
68
+ option: HTMLOptionElement
69
+ output: HTMLOutputElement
70
+ p: HTMLParagraphElement
71
+ picture: Base
72
+ pre: HTMLPreElement
73
+ progress: HTMLProgressElement
74
+ q: HTMLQuoteElement
75
+ rp: Base
76
+ rt: Base
77
+ ruby: Base
78
+ s: Base
79
+ samp: Base
80
+ section: Base
81
+ select: HTMLSelectElement
82
+ small: Base
83
+ source: HTMLSourceElement
84
+ span: HTMLSpanElement
85
+ strong: Base
86
+ sub: Base
87
+ summary: Base
88
+ sup: Base
89
+ svg: SVGSVGElement
90
+ table: HTMLTableElement
91
+ tbody: HTMLTableSectionElement
92
+ td: HTMLTableCellElement
93
+ template: HTMLTemplateElement
94
+ textarea: HTMLTextAreaElement
95
+ tfoot: HTMLTableSectionElement
96
+ th: HTMLTableCellElement
97
+ thead: HTMLTableSectionElement
98
+ time: HTMLTimeElement
99
+ tr: HTMLTableRowElement
100
+ track: HTMLTrackElement
101
+ u: Base
102
+ ul: HTMLUListElement
103
+ var: Base
104
+ video: HTMLVideoElement
105
+ wbr: Base
106
+ }
@@ -0,0 +1,151 @@
1
+ const HTML_TAGS = [
2
+ "a",
3
+ "abbr",
4
+ "address",
5
+ "area",
6
+ "article",
7
+ "aside",
8
+ "audio",
9
+ "b",
10
+ "bdi",
11
+ "bdo",
12
+ "big",
13
+ "blockquote",
14
+ "body",
15
+ "br",
16
+ "button",
17
+ "canvas",
18
+ "caption",
19
+ "cite",
20
+ "code",
21
+ "col",
22
+ "colgroup",
23
+ "data",
24
+ "datalist",
25
+ "dd",
26
+ "del",
27
+ "details",
28
+ "dfn",
29
+ "dialog",
30
+ "div",
31
+ "dl",
32
+ "dt",
33
+ "em",
34
+ "embed",
35
+ "fieldset",
36
+ "figcaption",
37
+ "figure",
38
+ "footer",
39
+ "form",
40
+ "h1",
41
+ "h2",
42
+ "h3",
43
+ "h4",
44
+ "h5",
45
+ "h6",
46
+ "header",
47
+ "hr",
48
+ "html",
49
+ "i",
50
+ "iframe",
51
+ "img",
52
+ "input",
53
+ "ins",
54
+ "kbd",
55
+ "label",
56
+ "legend",
57
+ "li",
58
+ "main",
59
+ "map",
60
+ "mark",
61
+ "meter",
62
+ "nav",
63
+ "object",
64
+ "ol",
65
+ "optgroup",
66
+ "option",
67
+ "output",
68
+ "p",
69
+ "picture",
70
+ "pre",
71
+ "progress",
72
+ "q",
73
+ "rp",
74
+ "rt",
75
+ "ruby",
76
+ "s",
77
+ "samp",
78
+ "section",
79
+ "select",
80
+ "small",
81
+ "source",
82
+ "span",
83
+ "strong",
84
+ "sub",
85
+ "summary",
86
+ "sup",
87
+ "svg",
88
+ "table",
89
+ "tbody",
90
+ "td",
91
+ "template",
92
+ "textarea",
93
+ "tfoot",
94
+ "th",
95
+ "thead",
96
+ "time",
97
+ "tr",
98
+ "track",
99
+ "u",
100
+ "ul",
101
+ "var",
102
+ "video",
103
+ "wbr",
104
+ ] as const
105
+
106
+ const HTML_TEXT_TAGS = [
107
+ "abbr",
108
+ "b",
109
+ "bdi",
110
+ "bdo",
111
+ "big",
112
+ "blockquote",
113
+ "cite",
114
+ "code",
115
+ "del",
116
+ "div",
117
+ "dl",
118
+ "dt",
119
+ "em",
120
+ "figcaption",
121
+ "h1",
122
+ "h2",
123
+ "h3",
124
+ "h4",
125
+ "h5",
126
+ "h6",
127
+ "i",
128
+ "ins",
129
+ "kbd",
130
+ "label",
131
+ "legend",
132
+ "li",
133
+ "p",
134
+ "pre",
135
+ "q",
136
+ "rp",
137
+ "rt",
138
+ "s",
139
+ "small",
140
+ "span",
141
+ "strong",
142
+ "sub",
143
+ "summary",
144
+ "sup",
145
+ "time",
146
+ "u",
147
+ ] as const
148
+
149
+ export type HTMLTags = (typeof HTML_TAGS)[number]
150
+ export type HTMLTextTags = (typeof HTML_TEXT_TAGS)[number]
151
+ export { HTML_TAGS, HTML_TEXT_TAGS }
@@ -0,0 +1,11 @@
1
+ import type { HTMLElementAttrs } from "./htmlElementAttrs"
2
+ import type { HTMLTags, HTMLTextTags } from "./htmlTags"
3
+ import { HTML_TAGS, HTML_TEXT_TAGS } from "./htmlTags"
4
+
5
+ type HTMLTagAttrsByTag<T extends HTMLTags> = T extends HTMLTags
6
+ ? HTMLElementAttrs[T]
7
+ : Record<string, never>
8
+
9
+ export type { HTMLElementAttrs, HTMLTagAttrsByTag, HTMLTags, HTMLTextTags }
10
+
11
+ export { HTML_TAGS, HTML_TEXT_TAGS }
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ import compose from "./compose"
2
+ import config, { init } from "./config"
3
+ import Provider, { context } from "./context"
4
+ import hoistNonReactStatics from "./hoistNonReactStatics"
5
+ import type { HTMLElementAttrs, HTMLTagAttrsByTag, HTMLTags, HTMLTextTags } from "./html"
6
+ import { HTML_TAGS, HTML_TEXT_TAGS } from "./html"
7
+ import type { IsEmpty } from "./isEmpty"
8
+ import isEmpty from "./isEmpty"
9
+ import isEqual from "./isEqual"
10
+ import type { PyreonUIProps, ThemeMode, ThemeModeInput } from "./PyreonUI"
11
+ import { PyreonUI, useMode } from "./PyreonUI"
12
+ import type { Render } from "./render"
13
+ import render from "./render"
14
+ import type { BreakpointKeys, Breakpoints } from "./types"
15
+ import useStableValue from "./useStableValue"
16
+ import { get, merge, omit, pick, set, throttle } from "./utils"
17
+
18
+ export type { CSSEngineConnector } from "./config"
19
+
20
+ export type {
21
+ BreakpointKeys,
22
+ Breakpoints,
23
+ HTMLElementAttrs,
24
+ HTMLTagAttrsByTag,
25
+ HTMLTags,
26
+ HTMLTextTags,
27
+ IsEmpty,
28
+ PyreonUIProps,
29
+ Render,
30
+ ThemeMode,
31
+ ThemeModeInput,
32
+ }
33
+
34
+ export {
35
+ compose,
36
+ config,
37
+ context,
38
+ get,
39
+ HTML_TAGS,
40
+ HTML_TEXT_TAGS,
41
+ hoistNonReactStatics,
42
+ init,
43
+ isEmpty,
44
+ isEqual,
45
+ merge,
46
+ omit,
47
+ Provider,
48
+ PyreonUI,
49
+ pick,
50
+ render,
51
+ set,
52
+ throttle,
53
+ useMode,
54
+ useStableValue,
55
+ }
package/src/isEmpty.ts ADDED
@@ -0,0 +1,20 @@
1
+ export type IsEmpty = <T extends Record<number | string, any> | any[] | null | undefined>(
2
+ param: T,
3
+ ) => T extends null | undefined
4
+ ? true
5
+ : keyof T extends never
6
+ ? true
7
+ : T extends T[]
8
+ ? T[number] extends never
9
+ ? true
10
+ : false
11
+ : false
12
+
13
+ const isEmpty = (<T extends Record<number | string, any> | any[] | null | undefined>(param: T) => {
14
+ if (!param) return true
15
+ if (typeof param !== "object") return true
16
+ if (Array.isArray(param)) return param.length === 0
17
+ return Object.keys(param).length === 0
18
+ }) as IsEmpty
19
+
20
+ export default isEmpty
package/src/isEqual.ts ADDED
@@ -0,0 +1,27 @@
1
+ const isArrayEqual = (a: unknown[], b: unknown[]): boolean => {
2
+ if (a.length !== b.length) return false
3
+ for (let i = 0; i < a.length; i++) {
4
+ if (!isEqual(a[i], b[i])) return false
5
+ }
6
+ return true
7
+ }
8
+
9
+ const isObjectEqual = (a: Record<string, unknown>, b: Record<string, unknown>): boolean => {
10
+ const aKeys = Object.keys(a)
11
+ if (aKeys.length !== Object.keys(b).length) return false
12
+ for (const key of aKeys) {
13
+ if (!Object.hasOwn(b, key)) return false
14
+ if (!isEqual(a[key], b[key])) return false
15
+ }
16
+ return true
17
+ }
18
+
19
+ const isEqual = (a: unknown, b: unknown): boolean => {
20
+ if (Object.is(a, b)) return true
21
+ if (typeof a !== typeof b || a == null || b == null || typeof a !== "object") return false
22
+ if (Array.isArray(a)) return Array.isArray(b) && isArrayEqual(a, b)
23
+ if (Array.isArray(b)) return false
24
+ return isObjectEqual(a as Record<string, unknown>, b as Record<string, unknown>)
25
+ }
26
+
27
+ export default isEqual
package/src/render.tsx ADDED
@@ -0,0 +1,44 @@
1
+ import type { ComponentFn, Props, VNodeChild } from "@pyreon/core"
2
+ import { h } from "@pyreon/core"
3
+
4
+ type RenderProps<T extends Record<string, unknown> | undefined> = (props: Partial<T>) => VNodeChild
5
+
6
+ /**
7
+ * Flexible element renderer that handles multiple content types:
8
+ * - Primitives (string, number) — returned as-is
9
+ * - Arrays — returned as-is
10
+ * - Component functions — created via h()
11
+ * - VNode objects — returned as-is (with props merged if provided)
12
+ * - Render props (functions) — called with attachProps
13
+ * - Falsy values — return null
14
+ */
15
+ export type Render = <T extends Record<string, any> | undefined>(
16
+ content?: ComponentFn | string | VNodeChild | VNodeChild[] | RenderProps<T>,
17
+ attachProps?: T,
18
+ ) => VNodeChild
19
+
20
+ const render: Render = (content, attachProps) => {
21
+ if (!content) return null
22
+
23
+ const t = typeof content
24
+ if (t === "string" || t === "number" || t === "boolean" || t === "bigint") {
25
+ return content as VNodeChild
26
+ }
27
+
28
+ if (Array.isArray(content)) {
29
+ return content as VNodeChild
30
+ }
31
+
32
+ if (typeof content === "function") {
33
+ return h(content as string | ComponentFn, (attachProps ?? {}) as Props)
34
+ }
35
+
36
+ // VNode object — return directly
37
+ if (typeof content === "object") {
38
+ return content as VNodeChild
39
+ }
40
+
41
+ return content as VNodeChild
42
+ }
43
+
44
+ export default render
package/src/types.ts ADDED
@@ -0,0 +1,5 @@
1
+ export interface Breakpoints {
2
+ [key: string]: number
3
+ }
4
+
5
+ export type BreakpointKeys = keyof Breakpoints
@@ -0,0 +1,21 @@
1
+ import { signal } from "@pyreon/reactivity"
2
+ import isEqual from "./isEqual"
3
+
4
+ /**
5
+ * Returns a referentially stable version of `value`. The returned reference
6
+ * only changes when the value is no longer deeply equal to the previous one.
7
+ *
8
+ * Pyreon equivalent of the React useStableValue hook — uses a signal
9
+ * internally to hold the stable reference.
10
+ */
11
+ const useStableValue = <T>(value: T): T => {
12
+ const ref = signal(value)
13
+
14
+ if (!isEqual(ref.peek(), value)) {
15
+ ref.set(value)
16
+ }
17
+
18
+ return ref.peek()
19
+ }
20
+
21
+ export default useStableValue
package/src/utils.ts ADDED
@@ -0,0 +1,157 @@
1
+ export const omit = <T extends Record<string, any>>(
2
+ obj: T | null | undefined,
3
+ keys?: readonly (string | keyof T)[],
4
+ ): Partial<T> => {
5
+ if (obj == null) return {} as Partial<T>
6
+ if (!keys || keys.length === 0) return { ...obj }
7
+ const result: Record<string, any> = {}
8
+ const keysSet = new Set(keys as readonly string[])
9
+ for (const key in obj) {
10
+ if (Object.hasOwn(obj, key) && !keysSet.has(key)) {
11
+ result[key] = obj[key]
12
+ }
13
+ }
14
+ return result as Partial<T>
15
+ }
16
+
17
+ export const pick = <T extends Record<string, any>>(
18
+ obj: T | null | undefined,
19
+ keys?: readonly (string | keyof T)[],
20
+ ): Partial<T> => {
21
+ if (obj == null) return {} as Partial<T>
22
+ if (!keys || keys.length === 0) return { ...obj }
23
+ const result: Record<string, any> = {}
24
+ for (const key of keys) {
25
+ const k = key as string
26
+ if (Object.hasOwn(obj, k)) {
27
+ result[k] = obj[k]
28
+ }
29
+ }
30
+ return result as Partial<T>
31
+ }
32
+
33
+ const PATH_RE = /[^.[\]]+/g
34
+
35
+ const parsePath = (path: string | string[]): string[] => {
36
+ if (Array.isArray(path)) return path
37
+ const parts = path.match(PATH_RE)
38
+ return parts ?? []
39
+ }
40
+
41
+ const isUnsafeKey = (key: string): boolean =>
42
+ key === "__proto__" || key === "prototype" || key === "constructor"
43
+
44
+ export const get = (obj: any, path: string | string[], defaultValue?: any): any => {
45
+ const keys = parsePath(path)
46
+ let result = obj
47
+ for (const key of keys) {
48
+ if (result == null || isUnsafeKey(key)) return defaultValue
49
+ result = result[key]
50
+ }
51
+ return result === undefined ? defaultValue : result
52
+ }
53
+
54
+ export const set = (
55
+ obj: Record<string, any>,
56
+ path: string | string[],
57
+ value: any,
58
+ ): Record<string, any> => {
59
+ const keys = parsePath(path)
60
+ let current = obj
61
+ for (let i = 0; i < keys.length - 1; i++) {
62
+ const key = keys[i] as string
63
+ if (isUnsafeKey(key)) return obj
64
+ const nextKey = keys[i + 1] as string
65
+ if (isUnsafeKey(nextKey)) return obj
66
+ if (current[key] == null) {
67
+ current[key] = /^\d+$/.test(nextKey) ? [] : {}
68
+ }
69
+ current = current[key]
70
+ }
71
+ const lastKey = keys[keys.length - 1]
72
+ if (lastKey != null && !isUnsafeKey(lastKey)) {
73
+ current[lastKey] = value
74
+ }
75
+ return obj
76
+ }
77
+
78
+ export const throttle = <T extends (...args: any[]) => any>(
79
+ fn: T,
80
+ wait: number = 0,
81
+ options?: { leading?: boolean; trailing?: boolean },
82
+ ): T & { cancel: () => void } => {
83
+ const leading = options?.leading !== false
84
+ const trailing = options?.trailing !== false
85
+ let lastCallTime: number | undefined
86
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
87
+ let lastArgs: any[] | undefined
88
+
89
+ const invoke = (args: any[]) => {
90
+ lastCallTime = Date.now()
91
+ fn(...args)
92
+ }
93
+
94
+ const startTrailingTimer = (args: any[], delay: number) => {
95
+ lastArgs = args
96
+ if (timeoutId !== undefined) return
97
+ timeoutId = setTimeout(() => {
98
+ timeoutId = undefined
99
+ if (lastArgs) {
100
+ invoke(lastArgs)
101
+ lastArgs = undefined
102
+ }
103
+ }, delay)
104
+ }
105
+
106
+ const throttled = (...args: any[]) => {
107
+ const now = Date.now()
108
+ const elapsed = lastCallTime === undefined ? wait : now - lastCallTime
109
+ if (elapsed >= wait) {
110
+ if (leading) {
111
+ invoke(args)
112
+ } else {
113
+ lastCallTime = now
114
+ if (trailing) startTrailingTimer(args, wait)
115
+ }
116
+ } else if (trailing) {
117
+ startTrailingTimer(args, wait - elapsed)
118
+ }
119
+ }
120
+
121
+ throttled.cancel = () => {
122
+ if (timeoutId !== undefined) {
123
+ clearTimeout(timeoutId)
124
+ timeoutId = undefined
125
+ }
126
+ lastArgs = undefined
127
+ lastCallTime = undefined
128
+ }
129
+
130
+ return throttled as T & { cancel: () => void }
131
+ }
132
+
133
+ const isPlainObject = (value: unknown): value is Record<string, any> =>
134
+ value !== null &&
135
+ typeof value === "object" &&
136
+ !Array.isArray(value) &&
137
+ Object.getPrototypeOf(value) === Object.prototype
138
+
139
+ export const merge = <T extends Record<string, any>>(
140
+ target: T,
141
+ ...sources: Record<string, any>[]
142
+ ): T => {
143
+ for (const source of sources) {
144
+ if (source == null) continue
145
+ for (const key of Object.keys(source)) {
146
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue
147
+ const targetVal = (target as Record<string, unknown>)[key]
148
+ const sourceVal = source[key]
149
+ if (isPlainObject(targetVal) && isPlainObject(sourceVal)) {
150
+ ;(target as Record<string, unknown>)[key] = merge({ ...targetVal }, sourceVal)
151
+ } else {
152
+ ;(target as Record<string, unknown>)[key] = sourceVal
153
+ }
154
+ }
155
+ }
156
+ return target
157
+ }