@mythicalos/react-ui 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.
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or Derivative
95
+ Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work, excluding
103
+ those notices that do not pertain to any part of the Derivative
104
+ Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and do
117
+ not modify the License. You may add Your own attribution notices
118
+ within Derivative Works that You distribute, alongside or as an
119
+ addendum to the NOTICE text from the Work, provided that such
120
+ additional attribution notices cannot be construed as modifying
121
+ the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 mythicalOS
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,5 @@
1
+ mythical-ui (@mythicalos/ui-core, @mythicalos/preact-ui, @mythicalos/react-ui, @mythicalos/shell)
2
+ Copyright 2026 mythicalOS
3
+
4
+ This product includes software developed by mythicalOS
5
+ (https://github.com/mythicalOS).
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @mythicalos/react-ui
2
+
3
+ The mythicalOS **thin React bindings** over `@mythicalos/ui-core` — the executable component
4
+ layer's React half. Apache-2.0.
5
+
6
+ ## The thin-binding principle
7
+
8
+ This package is render + framework wiring **only**. Every class string, poll-scheduling policy,
9
+ glyph map, typed-name-match decision, and toast text composition is derived by
10
+ `@mythicalos/ui-core`'s pure, framework-agnostic logic — this package never re-implements any of
11
+ it. A sibling `@mythicalos/preact-ui` binding renders the exact same markup from the exact same core
12
+ functions, so the two frameworks can never drift apart. Genuine React — no `preact/compat`.
13
+
14
+ `react`/`react-dom` are peer dependencies (the consuming app supplies the single instance); so is
15
+ `@mythicalos/tokens` (optional — only needed if you're not already loading it another way).
16
+
17
+ ## Install
18
+
19
+ ```sh
20
+ npm add @mythicalos/react-ui react react-dom
21
+ ```
22
+
23
+ ## Import
24
+
25
+ ```tsx
26
+ import { Button, ToastProvider } from "@mythicalos/react-ui";
27
+
28
+ // serve, in order:
29
+ import "@mythicalos/tokens/tokens.css";
30
+ import "@mythicalos/ui-core/styles.css"; // this package ships NO CSS of its own
31
+ ```
32
+
33
+ ## Exports
34
+
35
+ - `Button` — 5 variants × 6 states, loading spinner, inert+aria-busy.
36
+ - `Input`, `Toggle`, `Checkbox` — inputs with the "unconfigured is a valid state" neutral empty.
37
+ - `MaskedSecretInput` — presence-only secret slots; the value never round-trips to the UI.
38
+ - `EmptyState` — the designed empty moments (spine motif; never an error tone).
39
+ - `ConfirmDialog` (+ `typedNameMatches`), `Scrim` — modal danger confirms (Esc cancels, safe action
40
+ focused first, optional typed-name-match gate for irreversible deletes).
41
+ - `Toast`, `ToastProvider`, `composeToastText`, `ToastContext`, `useToast` — the toast bus.
42
+ - `usePoll`, `useInterval`, `shouldResetEpoch` — the request/response poll hooks.
43
+ - `Chip`, `Card`, `Avatar`, `StatusLine`, `SearchInput`, `Banner`, `Gauge` — the 7 atoms graduated
44
+ from design-export's proposed additions.
45
+ - Public types: every `*Props` type, the tone/variant enums (`BtnVariant`/`BtnState`, `ChipTone`,
46
+ `StatusTone`, `BannerTone`, `Tone`, `DialogBullet`, `ToastStatus`/`ToastSpec`/`ToastBus`), and the
47
+ hook contracts (`PollResult`, `PollEpochGuard`, `PollTickIO`, `GaugeGeom`).
48
+ - Re-exported core support API — so a consumer can import everything it needs from this one
49
+ package: `buttonClass`, `typedNameMatches`, `BULLET_ICON`, `composeToastText`, `TOAST_SEP`,
50
+ `chipClass`, `statusLineClass`, `bannerClass`, `BANNER_ICON`, `gaugeTone`, `gaugeGeom`,
51
+ `nextPollDelay`, `makePollEpochGuard`, `runPollTick`, `POLL_JITTER_RATIO`, `POLL_BACKOFF_CAP_MS`.
52
+
53
+ ## Parity with the Preact sibling
54
+
55
+ `@mythicalos/react-ui` mirrors `@mythicalos/preact-ui`'s export surface name-for-name — same
56
+ components, same hooks, same re-exported core symbols, same `*Props` type names. Two small,
57
+ deliberate API deltas from porting Preact idioms to idiomatic React (documented in full in each
58
+ component's header comment):
59
+
60
+ - `Chip`/`Card`/`Avatar`'s passthrough class prop is `className` here, not `class` — the one
61
+ intentional public-prop rename, because `className` is what every React consumer reaches for.
62
+ - `Input`/`SearchInput` keep the Preact binding's `onInput` prop **name** (so switching bindings
63
+ doesn't rename props at every call site) but wire it internally to React's `onChange` — which
64
+ fires on the same "every keystroke" cadence as the native `input` event Preact's `onInput`
65
+ listens to, so behavior matches, only the DOM event name under the hood differs.
66
+
67
+ Every class string either binding renders is produced by the exact same `@mythicalos/ui-core`
68
+ function call — the two bindings cannot silently drift apart.
69
+
70
+ ## Styles
71
+
72
+ This package ships **no stylesheet**. Serve `@mythicalos/ui-core/styles.css` (after
73
+ `@mythicalos/tokens`) — it carries every class this package's components emit, including the 7
74
+ atoms (`.my-chip`, `.my-status`, `.my-card`, `.my-avatar`, `.my-search`, `.my-banner`, `.my-gauge`).
75
+
76
+ ## License
77
+
78
+ Apache-2.0.
@@ -0,0 +1,5 @@
1
+ export interface AvatarProps {
2
+ initials: string;
3
+ className?: string;
4
+ }
5
+ export declare function Avatar(props: AvatarProps): import("react").JSX.Element;
package/dist/Avatar.js ADDED
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function Avatar(props) {
3
+ const { initials, className: cls = "" } = props;
4
+ return _jsx("span", { className: `my-avatar__initials ${cls}`, children: initials });
5
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from "react";
2
+ import { bannerClass, BANNER_ICON, type BannerTone } from "@mythicalos/ui-core/logic";
3
+ export { bannerClass, BANNER_ICON, type BannerTone };
4
+ export interface BannerProps {
5
+ tone?: BannerTone;
6
+ glyph?: string;
7
+ action?: ReactNode;
8
+ children?: ReactNode;
9
+ }
10
+ export declare function Banner(props: BannerProps): import("react").JSX.Element;
package/dist/Banner.js ADDED
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { bannerClass, BANNER_ICON } from "@mythicalos/ui-core/logic";
3
+ export { bannerClass, BANNER_ICON };
4
+ export function Banner(props) {
5
+ const { tone = "info", glyph, action, children } = props;
6
+ return (_jsxs("div", { className: bannerClass(tone), children: [_jsx("span", { className: "my-banner__glyph", children: glyph ?? BANNER_ICON[tone] }), _jsx("span", { children: children }), action] }));
7
+ }
@@ -0,0 +1,14 @@
1
+ import type { MouseEvent, ReactNode } from "react";
2
+ import { buttonClass, type BtnVariant, type BtnState } from "@mythicalos/ui-core/logic";
3
+ export { buttonClass, type BtnVariant, type BtnState };
4
+ export interface ButtonProps {
5
+ variant?: BtnVariant;
6
+ loading?: boolean;
7
+ disabled?: boolean;
8
+ small?: boolean;
9
+ type?: "button" | "submit";
10
+ title?: string;
11
+ onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
12
+ children?: ReactNode;
13
+ }
14
+ export declare function Button(props: ButtonProps): import("react").JSX.Element;
package/dist/Button.js ADDED
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { buttonClass } from "@mythicalos/ui-core/logic";
3
+ export { buttonClass };
4
+ export function Button(props) {
5
+ const { variant = "sec", loading = false, disabled = false, small = false } = props;
6
+ const inert = disabled || loading;
7
+ return (_jsxs("button", { type: props.type ?? "button", className: buttonClass(variant, { loading, disabled, small }), disabled: inert, "aria-busy": loading ? "true" : undefined, title: props.title, onClick: inert ? undefined : props.onClick, children: [loading ? _jsx("span", { className: "spin", "aria-hidden": "true" }) : null, props.children] }));
8
+ }
package/dist/Card.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from "react";
2
+ export interface CardProps {
3
+ title?: string;
4
+ flush?: boolean;
5
+ className?: string;
6
+ children?: ReactNode;
7
+ }
8
+ export declare function Card(props: CardProps): import("react").JSX.Element;
package/dist/Card.js ADDED
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function Card(props) {
3
+ const { title, flush = false, className: cls = "", children } = props;
4
+ return (_jsxs("section", { className: `my-card${flush ? " my-card--flush" : ""} ${cls}`, children: [title ? _jsx("h3", { className: "my-card__title", children: title }) : null, children] }));
5
+ }
package/dist/Chip.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { ReactNode } from "react";
2
+ import { chipClass, type ChipTone } from "@mythicalos/ui-core/logic";
3
+ export { chipClass, type ChipTone };
4
+ export interface ChipProps {
5
+ tone?: ChipTone;
6
+ className?: string;
7
+ children?: ReactNode;
8
+ }
9
+ export declare function Chip(props: ChipProps): import("react").JSX.Element;
package/dist/Chip.js ADDED
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { chipClass } from "@mythicalos/ui-core/logic";
3
+ export { chipClass };
4
+ export function Chip(props) {
5
+ const { tone = "neutral", className: cls = "" } = props;
6
+ return _jsx("span", { className: `${chipClass(tone)} ${cls}`, children: props.children });
7
+ }
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from "react";
2
+ import { typedNameMatches, BULLET_ICON, type DialogBullet } from "@mythicalos/ui-core/logic";
3
+ export { typedNameMatches, BULLET_ICON, type DialogBullet };
4
+ export declare function Scrim(props: {
5
+ children: ReactNode;
6
+ onCancel: () => void;
7
+ }): import("react").JSX.Element;
8
+ export interface ConfirmDialogProps {
9
+ title: string;
10
+ body: ReactNode;
11
+ bullets?: DialogBullet[];
12
+ confirmLabel: string;
13
+ cancelLabel?: string;
14
+ /** Typed-name-match gate (rules-of-use 9): when set, the danger action stays disabled until the
15
+ * operator types EXACTLY this string (e.g. the slot name being deleted). */
16
+ requireName?: string;
17
+ /** Overrides the typed-name entry hint ("Type <name> to confirm" by default). */
18
+ requireNameHint?: string;
19
+ loading?: boolean;
20
+ onConfirm: () => void;
21
+ onCancel: () => void;
22
+ }
23
+ /** A destructive confirm — danger FILL, safe action focused first (Esc cancels), with an optional
24
+ * typed-name-match gate for irreversible deletes. */
25
+ export declare function ConfirmDialog(props: ConfirmDialogProps): import("react").JSX.Element;
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @mythicalos/react-ui — confirm dialogs (ds/components-dialogs). Modal 400px, radius 16, modal
3
+ // shadow over the scrim, Esc = cancel, the safe action focused first. Danger FILL is reserved for
4
+ // genuine destructive confirms (book §7.7). The generic, fully prop-driven `ConfirmDialog` (with
5
+ // its optional typed-name-match gate — rules-of-use 9) + the exported `Scrim` are the shared
6
+ // commons; app-specific wrappers compose them in the app (design note §1b-D21: neutral prop
7
+ // names only — no product domain vocabulary leaks from this package).
8
+ //
9
+ // React twin of packages/preact-ui/src/ConfirmDialog.tsx. `typedNameMatches` and
10
+ // `BULLET_ICON`/`DialogBullet` are imported (never re-implemented) from `@mythicalos/ui-core`, same
11
+ // as the Preact sibling. Two Preact→React deltas: (1) the typed-name `<input>` now attaches
12
+ // `onChange` unconditionally (it always has local state behind it, so this is really a no-op
13
+ // change); (2) the bullets `<li>` list now carries an explicit `key` — React's dev-mode key
14
+ // warning fires at element-creation time (even under `renderToStaticMarkup`), while Preact never
15
+ // checks for one, so a bare `.map()` here would print console noise the Preact sibling never has.
16
+ // `window`'s native KeyboardEvent below is the DOM global, not React's synthetic type — no `react`
17
+ // import shadows it in this file.
18
+ import { useEffect, useRef, useState } from "react";
19
+ import { typedNameMatches, BULLET_ICON } from "@mythicalos/ui-core/logic";
20
+ export { typedNameMatches, BULLET_ICON };
21
+ export function Scrim(props) {
22
+ useEffect(() => {
23
+ const onKey = (e) => {
24
+ if (e.key === "Escape")
25
+ props.onCancel();
26
+ };
27
+ window.addEventListener("keydown", onKey);
28
+ return () => window.removeEventListener("keydown", onKey);
29
+ }, [props.onCancel]);
30
+ return (_jsx("div", { className: "scrim", role: "dialog", "aria-modal": "true", children: _jsx("div", { className: "dlg", children: props.children }) }));
31
+ }
32
+ /** A destructive confirm — danger FILL, safe action focused first (Esc cancels), with an optional
33
+ * typed-name-match gate for irreversible deletes. */
34
+ export function ConfirmDialog(props) {
35
+ const cancelRef = useRef(null);
36
+ const [typed, setTyped] = useState("");
37
+ useEffect(() => {
38
+ cancelRef.current?.focus();
39
+ }, []);
40
+ const nameOk = typedNameMatches(props.requireName, typed);
41
+ const confirmDisabled = props.loading === true || !nameOk;
42
+ return (_jsxs(Scrim, { onCancel: props.onCancel, children: [_jsx("h2", { children: props.title }), _jsx("p", { children: props.body }), props.bullets ? (_jsx("ul", { children: props.bullets.map((b, i) => (_jsxs("li", { children: [_jsx("i", { className: `ic-${b.tone}`, "aria-hidden": "true", children: BULLET_ICON[b.tone] }), _jsx("span", { children: b.text })] }, i))) })) : null, props.requireName !== undefined ? (_jsxs("label", { className: "field", children: [_jsx("span", { className: "field-label", children: props.requireNameHint ?? `Type ${props.requireName} to confirm` }), _jsx("input", { className: "input mono", type: "text", value: typed, placeholder: props.requireName, autoComplete: "off", autoCapitalize: "off", spellCheck: false, onChange: (e) => setTyped(e.target.value) })] })) : null, _jsxs("div", { className: "dlg-foot", children: [_jsx("button", { ref: cancelRef, type: "button", className: "btn btn--sec", onClick: props.onCancel, children: props.cancelLabel ?? "Cancel" }), _jsxs("button", { type: "button", className: `btn danf${confirmDisabled ? " is-disabled" : ""}`, disabled: confirmDisabled, "aria-busy": props.loading ? "true" : undefined, onClick: () => {
43
+ if (!confirmDisabled)
44
+ props.onConfirm();
45
+ }, children: [props.loading ? _jsx("span", { className: "spin", "aria-hidden": "true" }) : null, props.confirmLabel] })] }), _jsx("div", { className: "esc", children: "Esc cancels \u00B7 focus starts on the safe action" })] }));
46
+ }
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from "react";
2
+ export interface EmptyStateProps {
3
+ variant: "unconfigured" | "asleep" | "neutral";
4
+ title: string;
5
+ body: string;
6
+ actions?: ReactNode;
7
+ }
8
+ export declare function EmptyState(props: EmptyStateProps): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ function SpineArt(props) {
3
+ if (props.variant === "unconfigured") {
4
+ // solid up to the live petrol tip, dashed beyond; hollow muted nodes = not-yet
5
+ // (§1a-8: package-owned tokenized classes — no literal strokes, no gauge-block classes)
6
+ return (_jsxs("svg", { className: "spine-empty", viewBox: "0 0 200 36", role: "img", "aria-label": "setup is the live step", children: [_jsx("line", { className: "spine-track", x1: "10", y1: "18", x2: "118", y2: "18", strokeWidth: "3" }), _jsx("line", { className: "spine-dash", x1: "118", y1: "18", x2: "190", y2: "18", strokeWidth: "3", strokeDasharray: "2 7", strokeLinecap: "round" }), _jsx("circle", { className: "spine-node", cx: "10", cy: "18", r: "7", strokeWidth: "3" }), _jsx("circle", { className: "spine-node", cx: "64", cy: "18", r: "7", strokeWidth: "3" }), _jsx("circle", { className: "spine-here", cx: "118", cy: "18", r: "8" })] }));
7
+ }
8
+ if (props.variant === "asleep") {
9
+ return (_jsxs("svg", { className: "spine-empty", viewBox: "0 0 200 36", role: "img", "aria-label": "the hollow tip waits", children: [_jsx("line", { className: "spine-track", x1: "10", y1: "18", x2: "190", y2: "18", strokeWidth: "3" }), _jsx("circle", { className: "spine-dot", cx: "10", cy: "18", r: "7" }), _jsx("circle", { className: "spine-dot", cx: "64", cy: "18", r: "7" }), _jsx("circle", { className: "spine-dot", cx: "118", cy: "18", r: "7" }), _jsx("circle", { className: "spine-node", cx: "182", cy: "18", r: "8", strokeWidth: "3" })] }));
10
+ }
11
+ return null;
12
+ }
13
+ export function EmptyState(props) {
14
+ return (_jsxs("div", { className: "empty", children: [_jsx(SpineArt, { variant: props.variant }), _jsx("h2", { children: props.title }), _jsx("p", { children: props.body }), props.actions ? _jsx("div", { className: "ebtns", children: props.actions }) : null] }));
15
+ }
@@ -0,0 +1,9 @@
1
+ import { gaugeGeom, gaugeTone, type GaugeGeom, type Tone } from "@mythicalos/ui-core/logic";
2
+ export { gaugeGeom, gaugeTone, type GaugeGeom, type Tone };
3
+ export interface GaugeProps {
4
+ pct?: number;
5
+ size?: number;
6
+ stroke?: number;
7
+ showLabel?: boolean;
8
+ }
9
+ export declare function Gauge(props: GaugeProps): import("react").JSX.Element;
package/dist/Gauge.js ADDED
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @mythicalos/react-ui — circular gauge for context/usage. `pct` 0–100. Fill color steps by
3
+ // threshold per token rule #4: accent < 75%, warn 75–89%, error ≥ 90%. Pure SVG presentation
4
+ // attributes (no inline CSS) — the stroke color is a class, the arc is a dash attribute.
5
+ //
6
+ // React twin of packages/preact-ui/src/Gauge.tsx. `gaugeGeom`/`gaugeTone` (geometry + threshold
7
+ // math) are imported from `@mythicalos/ui-core`, never reimplemented — this binding only renders
8
+ // the SVG from the values those functions return. SVG stroke attributes go through React's
9
+ // camelCase props (`strokeWidth`/`strokeDasharray`/`strokeDashoffset`), which serialize back to the
10
+ // same kebab-case DOM attributes as the Preact sibling's output.
11
+ import { gaugeGeom, gaugeTone } from "@mythicalos/ui-core/logic";
12
+ export { gaugeGeom, gaugeTone };
13
+ export function Gauge(props) {
14
+ const { pct = 0, size = 44, stroke = 4, showLabel = true } = props;
15
+ const { r, circ, offset } = gaugeGeom(size, stroke, pct);
16
+ const tone = gaugeTone(pct);
17
+ // gaugeGeom/gaugeTone each clamp `pct` to 0–100 independently (ui-core's gauge.ts: "so either
18
+ // can be called on its own ... and stay consistent with the other"); the label needs that same
19
+ // 0–100 clamp to restate the identical number as a percentage — trivial arithmetic, not the
20
+ // tone/geometry derivation itself, which stays entirely in gaugeTone/gaugeGeom above.
21
+ const clampedPct = Math.round(Math.max(0, Math.min(100, pct)));
22
+ return (_jsxs("span", { className: "my-gauge__wrap", children: [_jsx("span", { className: "my-gauge", children: _jsxs("svg", { width: size, height: size, children: [_jsx("circle", { className: "my-gauge__track", cx: size / 2, cy: size / 2, r: r, strokeWidth: stroke }), _jsx("circle", { className: `my-gauge__fill my-gauge__fill--${tone}`, cx: size / 2, cy: size / 2, r: r, strokeWidth: stroke, strokeDasharray: circ, strokeDashoffset: offset })] }) }), showLabel ? _jsxs("span", { className: "my-gauge__label", children: [clampedPct, "%"] }) : null] }));
23
+ }
@@ -0,0 +1,31 @@
1
+ import type { ReactNode, KeyboardEvent } from "react";
2
+ export interface InputProps {
3
+ label?: ReactNode;
4
+ value?: string;
5
+ placeholder?: string;
6
+ error?: string;
7
+ help?: ReactNode;
8
+ disabled?: boolean;
9
+ readOnly?: boolean;
10
+ mono?: boolean;
11
+ dirty?: boolean;
12
+ type?: string;
13
+ id?: string;
14
+ onInput?: (value: string) => void;
15
+ onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;
16
+ }
17
+ export declare function Input(props: InputProps): import("react").JSX.Element;
18
+ export interface ToggleProps {
19
+ on: boolean;
20
+ disabled?: boolean;
21
+ label?: string;
22
+ onToggle?: (next: boolean) => void;
23
+ }
24
+ export declare function Toggle(props: ToggleProps): import("react").JSX.Element;
25
+ export interface CheckboxProps {
26
+ checked: boolean;
27
+ disabled?: boolean;
28
+ label?: ReactNode;
29
+ onToggle?: (next: boolean) => void;
30
+ }
31
+ export declare function Checkbox(props: CheckboxProps): import("react").JSX.Element;
package/dist/Input.js ADDED
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ export function Input(props) {
3
+ const cls = ["input"];
4
+ if (props.error)
5
+ cls.push("is-err");
6
+ if (props.mono)
7
+ cls.push("mono");
8
+ if (props.dirty)
9
+ cls.push("is-dirty");
10
+ if (props.readOnly)
11
+ cls.push("readonly-input");
12
+ const field = (_jsxs(_Fragment, { children: [_jsx("input", { id: props.id, type: props.type ?? "text", className: cls.join(" "), value: props.value ?? "", placeholder: props.placeholder, disabled: props.disabled, readOnly: props.readOnly,
13
+ // Local single-user config UI: these are identifiers/paths/config values (slugs, names,
14
+ // model ids, urls), never autofill targets — kill browser autocomplete, the annoying
15
+ // first-letter auto-capitalize, autocorrect, and spellcheck squiggles on all of them.
16
+ autoComplete: "off", autoCapitalize: "off", autoCorrect: "off", spellCheck: false, "aria-invalid": props.error ? "true" : undefined, onChange: (e) => props.onInput?.(e.target.value), onKeyDown: props.onKeyDown }), props.help && !props.error ? _jsx("div", { className: "help", children: props.help }) : null, props.error ? (_jsxs("div", { className: "emsg", children: [_jsx("span", { "aria-hidden": "true", children: "\u26A0" }), _jsx("span", { children: props.error })] })) : null] }));
17
+ if (!props.label)
18
+ return field;
19
+ return (_jsxs("label", { className: "field", children: [_jsx("span", { className: "field-label", children: props.label }), field] }));
20
+ }
21
+ export function Toggle(props) {
22
+ const cls = ["tog"];
23
+ if (!props.on)
24
+ cls.push("is-off");
25
+ if (props.disabled)
26
+ cls.push("is-disabled");
27
+ return (_jsx("button", { type: "button", className: cls.join(" "), role: "switch", "aria-checked": props.on ? "true" : "false", "aria-label": props.label, disabled: props.disabled, onClick: props.disabled ? undefined : () => props.onToggle?.(!props.on), children: _jsx("i", { "aria-hidden": "true" }) }));
28
+ }
29
+ export function Checkbox(props) {
30
+ const cls = ["cb"];
31
+ if (props.checked)
32
+ cls.push("is-on");
33
+ if (props.disabled)
34
+ cls.push("is-disabled");
35
+ const box = (_jsx("button", { type: "button", className: cls.join(" "), role: "checkbox", "aria-checked": props.checked ? "true" : "false", disabled: props.disabled, onClick: props.disabled ? undefined : () => props.onToggle?.(!props.checked), children: props.checked ? _jsx("span", { "aria-hidden": "true", children: "\u2713" }) : null }));
36
+ if (!props.label)
37
+ return box;
38
+ return (_jsxs("label", { className: "check-row", children: [box, _jsx("span", { children: props.label })] }));
39
+ }
@@ -0,0 +1,15 @@
1
+ export interface MaskedSecretInputProps {
2
+ label: string;
3
+ /** Full slot name, e.g. "model/claude" — shown as the chip. */
4
+ slot: string;
5
+ /** True when the server's secrets list contains `slot`. */
6
+ filled: boolean;
7
+ /** The server's masked echo, when known (e.g. "sk-…3kfa"). */
8
+ masked?: string;
9
+ /** Labels the entry lane so the operator knows what to paste. */
10
+ entryHint?: string;
11
+ disabled?: boolean;
12
+ /** Called with the raw entered value on submit — the parent PUTs it and never reads it back. */
13
+ onSubmit?: (value: string) => void;
14
+ }
15
+ export declare function MaskedSecretInput(props: MaskedSecretInputProps): import("react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @mythicalos/react-ui — masked secret input with slot chip (ds/components-inputs "secret slots").
3
+ // A filled slot shows the server's mask (readonly — the value never round-trips to the UI) + the
4
+ // slot chip + a "Replace" affordance that re-opens entry; an unset slot shows the dashed empty
5
+ // chip ("unconfigured is a valid state"). Submitting a new value is the PARENT's concern (it PUTs
6
+ // to the secrets route and never reads the value back); this component is presentation + entry only.
7
+ //
8
+ // React twin of packages/preact-ui/src/MaskedSecretInput.tsx. Both the filled-mask input (readOnly,
9
+ // no handler needed — React's controlled-without-onChange warning is suppressed by `readOnly`) and
10
+ // the entry input (local `useState`, always has a real `onChange`) are already React-safe with no
11
+ // extra wiring required.
12
+ import { useState } from "react";
13
+ import { Button } from "./Button.js";
14
+ export function MaskedSecretInput(props) {
15
+ const [editing, setEditing] = useState(!props.filled);
16
+ const [value, setValue] = useState("");
17
+ const chip = props.filled ? (_jsx("span", { className: "chip", children: props.slot })) : (_jsx("span", { className: "chip chip--empty", children: "slot empty" }));
18
+ if (props.filled && !editing) {
19
+ return (_jsxs("label", { className: "field", children: [_jsx("span", { className: "field-label", children: props.label }), _jsxs("div", { className: "slot", children: [_jsx("input", { className: "input mono readonly-input", value: props.masked ?? "••••••••", readOnly: true }), chip, _jsx("button", { type: "button", className: "rep", disabled: props.disabled, onClick: () => setEditing(true), children: "Replace" })] }), _jsx("div", { className: "help", children: "Readonly mask; Replace re-opens entry. The value never round-trips to the UI." })] }));
20
+ }
21
+ return (_jsxs("label", { className: "field", children: [_jsx("span", { className: "field-label", children: props.label }), _jsxs("div", { className: "slot", children: [_jsx("input", { className: "input mono",
22
+ // diff r1-F10 (S1a-11): a replacement credential is a SECRET while typed — masked entry
23
+ // (type=password) + new-password so the browser neither displays nor offers to save it.
24
+ type: "password", value: value, placeholder: props.entryHint ?? "paste token…", disabled: props.disabled,
25
+ // A pasted credential: never autofill/autocapitalize/autocorrect/spellcheck it.
26
+ autoComplete: "new-password", autoCapitalize: "off", autoCorrect: "off", spellCheck: false, onChange: (e) => setValue(e.target.value) }), chip, _jsx(Button, { variant: "sec", small: true, disabled: props.disabled || value.trim().length === 0, onClick: () => {
27
+ props.onSubmit?.(value);
28
+ setValue("");
29
+ if (props.filled)
30
+ setEditing(false);
31
+ }, children: "Save" })] }), _jsx("div", { className: "help", children: "Unconfigured is a valid state \u2014 no error until submit." })] }));
32
+ }
@@ -0,0 +1,8 @@
1
+ import type { ChangeEvent, MouseEvent } from "react";
2
+ export interface SearchInputProps {
3
+ value?: string;
4
+ placeholder?: string;
5
+ onInput?: (e: ChangeEvent<HTMLInputElement>) => void;
6
+ onClear?: (e: MouseEvent<HTMLButtonElement>) => void;
7
+ }
8
+ export declare function SearchInput(props: SearchInputProps): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const noop = () => { };
3
+ export function SearchInput(props) {
4
+ const { value, onInput, onClear, placeholder = "Search…" } = props;
5
+ return (_jsxs("div", { className: "my-search", children: [_jsx("span", { className: "my-search__icon", children: "\u2315" }), _jsx("input", { className: "my-search__input", value: value, onChange: onInput ?? noop, placeholder: placeholder }), value ? (_jsx("button", { type: "button", className: "my-search__clear", title: "Clear", onClick: onClear, children: "\u2715" })) : null] }));
6
+ }
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from "react";
2
+ import { statusLineClass, type StatusTone } from "@mythicalos/ui-core/logic";
3
+ export { statusLineClass, type StatusTone };
4
+ export interface StatusLineProps {
5
+ tone?: StatusTone;
6
+ children?: ReactNode;
7
+ }
8
+ export declare function StatusLine(props: StatusLineProps): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { statusLineClass } from "@mythicalos/ui-core/logic";
3
+ export { statusLineClass };
4
+ export function StatusLine(props) {
5
+ const { tone = "ok" } = props;
6
+ return (_jsxs("span", { className: statusLineClass(tone), children: [_jsx("span", { className: "my-status__dot" }), props.children] }));
7
+ }
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from "react";
2
+ import { type ToastStatus } from "./toast-context.js";
3
+ import { composeToastText, TOAST_SEP } from "@mythicalos/ui-core/logic";
4
+ export { composeToastText, TOAST_SEP };
5
+ export declare function ToastProvider(props: {
6
+ children: ReactNode;
7
+ }): import("react").JSX.Element;
8
+ export interface ToastProps {
9
+ status: ToastStatus;
10
+ title: string;
11
+ body?: string;
12
+ action?: {
13
+ label: string;
14
+ onAction: () => void;
15
+ };
16
+ onClose?: () => void;
17
+ }
18
+ export declare function Toast(props: ToastProps): import("react").JSX.Element;
package/dist/Toast.js ADDED
@@ -0,0 +1,51 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // @mythicalos/react-ui — toasts (ds/components-toasts): tone-colored (ok = surface/ink/border;
3
+ // warn/error/info = soft fill + status ink/border), icon + bold lead carry the state (never color
4
+ // alone). Auto-dismiss every tone on one cadence; each stacked toast runs its own timer. Max 3
5
+ // visible — older collapse into "+n more".
6
+ //
7
+ // React twin of packages/preact-ui/src/Toast.tsx. `composeToastText`/`TOAST_SEP` are imported
8
+ // (never re-implemented) from `@mythicalos/ui-core`; the tone→icon/dismiss-cadence lookup tables
9
+ // stay local, same as the Preact sibling (literal presentational maps, not branchy derivation).
10
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
11
+ import { ToastContext } from "./toast-context.js";
12
+ import { composeToastText, TOAST_SEP } from "@mythicalos/ui-core/logic";
13
+ export { composeToastText, TOAST_SEP };
14
+ const ICON = { ok: "✓", info: "ℹ", warn: "▲", error: "⬤" };
15
+ const DISMISS_MS = { ok: 2600, info: 2600, warn: 2600, error: 2600 };
16
+ export function ToastProvider(props) {
17
+ const [toasts, setToasts] = useState([]);
18
+ const nextId = useRef(1);
19
+ const timers = useRef(new Map());
20
+ const dismiss = useCallback((id) => {
21
+ setToasts((prev) => prev.filter((t) => t.id !== id));
22
+ const timer = timers.current.get(id);
23
+ if (timer) {
24
+ clearTimeout(timer);
25
+ timers.current.delete(id);
26
+ }
27
+ }, []);
28
+ const push = useCallback((spec) => {
29
+ const id = nextId.current++;
30
+ setToasts((prev) => [...prev, { ...spec, id }]);
31
+ const ms = DISMISS_MS[spec.status];
32
+ if (ms !== null) {
33
+ timers.current.set(id, setTimeout(() => dismiss(id), ms));
34
+ }
35
+ }, [dismiss]);
36
+ useEffect(() => {
37
+ const map = timers.current;
38
+ return () => {
39
+ for (const t of map.values())
40
+ clearTimeout(t);
41
+ map.clear();
42
+ };
43
+ }, []);
44
+ const bus = useMemo(() => ({ push }), [push]);
45
+ const visible = toasts.slice(-3);
46
+ const hiddenCount = toasts.length - visible.length;
47
+ return (_jsxs(ToastContext.Provider, { value: bus, children: [props.children, _jsxs("div", { className: "toast-stack", "aria-live": "polite", children: [hiddenCount > 0 ? _jsxs("div", { className: "toast-more", children: ["+", hiddenCount, " more"] }) : null, visible.map((t) => (_jsx(Toast, { status: t.status, title: t.title, body: t.body, action: t.action, onClose: () => dismiss(t.id) }, t.id)))] })] }));
48
+ }
49
+ export function Toast(props) {
50
+ return (_jsxs("div", { className: `toast toast--${props.status}`, role: "status", children: [_jsx("i", { className: "toast-icon", "aria-hidden": "true", children: ICON[props.status] }), _jsxs("span", { children: [_jsx("b", { children: props.title }), props.body ? (_jsxs(_Fragment, { children: [TOAST_SEP, props.body] })) : null] }), props.action ? (_jsx("button", { type: "button", className: "toast-action", onClick: props.action.onAction, children: props.action.label })) : null, _jsx("button", { type: "button", className: "toast-x", "aria-label": "Dismiss", onClick: props.onClose, children: "\u2715" })] }));
51
+ }
@@ -0,0 +1,21 @@
1
+ import { nextPollDelay, makePollEpochGuard, runPollTick, shouldResetEpoch, POLL_JITTER_RATIO, POLL_BACKOFF_CAP_MS, type PollResult, type PollEpochGuard, type PollTickIO } from "@mythicalos/ui-core/logic";
2
+ export { nextPollDelay, makePollEpochGuard, runPollTick, shouldResetEpoch, POLL_JITTER_RATIO, POLL_BACKOFF_CAP_MS, type PollResult, type PollEpochGuard, type PollTickIO, };
3
+ /**
4
+ * Poll `fn` on the `ms` cadence while `enabled`, under the U7 policy: every delay ±20% jittered,
5
+ * exponential ×2 backoff on consecutive failures capped at 60 s (reset on success), and
6
+ * `document.hidden` PAUSES the loop (immediate refresh when visibility returns). Fires immediately
7
+ * on mount / when a dep changes, cancels in-flight results after unmount, and never overlaps a
8
+ * slow response with the next tick — ONE scheduler per call site, never parallel duplicate polls.
9
+ * `fn` should resolve (never reject) with the value; thrown errors surface in `error`.
10
+ *
11
+ * Pass an `epochKey` to opt into epoch isolation: a change of key (or an `enabled` false→true
12
+ * transition) clears the retained `data`/`error` and returns to `loading` until the new epoch's
13
+ * first settle. Without `epochKey` the hook retains data/error across toggles and dep changes.
14
+ */
15
+ export declare function usePoll<T>(fn: () => Promise<T>, ms: number, opts?: {
16
+ enabled?: boolean;
17
+ deps?: readonly unknown[];
18
+ epochKey?: unknown;
19
+ }): PollResult<T>;
20
+ /** Run `fn` on a fixed interval while `enabled` (no return value; for fire-and-forget effects). */
21
+ export declare function useInterval(fn: () => void, ms: number, enabled?: boolean): void;
package/dist/hooks.js ADDED
@@ -0,0 +1,136 @@
1
+ // @mythicalos/react-ui — small shared hooks (polling + interval). A family UI talks plain
2
+ // request/response to its service; pages poll on their cadence via `usePoll`. Extends the U7
3
+ // polling policy (design note §1b-D7, r2-F8): per-call interval, ±20% jitter on every delay,
4
+ // exponential ×2 backoff on consecutive failures capped at 60 s (reset on success), and
5
+ // `document.hidden` pauses the loop with an immediate refresh on visibility return.
6
+ //
7
+ // React twin of packages/preact-ui/src/hooks.ts. The pure, DOM-free scheduling math
8
+ // (`nextPollDelay`, `makePollEpochGuard`, `runPollTick`, `shouldResetEpoch` — the r3-F1 fire-time
9
+ // epoch stamp) lives in `@mythicalos/ui-core` and is imported, never reimplemented — identical to
10
+ // the Preact sibling. Only the actual React hooks (`usePoll`/`useInterval`, which import `react`
11
+ // instead of `preact/hooks`) differ; the hook bodies below are otherwise verbatim.
12
+ import { useEffect, useRef, useState } from "react";
13
+ import { nextPollDelay, makePollEpochGuard, runPollTick, shouldResetEpoch, POLL_JITTER_RATIO, POLL_BACKOFF_CAP_MS, } from "@mythicalos/ui-core/logic";
14
+ export { nextPollDelay, makePollEpochGuard, runPollTick, shouldResetEpoch, POLL_JITTER_RATIO, POLL_BACKOFF_CAP_MS, };
15
+ /**
16
+ * Poll `fn` on the `ms` cadence while `enabled`, under the U7 policy: every delay ±20% jittered,
17
+ * exponential ×2 backoff on consecutive failures capped at 60 s (reset on success), and
18
+ * `document.hidden` PAUSES the loop (immediate refresh when visibility returns). Fires immediately
19
+ * on mount / when a dep changes, cancels in-flight results after unmount, and never overlaps a
20
+ * slow response with the next tick — ONE scheduler per call site, never parallel duplicate polls.
21
+ * `fn` should resolve (never reject) with the value; thrown errors surface in `error`.
22
+ *
23
+ * Pass an `epochKey` to opt into epoch isolation: a change of key (or an `enabled` false→true
24
+ * transition) clears the retained `data`/`error` and returns to `loading` until the new epoch's
25
+ * first settle. Without `epochKey` the hook retains data/error across toggles and dep changes.
26
+ */
27
+ export function usePoll(fn, ms, opts = {}) {
28
+ const enabled = opts.enabled ?? true;
29
+ const deps = opts.deps ?? [];
30
+ const useEpoch = "epochKey" in opts;
31
+ const epochKey = opts.epochKey;
32
+ const [data, setData] = useState(undefined);
33
+ const [error, setError] = useState(undefined);
34
+ const [loading, setLoading] = useState(enabled);
35
+ const [nonce, setNonce] = useState(0);
36
+ const fnRef = useRef(fn);
37
+ fnRef.current = fn;
38
+ // diff r3-F1: one persistent guard per call site — the render-time reset below invalidates it
39
+ // IMMEDIATELY, so an in-flight response for the old epoch is dropped even though the old
40
+ // effect's cleanup (which flips `alive`) only runs after paint.
41
+ const guardRef = useRef(null);
42
+ if (guardRef.current === null)
43
+ guardRef.current = makePollEpochGuard();
44
+ const guard = guardRef.current;
45
+ const epochRef = useRef({ key: epochKey, enabled });
46
+ if (shouldResetEpoch(useEpoch, epochRef.current, epochKey, enabled)) {
47
+ guard.invalidate(); // FIRST — stale in-flight responses die here, not at deferred cleanup (r3-F1)
48
+ if (data !== undefined)
49
+ setData(undefined);
50
+ if (error !== undefined)
51
+ setError(undefined);
52
+ if (enabled && !loading)
53
+ setLoading(true);
54
+ }
55
+ if (useEpoch)
56
+ epochRef.current = { key: epochKey, enabled };
57
+ useEffect(() => {
58
+ if (!enabled) {
59
+ setLoading(false);
60
+ return;
61
+ }
62
+ let alive = true;
63
+ let timer;
64
+ let stopped = false;
65
+ let failures = 0; // consecutive failed reads — drives the ×2 backoff ladder (reset on success)
66
+ const isHidden = () => typeof document !== "undefined" && document.hidden === true;
67
+ const schedule = () => {
68
+ if (!alive || stopped)
69
+ return;
70
+ // U7 pause: while the document is hidden no timer is armed — the
71
+ // visibilitychange handler below refreshes immediately on return.
72
+ if (isHidden())
73
+ return;
74
+ timer = setTimeout(() => void tick(), nextPollDelay(ms, failures));
75
+ };
76
+ // diff r3-F1: every response routes through runPollTick — stamped with the epoch at fire
77
+ // time, applied only while that epoch is still live. A stale settle leaves loading alone
78
+ // (the reset already set it for the new epoch) but still rearms via schedule()'s own guards.
79
+ const tick = () => runPollTick({
80
+ fetch: () => fnRef.current(),
81
+ guard,
82
+ isAlive: () => alive && !stopped,
83
+ isHidden,
84
+ onSuccess: (v) => {
85
+ failures = 0;
86
+ setData(v);
87
+ setError(undefined);
88
+ },
89
+ onFailure: (message) => {
90
+ failures += 1;
91
+ setError(message);
92
+ },
93
+ onSettled: (applied) => {
94
+ if (applied)
95
+ setLoading(false);
96
+ schedule();
97
+ },
98
+ });
99
+ const onVisibility = () => {
100
+ if (!alive || stopped)
101
+ return;
102
+ if (timer) {
103
+ clearTimeout(timer);
104
+ timer = undefined;
105
+ }
106
+ if (!isHidden())
107
+ void tick(); // immediate refresh on visibility return
108
+ };
109
+ if (typeof document !== "undefined")
110
+ document.addEventListener("visibilitychange", onVisibility);
111
+ setLoading(true);
112
+ void tick();
113
+ return () => {
114
+ alive = false;
115
+ stopped = true;
116
+ guard.invalidate(); // r3-F1: teardown supersedes the epoch for legacy (no-epochKey) callers too
117
+ if (timer)
118
+ clearTimeout(timer);
119
+ if (typeof document !== "undefined")
120
+ document.removeEventListener("visibilitychange", onVisibility);
121
+ };
122
+ // eslint-disable-next-line react-hooks/exhaustive-deps
123
+ }, [enabled, ms, nonce, epochKey, ...deps]);
124
+ return { data, error, loading, refresh: () => setNonce((n) => n + 1) };
125
+ }
126
+ /** Run `fn` on a fixed interval while `enabled` (no return value; for fire-and-forget effects). */
127
+ export function useInterval(fn, ms, enabled = true) {
128
+ const fnRef = useRef(fn);
129
+ fnRef.current = fn;
130
+ useEffect(() => {
131
+ if (!enabled)
132
+ return;
133
+ const id = setInterval(() => fnRef.current(), ms);
134
+ return () => clearInterval(id);
135
+ }, [ms, enabled]);
136
+ }
@@ -0,0 +1,15 @@
1
+ export { Button, buttonClass, type ButtonProps, type BtnVariant, type BtnState } from "./Button.js";
2
+ export { Input, Toggle, Checkbox, type InputProps, type ToggleProps, type CheckboxProps } from "./Input.js";
3
+ export { MaskedSecretInput, type MaskedSecretInputProps } from "./MaskedSecretInput.js";
4
+ export { EmptyState, type EmptyStateProps } from "./EmptyState.js";
5
+ export { Scrim, ConfirmDialog, typedNameMatches, BULLET_ICON, type DialogBullet, type ConfirmDialogProps, } from "./ConfirmDialog.js";
6
+ export { Toast, ToastProvider, composeToastText, TOAST_SEP, type ToastProps, } from "./Toast.js";
7
+ export { ToastContext, useToast, type ToastStatus, type ToastSpec, type ToastBus, } from "./toast-context.js";
8
+ export { usePoll, useInterval, shouldResetEpoch, nextPollDelay, makePollEpochGuard, runPollTick, POLL_JITTER_RATIO, POLL_BACKOFF_CAP_MS, type PollResult, type PollEpochGuard, type PollTickIO, } from "./hooks.js";
9
+ export { Chip, chipClass, type ChipProps, type ChipTone } from "./Chip.js";
10
+ export { Card, type CardProps } from "./Card.js";
11
+ export { Avatar, type AvatarProps } from "./Avatar.js";
12
+ export { StatusLine, statusLineClass, type StatusLineProps, type StatusTone } from "./StatusLine.js";
13
+ export { SearchInput, type SearchInputProps } from "./SearchInput.js";
14
+ export { Banner, bannerClass, BANNER_ICON, type BannerProps, type BannerTone } from "./Banner.js";
15
+ export { Gauge, gaugeTone, gaugeGeom, type GaugeProps, type GaugeGeom, type Tone } from "./Gauge.js";
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ // @mythicalos/react-ui — barrel. React twin of packages/preact-ui/src/index.ts — the SAME export
2
+ // surface (components, hooks, re-exported core symbols, Props types) so the two bindings can never
3
+ // drift. The two intentional prop-name deltas (Chip/Card/Avatar's `class` → `className`) live on
4
+ // the component types themselves, not the barrel — see each file's header comment and the task
5
+ // report. Serve `@mythicalos/ui-core/styles.css` (after `@mythicalos/tokens`) so these components'
6
+ // classes resolve — this package ships no CSS of its own.
7
+ export { Button, buttonClass } from "./Button.js";
8
+ export { Input, Toggle, Checkbox } from "./Input.js";
9
+ export { MaskedSecretInput } from "./MaskedSecretInput.js";
10
+ export { EmptyState } from "./EmptyState.js";
11
+ export { Scrim, ConfirmDialog, typedNameMatches, BULLET_ICON, } from "./ConfirmDialog.js";
12
+ export { Toast, ToastProvider, composeToastText, TOAST_SEP, } from "./Toast.js";
13
+ export { ToastContext, useToast, } from "./toast-context.js";
14
+ export { usePoll, useInterval, shouldResetEpoch, nextPollDelay, makePollEpochGuard, runPollTick, POLL_JITTER_RATIO, POLL_BACKOFF_CAP_MS, } from "./hooks.js";
15
+ // ── the 7 new atoms (design-export's "proposed additions" — Task 6 graduation) ──
16
+ export { Chip, chipClass } from "./Chip.js";
17
+ export { Card } from "./Card.js";
18
+ export { Avatar } from "./Avatar.js";
19
+ export { StatusLine, statusLineClass } from "./StatusLine.js";
20
+ export { SearchInput } from "./SearchInput.js";
21
+ export { Banner, bannerClass, BANNER_ICON } from "./Banner.js";
22
+ export { Gauge, gaugeTone, gaugeGeom } from "./Gauge.js";
@@ -0,0 +1,4 @@
1
+ import { type ToastStatus, type ToastSpec, type ToastBus } from "@mythicalos/ui-core/logic";
2
+ export { type ToastStatus, type ToastSpec, type ToastBus };
3
+ export declare const ToastContext: import("react").Context<ToastBus>;
4
+ export declare function useToast(): ToastBus;
@@ -0,0 +1,14 @@
1
+ // @mythicalos/react-ui — the toast bus context (decoupled from any app API client). Shared with the
2
+ // Preact sibling so both family UIs can push toasts through one implementation shape. The app
3
+ // provides <ToastProvider> at its root and components push via useToast().
4
+ //
5
+ // React twin of packages/preact-ui/src/toast-context.ts. `ToastStatus`/`ToastSpec`/`ToastBus` are
6
+ // imported from `@mythicalos/ui-core` (framework-agnostic); only the `createContext`/`useContext`
7
+ // wiring differs — `react` instead of `preact`/`preact/hooks`.
8
+ import { createContext, useContext } from "react";
9
+ import {} from "@mythicalos/ui-core/logic";
10
+ export {};
11
+ export const ToastContext = createContext({ push: () => { } });
12
+ export function useToast() {
13
+ return useContext(ToastContext);
14
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@mythicalos/react-ui",
3
+ "version": "0.1.0",
4
+ "description": "mythicalOS thin React bindings over @mythicalos/ui-core — Button, Input/Toggle/Checkbox, MaskedSecretInput, EmptyState, ConfirmDialog/Scrim, Toast/ToastProvider, Chip, Card, Avatar, StatusLine, SearchInput, Banner, Gauge, and the usePoll/useInterval hooks. Render + framework wiring only — every class string, poll-scheduling math, glyph map, and text composition is derived by @mythicalos/ui-core so this binding and its Preact sibling can never drift. Apache-2.0.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/mythicalOS/mythical-ui.git",
14
+ "directory": "packages/react-ui"
15
+ },
16
+ "homepage": "https://github.com/mythicalOS/mythical-ui#packages",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": ["dist", "README.md", "NOTICE"],
24
+ "peerDependencies": {
25
+ "react": "^18.2.0 || ^19",
26
+ "react-dom": "^18.2.0 || ^19",
27
+ "@mythicalos/tokens": "^0.5.11"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "@mythicalos/tokens": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "@mythicalos/ui-core": "^0.1.0"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.build.json",
39
+ "typecheck": "tsc --noEmit",
40
+ "test": "bun test",
41
+ "prepublishOnly": "bun run build && bun test && bun run typecheck"
42
+ },
43
+ "devDependencies": {
44
+ "react": "^19.2.8",
45
+ "react-dom": "^19.2.8",
46
+ "@types/react": "^19",
47
+ "@types/react-dom": "^19",
48
+ "typescript": "^5"
49
+ }
50
+ }