@pyreon/ui-core 0.24.5 → 0.25.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/src/config.ts DELETED
@@ -1,57 +0,0 @@
1
- import type { StyledFunction } from '@pyreon/styler'
2
- import { css, keyframes, styled } from '@pyreon/styler'
3
- import type { HTMLTags } from './html'
4
-
5
- /**
6
- * Describes the shape of the CSS-in-JS engine.
7
- * Pyreon uses @pyreon/styler directly — no connector abstraction needed.
8
- * This type is kept for API compatibility with downstream packages.
9
- */
10
- export interface CSSEngineConnector {
11
- css: typeof css
12
- styled: typeof styled
13
- keyframes: typeof keyframes
14
- }
15
-
16
- interface PlatformConfig {
17
- component: string | HTMLTags
18
- textComponent: string | HTMLTags
19
- createMediaQueries?: (props: {
20
- breakpoints: Record<string, number>
21
- rootSize: number
22
- css: CSSEngineConnector['css']
23
- }) => Record<string, (...args: any[]) => any>
24
- }
25
-
26
- type InitConfig = Partial<CSSEngineConnector & PlatformConfig>
27
-
28
- /**
29
- * Configuration singleton that bridges the UI system with the CSS engine.
30
- * All packages reference config.css, config.styled, etc.
31
- *
32
- * In Pyreon, the engine is @pyreon/styler and is available immediately —
33
- * no lazy initialization or connector pattern needed.
34
- */
35
- class Configuration {
36
- css = css
37
- styled: StyledFunction = styled
38
- keyframes = keyframes
39
- component: string | HTMLTags = 'div'
40
- textComponent: string | HTMLTags = 'span'
41
- createMediaQueries: PlatformConfig['createMediaQueries'] = undefined
42
-
43
- init = (props: InitConfig) => {
44
- if (props.css) this.css = props.css
45
- if (props.styled) this.styled = props.styled
46
- if (props.keyframes) this.keyframes = props.keyframes
47
- if (props.component) this.component = props.component
48
- if (props.textComponent) this.textComponent = props.textComponent
49
- if (props.createMediaQueries) this.createMediaQueries = props.createMediaQueries
50
- }
51
- }
52
-
53
- const config = new Configuration()
54
- const { init } = config
55
-
56
- export default config
57
- export { init }
package/src/context.tsx DELETED
@@ -1,80 +0,0 @@
1
- import type { VNodeChild } from '@pyreon/core'
2
- import { createReactiveContext, nativeCompat, provide } from '@pyreon/core'
3
- import isEmpty from './isEmpty'
4
- import type { Breakpoints } from './types'
5
-
6
- /**
7
- * Core context value shared across all @pyreon UI packages.
8
- */
9
- export interface CoreContextValue {
10
- theme: Record<string, unknown>
11
- mode: 'light' | 'dark'
12
- isDark: boolean
13
- isLight: boolean
14
- }
15
-
16
- /**
17
- * Internal reactive context shared across all @pyreon packages.
18
- * Carries the theme object, mode, and derived dark/light flags.
19
- *
20
- * ReactiveContext means useContext() returns `() => CoreContextValue`.
21
- */
22
- const context = createReactiveContext<CoreContextValue>({
23
- theme: {},
24
- mode: 'light',
25
- isDark: false,
26
- isLight: true,
27
- })
28
-
29
- type Theme = Partial<
30
- {
31
- rootSize: number
32
- breakpoints: Breakpoints
33
- } & Record<string, any>
34
- >
35
-
36
- type ProviderType = Partial<
37
- {
38
- theme: Theme
39
- children: VNodeChild
40
- } & Record<string, any>
41
- >
42
-
43
- /**
44
- * @internal Low-level provider — use `PyreonUI` from `@pyreon/ui-core` instead.
45
- *
46
- * Provider that feeds the internal Pyreon context with the theme.
47
- * When no theme is supplied, renders children directly.
48
- *
49
- * @deprecated Prefer `<PyreonUI theme={theme}>` which handles all context layers.
50
- */
51
- function Provider({ theme, children, ...props }: ProviderType): VNodeChild {
52
- if (process.env.NODE_ENV !== 'production') {
53
- // oxlint-disable-next-line no-console
54
- console.warn(
55
- '[Pyreon] CoreProvider is internal. Use <PyreonUI theme={theme}> instead — it handles all context layers (styler, core, mode) in one component.',
56
- )
57
- }
58
- if (isEmpty(theme) || !theme) return children ?? null
59
-
60
- provide(context, () => ({
61
- theme: theme as Record<string, unknown>,
62
- mode: (props.mode as 'light' | 'dark') ?? 'light',
63
- isDark: props.isDark as boolean ?? false,
64
- isLight: props.isLight as boolean ?? true,
65
- ...props,
66
- }))
67
-
68
- return children ?? null
69
- }
70
-
71
- // Mark as native — even though @internal, PyreonUI invokes this internally
72
- // AND the JSX inside PyreonUI's body still routes through the active jsx()
73
- // runtime (which is the compat one in compat-mode apps). Without the marker,
74
- // CoreProvider's body runs inside the compat wrapper's runUntracked and its
75
- // provide() call is swallowed.
76
- nativeCompat(Provider)
77
-
78
- export { context }
79
-
80
- export default Provider
@@ -1,59 +0,0 @@
1
- const KNOWN_STATICS: Record<string, true> = {
2
- name: true,
3
- length: true,
4
- prototype: true,
5
- caller: true,
6
- callee: true,
7
- arguments: true,
8
- arity: true,
9
- }
10
-
11
- const COMPONENT_STATICS: Record<string, true> = {
12
- displayName: true,
13
- defaultProps: true,
14
- }
15
-
16
- /**
17
- * Copies non-framework static properties from a source component to a target.
18
- *
19
- * Pyreon equivalent of hoistNonReactStatics — simplified since Pyreon
20
- * components are plain functions without React-specific statics like
21
- * contextType, propTypes, getDerivedStateFromProps, etc.
22
- */
23
- const hoistNonReactStatics = <T, S>(
24
- target: T,
25
- source: S,
26
- excludeList?: Record<string, true>,
27
- ): T => {
28
- if (typeof source === 'string') return target
29
-
30
- const proto = Object.getPrototypeOf(source)
31
- if (proto && proto !== Object.prototype) {
32
- hoistNonReactStatics(target, proto, excludeList)
33
- }
34
-
35
- const keys: (string | symbol)[] = [
36
- ...Object.getOwnPropertyNames(source),
37
- ...Object.getOwnPropertySymbols(source),
38
- ]
39
-
40
- for (const key of keys) {
41
- const k = key as string
42
- if (KNOWN_STATICS[k] || excludeList?.[k] || COMPONENT_STATICS[k]) {
43
- continue
44
- }
45
-
46
- const descriptor = Object.getOwnPropertyDescriptor(source, key)
47
- if (descriptor) {
48
- try {
49
- Object.defineProperty(target, key, descriptor)
50
- } catch {
51
- // Silently skip non-configurable properties
52
- }
53
- }
54
- }
55
-
56
- return target
57
- }
58
-
59
- export default hoistNonReactStatics
@@ -1,106 +0,0 @@
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
- }
@@ -1,151 +0,0 @@
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 }
package/src/html/index.ts DELETED
@@ -1,11 +0,0 @@
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 DELETED
@@ -1,57 +0,0 @@
1
- import compose from './compose'
2
- import config, { init } from './config'
3
- import type { CoreContextValue } from './context'
4
- import Provider, { context } from './context'
5
- import hoistNonReactStatics from './hoistNonReactStatics'
6
- import type { HTMLElementAttrs, HTMLTagAttrsByTag, HTMLTags, HTMLTextTags } from './html'
7
- import { HTML_TAGS, HTML_TEXT_TAGS } from './html'
8
- import type { IsEmpty } from './isEmpty'
9
- import isEmpty from './isEmpty'
10
- import isEqual from './isEqual'
11
- import type { PyreonUIProps, ThemeMode, ThemeModeInput } from './PyreonUI'
12
- import { PyreonUI, useMode } from './PyreonUI'
13
- import type { Render } from './render'
14
- import render from './render'
15
- import type { BreakpointKeys, Breakpoints } from './types'
16
- import useStableValue from './useStableValue'
17
- import { get, merge, omit, pick, set, throttle } from './utils'
18
-
19
- export type { CSSEngineConnector } from './config'
20
-
21
- export type {
22
- BreakpointKeys,
23
- Breakpoints,
24
- CoreContextValue,
25
- HTMLElementAttrs,
26
- HTMLTagAttrsByTag,
27
- HTMLTags,
28
- HTMLTextTags,
29
- IsEmpty,
30
- PyreonUIProps,
31
- Render,
32
- ThemeMode,
33
- ThemeModeInput,
34
- }
35
-
36
- export {
37
- compose,
38
- config,
39
- context,
40
- get,
41
- HTML_TAGS,
42
- HTML_TEXT_TAGS,
43
- hoistNonReactStatics,
44
- init,
45
- isEmpty,
46
- isEqual,
47
- merge,
48
- omit,
49
- Provider,
50
- PyreonUI,
51
- pick,
52
- render,
53
- set,
54
- throttle,
55
- useMode,
56
- useStableValue,
57
- }
package/src/isEmpty.ts DELETED
@@ -1,20 +0,0 @@
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 DELETED
@@ -1,27 +0,0 @@
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/manifest.ts DELETED
@@ -1,104 +0,0 @@
1
- import { defineManifest } from '@pyreon/manifest'
2
-
3
- export default defineManifest({
4
- name: '@pyreon/ui-core',
5
- title: 'UI Provider + Config',
6
- tagline:
7
- 'Unified `PyreonUI` provider (theme + mode + config), `useMode()` hook, init() escape hatch',
8
- description:
9
- 'Foundation layer for the Pyreon UI system. `PyreonUI` is the single provider replacing the previous theme / mode / config split — it accepts a theme, a `mode` of `"light" | "dark" | "system"`, and an optional `inversed` flip, then auto-detects OS preference via `prefers-color-scheme` when `mode="system"`. `useMode()` returns the resolved mode as a reactive signal. The package also exposes the `init()` escape hatch (called internally by `PyreonUI` but available for SSR / test setups), the static `HTML_TAGS` / `HTML_TEXT_TAGS` lists used by the bases, and zero-dep utilities (`get`, `set`, `merge`, `pick`, `omit`, `throttle`, `isEmpty`, `isEqual`).',
10
- category: 'browser',
11
- features: [
12
- 'PyreonUI({ theme, mode, inversed }) — single provider replaces 3 separate providers',
13
- 'mode="system" auto-detects OS preference via matchMedia and updates reactively',
14
- 'useMode() returns Signal<"light" | "dark"> resolved against system preference + inversed',
15
- 'init() callable directly for custom environments (tests, SSR without PyreonUI)',
16
- 'enrichTheme() (re-exported from @pyreon/unistyle) merges user theme with defaults',
17
- 'Zero-dep utilities: get, set, merge, pick, omit, throttle, isEmpty, isEqual',
18
- 'HTML_TAGS / HTML_TEXT_TAGS constants drive Element / Text base tag dispatching',
19
- ],
20
- longExample: `import { PyreonUI, useMode } from '@pyreon/ui-core'
21
- import { enrichTheme } from '@pyreon/unistyle'
22
-
23
- // Single provider — wraps theme, mode, and config in one tree
24
- const theme = enrichTheme({
25
- colors: { primary: '#3b82f6', secondary: '#6366f1' },
26
- fonts: { body: 'Inter, sans-serif' },
27
- })
28
-
29
- const App = () => (
30
- <PyreonUI theme={theme} mode="system">
31
- <MyApp />
32
- </PyreonUI>
33
- )
34
-
35
- // useMode() reads the resolved mode reactively
36
- function ThemeBadge() {
37
- const mode = useMode()
38
- return <div class={mode() === 'dark' ? 'badge-dark' : 'badge-light'}>{mode()}</div>
39
- }
40
-
41
- // inversed flips the resolved mode (light → dark and vice versa)
42
- const InvertedSection = () => (
43
- <PyreonUI inversed>
44
- <Sidebar />
45
- </PyreonUI>
46
- )`,
47
- api: [
48
- {
49
- name: 'PyreonUI',
50
- kind: 'component',
51
- signature:
52
- "(props: { theme?: Theme; mode?: 'light' | 'dark' | 'system'; inversed?: boolean; children: VNodeChild }) => VNodeChild",
53
- summary:
54
- "Unified provider replacing the previous theme / mode / config split (3 nested providers became 1). Accepts an enriched `theme` object (merge with defaults via `enrichTheme()`), a `mode` of `'light' | 'dark' | 'system'`, and an optional `inversed` flip. When `mode='system'`, the provider subscribes to `matchMedia('(prefers-color-scheme: dark)')` and re-resolves the mode reactively. Calls `init()` internally so consumers don\\\'t need to wire it up themselves. Whole-theme swaps (user-preference themes) propagate through the styler resolver and re-resolve CSS without remounting the VNode.",
55
- example: `import { PyreonUI } from "@pyreon/ui-core"
56
- import { enrichTheme } from "@pyreon/unistyle"
57
-
58
- const theme = enrichTheme({ colors: { primary: "#3b82f6" } })
59
-
60
- <PyreonUI theme={theme} mode="system">
61
- <App />
62
- </PyreonUI>
63
-
64
- // mode="system" auto-detects OS dark mode via prefers-color-scheme
65
- // inversed flips the resolved mode (light↔dark)`,
66
- mistakes: [
67
- 'Using `ThemeProvider` + `ModeProvider` + `ConfigProvider` separately — `PyreonUI` is the single replacement covering all three',
68
- 'Forgetting `enrichTheme()` — raw theme objects miss default breakpoints / spacing / unit utilities',
69
- 'Destructuring `props` inside the provider — components run once; destructuring captures values at setup. Read `props.mode` lazily inside reactive scopes',
70
- 'Re-augmenting the `ThemeDefault` / `StylesDefault` interfaces in your app — `@pyreon/ui-theme` already augments them; double-augmentation throws TS2320',
71
- ],
72
- seeAlso: ['useMode', 'enrichTheme', 'init'],
73
- },
74
- {
75
- name: 'useMode',
76
- kind: 'hook',
77
- signature: "useMode(): Signal<'light' | 'dark'>",
78
- summary:
79
- "Returns the currently resolved mode as a reactive signal — `'light'` or `'dark'`. When the nearest `PyreonUI` ancestor uses `mode='system'`, the signal reflects the OS preference and updates when the user changes their system setting. When `inversed` is true on any ancestor, the mode is flipped before resolution. Component-scoped subscription — readers re-run only when the resolved mode actually changes.",
80
- example: `import { useMode } from "@pyreon/ui-core"
81
-
82
- const mode = useMode()
83
- // mode() returns "light" or "dark" (resolved, reactive)
84
- // Reflects OS preference when PyreonUI mode="system"`,
85
- mistakes: [
86
- 'Reading `useMode()` without calling it — the value is a `Signal`; use `mode()` to read',
87
- 'Using `useMode()` outside any `PyreonUI` ancestor — falls back to a default but loses the reactive system / inversed handling',
88
- ],
89
- seeAlso: ['PyreonUI'],
90
- },
91
- ],
92
- gotchas: [
93
- {
94
- label: 'Provider replacement',
95
- note:
96
- 'The legacy split (separate theme / mode / config providers) is removed. `PyreonUI` is the only correct mount; calling `init()` directly is the escape hatch for SSR or test environments where the provider tree is unavailable.',
97
- },
98
- {
99
- label: 'System-mode subscription',
100
- note:
101
- '`mode="system"` lazily creates a `matchMedia(\'(prefers-color-scheme: dark)\')` subscription on first read; the listener stays alive for the document lifetime, so a single subscription handles every `useMode()` consumer.',
102
- },
103
- ],
104
- })