@mercury-fx/effector 2.4.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 +21 -0
- package/README.md +36 -0
- package/dist/bridge.d.ts +31 -0
- package/dist/channel.d.ts +56 -0
- package/dist/cooldown.d.ts +11 -0
- package/dist/disclosure.d.ts +52 -0
- package/dist/form.d.ts +50 -0
- package/dist/formatter.d.ts +39 -0
- package/dist/index.d.ts +15 -0
- package/dist/mercury-effector.js +314 -0
- package/dist/resource.d.ts +28 -0
- package/dist/strength.d.ts +21 -0
- package/dist/theme.d.ts +8 -0
- package/dist/toast.d.ts +33 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jonnify <mercury@jonnify.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @mercury-fx/effector
|
|
2
|
+
|
|
3
|
+
Effector-backed state adapters for
|
|
4
|
+
[`@mercury-fx/ui`](https://www.npmjs.com/package/@mercury-fx/ui): a theme store,
|
|
5
|
+
a toast model + `<Toaster/>`, a `createForm` field factory, password strength,
|
|
6
|
+
and a cooldown timer. State lives **outside** React, so the components stay
|
|
7
|
+
presentational.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i @mercury-fx/effector
|
|
13
|
+
npm i @mercury-fx/ui react effector effector-react # peer dependencies
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Use
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import "@mercury-fx/ui/styles.css";
|
|
20
|
+
import { Toaster, toast, useTheme, toggleTheme } from "@mercury-fx/effector";
|
|
21
|
+
|
|
22
|
+
function App() {
|
|
23
|
+
const theme = useTheme(); // "light" | "dark"
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<button onClick={() => toggleTheme()}>{theme}</button>
|
|
27
|
+
<button onClick={() => toast.success("Saved")}>Save</button>
|
|
28
|
+
<Toaster />
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
MIT
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bridgeChannel — lift a host bridge into the `ChannelLike` contract.
|
|
3
|
+
*
|
|
4
|
+
* A LiveView-style host hands an embedded island a bridge — outbound
|
|
5
|
+
* `pushEvent(event, payload)` plus an `onServerEvent(cb)` fan-out of named
|
|
6
|
+
* server frames — rather than a raw Phoenix channel. Both transports terminate
|
|
7
|
+
* the same server-side PubSub, so this adapter maps the bridge onto
|
|
8
|
+
* `ChannelLike` and `createChannel().bind(...)` (and every model built over
|
|
9
|
+
* it) runs unchanged on either transport.
|
|
10
|
+
*
|
|
11
|
+
* Semantics, stated honestly:
|
|
12
|
+
* - `join()` resolves "ok" synchronously with `joinReply` — the host delivered
|
|
13
|
+
* the initial state before the island mounted (the moral twin of a Phoenix
|
|
14
|
+
* join reply), so there is nothing to wait for and no way to fail.
|
|
15
|
+
* - `push()` forwards to `bridge.pushEvent` and acks "ok" immediately —
|
|
16
|
+
* fire-and-forget; a host reply path, where one exists, is a bridge-surface
|
|
17
|
+
* extension, not this adapter's invention.
|
|
18
|
+
* - `onClose`/`onError` are no-ops: the host socket owns lifecycle and
|
|
19
|
+
* reconnection; the bridge never surfaces them.
|
|
20
|
+
*/
|
|
21
|
+
import type { ChannelLike } from "./channel";
|
|
22
|
+
/** The host-bridge shape an embedding page hands its island. */
|
|
23
|
+
export interface HostBridge {
|
|
24
|
+
pushEvent(event: string, payload: unknown): void;
|
|
25
|
+
onServerEvent(cb: (name: string, payload: unknown) => void): () => void;
|
|
26
|
+
}
|
|
27
|
+
export interface BridgeChannelOptions {
|
|
28
|
+
/** Delivered as the join "ok" reply (the host's initial state). */
|
|
29
|
+
joinReply?: unknown;
|
|
30
|
+
}
|
|
31
|
+
export declare function bridgeChannel(bridge: HostBridge, opts?: BridgeChannelOptions): ChannelLike;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createChannel — an Effector plug for a Phoenix channel.
|
|
3
|
+
*
|
|
4
|
+
* The state lives outside React, matching the other Mercury plugs: a channel's
|
|
5
|
+
* lifecycle (joining/joined/errored/closed), its inbound messages, and its
|
|
6
|
+
* outbound pushes are Effector units. The plug is structurally typed against a
|
|
7
|
+
* Phoenix channel (`ChannelLike`), so this package takes no `@echo/phoenix`
|
|
8
|
+
* dependency — any object with the channel shape plugs in.
|
|
9
|
+
*
|
|
10
|
+
* "Hot plug": `bind(channel, inbound)` wires a live channel into the model at
|
|
11
|
+
* runtime (join + the inbound listeners) and returns an unbind that removes the
|
|
12
|
+
* listeners and detaches. Bind a fresh channel when the room changes; unbind on
|
|
13
|
+
* teardown. A React status hook is provided for convenience; components stay
|
|
14
|
+
* presentational.
|
|
15
|
+
*/
|
|
16
|
+
import { type Store } from "effector";
|
|
17
|
+
/** The receive-chain a Phoenix push returns. */
|
|
18
|
+
export interface PushLike {
|
|
19
|
+
receive(status: string, callback: (response?: unknown) => void): PushLike;
|
|
20
|
+
}
|
|
21
|
+
/** The subset of a Phoenix channel this plug uses. A real `@echo/phoenix`
|
|
22
|
+
* `Channel` satisfies it structurally. */
|
|
23
|
+
export interface ChannelLike {
|
|
24
|
+
join(timeout?: number): PushLike;
|
|
25
|
+
on(event: string, callback: (payload?: unknown) => void): number;
|
|
26
|
+
off(event: string, ref?: number): void;
|
|
27
|
+
push(event: string, payload: object, timeout?: number): PushLike;
|
|
28
|
+
onClose(callback: (payload?: unknown) => void): void;
|
|
29
|
+
onError(callback: (reason?: unknown) => void): number;
|
|
30
|
+
leave(timeout?: number): PushLike;
|
|
31
|
+
}
|
|
32
|
+
export type ChannelStatus = "idle" | "joining" | "joined" | "errored" | "closed";
|
|
33
|
+
/** A single inbound frame: the event name and its payload. */
|
|
34
|
+
export interface ChannelMessage {
|
|
35
|
+
event: string;
|
|
36
|
+
payload: unknown;
|
|
37
|
+
}
|
|
38
|
+
export interface ChannelModel {
|
|
39
|
+
/** Reactive lifecycle status. */
|
|
40
|
+
$status: Store<ChannelStatus>;
|
|
41
|
+
/** The last join/close/error reason, or null. */
|
|
42
|
+
$error: Store<unknown | null>;
|
|
43
|
+
/** Fires with the join reply payload once the channel joins. */
|
|
44
|
+
joined: import("effector").Event<unknown>;
|
|
45
|
+
/** Every inbound frame for a bound event name. */
|
|
46
|
+
message: import("effector").Event<ChannelMessage>;
|
|
47
|
+
/** Fire-and-forget outbound push (routed to the bound channel). */
|
|
48
|
+
push: (event: string, payload?: object) => void;
|
|
49
|
+
/** Awaitable outbound push; resolves on "ok", rejects on "error"/"timeout". */
|
|
50
|
+
pushAsync: (event: string, payload?: object) => Promise<unknown>;
|
|
51
|
+
/** Plug a live channel in: join + listen. Returns an unbind. */
|
|
52
|
+
bind: (channel: ChannelLike, inbound: readonly string[]) => () => void;
|
|
53
|
+
/** React hook for the status store. */
|
|
54
|
+
useStatus: () => ChannelStatus;
|
|
55
|
+
}
|
|
56
|
+
export declare function createChannel(): ChannelModel;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createCooldown — a countdown timer as Effector state, for "resend in 30s"
|
|
3
|
+
* style rate-limit affordances. The interval lives outside React; components
|
|
4
|
+
* read `useCooldown()` and call `start(seconds)`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createCooldown(): {
|
|
7
|
+
$remaining: import("effector").StoreWritable<number>;
|
|
8
|
+
start: (seconds: number) => void;
|
|
9
|
+
stop: () => void;
|
|
10
|
+
useCooldown: () => number;
|
|
11
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mercury-fx/effector — the overlay disclosure bridge (mx.7.4 §E).
|
|
3
|
+
*
|
|
4
|
+
* The optional driver for the presentational overlays (`Dialog`/`AlertDialog`/
|
|
5
|
+
* `Popover`). It PRODUCES their controlled state and manages the cross-overlay
|
|
6
|
+
* concern a single component cannot — a global body-scroll-lock — exactly as
|
|
7
|
+
* `theme`/`toast` drive their components from the outside. `@mercury-fx/ui` gains
|
|
8
|
+
* NO dependency on this: the arrow is effector -> ui only (INV-EFFECTOR).
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* createDisclosure — a controlled open/close model for one overlay instance, as
|
|
12
|
+
* Effector state (the `createCooldown` factory idiom). Each call returns an
|
|
13
|
+
* INDEPENDENT model; a consumer wires `useOpen()` into an overlay's `open` prop
|
|
14
|
+
* and `close`/`open`/`toggle` into its `onClose`/`onOpenChange`. The overlay
|
|
15
|
+
* stays the source of truth for its DOM; the model is the source of truth for
|
|
16
|
+
* whether it is open.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createDisclosure(opts?: {
|
|
19
|
+
defaultOpen?: boolean;
|
|
20
|
+
}): {
|
|
21
|
+
$open: import("effector").StoreWritable<boolean>;
|
|
22
|
+
open: import("effector").EventCallable<void>;
|
|
23
|
+
close: import("effector").EventCallable<void>;
|
|
24
|
+
toggle: import("effector").EventCallable<void>;
|
|
25
|
+
useOpen: () => boolean;
|
|
26
|
+
};
|
|
27
|
+
/** The id of one open overlay in the global stack. */
|
|
28
|
+
export type OverlayId = string;
|
|
29
|
+
/** Register an overlay's id when it opens (modal consumers only). */
|
|
30
|
+
export declare const pushOverlay: import("effector").EventCallable<string>;
|
|
31
|
+
/** Unregister an overlay's id when it closes. */
|
|
32
|
+
export declare const popOverlay: import("effector").EventCallable<string>;
|
|
33
|
+
/**
|
|
34
|
+
* $openOverlays — the LIFO stack of open overlay ids. Push appends (the last
|
|
35
|
+
* opened is topmost, for `Escape`-topmost + z-ordering); pop removes the most
|
|
36
|
+
* recent occurrence of the id (defensive against a doubled push).
|
|
37
|
+
*/
|
|
38
|
+
export declare const $openOverlays: import("effector").StoreWritable<string[]>;
|
|
39
|
+
/** True while any overlay is open. */
|
|
40
|
+
export declare const $anyOverlayOpen: import("effector").Store<boolean>;
|
|
41
|
+
/** The topmost (most-recently opened) overlay id, or null when none is open. */
|
|
42
|
+
export declare const $topOverlay: import("effector").Store<string | null>;
|
|
43
|
+
/** Reactive "is any overlay open" for components. */
|
|
44
|
+
export declare const useAnyOverlayOpen: () => boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Call once at app start to lock body scroll while any overlay is open, padding-
|
|
47
|
+
* compensated so the layout does not shift. Idempotent (the `initTheme` idiom);
|
|
48
|
+
* SSR-guarded. Opt-in: the singleton does NOT auto-fire from `createDisclosure` —
|
|
49
|
+
* a MODAL consumer calls `pushOverlay`/`popOverlay`; a non-modal `Popover` does
|
|
50
|
+
* not, so modals lock scroll and popovers do not.
|
|
51
|
+
*/
|
|
52
|
+
export declare function initOverlayLock(): void;
|
package/dist/form.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type FormErrors<V> = Partial<Record<keyof V, string>>;
|
|
2
|
+
export interface FormConfig<V> {
|
|
3
|
+
initialValues: V;
|
|
4
|
+
/** Pure validator: return a map of field → message for invalid fields. */
|
|
5
|
+
validate?: (values: V) => FormErrors<V>;
|
|
6
|
+
/**
|
|
7
|
+
* Optional async submit handler. Runs only when the form is valid; while it
|
|
8
|
+
* is pending `$submitting` (and `useForm().submitting`) is true.
|
|
9
|
+
*/
|
|
10
|
+
onSubmit?: (values: V) => void | Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export interface FieldBinding<T> {
|
|
13
|
+
value: T;
|
|
14
|
+
error: string | undefined;
|
|
15
|
+
onChange: (value: T) => void;
|
|
16
|
+
onBlur: () => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* createForm — an Effector-backed form model + React hooks.
|
|
20
|
+
* Components stay presentational; the stores live outside React.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createForm<V extends Record<string, unknown>>(config: FormConfig<V>): {
|
|
23
|
+
$values: import("effector").StoreWritable<V>;
|
|
24
|
+
$errors: import("effector").Store<Partial<Record<keyof V, string>>>;
|
|
25
|
+
$touched: import("effector").StoreWritable<Partial<Record<keyof V, boolean>>>;
|
|
26
|
+
$isValid: import("effector").Store<boolean>;
|
|
27
|
+
$submitting: import("effector").StoreWritable<boolean>;
|
|
28
|
+
changed: import("effector").EventCallable<{
|
|
29
|
+
name: keyof V;
|
|
30
|
+
value: V[keyof V];
|
|
31
|
+
}>;
|
|
32
|
+
blurred: import("effector").EventCallable<keyof V>;
|
|
33
|
+
submitted: import("effector").EventCallable<void>;
|
|
34
|
+
reset: import("effector").EventCallable<void>;
|
|
35
|
+
submit: () => Promise<boolean>;
|
|
36
|
+
useField: <K extends keyof V>(name: K) => FieldBinding<V[K]>;
|
|
37
|
+
useForm: () => {
|
|
38
|
+
values: V;
|
|
39
|
+
errors: Partial<Record<keyof V, string>>;
|
|
40
|
+
touched: Partial<Record<keyof V, boolean>>;
|
|
41
|
+
isValid: boolean;
|
|
42
|
+
submitting: boolean;
|
|
43
|
+
setField: <K extends keyof V>(name: K, value: V[K]) => {
|
|
44
|
+
name: keyof V;
|
|
45
|
+
value: V[keyof V];
|
|
46
|
+
};
|
|
47
|
+
submit: () => Promise<boolean>;
|
|
48
|
+
reset: () => void;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type EventCallable, type Store } from "effector";
|
|
2
|
+
import { type Formatter, type MonthFormat, type YearFormat } from "@mercury-fx/ui";
|
|
3
|
+
/**
|
|
4
|
+
* createFormatterModel — an Effector-backed wrapper around `@mercury-fx/ui`'s
|
|
5
|
+
* `createFormatter`. The locale and month/year styles live in stores; the
|
|
6
|
+
* formatter reads them live, so changing any of them re-renders every component
|
|
7
|
+
* that called `useFormatter()`. Components stay presentational; this wires state.
|
|
8
|
+
*/
|
|
9
|
+
export interface FormatterModelOptions {
|
|
10
|
+
/** Initial BCP-47 locale. Defaults to the browser locale, else `"en-US"`. */
|
|
11
|
+
locale?: string;
|
|
12
|
+
/** Initial month rendering for `fullMonthAndYear`. Defaults to `"long"`. */
|
|
13
|
+
monthFormat?: MonthFormat;
|
|
14
|
+
/** Initial year rendering for `fullMonthAndYear`. Defaults to `"numeric"`. */
|
|
15
|
+
yearFormat?: YearFormat;
|
|
16
|
+
}
|
|
17
|
+
export interface FormatterModel {
|
|
18
|
+
/** Active locale store. */
|
|
19
|
+
$locale: Store<string>;
|
|
20
|
+
/** Active month-format store. */
|
|
21
|
+
$monthFormat: Store<MonthFormat>;
|
|
22
|
+
/** Active year-format store. */
|
|
23
|
+
$yearFormat: Store<YearFormat>;
|
|
24
|
+
/** Set the active locale. */
|
|
25
|
+
setLocale: EventCallable<string>;
|
|
26
|
+
/** Set how months render in `fullMonthAndYear`. */
|
|
27
|
+
setMonthFormat: EventCallable<MonthFormat>;
|
|
28
|
+
/** Set how years render in `fullMonthAndYear`. */
|
|
29
|
+
setYearFormat: EventCallable<YearFormat>;
|
|
30
|
+
/**
|
|
31
|
+
* The formatter instance. Stable across renders — its methods read the stores
|
|
32
|
+
* live. Use directly outside React; inside React prefer {@link FormatterModel.useFormatter}
|
|
33
|
+
* so the component re-renders when inputs change.
|
|
34
|
+
*/
|
|
35
|
+
formatter: Formatter;
|
|
36
|
+
/** React hook: subscribes to the stores and returns the (stable) formatter. */
|
|
37
|
+
useFormatter: () => Formatter;
|
|
38
|
+
}
|
|
39
|
+
export declare function createFormatterModel(opts?: FormatterModelOptions): FormatterModel;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mercury-fx/effector — pluggable Effector state for Mercury.
|
|
3
|
+
* Theme store, toast model + <Toaster/>, and a createForm factory.
|
|
4
|
+
* Mercury UI components stay presentational; this wires the state.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./theme";
|
|
7
|
+
export * from "./toast";
|
|
8
|
+
export * from "./form";
|
|
9
|
+
export * from "./resource";
|
|
10
|
+
export * from "./strength";
|
|
11
|
+
export * from "./cooldown";
|
|
12
|
+
export * from "./formatter";
|
|
13
|
+
export * from "./channel";
|
|
14
|
+
export * from "./bridge";
|
|
15
|
+
export * from "./disclosure";
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { createEvent as r, createStore as m, createEffect as D, sample as M } from "effector";
|
|
2
|
+
import { useUnit as b } from "effector-react";
|
|
3
|
+
import { jsx as R } from "react/jsx-runtime";
|
|
4
|
+
import { Alert as X, createFormatter as H } from "@mercury-fx/ui";
|
|
5
|
+
const Y = "mercury-theme";
|
|
6
|
+
function J() {
|
|
7
|
+
if (typeof localStorage < "u") {
|
|
8
|
+
const e = localStorage.getItem(Y);
|
|
9
|
+
if (e === "light" || e === "dark") return e;
|
|
10
|
+
}
|
|
11
|
+
return "light";
|
|
12
|
+
}
|
|
13
|
+
const N = r(), Q = r(), _ = m(J()).on(N, (e, t) => t).on(Q, (e) => e === "dark" ? "light" : "dark");
|
|
14
|
+
function L(e) {
|
|
15
|
+
if (typeof document < "u") {
|
|
16
|
+
const t = document.documentElement;
|
|
17
|
+
t.classList.remove("light-theme", "dark-theme"), t.classList.add(e === "dark" ? "dark-theme" : "light-theme");
|
|
18
|
+
}
|
|
19
|
+
if (typeof localStorage < "u")
|
|
20
|
+
try {
|
|
21
|
+
localStorage.setItem(Y, e);
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
let P = !1;
|
|
26
|
+
function fe() {
|
|
27
|
+
P || (P = !0, L(_.getState()), _.watch(L));
|
|
28
|
+
}
|
|
29
|
+
const me = () => b(_);
|
|
30
|
+
let ee = 0;
|
|
31
|
+
const k = r(), A = r(), te = r(), Z = k.map((e) => {
|
|
32
|
+
const t = typeof e == "string" ? { message: e } : e;
|
|
33
|
+
return {
|
|
34
|
+
id: ++ee,
|
|
35
|
+
tone: t.tone ?? "info",
|
|
36
|
+
title: t.title,
|
|
37
|
+
message: t.message,
|
|
38
|
+
duration: t.duration ?? 4e3
|
|
39
|
+
};
|
|
40
|
+
}), ne = m([]).on(Z, (e, t) => [...e, t]).on(A, (e, t) => e.filter((n) => n.id !== t)).reset(te), q = D(
|
|
41
|
+
(e) => new Promise((t) => setTimeout(() => t(e.id), e.duration))
|
|
42
|
+
);
|
|
43
|
+
M({ clock: Z.filter({ fn: (e) => e.duration > 0 }), target: q });
|
|
44
|
+
M({ clock: q.doneData, target: A });
|
|
45
|
+
const ge = {
|
|
46
|
+
show: (e) => k(e),
|
|
47
|
+
info: (e, t = {}) => k({ ...t, tone: "info", message: e }),
|
|
48
|
+
success: (e, t = {}) => k({ ...t, tone: "success", message: e }),
|
|
49
|
+
warning: (e, t = {}) => k({ ...t, tone: "warning", message: e }),
|
|
50
|
+
error: (e, t = {}) => k({ ...t, tone: "danger", message: e })
|
|
51
|
+
}, oe = () => b(ne), re = {
|
|
52
|
+
"top-end": { top: 16, right: 16, alignItems: "flex-end" },
|
|
53
|
+
"bottom-end": { bottom: 16, right: 16, alignItems: "flex-end" },
|
|
54
|
+
"bottom-center": { bottom: 16, left: "50%", transform: "translateX(-50%)", alignItems: "center" }
|
|
55
|
+
};
|
|
56
|
+
function he({ position: e = "bottom-end" }) {
|
|
57
|
+
const t = oe();
|
|
58
|
+
return /* @__PURE__ */ R(
|
|
59
|
+
"div",
|
|
60
|
+
{
|
|
61
|
+
style: {
|
|
62
|
+
position: "fixed",
|
|
63
|
+
zIndex: 200,
|
|
64
|
+
display: "flex",
|
|
65
|
+
flexDirection: "column",
|
|
66
|
+
gap: 10,
|
|
67
|
+
pointerEvents: "none",
|
|
68
|
+
...re[e]
|
|
69
|
+
},
|
|
70
|
+
children: t.map((n) => /* @__PURE__ */ R("div", { style: { pointerEvents: "auto", width: 360, maxWidth: "calc(100vw - 32px)", boxShadow: "var(--shadow-300)", borderRadius: "var(--radius-12)" }, children: /* @__PURE__ */ R(X, { tone: n.tone, title: n.title, dismissible: !0, onDismiss: () => A(n.id), children: n.message }) }, n.id))
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
function pe(e) {
|
|
75
|
+
const { initialValues: t, validate: n, onSubmit: s } = e, o = Object.keys(t), i = r(), l = r(), g = r(), p = r(), u = r(), d = r(), v = m(t).on(i, (a, { name: S, value: F }) => ({ ...a, [S]: F })).reset(d), y = m({}).on(l, (a, S) => ({ ...a, [S]: !0 })).on(g, () => Object.fromEntries(o.map((a) => [a, !0]))).reset(d), w = v.map((a) => n ? n(a) : {}), x = w.map((a) => Object.keys(a).length === 0), c = m(!1).on(p, () => !0).on(u, () => !1).reset(d);
|
|
76
|
+
async function f() {
|
|
77
|
+
if (g(), !(!n || Object.keys(n(v.getState())).length === 0)) return !1;
|
|
78
|
+
if (s) {
|
|
79
|
+
p();
|
|
80
|
+
try {
|
|
81
|
+
await s(v.getState());
|
|
82
|
+
} finally {
|
|
83
|
+
u();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return !0;
|
|
87
|
+
}
|
|
88
|
+
function $(a) {
|
|
89
|
+
const [S, F, O] = b([v, w, y]);
|
|
90
|
+
return {
|
|
91
|
+
value: S[a],
|
|
92
|
+
error: O[a] ? F[a] : void 0,
|
|
93
|
+
onChange: (j) => i({ name: a, value: j }),
|
|
94
|
+
onBlur: () => l(a)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function h() {
|
|
98
|
+
const [a, S, F, O, j] = b([v, w, y, x, c]);
|
|
99
|
+
return {
|
|
100
|
+
values: a,
|
|
101
|
+
errors: S,
|
|
102
|
+
touched: F,
|
|
103
|
+
isValid: O,
|
|
104
|
+
submitting: j,
|
|
105
|
+
setField: (G, K) => i({ name: G, value: K }),
|
|
106
|
+
submit: f,
|
|
107
|
+
reset: () => d()
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return { $values: v, $errors: w, $touched: y, $isValid: x, $submitting: c, changed: i, blurred: l, submitted: g, reset: d, submit: f, useField: $, useForm: h };
|
|
111
|
+
}
|
|
112
|
+
function ve(e, t = {}) {
|
|
113
|
+
const n = D(() => e()), s = r(), o = m(t.initialData ?? null).on(n.doneData, (v, y) => y).reset(s), i = m(null).on(n.failData, (v, y) => y instanceof Error ? y : new Error(String(y))).reset(n).reset(s), l = n.pending, g = m("idle").on(n, () => "loading").on(n.done, () => "success").on(n.fail, () => "error").reset(s), p = () => n(), u = () => n();
|
|
114
|
+
function d() {
|
|
115
|
+
const [v, y, w, x] = b([o, i, l, g]);
|
|
116
|
+
return { data: v, error: y, loading: w, status: x, load: p, refresh: u, reset: () => s() };
|
|
117
|
+
}
|
|
118
|
+
return { $data: o, $error: i, $loading: l, $status: g, load: p, refresh: u, reset: s, useResource: d };
|
|
119
|
+
}
|
|
120
|
+
function ye(e) {
|
|
121
|
+
const t = {
|
|
122
|
+
length: e.length >= 8,
|
|
123
|
+
mixedCase: /[a-z]/.test(e) && /[A-Z]/.test(e),
|
|
124
|
+
number: /[0-9]/.test(e),
|
|
125
|
+
symbol: /[^a-zA-Z0-9]/.test(e)
|
|
126
|
+
};
|
|
127
|
+
let n = 0;
|
|
128
|
+
t.length && (n += 34), t.mixedCase && (n += 33), (t.number || t.symbol) && (n += 33), n = Math.min(100, n);
|
|
129
|
+
const s = e.length === 0 ? "" : n < 40 ? "Weak" : n < 75 ? "Fair" : "Strong", o = n < 40 ? "negative" : n < 75 ? "caution" : "positive";
|
|
130
|
+
return { score: n, label: s, variant: o, rules: t };
|
|
131
|
+
}
|
|
132
|
+
function be() {
|
|
133
|
+
const e = r(), t = r(), n = r(), s = m(0).on(e, (u, d) => Math.max(0, Math.floor(d))).on(t, (u) => Math.max(0, u - 1)).on(n, () => 0);
|
|
134
|
+
let o = null;
|
|
135
|
+
function i() {
|
|
136
|
+
o !== null && (clearInterval(o), o = null);
|
|
137
|
+
}
|
|
138
|
+
function l(u) {
|
|
139
|
+
i(), e(u), o = setInterval(() => {
|
|
140
|
+
t(), s.getState() <= 0 && i();
|
|
141
|
+
}, 1e3);
|
|
142
|
+
}
|
|
143
|
+
function g() {
|
|
144
|
+
i(), n();
|
|
145
|
+
}
|
|
146
|
+
return { $remaining: s, start: l, stop: g, useCooldown: () => b(s) };
|
|
147
|
+
}
|
|
148
|
+
function se() {
|
|
149
|
+
return typeof navigator < "u" && navigator.language ? navigator.language : "en-US";
|
|
150
|
+
}
|
|
151
|
+
function T(e) {
|
|
152
|
+
return {
|
|
153
|
+
get current() {
|
|
154
|
+
return e.getState();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function Se(e = {}) {
|
|
159
|
+
const t = r(), n = r(), s = r(), o = m(e.locale ?? se()).on(t, (u, d) => d), i = m(e.monthFormat ?? "long").on(
|
|
160
|
+
n,
|
|
161
|
+
(u, d) => d
|
|
162
|
+
), l = m(e.yearFormat ?? "numeric").on(
|
|
163
|
+
s,
|
|
164
|
+
(u, d) => d
|
|
165
|
+
), g = H({
|
|
166
|
+
locale: T(o),
|
|
167
|
+
monthFormat: T(i),
|
|
168
|
+
yearFormat: T(l)
|
|
169
|
+
});
|
|
170
|
+
function p() {
|
|
171
|
+
return b([o, i, l]), g;
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
$locale: o,
|
|
175
|
+
$monthFormat: i,
|
|
176
|
+
$yearFormat: l,
|
|
177
|
+
setLocale: t,
|
|
178
|
+
setMonthFormat: n,
|
|
179
|
+
setYearFormat: s,
|
|
180
|
+
formatter: g,
|
|
181
|
+
useFormatter: p
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function we() {
|
|
185
|
+
const e = r(), t = r(), n = r(), s = r(), o = r(), i = r(), l = r(), g = r(), p = m(null).on(e, (c, f) => f).reset(t), u = m("idle").on(n, () => "joining").on(s, () => "joined").on(o, () => "errored").on(i, () => "closed").reset(t), d = m(null).on(o, (c, f) => f).reset([n, t]), v = D(
|
|
186
|
+
({ channel: c, event: f, payload: $ }) => new Promise((h, a) => {
|
|
187
|
+
c.push(f, $).receive("ok", h).receive("error", a).receive("timeout", () => a(new Error("push timeout")));
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
M({
|
|
191
|
+
clock: g,
|
|
192
|
+
source: p,
|
|
193
|
+
filter: (c) => c !== null,
|
|
194
|
+
fn: (c, f) => ({ channel: c, event: f.event, payload: f.payload }),
|
|
195
|
+
target: v
|
|
196
|
+
});
|
|
197
|
+
function y(c, f) {
|
|
198
|
+
e(c), n();
|
|
199
|
+
const $ = f.map((h) => {
|
|
200
|
+
const a = c.on(h, (S) => l({ event: h, payload: S }));
|
|
201
|
+
return [h, a];
|
|
202
|
+
});
|
|
203
|
+
return c.onClose(() => i()), c.onError((h) => o(h)), c.join().receive("ok", (h) => s(h)).receive("error", (h) => o(h)).receive("timeout", () => o(new Error("join timeout"))), () => {
|
|
204
|
+
for (const [h, a] of $) c.off(h, a);
|
|
205
|
+
t();
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function w(c, f = {}) {
|
|
209
|
+
g({ event: c, payload: f });
|
|
210
|
+
}
|
|
211
|
+
function x(c, f = {}) {
|
|
212
|
+
const $ = p.getState();
|
|
213
|
+
return $ === null ? Promise.reject(new Error("no channel bound")) : v({ channel: $, event: c, payload: f });
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
$status: u,
|
|
217
|
+
$error: d,
|
|
218
|
+
joined: s,
|
|
219
|
+
message: l,
|
|
220
|
+
push: w,
|
|
221
|
+
pushAsync: x,
|
|
222
|
+
bind: y,
|
|
223
|
+
useStatus: () => b(u)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function C(e) {
|
|
227
|
+
const t = {
|
|
228
|
+
receive(n, s) {
|
|
229
|
+
return n === "ok" && s(e), t;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
return t;
|
|
233
|
+
}
|
|
234
|
+
function $e(e, t = {}) {
|
|
235
|
+
const n = /* @__PURE__ */ new Map();
|
|
236
|
+
let s = 1;
|
|
237
|
+
return {
|
|
238
|
+
join: () => C(t.joinReply),
|
|
239
|
+
on(o, i) {
|
|
240
|
+
const l = s++, g = e.onServerEvent((p, u) => {
|
|
241
|
+
p === o && i(u);
|
|
242
|
+
});
|
|
243
|
+
return n.set(l, g), l;
|
|
244
|
+
},
|
|
245
|
+
off(o, i) {
|
|
246
|
+
i !== void 0 && (n.get(i)?.(), n.delete(i));
|
|
247
|
+
},
|
|
248
|
+
push(o, i) {
|
|
249
|
+
return e.pushEvent(o, i), C();
|
|
250
|
+
},
|
|
251
|
+
onClose() {
|
|
252
|
+
},
|
|
253
|
+
onError() {
|
|
254
|
+
return 0;
|
|
255
|
+
},
|
|
256
|
+
leave() {
|
|
257
|
+
for (const o of n.values()) o();
|
|
258
|
+
return n.clear(), C();
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function xe(e) {
|
|
263
|
+
const t = r(), n = r(), s = r(), o = m(e?.defaultOpen ?? !1).on(t, () => !0).on(n, () => !1).on(s, (l) => !l);
|
|
264
|
+
return { $open: o, open: t, close: n, toggle: s, useOpen: () => b(o) };
|
|
265
|
+
}
|
|
266
|
+
const ie = r(), ae = r(), B = m([]).on(ie, (e, t) => [...e, t]).on(ae, (e, t) => {
|
|
267
|
+
const n = e.lastIndexOf(t);
|
|
268
|
+
return n === -1 ? e : [...e.slice(0, n), ...e.slice(n + 1)];
|
|
269
|
+
}), I = B.map((e) => e.length > 0), ke = B.map((e) => e.at(-1) ?? null), Fe = () => b(I);
|
|
270
|
+
let W = !1, E = !1, z = "", V = "";
|
|
271
|
+
function U(e) {
|
|
272
|
+
if (typeof document > "u") return;
|
|
273
|
+
const t = document.body;
|
|
274
|
+
if (e && !E) {
|
|
275
|
+
E = !0, z = t.style.overflow, V = t.style.paddingRight;
|
|
276
|
+
const n = window.innerWidth - document.documentElement.clientWidth;
|
|
277
|
+
if (t.style.overflow = "hidden", n > 0) {
|
|
278
|
+
const s = parseFloat(getComputedStyle(t).paddingRight) || 0;
|
|
279
|
+
t.style.paddingRight = `${s + n}px`;
|
|
280
|
+
}
|
|
281
|
+
} else !e && E && (E = !1, t.style.overflow = z, t.style.paddingRight = V);
|
|
282
|
+
}
|
|
283
|
+
function Ee() {
|
|
284
|
+
W || (W = !0, !(typeof document > "u") && (U(I.getState()), I.watch(U)));
|
|
285
|
+
}
|
|
286
|
+
export {
|
|
287
|
+
I as $anyOverlayOpen,
|
|
288
|
+
B as $openOverlays,
|
|
289
|
+
_ as $theme,
|
|
290
|
+
ne as $toasts,
|
|
291
|
+
ke as $topOverlay,
|
|
292
|
+
he as Toaster,
|
|
293
|
+
$e as bridgeChannel,
|
|
294
|
+
te as clearToasts,
|
|
295
|
+
we as createChannel,
|
|
296
|
+
be as createCooldown,
|
|
297
|
+
xe as createDisclosure,
|
|
298
|
+
pe as createForm,
|
|
299
|
+
Se as createFormatterModel,
|
|
300
|
+
ve as createResource,
|
|
301
|
+
A as dismissToast,
|
|
302
|
+
Ee as initOverlayLock,
|
|
303
|
+
fe as initTheme,
|
|
304
|
+
ye as passwordStrength,
|
|
305
|
+
ae as popOverlay,
|
|
306
|
+
ie as pushOverlay,
|
|
307
|
+
N as setTheme,
|
|
308
|
+
k as showToast,
|
|
309
|
+
ge as toast,
|
|
310
|
+
Q as toggleTheme,
|
|
311
|
+
Fe as useAnyOverlayOpen,
|
|
312
|
+
me as useTheme,
|
|
313
|
+
oe as useToasts
|
|
314
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type ResourceStatus = "idle" | "loading" | "success" | "error";
|
|
2
|
+
export interface ResourceConfig<T> {
|
|
3
|
+
/** Seed value shown before the first load. */
|
|
4
|
+
initialData?: T;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* createResource — an Effector-backed async read model + a React hook (the read
|
|
8
|
+
* twin of createForm). One effect wraps the fetcher; the stores derive from it.
|
|
9
|
+
* State lives outside React; components stay presentational.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createResource<T>(fetcher: () => Promise<T>, config?: ResourceConfig<T>): {
|
|
12
|
+
$data: import("effector").StoreWritable<T | null>;
|
|
13
|
+
$error: import("effector").StoreWritable<Error | null>;
|
|
14
|
+
$loading: import("effector").Store<boolean>;
|
|
15
|
+
$status: import("effector").StoreWritable<ResourceStatus>;
|
|
16
|
+
load: () => Promise<T>;
|
|
17
|
+
refresh: () => Promise<T>;
|
|
18
|
+
reset: import("effector").EventCallable<void>;
|
|
19
|
+
useResource: () => {
|
|
20
|
+
data: T | null;
|
|
21
|
+
error: Error | null;
|
|
22
|
+
loading: boolean;
|
|
23
|
+
status: ResourceStatus;
|
|
24
|
+
load: () => Promise<T>;
|
|
25
|
+
refresh: () => Promise<T>;
|
|
26
|
+
reset: () => void;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* passwordStrength — a pure scorer for password fields. Pairs with the
|
|
3
|
+
* <PasswordStrength /> meter and <Checklist /> in @mercury-fx/ui: feed `score` /
|
|
4
|
+
* `label` / `variant` to the meter and `rules` to the checklist.
|
|
5
|
+
*/
|
|
6
|
+
export type StrengthVariant = "negative" | "caution" | "positive";
|
|
7
|
+
export interface PasswordRules {
|
|
8
|
+
length: boolean;
|
|
9
|
+
mixedCase: boolean;
|
|
10
|
+
number: boolean;
|
|
11
|
+
symbol: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface PasswordStrengthResult {
|
|
14
|
+
/** 0–100. */
|
|
15
|
+
score: number;
|
|
16
|
+
/** "" while empty, else Weak / Fair / Strong. */
|
|
17
|
+
label: "" | "Weak" | "Fair" | "Strong";
|
|
18
|
+
variant: StrengthVariant;
|
|
19
|
+
rules: PasswordRules;
|
|
20
|
+
}
|
|
21
|
+
export declare function passwordStrength(password: string): PasswordStrengthResult;
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type Theme = "light" | "dark";
|
|
2
|
+
export declare const setTheme: import("effector").EventCallable<Theme>;
|
|
3
|
+
export declare const toggleTheme: import("effector").EventCallable<void>;
|
|
4
|
+
export declare const $theme: import("effector").StoreWritable<Theme>;
|
|
5
|
+
/** Call once at app start to sync <html> with the store. Idempotent. */
|
|
6
|
+
export declare function initTheme(): void;
|
|
7
|
+
/** Reactive theme value for components. */
|
|
8
|
+
export declare const useTheme: () => Theme;
|
package/dist/toast.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AlertTone } from "@mercury-fx/ui";
|
|
2
|
+
export interface ToastOptions {
|
|
3
|
+
tone?: AlertTone;
|
|
4
|
+
title?: string;
|
|
5
|
+
message?: string;
|
|
6
|
+
/** Auto-dismiss after N ms. Default 4000. Pass 0 to keep open. */
|
|
7
|
+
duration?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ToastItem {
|
|
10
|
+
id: number;
|
|
11
|
+
tone: AlertTone;
|
|
12
|
+
title?: string;
|
|
13
|
+
message?: string;
|
|
14
|
+
duration: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const showToast: import("effector").EventCallable<string | ToastOptions>;
|
|
17
|
+
export declare const dismissToast: import("effector").EventCallable<number>;
|
|
18
|
+
export declare const clearToasts: import("effector").EventCallable<void>;
|
|
19
|
+
export declare const $toasts: import("effector").StoreWritable<ToastItem[]>;
|
|
20
|
+
/** Imperative helper: `toast.success("Saved")`, `toast.error({ title, message })`. */
|
|
21
|
+
export declare const toast: {
|
|
22
|
+
show: (o: ToastOptions | string) => string | ToastOptions;
|
|
23
|
+
info: (message: string, o?: Omit<ToastOptions, "tone" | "message">) => string | ToastOptions;
|
|
24
|
+
success: (message: string, o?: Omit<ToastOptions, "tone" | "message">) => string | ToastOptions;
|
|
25
|
+
warning: (message: string, o?: Omit<ToastOptions, "tone" | "message">) => string | ToastOptions;
|
|
26
|
+
error: (message: string, o?: Omit<ToastOptions, "tone" | "message">) => string | ToastOptions;
|
|
27
|
+
};
|
|
28
|
+
export declare const useToasts: () => ToastItem[];
|
|
29
|
+
export type ToasterPosition = "top-end" | "bottom-end" | "bottom-center";
|
|
30
|
+
/** Drop once near the app root. Renders live toasts as Mercury Alerts. */
|
|
31
|
+
export declare function Toaster({ position }: {
|
|
32
|
+
position?: ToasterPosition;
|
|
33
|
+
}): import("react").JSX.Element;
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mercury-fx/effector",
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "Effector adapter for Mercury — theme store, toast model + Toaster, and a form/field factory. Components stay presentational.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/jonny-novikov/mercury-fx.git",
|
|
11
|
+
"directory": "packages/mercury-effector"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/jonny-novikov/mercury-fx/tree/master/packages/mercury-effector#readme",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"imports": {
|
|
18
|
+
"#*": "./src/*"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/mercury-effector.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"effector": ">=23",
|
|
31
|
+
"effector-react": ">=23",
|
|
32
|
+
"react": ">=18",
|
|
33
|
+
"@mercury-fx/ui": "2.4.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/react": "^19",
|
|
37
|
+
"@vitejs/plugin-react": "^4.3.3",
|
|
38
|
+
"effector": "^23.3.0",
|
|
39
|
+
"effector-react": "^23.3.0",
|
|
40
|
+
"react": "^19",
|
|
41
|
+
"typescript": "~5.9.3",
|
|
42
|
+
"vite": "^7",
|
|
43
|
+
"@mercury-fx/ui": "2.4.0",
|
|
44
|
+
"@mercury-fx/core": "2.4.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "vite build && tsc -p tsconfig.build.json",
|
|
48
|
+
"dev": "vite build --watch",
|
|
49
|
+
"typecheck": "tsc --noEmit"
|
|
50
|
+
}
|
|
51
|
+
}
|