@violice/rmu 0.2.7 → 0.2.9

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/dist/index.mjs CHANGED
@@ -1,204 +1,2 @@
1
- import React, { Fragment, createContext, useContext, useEffect, useReducer } from "react";
2
-
3
- //#region src/rmu-context.ts
4
- const RMUContext = createContext(null);
5
-
6
- //#endregion
7
- //#region src/emitter.ts
8
- const listenersByType = {};
9
- const emitter = {
10
- subscribe(type, handler) {
11
- if (!listenersByType[type]) listenersByType[type] = /* @__PURE__ */ new Set();
12
- listenersByType[type].add(handler);
13
- },
14
- unsubscribe(type, handler) {
15
- const listeners = listenersByType[type];
16
- if (listeners) {
17
- listeners.delete(handler);
18
- if (listeners.size === 0) delete listenersByType[type];
19
- }
20
- },
21
- emit(type, payload) {
22
- const listeners = listenersByType[type];
23
- if (!listeners) return;
24
- [...listeners].forEach((handler) => {
25
- try {
26
- handler(payload);
27
- } catch {}
28
- });
29
- }
30
- };
31
-
32
- //#endregion
33
- //#region src/events.ts
34
- const RMU_EVENTS = {
35
- open: "rmu:open-modal",
36
- close: "rmu:close-modal"
37
- };
38
- const openModal = (modalComponent, config = {}) => {
39
- const modalId = `rmu-modal-${(/* @__PURE__ */ new Date()).getTime().toString()}`;
40
- const { outletId = "rmu-default-outlet" } = config;
41
- emitter.emit(RMU_EVENTS.open, {
42
- modalId,
43
- modalComponent,
44
- outletId
45
- });
46
- return {
47
- modalId,
48
- outletId
49
- };
50
- };
51
- const closeModal = ({ modalId, outletId }) => {
52
- emitter.emit(RMU_EVENTS.close, {
53
- modalId,
54
- outletId
55
- });
56
- };
57
-
58
- //#endregion
59
- //#region src/use-rmu-events.ts
60
- const useRMUEvents = (ctx) => {
61
- const events = {
62
- open: (payload) => ctx.openModal(payload),
63
- close: (payload) => ctx.closeModal(payload)
64
- };
65
- useEffect(() => {
66
- Object.keys(events).forEach((event) => {
67
- emitter.subscribe(RMU_EVENTS[event], events[event]);
68
- });
69
- return () => {
70
- Object.keys(events).forEach((event) => {
71
- emitter.unsubscribe(RMU_EVENTS[event], events[event]);
72
- });
73
- };
74
- }, []);
75
- };
76
-
77
- //#endregion
78
- //#region src/use-rmu-state.ts
79
- const ACTIONS = {
80
- openModal: "rmu:open-modal",
81
- closeModal: "rmu:close-modal",
82
- addOutlet: "rmu:add-modal",
83
- removeOutlet: "rmu:remove-outlet"
84
- };
85
- const reducer = (state, action) => {
86
- switch (action.type) {
87
- case ACTIONS.openModal: {
88
- const { modalId, modalComponent, outletId } = action.payload;
89
- const modalOutlet = state.outlets[outletId];
90
- if (!modalOutlet) throw new Error(`Outlet with id ${outletId} not found`);
91
- return {
92
- ...state,
93
- outlets: {
94
- ...state.outlets,
95
- [outletId]: {
96
- ...modalOutlet,
97
- [modalId]: modalComponent
98
- }
99
- }
100
- };
101
- }
102
- case ACTIONS.closeModal: {
103
- const { modalId, outletId } = action.payload;
104
- const modalOutlet = state.outlets[outletId];
105
- if (!modalOutlet) throw new Error(`Outlet with id ${outletId} not found`);
106
- const { [modalId]: _removed,...restModals } = modalOutlet;
107
- return {
108
- ...state,
109
- outlets: {
110
- ...state.outlets,
111
- [outletId]: restModals
112
- }
113
- };
114
- }
115
- case ACTIONS.addOutlet: {
116
- const { outletId } = action.payload;
117
- if (!!state.outlets[outletId]) throw new Error(`Outlet with id ${outletId} already exists`);
118
- return {
119
- ...state,
120
- outlets: {
121
- ...state.outlets,
122
- [outletId]: {}
123
- }
124
- };
125
- }
126
- case ACTIONS.removeOutlet: {
127
- const { outletId } = action.payload;
128
- const { [outletId]: _removed,...restOutlets } = state.outlets;
129
- return {
130
- ...state,
131
- outlets: restOutlets
132
- };
133
- }
134
- default: return state;
135
- }
136
- };
137
- const useRMUState = () => {
138
- const [state, dispatch] = useReducer(reducer, { outlets: {} });
139
- const openModal$1 = ({ modalId, modalComponent, outletId }) => {
140
- dispatch({
141
- type: ACTIONS.openModal,
142
- payload: {
143
- modalId,
144
- modalComponent,
145
- outletId
146
- }
147
- });
148
- };
149
- const closeModal$1 = ({ modalId, outletId }) => {
150
- dispatch({
151
- type: ACTIONS.closeModal,
152
- payload: {
153
- modalId,
154
- outletId
155
- }
156
- });
157
- };
158
- const addOutlet = (outletId) => {
159
- dispatch({
160
- type: ACTIONS.addOutlet,
161
- payload: { outletId }
162
- });
163
- };
164
- const removeOutlet = (outletId) => {
165
- dispatch({
166
- type: ACTIONS.removeOutlet,
167
- payload: { outletId }
168
- });
169
- };
170
- return {
171
- ...state,
172
- openModal: openModal$1,
173
- closeModal: closeModal$1,
174
- addOutlet,
175
- removeOutlet
176
- };
177
- };
178
-
179
- //#endregion
180
- //#region src/rmu-provider.tsx
181
- const RMUProvider = ({ children }) => {
182
- const state = useRMUState();
183
- useRMUEvents(state);
184
- return /* @__PURE__ */ React.createElement(RMUContext.Provider, { value: state }, children);
185
- };
186
-
187
- //#endregion
188
- //#region src/rmu-outlet.tsx
189
- const RMUOutlet = ({ outletId = "rmu-default-outlet" }) => {
190
- const ctx = useContext(RMUContext);
191
- if (!ctx) throw new Error("RMUProvider not found in component tree");
192
- const { outlets, addOutlet, removeOutlet } = ctx;
193
- useEffect(() => {
194
- addOutlet(outletId);
195
- return () => {
196
- removeOutlet(outletId);
197
- };
198
- }, []);
199
- const modals = outlets[outletId] ?? {};
200
- return /* @__PURE__ */ React.createElement(React.Fragment, null, Object.entries(modals).map(([modalId, modalComponent]) => /* @__PURE__ */ React.createElement(Fragment, { key: modalId }, modalComponent)));
201
- };
202
-
203
- //#endregion
204
- export { RMUOutlet, RMUProvider, closeModal, openModal };
1
+ import{Fragment as e,createContext as t,useContext as n,useEffect as r,useReducer as i}from"react";import{Fragment as a,jsx as o}from"react/jsx-runtime";const s=t(null);let c=function(e){return e.OpenModal=`RMU:OPEN_MODAL`,e.CloseModal=`RMU:CLOSE_MODAL`,e}({});const l=`RMU:DEFAULT_OUTLET`,u={open:c.OpenModal,close:c.CloseModal},d={},f={subscribe(e,t){d[e]||(d[e]=new Set),d[e].add(t)},unsubscribe(e,t){let n=d[e];n&&(n.delete(t),n.size===0&&delete d[e])},emit(e,t){let n=d[e];n&&[...n].forEach(e=>{try{e(t)}catch{}})},_clear(){Object.keys(d).forEach(e=>{delete d[e]})}},p=e=>{let t={open:t=>e.openModal(t),close:t=>e.closeModal(t)};r(()=>(Object.keys(t).forEach(e=>{f.subscribe(u[e],t[e])}),()=>{Object.keys(t).forEach(e=>{f.unsubscribe(u[e],t[e])})}),[])},m={openModal:`rmu:open-modal`,closeModal:`rmu:close-modal`,addOutlet:`rmu:add-modal`,removeOutlet:`rmu:remove-outlet`},h=(e,t)=>{switch(t.type){case m.openModal:{let{modalId:n,modalComponent:r,outletId:i}=t.payload,a=e.outlets[i];if(!a)throw Error(`Outlet with id ${i} not found`);return{...e,outlets:{...e.outlets,[i]:{...a,[n]:r}}}}case m.closeModal:{let{modalId:n,outletId:r}=t.payload,i=e.outlets[r];if(!i)throw Error(`Outlet with id ${r} not found`);let{[n]:a,...o}=i;return{...e,outlets:{...e.outlets,[r]:o}}}case m.addOutlet:{let{outletId:n}=t.payload;if(e.outlets[n])throw Error(`Outlet with id ${n} already exists`);return{...e,outlets:{...e.outlets,[n]:{}}}}case m.removeOutlet:{let{outletId:n}=t.payload,{[n]:r,...i}=e.outlets;return{...e,outlets:i}}default:return e}},g=()=>{let[e,t]=i(h,{outlets:{}}),n=({modalId:e,modalComponent:n,outletId:r})=>{t({type:m.openModal,payload:{modalId:e,modalComponent:n,outletId:r}})},r=({modalId:e,outletId:n})=>{t({type:m.closeModal,payload:{modalId:e,outletId:n}})},a=e=>{t({type:m.addOutlet,payload:{outletId:e}})},o=e=>{t({type:m.removeOutlet,payload:{outletId:e}})};return{...e,openModal:n,closeModal:r,addOutlet:a,removeOutlet:o}},_=({children:e})=>{let t=g();return p(t),o(s.Provider,{value:t,children:e})},v=({outletId:t=l})=>{let i=n(s);if(!i)throw Error(`RMUProvider not found in component tree`);let{outlets:c,addOutlet:u,removeOutlet:d}=i;r(()=>(u(t),()=>{d(t)}),[]);let f=c[t]??{};return o(a,{children:Object.entries(f).map(([t,n])=>o(e,{children:n},t))})},y=(e,t={})=>{let n=`rmu-modal-${new Date().getTime().toString()}`,{outletId:r=l}=t,i={modalId:n,modalComponent:e,outletId:r};return f.emit(c.OpenModal,i),{modalId:n,outletId:r}},b=({modalId:e,outletId:t})=>{let n={modalId:e,outletId:t};f.emit(c.CloseModal,n)};export{v as RMUOutlet,_ as RMUProvider,b as closeModal,y as openModal};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/rmu-context.ts","../src/types.ts","../src/constants.ts","../src/emitter.ts","../src/use-rmu-events.ts","../src/use-rmu-state.ts","../src/rmu-provider.tsx","../src/rmu-outlet.tsx","../src/events.ts"],"sourcesContent":["import { createContext } from 'react';\nimport { RMUContextState } from './types';\n\nexport const RMUContext = createContext<RMUContextState | null>(null);\n","import { ReactNode } from 'react';\n\nexport enum RMUEventType {\n OpenModal = 'RMU:OPEN_MODAL',\n CloseModal = 'RMU:CLOSE_MODAL',\n}\n\nexport type RMUContextState = {\n outlets: Record<string, Record<string, ReactNode>>;\n openModal: (payload: OpenModalPayload) => void;\n closeModal: (payload: CloseModalPayload) => void;\n addOutlet: (outletId: string) => void;\n removeOutlet: (outletId: string) => void;\n};\n\nexport type OpenModalPayload = {\n modalId: string;\n modalComponent: ReactNode;\n outletId: string;\n};\n\nexport type CloseModalPayload = {\n modalId: string;\n outletId: string;\n};\n\nexport type RMUEventPayload = OpenModalPayload | CloseModalPayload;\n","import { RMUEventType } from \"./types\";\n\nexport const RMU_DEFAULT_OUTLET_ID = 'RMU:DEFAULT_OUTLET' as const;\n\nexport const RMU_EVENTS = {\n open: RMUEventType.OpenModal,\n close: RMUEventType.CloseModal,\n} as const;\n","export type EventHandler<T = unknown> = (payload: T) => void;\n\ntype EventListenersMap = Record<string, Set<EventHandler<unknown>>>;\n\nconst listenersByType: EventListenersMap = {};\n\nexport const emitter = {\n subscribe<T>(type: string, handler: EventHandler<T>) {\n if (!listenersByType[type]) {\n listenersByType[type] = new Set<EventHandler<unknown>>();\n }\n listenersByType[type].add(handler as EventHandler<unknown>);\n },\n\n unsubscribe<T>(type: string, handler: EventHandler<T>) {\n const listeners = listenersByType[type];\n if (listeners) {\n listeners.delete(handler as EventHandler<unknown>);\n if (listeners.size === 0) {\n delete listenersByType[type];\n }\n }\n },\n\n emit<T>(type: string, payload: T) {\n const listeners = listenersByType[type];\n if (listeners) {\n // Clone to avoid issues if handlers mutate subscription during emit\n [...listeners].forEach((handler) => {\n try {\n handler(payload);\n } catch {\n // Swallow to ensure one handler error does not break others\n }\n });\n }\n },\n\n // For testing purposes only\n _clear() {\n Object.keys(listenersByType).forEach((key) => {\n delete listenersByType[key];\n });\n },\n};\n","import { useEffect } from 'react';\nimport { RMUContextState, OpenModalPayload, CloseModalPayload } from './types';\nimport { RMU_EVENTS } from './constants';\nimport { emitter } from './emitter';\n\nexport const useRMUEvents = (ctx: RMUContextState) => {\n const events = {\n open: (payload: OpenModalPayload) => ctx.openModal(payload),\n close: (payload: CloseModalPayload) => ctx.closeModal(payload),\n };\n\n useEffect(() => {\n (Object.keys(events) as (keyof typeof events)[]).forEach(event => {\n emitter.subscribe(RMU_EVENTS[event], events[event]);\n });\n\n return () => {\n (Object.keys(events) as (keyof typeof events)[]).forEach(event => {\n emitter.unsubscribe(RMU_EVENTS[event], events[event]);\n });\n };\n }, []);\n};\n","import { useReducer } from 'react';\nimport { RMUContextState } from './types';\n\nconst ACTIONS = {\n openModal: 'rmu:open-modal',\n closeModal: 'rmu:close-modal',\n addOutlet: 'rmu:add-modal',\n removeOutlet: 'rmu:remove-outlet',\n} as const;\n\nconst reducer = (\n state: {\n outlets: RMUContextState['outlets'];\n },\n action: {\n type: string;\n payload: Record<string, any>;\n }\n) => {\n switch (action.type) {\n case ACTIONS.openModal: {\n const { modalId, modalComponent, outletId } = action.payload;\n const modalOutlet = state.outlets[outletId];\n\n if (!modalOutlet) {\n throw new Error(`Outlet with id ${outletId} not found`);\n }\n\n return {\n ...state,\n outlets: {\n ...state.outlets,\n [outletId]: {\n ...modalOutlet,\n [modalId]: modalComponent,\n },\n },\n };\n }\n case ACTIONS.closeModal: {\n const { modalId, outletId } = action.payload;\n\n const modalOutlet = state.outlets[outletId];\n if (!modalOutlet) {\n throw new Error(`Outlet with id ${outletId} not found`);\n }\n\n const { [modalId]: _removed, ...restModals } = modalOutlet;\n\n return {\n ...state,\n outlets: {\n ...state.outlets,\n [outletId]: restModals,\n },\n };\n }\n case ACTIONS.addOutlet: {\n const { outletId } = action.payload;\n\n if (!!state.outlets[outletId]) {\n throw new Error(`Outlet with id ${outletId} already exists`);\n }\n\n return {\n ...state,\n outlets: {\n ...state.outlets,\n [outletId]: {},\n },\n };\n }\n case ACTIONS.removeOutlet: {\n const { outletId } = action.payload;\n\n const { [outletId]: _removed, ...restOutlets } = state.outlets;\n\n return {\n ...state,\n outlets: restOutlets,\n };\n }\n default:\n return state;\n }\n};\n\nexport const useRMUState = () => {\n const [state, dispatch] = useReducer(reducer, {\n outlets: {},\n });\n\n const openModal: RMUContextState['openModal'] = ({\n modalId,\n modalComponent,\n outletId,\n }) => {\n dispatch({\n type: ACTIONS.openModal,\n payload: { modalId, modalComponent, outletId },\n });\n };\n\n const closeModal: RMUContextState['closeModal'] = ({ modalId, outletId }) => {\n dispatch({ type: ACTIONS.closeModal, payload: { modalId, outletId } });\n };\n\n const addOutlet: RMUContextState['addOutlet'] = outletId => {\n dispatch({ type: ACTIONS.addOutlet, payload: { outletId } });\n };\n\n const removeOutlet: RMUContextState['removeOutlet'] = outletId => {\n dispatch({ type: ACTIONS.removeOutlet, payload: { outletId } });\n };\n\n return {\n ...state,\n openModal,\n closeModal,\n addOutlet,\n removeOutlet,\n };\n};\n","import React from 'react';\nimport { RMUContext } from './rmu-context';\nimport { useRMUEvents } from './use-rmu-events';\nimport { useRMUState } from './use-rmu-state';\n\nexport const RMUProvider = ({ children }: { children: React.ReactNode }) => {\n const state = useRMUState();\n\n useRMUEvents(state);\n\n return <RMUContext.Provider value={state}>{children}</RMUContext.Provider>;\n};\n","import React, { Fragment, useContext, useEffect } from 'react';\nimport { RMUContext } from './rmu-context';\nimport { RMU_DEFAULT_OUTLET_ID } from './constants';\n\nexport const RMUOutlet = ({ outletId = RMU_DEFAULT_OUTLET_ID }) => {\n const ctx = useContext(RMUContext);\n\n if (!ctx) {\n throw new Error('RMUProvider not found in component tree');\n }\n\n const { outlets, addOutlet, removeOutlet } = ctx;\n\n useEffect(() => {\n addOutlet(outletId);\n return () => {\n removeOutlet(outletId);\n };\n }, []);\n\n const modals = outlets[outletId] ?? {};\n\n return (\n <>\n {Object.entries(modals).map(([modalId, modalComponent]) => (\n <Fragment key={modalId}>{modalComponent}</Fragment>\n ))}\n </>\n );\n};\n\nexport default RMUOutlet;\n","import { ReactNode } from 'react';\nimport { OpenModalPayload, CloseModalPayload, RMUEventType } from './types';\nimport { emitter } from './emitter';\nimport { RMU_DEFAULT_OUTLET_ID } from './constants';\n\nexport const openModal = (\n modalComponent: ReactNode,\n config: { outletId?: string } = {}\n): { modalId: string; outletId: string } => {\n const modalId = `rmu-modal-${new Date().getTime().toString()}`;\n const { outletId = RMU_DEFAULT_OUTLET_ID } = config;\n const payload: OpenModalPayload = {\n modalId,\n modalComponent,\n outletId,\n };\n emitter.emit<OpenModalPayload>(RMUEventType.OpenModal, payload);\n return { modalId, outletId };\n};\n\nexport const closeModal = ({\n modalId,\n outletId,\n}: {\n modalId: string;\n outletId: string;\n}): void => {\n const payload: CloseModalPayload = { modalId, outletId };\n emitter.emit<CloseModalPayload>(RMUEventType.CloseModal, payload);\n};\n"],"mappings":"yJAGA,MAAa,EAAa,EAAsC,KAAK,CCDrE,IAAY,EAAL,SAAA,EAAA,OACL,GAAA,UAAA,iBACA,EAAA,WAAA,wBACD,CCHD,MAAa,EAAwB,qBAExB,EAAa,CACxB,KAAM,EAAa,UACnB,MAAO,EAAa,WACrB,CCHK,EAAqC,EAAE,CAEhC,EAAU,CACrB,UAAa,EAAc,EAA0B,CAC9C,EAAgB,KACnB,EAAgB,GAAQ,IAAI,KAE9B,EAAgB,GAAM,IAAI,EAAiC,EAG7D,YAAe,EAAc,EAA0B,CACrD,IAAM,EAAY,EAAgB,GAC9B,IACF,EAAU,OAAO,EAAiC,CAC9C,EAAU,OAAS,GACrB,OAAO,EAAgB,KAK7B,KAAQ,EAAc,EAAY,CAChC,IAAM,EAAY,EAAgB,GAC9B,GAEF,CAAC,GAAG,EAAU,CAAC,QAAS,GAAY,CAClC,GAAI,CACF,EAAQ,EAAQ,MACV,IAGR,EAKN,QAAS,CACP,OAAO,KAAK,EAAgB,CAAC,QAAS,GAAQ,CAC5C,OAAO,EAAgB,IACvB,EAEL,CCvCY,EAAgB,GAAyB,CACpD,IAAM,EAAS,CACb,KAAO,GAA8B,EAAI,UAAU,EAAQ,CAC3D,MAAQ,GAA+B,EAAI,WAAW,EAAQ,CAC/D,CAED,OACG,OAAO,KAAK,EAAO,CAA6B,QAAQ,GAAS,CAChE,EAAQ,UAAU,EAAW,GAAQ,EAAO,GAAO,EACnD,KAEW,CACV,OAAO,KAAK,EAAO,CAA6B,QAAQ,GAAS,CAChE,EAAQ,YAAY,EAAW,GAAQ,EAAO,GAAO,EACrD,GAEH,EAAE,CAAC,EClBF,EAAU,CACd,UAAW,iBACX,WAAY,kBACZ,UAAW,gBACX,aAAc,oBACf,CAEK,GACJ,EAGA,IAIG,CACH,OAAQ,EAAO,KAAf,CACE,KAAK,EAAQ,UAAW,CACtB,GAAM,CAAE,UAAS,iBAAgB,YAAa,EAAO,QAC/C,EAAc,EAAM,QAAQ,GAElC,GAAI,CAAC,EACH,MAAU,MAAM,kBAAkB,EAAS,YAAY,CAGzD,MAAO,CACL,GAAG,EACH,QAAS,CACP,GAAG,EAAM,SACR,GAAW,CACV,GAAG,GACF,GAAU,EACZ,CACF,CACF,CAEH,KAAK,EAAQ,WAAY,CACvB,GAAM,CAAE,UAAS,YAAa,EAAO,QAE/B,EAAc,EAAM,QAAQ,GAClC,GAAI,CAAC,EACH,MAAU,MAAM,kBAAkB,EAAS,YAAY,CAGzD,GAAM,EAAG,GAAU,EAAU,GAAG,GAAe,EAE/C,MAAO,CACL,GAAG,EACH,QAAS,CACP,GAAG,EAAM,SACR,GAAW,EACb,CACF,CAEH,KAAK,EAAQ,UAAW,CACtB,GAAM,CAAE,YAAa,EAAO,QAE5B,GAAM,EAAM,QAAQ,GAClB,MAAU,MAAM,kBAAkB,EAAS,iBAAiB,CAG9D,MAAO,CACL,GAAG,EACH,QAAS,CACP,GAAG,EAAM,SACR,GAAW,EAAE,CACf,CACF,CAEH,KAAK,EAAQ,aAAc,CACzB,GAAM,CAAE,YAAa,EAAO,QAEtB,EAAG,GAAW,EAAU,GAAG,GAAgB,EAAM,QAEvD,MAAO,CACL,GAAG,EACH,QAAS,EACV,CAEH,QACE,OAAO,IAIA,MAAoB,CAC/B,GAAM,CAAC,EAAO,GAAY,EAAW,EAAS,CAC5C,QAAS,EAAE,CACZ,CAAC,CAEI,GAA2C,CAC/C,UACA,iBACA,cACI,CACJ,EAAS,CACP,KAAM,EAAQ,UACd,QAAS,CAAE,UAAS,iBAAgB,WAAU,CAC/C,CAAC,EAGE,GAA6C,CAAE,UAAS,cAAe,CAC3E,EAAS,CAAE,KAAM,EAAQ,WAAY,QAAS,CAAE,UAAS,WAAU,CAAE,CAAC,EAGlE,EAA0C,GAAY,CAC1D,EAAS,CAAE,KAAM,EAAQ,UAAW,QAAS,CAAE,WAAU,CAAE,CAAC,EAGxD,EAAgD,GAAY,CAChE,EAAS,CAAE,KAAM,EAAQ,aAAc,QAAS,CAAE,WAAU,CAAE,CAAC,EAGjE,MAAO,CACL,GAAG,EACH,YACA,aACA,YACA,eACD,ECpHU,GAAe,CAAE,cAA8C,CAC1E,IAAM,EAAQ,GAAa,CAI3B,OAFA,EAAa,EAAM,CAEZ,EAAC,EAAW,SAAZ,CAAqB,MAAO,EAAQ,WAA+B,CAAA,ECN/D,GAAa,CAAE,WAAW,KAA4B,CACjE,IAAM,EAAM,EAAW,EAAW,CAElC,GAAI,CAAC,EACH,MAAU,MAAM,0CAA0C,CAG5D,GAAM,CAAE,UAAS,YAAW,gBAAiB,EAE7C,OACE,EAAU,EAAS,KACN,CACX,EAAa,EAAS,GAEvB,EAAE,CAAC,CAEN,IAAM,EAAS,EAAQ,IAAa,EAAE,CAEtC,OACE,EAAA,EAAA,CAAA,SACG,OAAO,QAAQ,EAAO,CAAC,KAAK,CAAC,EAAS,KACrC,EAAC,EAAD,CAAA,SAAyB,EAA0B,CAApC,EAAoC,CACnD,CACD,CAAA,ECtBM,GACX,EACA,EAAgC,EAAE,GACQ,CAC1C,IAAM,EAAU,aAAa,IAAI,MAAM,CAAC,SAAS,CAAC,UAAU,GACtD,CAAE,WAAW,GAA0B,EACvC,EAA4B,CAChC,UACA,iBACA,WACD,CAED,OADA,EAAQ,KAAuB,EAAa,UAAW,EAAQ,CACxD,CAAE,UAAS,WAAU,EAGjB,GAAc,CACzB,UACA,cAIU,CACV,IAAM,EAA6B,CAAE,UAAS,WAAU,CACxD,EAAQ,KAAwB,EAAa,WAAY,EAAQ"}
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@violice/rmu",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
+ "type": "module",
4
5
  "author": "Sergey Ivashko",
5
6
  "license": "MIT",
6
7
  "files": [
@@ -8,18 +9,20 @@
8
9
  "src"
9
10
  ],
10
11
  "engines": {
11
- "node": ">=20"
12
+ "node": ">=24"
12
13
  },
13
14
  "scripts": {
14
- "start": "tsdown --watch",
15
+ "dev": "tsdown --watch",
15
16
  "build": "tsdown",
16
- "prepare": "tsdown",
17
17
  "size": "size-limit",
18
- "analyze": "size-limit --why"
18
+ "size:why": "size-limit --why",
19
+ "test": "vitest --watch=false",
20
+ "test:watch": "vitest",
21
+ "test:coverage": "vitest run --coverage"
19
22
  },
20
23
  "peerDependencies": {
21
- "react": ">=16",
22
- "react-native": ">=0.60"
24
+ "react": ">=18",
25
+ "react-native": ">=0.69"
23
26
  },
24
27
  "peerDependenciesMeta": {
25
28
  "react-native": {
@@ -32,21 +35,21 @@
32
35
  "singleQuote": true,
33
36
  "trailingComma": "es5"
34
37
  },
35
- "main": "dist/index.js",
38
+ "main": "dist/index.cjs",
36
39
  "module": "dist/index.mjs",
37
40
  "types": "dist/index.d.ts",
38
- "react-native": "dist/index.js",
41
+ "react-native": "dist/index.cjs",
39
42
  "exports": {
40
43
  ".": {
41
44
  "types": "./dist/index.d.ts",
42
45
  "import": "./dist/index.mjs",
43
- "require": "./dist/index.js"
46
+ "require": "./dist/index.cjs"
44
47
  }
45
48
  },
46
49
  "sideEffects": false,
47
50
  "size-limit": [
48
51
  {
49
- "path": "dist/index.js",
52
+ "path": "dist/index.cjs",
50
53
  "limit": "2 KB"
51
54
  },
52
55
  {
@@ -55,14 +58,19 @@
55
58
  }
56
59
  ],
57
60
  "devDependencies": {
58
- "@size-limit/preset-small-lib": "^11.2.0",
59
- "@types/react": "^18.3.25",
60
- "@types/react-dom": "^18.3.7",
61
- "react": "^18.3.1",
62
- "react-dom": "^18.3.1",
63
- "size-limit": "^11.2.0",
64
- "tsdown": "^0.15.6",
61
+ "@size-limit/preset-small-lib": "^12.0.1",
62
+ "@testing-library/dom": "^10.4.1",
63
+ "@testing-library/react": "^16.3.2",
64
+ "@types/react": "^19.2.14",
65
+ "@types/react-dom": "^19.2.3",
66
+ "@vitest/coverage-v8": "^4.1.4",
67
+ "jsdom": "^26.0.0",
68
+ "react": "^19.2.5",
69
+ "react-dom": "^19.2.5",
70
+ "size-limit": "^12.0.1",
71
+ "tsdown": "^0.21.8",
65
72
  "tslib": "^2.8.1",
66
- "typescript": "^5.9.3"
73
+ "typescript": "^6.0.2",
74
+ "vitest": "^4.1.4"
67
75
  }
68
76
  }
@@ -0,0 +1,8 @@
1
+ import { RMUEventType } from "./types";
2
+
3
+ export const RMU_DEFAULT_OUTLET_ID = 'RMU:DEFAULT_OUTLET' as const;
4
+
5
+ export const RMU_EVENTS = {
6
+ open: RMUEventType.OpenModal,
7
+ close: RMUEventType.CloseModal,
8
+ } as const;
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { emitter } from './emitter';
3
+
4
+ describe('emitter', () => {
5
+ it('should subscribe and emit events', () => {
6
+ const handler = vi.fn();
7
+ emitter.subscribe('test', handler);
8
+ emitter.emit('test', 'payload');
9
+ expect(handler).toHaveBeenCalledWith('payload');
10
+ });
11
+
12
+ it('should unsubscribe from events', () => {
13
+ const handler = vi.fn();
14
+ emitter.subscribe('test', handler);
15
+ emitter.unsubscribe('test', handler);
16
+ emitter.emit('test', 'payload');
17
+ expect(handler).not.toHaveBeenCalled();
18
+ });
19
+
20
+ it('should handle multiple handlers', () => {
21
+ const handler1 = vi.fn();
22
+ const handler2 = vi.fn();
23
+ emitter.subscribe('test', handler1);
24
+ emitter.subscribe('test', handler2);
25
+ emitter.emit('test', 'data');
26
+ expect(handler1).toHaveBeenCalled();
27
+ expect(handler2).toHaveBeenCalled();
28
+ });
29
+
30
+ it('should not break when emitting to non-existent event', () => {
31
+ expect(() => emitter.emit('non-existent', 'data')).not.toThrow();
32
+ });
33
+
34
+ it('should pass payload to handlers', () => {
35
+ const handler = vi.fn();
36
+ const payload = { id: 1, name: 'test' };
37
+ emitter.subscribe('test', handler);
38
+ emitter.emit('test', payload);
39
+ expect(handler).toHaveBeenCalledWith(payload);
40
+ });
41
+
42
+ it('should delete event type when all handlers are unsubscribed', () => {
43
+ const handler1 = vi.fn();
44
+ const handler2 = vi.fn();
45
+
46
+ // Subscribe multiple handlers
47
+ emitter.subscribe('cleanup-test', handler1);
48
+ emitter.subscribe('cleanup-test', handler2);
49
+
50
+ // Emit should work
51
+ emitter.emit('cleanup-test', 'data');
52
+ expect(handler1).toHaveBeenCalled();
53
+ expect(handler2).toHaveBeenCalled();
54
+
55
+ // Unsubscribe first handler - event type should still exist
56
+ emitter.unsubscribe('cleanup-test', handler1);
57
+ emitter.emit('cleanup-test', 'data2');
58
+ expect(handler2).toHaveBeenCalledTimes(2);
59
+
60
+ // Unsubscribe second handler - event type should be deleted
61
+ emitter.unsubscribe('cleanup-test', handler2);
62
+
63
+ // Emit should not throw and handlers should not be called
64
+ expect(() => emitter.emit('cleanup-test', 'data3')).not.toThrow();
65
+ expect(handler1).toHaveBeenCalledTimes(1);
66
+ expect(handler2).toHaveBeenCalledTimes(2);
67
+ });
68
+
69
+ it('should handle errors in handlers without breaking other handlers', () => {
70
+ const errorHandler = vi.fn(() => {
71
+ throw new Error('Handler error');
72
+ });
73
+ const normalHandler = vi.fn();
74
+
75
+ emitter.subscribe('error-test', errorHandler);
76
+ emitter.subscribe('error-test', normalHandler);
77
+
78
+ // Emit should not throw even if one handler throws
79
+ expect(() => emitter.emit('error-test', 'data')).not.toThrow();
80
+
81
+ // Both handlers should have been called
82
+ expect(errorHandler).toHaveBeenCalled();
83
+ expect(normalHandler).toHaveBeenCalled();
84
+ });
85
+ });
package/src/emitter.ts CHANGED
@@ -1,37 +1,45 @@
1
- type EventHandler = (payload: any) => void;
1
+ export type EventHandler<T = unknown> = (payload: T) => void;
2
2
 
3
- type EventListenersMap = Record<string, Set<EventHandler>>;
3
+ type EventListenersMap = Record<string, Set<EventHandler<unknown>>>;
4
4
 
5
5
  const listenersByType: EventListenersMap = {};
6
6
 
7
7
  export const emitter = {
8
- subscribe(type: string, handler: EventHandler) {
8
+ subscribe<T>(type: string, handler: EventHandler<T>) {
9
9
  if (!listenersByType[type]) {
10
- listenersByType[type] = new Set<EventHandler>();
10
+ listenersByType[type] = new Set<EventHandler<unknown>>();
11
11
  }
12
- listenersByType[type].add(handler);
12
+ listenersByType[type].add(handler as EventHandler<unknown>);
13
13
  },
14
14
 
15
- unsubscribe(type: string, handler: EventHandler) {
15
+ unsubscribe<T>(type: string, handler: EventHandler<T>) {
16
16
  const listeners = listenersByType[type];
17
17
  if (listeners) {
18
- listeners.delete(handler);
18
+ listeners.delete(handler as EventHandler<unknown>);
19
19
  if (listeners.size === 0) {
20
20
  delete listenersByType[type];
21
21
  }
22
22
  }
23
23
  },
24
24
 
25
- emit(type: string, payload: any) {
25
+ emit<T>(type: string, payload: T) {
26
26
  const listeners = listenersByType[type];
27
- if (!listeners) return;
28
- // Clone to avoid issues if handlers mutate subscription during emit
29
- [...listeners].forEach(handler => {
30
- try {
31
- handler(payload);
32
- } catch {
33
- // Swallow to ensure one handler error does not break others
34
- }
27
+ if (listeners) {
28
+ // Clone to avoid issues if handlers mutate subscription during emit
29
+ [...listeners].forEach((handler) => {
30
+ try {
31
+ handler(payload);
32
+ } catch {
33
+ // Swallow to ensure one handler error does not break others
34
+ }
35
+ });
36
+ }
37
+ },
38
+
39
+ // For testing purposes only
40
+ _clear() {
41
+ Object.keys(listenersByType).forEach((key) => {
42
+ delete listenersByType[key];
35
43
  });
36
44
  },
37
45
  };
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { openModal, closeModal } from './events';
4
+ import { RMU_EVENTS, RMU_DEFAULT_OUTLET_ID } from './constants';
5
+ import { emitter } from './emitter';
6
+
7
+ describe('events', () => {
8
+ const mockComponent = React.createElement('div', null, 'Test Modal');
9
+
10
+ beforeEach(() => {
11
+ vi.clearAllMocks();
12
+ });
13
+
14
+ describe('openModal', () => {
15
+ it('should emit open event with default outlet', () => {
16
+ const emitSpy = vi.spyOn(emitter, 'emit');
17
+ const result = openModal(mockComponent);
18
+
19
+ expect(emitSpy).toHaveBeenCalledWith(RMU_EVENTS.open, {
20
+ modalId: expect.stringMatching(/^rmu-modal-\d+$/),
21
+ modalComponent: mockComponent,
22
+ outletId: RMU_DEFAULT_OUTLET_ID,
23
+ });
24
+ expect(result).toHaveProperty('modalId');
25
+ expect(result).toHaveProperty('outletId', RMU_DEFAULT_OUTLET_ID);
26
+ });
27
+
28
+ it('should emit open event with custom outlet', () => {
29
+ const emitSpy = vi.spyOn(emitter, 'emit');
30
+ const result = openModal(mockComponent, { outletId: 'custom-outlet' });
31
+
32
+ expect(emitSpy).toHaveBeenCalledWith(RMU_EVENTS.open, {
33
+ modalId: expect.stringMatching(/^rmu-modal-\d+$/),
34
+ modalComponent: mockComponent,
35
+ outletId: 'custom-outlet',
36
+ });
37
+ expect(result.outletId).toBe('custom-outlet');
38
+ });
39
+
40
+ it('should generate unique modal IDs', async () => {
41
+ const result1 = openModal(mockComponent);
42
+ await new Promise(resolve => setTimeout(resolve, 10));
43
+ const result2 = openModal(mockComponent);
44
+ expect(result1.modalId).not.toBe(result2.modalId);
45
+ });
46
+ });
47
+
48
+ describe('closeModal', () => {
49
+ it('should emit close event', () => {
50
+ const emitSpy = vi.spyOn(emitter, 'emit');
51
+ closeModal({ modalId: 'test-modal', outletId: 'test-outlet' });
52
+
53
+ expect(emitSpy).toHaveBeenCalledWith(RMU_EVENTS.close, {
54
+ modalId: 'test-modal',
55
+ outletId: 'test-outlet',
56
+ });
57
+ });
58
+ });
59
+ });
package/src/events.ts CHANGED
@@ -1,29 +1,30 @@
1
1
  import { ReactNode } from 'react';
2
- import { RMUContextState } from './types';
2
+ import { OpenModalPayload, CloseModalPayload, RMUEventType } from './types';
3
3
  import { emitter } from './emitter';
4
-
5
- export const RMU_EVENTS = {
6
- open: 'rmu:open-modal',
7
- close: 'rmu:close-modal',
8
- } as const;
4
+ import { RMU_DEFAULT_OUTLET_ID } from './constants';
9
5
 
10
6
  export const openModal = (
11
7
  modalComponent: ReactNode,
12
8
  config: { outletId?: string } = {}
13
- ) => {
9
+ ): { modalId: string; outletId: string } => {
14
10
  const modalId = `rmu-modal-${new Date().getTime().toString()}`;
15
- const { outletId = 'rmu-default-outlet' } = config;
16
- emitter.emit(RMU_EVENTS.open, {
11
+ const { outletId = RMU_DEFAULT_OUTLET_ID } = config;
12
+ const payload: OpenModalPayload = {
17
13
  modalId,
18
14
  modalComponent,
19
15
  outletId,
20
- });
16
+ };
17
+ emitter.emit<OpenModalPayload>(RMUEventType.OpenModal, payload);
21
18
  return { modalId, outletId };
22
19
  };
23
20
 
24
- export const closeModal: RMUContextState['closeModal'] = ({
21
+ export const closeModal = ({
25
22
  modalId,
26
23
  outletId,
27
- }) => {
28
- emitter.emit(RMU_EVENTS.close, { modalId, outletId });
24
+ }: {
25
+ modalId: string;
26
+ outletId: string;
27
+ }): void => {
28
+ const payload: CloseModalPayload = { modalId, outletId };
29
+ emitter.emit<CloseModalPayload>(RMUEventType.CloseModal, payload);
29
30
  };