@alexandregme/zljs 1.16.0 → 1.18.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/CHANGELOG.md CHANGED
@@ -8,6 +8,48 @@ A change that needs action in an app has a **Migration** note. When updating, re
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.18.0] - 2026-11-02
12
+
13
+ ### Added
14
+
15
+ - `AppProviders` in `@alexandregme/zljs/next`: the app's global configuration, with the Redux store, `Loader`, `Toaster` and the route in `useRouterStore`. It takes `children`, `reducers`, `coreReducers`, `middlewares` and `preloadedState`
16
+ - `PageConfig` in `@alexandregme/zljs/next`: a feature's state configuration, with `title`, `reducers` and `initPage`. It renders only its children
17
+
18
+ ### Changed
19
+
20
+ - `ReduxProvider` creates a store per mount. It was a single store in the module, which on the server was shared between requests. Props changed after the first render no longer change the reducers. A page's reducers still change with `setDynamicReducers`, which is now required on `ReduxStore`
21
+ - `InitPageFunction` moved to `@alexandregme/zljs/next`. Its `dispatch` accepts thunks
22
+
23
+ ### Removed
24
+
25
+ - `AppWrapper` and `AppWrapperProps`. The route it synced never ran on the App Router, because `next/compat/router` returns `null` there
26
+ - `PageWrapper` and `PageWrapperProps`
27
+ - `DocumentWrapper` and `DocumentWrapperProps`
28
+ - `next-auth` from the peer dependencies. An app that uses it keeps it in its own `package.json`
29
+
30
+ #### Migration
31
+
32
+ Search the app for `AppWrapper`, `PageWrapper`, `InitPageFunction` and `DocumentWrapper` imported from `@alexandregme/zljs`. A local component with the same name, as in `ruralissima-diagnostico`, stays.
33
+
34
+ - `AppWrapper`: use `AppProviders` from `@alexandregme/zljs/next`, with the same `reducers`, `middlewares` and `coreReducers`, in a `'use client'` file of the app, because a Server Component can't pass a function. An app that used the session adds `SessionProvider` from `next-auth/react` around it, and installs `next-auth` if it isn't in its `package.json`
35
+ - `PageWrapper`: use `PageConfig` from `@alexandregme/zljs/next`, with the same `title`, `reducers` and `initPage`, and start the file that renders it with `'use client'`
36
+ - `InitPageFunction`: import it from `@alexandregme/zljs/next`
37
+ - `DocumentWrapper`: write `<html lang="…">` and `<body>` directly in `app/layout.tsx`
38
+
39
+ ## [1.17.0] - 2026-11-02
40
+
41
+ ### Added
42
+
43
+ - `ReduxProvider`, the new name of `GlobalState`, with the types `ReduxProviderProps` and `ReduxStore`. It behaves the same
44
+
45
+ ### Removed
46
+
47
+ - `GlobalState`, `GlobalStateProps` and `ExtendedStore`
48
+
49
+ #### Migration
50
+
51
+ Search the app for `GlobalState`, `GlobalStateProps` and `ExtendedStore` imported from `@alexandregme/zljs`, and replace them with `ReduxProvider`, `ReduxProviderProps` and `ReduxStore`. The props don't change.
52
+
11
53
  ## [1.16.0] - 2026-11-02
12
54
 
13
55
  ### Added
@@ -207,7 +249,9 @@ Replace the shade with the class of the `color` being tested, for example `/(^|\
207
249
 
208
250
  The message of `Error` is `text-zl-danger`. `none` and `ghost` didn't change.
209
251
 
210
- [Unreleased]: https://github.com/alexandregme/zljs/compare/v1.16.0...HEAD
252
+ [Unreleased]: https://github.com/alexandregme/zljs/compare/v1.18.0...HEAD
253
+ [1.18.0]: https://github.com/alexandregme/zljs/compare/v1.17.0...v1.18.0
254
+ [1.17.0]: https://github.com/alexandregme/zljs/compare/v1.16.0...v1.17.0
211
255
  [1.16.0]: https://github.com/alexandregme/zljs/compare/v1.15.0...v1.16.0
212
256
  [1.15.0]: https://github.com/alexandregme/zljs/compare/v1.14.0...v1.15.0
213
257
  [1.14.0]: https://github.com/alexandregme/zljs/compare/v1.13.0...v1.14.0
package/README.md CHANGED
@@ -25,7 +25,7 @@ Checked items exist today; unchecked items are planned, not implemented.
25
25
  pnpm add @alexandregme/zljs
26
26
  ```
27
27
 
28
- Peer dependencies: `react@19`, `react-dom@19`, `next@>=16`, `next-auth@4`, `@reduxjs/toolkit@2`, `react-redux@9`.
28
+ Peer dependencies: `react@19`, `react-dom@19`, `next@>=16`, `@reduxjs/toolkit@2`, `react-redux@9`.
29
29
 
30
30
  Import the global styles once (e.g. in your root layout). It already tells Tailwind CSS v4 to scan the library classes, so no `@source` is needed:
31
31
 
@@ -0,0 +1,366 @@
1
+ "use client";
2
+
3
+ // core/icon/icon.tsx
4
+ import * as LucideIcons from "lucide-react";
5
+
6
+ // core/icon/icon.const.ts
7
+ import { SiWhatsapp, SiFacebook, SiInstagram, SiShopify } from "react-icons/si";
8
+ var iconBrands = {
9
+ SiWhatsapp,
10
+ SiFacebook,
11
+ SiInstagram,
12
+ SiShopify
13
+ };
14
+ var iconSizes = {
15
+ sm: 12,
16
+ md: 16,
17
+ lg: 24
18
+ };
19
+
20
+ // core/icon/icon.styles.ts
21
+ import { tv } from "tailwind-variants";
22
+
23
+ // core/types/colors/colors.const.ts
24
+ var semanticTextColors = {
25
+ primary: "text-zl-primary",
26
+ success: "text-zl-success",
27
+ danger: "text-zl-danger",
28
+ warning: "text-zl-warning",
29
+ info: "text-zl-info",
30
+ muted: "text-zl-muted",
31
+ none: ""
32
+ };
33
+
34
+ // core/icon/icon.styles.ts
35
+ var icon = tv({
36
+ variants: {
37
+ color: semanticTextColors
38
+ }
39
+ });
40
+
41
+ // core/icon/icon.tsx
42
+ import { Fragment, jsx } from "react/jsx-runtime";
43
+ var Icon = ({
44
+ name,
45
+ size = "md",
46
+ color,
47
+ "aria-label": ariaLabel
48
+ }) => {
49
+ const isBrand = name.startsWith("Si");
50
+ const IconComponent = isBrand ? iconBrands[name] : LucideIcons[name];
51
+ if (!IconComponent) return /* @__PURE__ */ jsx(Fragment, {});
52
+ return /* @__PURE__ */ jsx(
53
+ "span",
54
+ {
55
+ "data-testid": name,
56
+ className: icon({ color }),
57
+ role: ariaLabel ? "img" : void 0,
58
+ "aria-label": ariaLabel,
59
+ children: /* @__PURE__ */ jsx(IconComponent, { size: iconSizes[size], "aria-hidden": "true" })
60
+ }
61
+ );
62
+ };
63
+
64
+ // core/text/text.styles.ts
65
+ import { tv as tv2 } from "tailwind-variants";
66
+ var text = tv2({
67
+ variants: {
68
+ variant: {
69
+ body: "text-base",
70
+ caption: "text-sm",
71
+ display: "text-3xl font-bold"
72
+ },
73
+ color: semanticTextColors
74
+ },
75
+ defaultVariants: {
76
+ variant: "body"
77
+ }
78
+ });
79
+
80
+ // core/text/text.tsx
81
+ import { jsx as jsx2 } from "react/jsx-runtime";
82
+ var Text = ({
83
+ children,
84
+ as: Tag = "p",
85
+ variant = "body",
86
+ color
87
+ }) => {
88
+ return /* @__PURE__ */ jsx2(Tag, { "data-testid": "text", className: text({ variant, color }), children });
89
+ };
90
+
91
+ // core/heading/heading.const.ts
92
+ var headingTags = {
93
+ 1: "h1",
94
+ 2: "h2",
95
+ 3: "h3",
96
+ 4: "h4",
97
+ 5: "h5",
98
+ 6: "h6"
99
+ };
100
+
101
+ // core/heading/heading.styles.ts
102
+ import { tv as tv3 } from "tailwind-variants";
103
+ var heading = tv3({
104
+ variants: {
105
+ level: {
106
+ 1: "text-3xl font-bold",
107
+ 2: "text-2xl font-bold",
108
+ 3: "text-xl font-semibold",
109
+ 4: "text-lg font-semibold",
110
+ 5: "text-base font-medium",
111
+ 6: "text-sm font-medium"
112
+ },
113
+ color: semanticTextColors
114
+ },
115
+ defaultVariants: {
116
+ level: 1
117
+ }
118
+ });
119
+
120
+ // core/heading/heading.tsx
121
+ import { jsx as jsx3 } from "react/jsx-runtime";
122
+ var Heading = ({ children, level = 1, color }) => {
123
+ const Tag = headingTags[level];
124
+ return /* @__PURE__ */ jsx3(Tag, { className: heading({ level, color }), children });
125
+ };
126
+
127
+ // core/state/toast/toast.store.ts
128
+ import { create } from "zustand";
129
+ import { devtools } from "zustand/middleware";
130
+ var toastId = 0;
131
+ var useToastStore = create()(
132
+ devtools(
133
+ (set, get) => ({
134
+ toasts: [],
135
+ addToast: (message, type = "error") => {
136
+ const id = ++toastId;
137
+ set(
138
+ (s) => ({ toasts: [...s.toasts, { id, message, type }] }),
139
+ false,
140
+ "addToast"
141
+ );
142
+ return id;
143
+ },
144
+ removeToast: (id) => set(
145
+ (s) => ({ toasts: s.toasts.filter((t) => t.id !== id) }),
146
+ false,
147
+ "removeToast"
148
+ ),
149
+ success: (message) => get().addToast(message, "success"),
150
+ error: (message) => get().addToast(message, "error"),
151
+ info: (message) => get().addToast(message, "info"),
152
+ warning: (message) => get().addToast(message, "warning")
153
+ }),
154
+ { name: "zljs", store: "toast" }
155
+ )
156
+ );
157
+
158
+ // core/toaster/toaster.tsx
159
+ import { useEffect } from "react";
160
+ import { Toaster as SonnerToaster, toast } from "sonner";
161
+ import { jsx as jsx4 } from "react/jsx-runtime";
162
+ var Toaster = () => {
163
+ const toasts = useToastStore((s) => s.toasts);
164
+ const removeToast = useToastStore((s) => s.removeToast);
165
+ useEffect(() => {
166
+ toasts.forEach((t) => {
167
+ toast[t.type](t.message, {
168
+ id: t.id,
169
+ onDismiss: () => removeToast(t.id)
170
+ });
171
+ removeToast(t.id);
172
+ });
173
+ }, [toasts, removeToast]);
174
+ return /* @__PURE__ */ jsx4(
175
+ SonnerToaster,
176
+ {
177
+ position: "bottom-right",
178
+ richColors: true,
179
+ closeButton: true,
180
+ duration: 4e3,
181
+ expand: true,
182
+ visibleToasts: 5
183
+ }
184
+ );
185
+ };
186
+
187
+ // core/toaster/index.ts
188
+ import { toast as toast2 } from "sonner";
189
+
190
+ // core/state/app/app.store.ts
191
+ import { create as create2 } from "zustand";
192
+ import { devtools as devtools2 } from "zustand/middleware";
193
+ var useAppStore = create2()(
194
+ devtools2(
195
+ (set) => ({
196
+ fetchingCalls: 0,
197
+ fetching: false,
198
+ startFetching: () => set(
199
+ (s) => {
200
+ const fetchingCalls = s.fetchingCalls + 1;
201
+ return { fetchingCalls, fetching: true };
202
+ },
203
+ false,
204
+ "startFetching"
205
+ ),
206
+ finishFetching: () => set(
207
+ (s) => {
208
+ const fetchingCalls = Math.max(0, s.fetchingCalls - 1);
209
+ return { fetchingCalls, fetching: fetchingCalls > 0 };
210
+ },
211
+ false,
212
+ "finishFetching"
213
+ )
214
+ }),
215
+ { name: "zljs", store: "app" }
216
+ )
217
+ );
218
+
219
+ // core/redux/redux-provider/redux-provider.tsx
220
+ import { useState } from "react";
221
+ import { Provider } from "react-redux";
222
+
223
+ // core/redux/store/store.ts
224
+ import {
225
+ combineReducers,
226
+ configureStore
227
+ } from "@reduxjs/toolkit";
228
+ var createReduxStore = ({
229
+ reducers,
230
+ middlewares,
231
+ preloadedState,
232
+ coreReducers
233
+ }) => {
234
+ let dynamicReducers = {};
235
+ const createReducer = () => combineReducers({
236
+ ...dynamicReducers,
237
+ ...reducers,
238
+ ...coreReducers
239
+ });
240
+ const store = configureStore({
241
+ reducer: createReducer(),
242
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middlewares),
243
+ preloadedState
244
+ });
245
+ store.setDynamicReducers = (newDynamicReducers = {}) => {
246
+ dynamicReducers = newDynamicReducers;
247
+ store.replaceReducer(createReducer());
248
+ };
249
+ return store;
250
+ };
251
+
252
+ // core/redux/redux-provider/redux-provider.tsx
253
+ import { jsx as jsx5 } from "react/jsx-runtime";
254
+ var ReduxProvider = ({
255
+ children,
256
+ reducers = {},
257
+ middlewares = [],
258
+ preloadedState = {},
259
+ coreReducers = {}
260
+ }) => {
261
+ const [store] = useState(
262
+ () => createReduxStore({
263
+ reducers,
264
+ middlewares,
265
+ preloadedState,
266
+ coreReducers
267
+ })
268
+ );
269
+ return /* @__PURE__ */ jsx5(Provider, { store, children });
270
+ };
271
+
272
+ // core/state/router/router.utils.ts
273
+ var splitURLasPath = (urlAsPath) => {
274
+ let queryWithoutPath = "";
275
+ if (urlAsPath?.indexOf("?") >= 0) {
276
+ const [, query] = urlAsPath.split("?");
277
+ queryWithoutPath = query ?? "";
278
+ }
279
+ if (urlAsPath?.indexOf("#") >= 0) {
280
+ const [, query] = urlAsPath.split("#");
281
+ queryWithoutPath = query ?? "";
282
+ }
283
+ const parametersAsObject = {};
284
+ if (queryWithoutPath.length > 0) {
285
+ queryWithoutPath.split("&").forEach((p) => {
286
+ const [parameter, value] = p.split("=");
287
+ if (parameter) {
288
+ parametersAsObject[parameter] = value ?? "";
289
+ }
290
+ });
291
+ }
292
+ return parametersAsObject;
293
+ };
294
+
295
+ // core/state/router/router.store.ts
296
+ import { create as create3 } from "zustand";
297
+ import { devtools as devtools3 } from "zustand/middleware";
298
+ var useRouterStore = create3()(
299
+ devtools3(
300
+ (set) => ({
301
+ urlAsPath: "/",
302
+ query: {},
303
+ setRoute: (urlAsPath) => set({ urlAsPath, query: splitURLasPath(urlAsPath) }, false, "setRoute")
304
+ }),
305
+ { name: "zljs", store: "router" }
306
+ )
307
+ );
308
+
309
+ // core/state/page/page.store.ts
310
+ import { create as create4 } from "zustand";
311
+ import { devtools as devtools4 } from "zustand/middleware";
312
+ var usePageStore = create4()(
313
+ devtools4(
314
+ (set) => ({
315
+ title: "",
316
+ setTitle: (title) => set({ title }, false, "setTitle")
317
+ }),
318
+ { name: "zljs", store: "page" }
319
+ )
320
+ );
321
+
322
+ // core/spinner/spinner.const.ts
323
+ var spinnerIcons = {
324
+ circle: "LoaderCircle",
325
+ dots: "Loader",
326
+ refresh: "RefreshCw"
327
+ };
328
+
329
+ // core/spinner/spinner.styles.ts
330
+ import { tv as tv4 } from "tailwind-variants";
331
+ var spinner = tv4({
332
+ base: "inline-flex animate-spin text-white"
333
+ });
334
+
335
+ // core/spinner/spinner.tsx
336
+ import { jsx as jsx6 } from "react/jsx-runtime";
337
+ var Spinner = ({ variant = "circle", size = "md" }) => /* @__PURE__ */ jsx6("span", { "data-testid": "spinner", className: spinner(), children: /* @__PURE__ */ jsx6(Icon, { name: spinnerIcons[variant], size }) });
338
+
339
+ // core/loader/loader.styles.ts
340
+ import { tv as tv5 } from "tailwind-variants";
341
+ var overlay = tv5({
342
+ base: "flex fixed justify-center items-center w-full h-full bg-black/20 z-50"
343
+ });
344
+
345
+ // core/loader/loader.tsx
346
+ import { jsx as jsx7 } from "react/jsx-runtime";
347
+ var Loader = ({ label = "Loading" }) => {
348
+ const fetching = useAppStore((s) => s.fetching);
349
+ if (!fetching) return null;
350
+ return /* @__PURE__ */ jsx7("div", { role: "status", "aria-label": label, className: overlay(), children: /* @__PURE__ */ jsx7(Spinner, { size: "lg" }) });
351
+ };
352
+
353
+ export {
354
+ Icon,
355
+ Text,
356
+ Heading,
357
+ useToastStore,
358
+ Toaster,
359
+ useAppStore,
360
+ ReduxProvider,
361
+ splitURLasPath,
362
+ useRouterStore,
363
+ usePageStore,
364
+ Spinner,
365
+ Loader
366
+ };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ChangeEvent, FormEvent, HTMLInputAutoCompleteAttribute, FocusEvent, KeyboardEvent, Ref } from 'react';
3
- import { S as SemanticColor, D as DataColor } from './colors-CvgrDPVc.js';
3
+ import { S as SemanticColor, D as DataColor, R as ReduxProviderProps } from './redux-provider.interface-BmROR8Ev.js';
4
4
  export { FormInput, FormInputProps, FormProvider, FormProviderProps } from './rhf/index.js';
5
5
  export { toast } from 'sonner';
6
6
  import * as zustand from 'zustand';
7
- import { Store, Reducer, Middleware, Dispatch } from '@reduxjs/toolkit';
7
+ import { Store, Reducer } from '@reduxjs/toolkit';
8
8
  import 'react-hook-form';
9
9
 
10
10
  /**
@@ -362,18 +362,11 @@ declare const useModalStore: zustand.UseBoundStore<Omit<zustand.StoreApi<ModalSt
362
362
  };
363
363
  }>;
364
364
 
365
- interface ExtendedStore extends Store {
366
- setDynamicReducers?: (reducers: Record<string, Reducer>) => void;
367
- }
368
- interface GlobalStateProps {
369
- children: ReactNode;
370
- reducers?: Record<string, Reducer>;
371
- middlewares?: Middleware[];
372
- preloadedState?: Record<string, unknown>;
373
- coreReducers?: Record<string, Reducer>;
374
- }
365
+ declare const ReduxProvider: ({ children, reducers, middlewares, preloadedState, coreReducers, }: ReduxProviderProps) => react.JSX.Element;
375
366
 
376
- declare const GlobalState: ({ children, reducers, middlewares, preloadedState, coreReducers, }: GlobalStateProps) => react.JSX.Element;
367
+ interface ReduxStore extends Store {
368
+ setDynamicReducers: (reducers: Record<string, Reducer>) => void;
369
+ }
377
370
 
378
371
  interface RouterState {
379
372
  urlAsPath: string;
@@ -424,16 +417,6 @@ declare const usePageStore: zustand.UseBoundStore<Omit<zustand.StoreApi<PageStat
424
417
  };
425
418
  }>;
426
419
 
427
- type InitPageFunction = (dispatch: Dispatch) => void;
428
- interface PageWrapperProps {
429
- title: string;
430
- children?: ReactNode;
431
- reducers?: Record<string, Reducer>;
432
- initPage?: InitPageFunction;
433
- }
434
-
435
- declare const PageWrapper: ({ reducers, title, initPage, children, }: PageWrapperProps) => react.JSX.Element;
436
-
437
420
  interface ConnectedModalProps {
438
421
  modalId: string;
439
422
  title: string;
@@ -458,23 +441,6 @@ interface LoaderProps {
458
441
 
459
442
  declare const Loader: ({ label }: LoaderProps) => react.JSX.Element | null;
460
443
 
461
- interface AppWrapperProps {
462
- children: ReactNode;
463
- reducers?: Record<string, Reducer>;
464
- middlewares?: Middleware[];
465
- coreReducers?: Record<string, Reducer>;
466
- }
467
-
468
- declare const AppWrapper: ({ children, reducers, middlewares, coreReducers, }: AppWrapperProps) => react.JSX.Element;
469
-
470
- interface DocumentWrapperProps {
471
- children: ReactNode;
472
- lang?: string;
473
- className?: string;
474
- }
475
-
476
- declare const DocumentWrapper: ({ children, lang, className, }: DocumentWrapperProps) => react.JSX.Element;
477
-
478
444
  interface HttpClientConfig {
479
445
  baseUrl?: string;
480
446
  headers?: Record<string, string>;
@@ -498,5 +464,5 @@ declare class HTTPClient {
498
464
  patch<T = unknown>(url: string, config?: HttpRequestConfig): Promise<T | undefined>;
499
465
  }
500
466
 
501
- export { AppWrapper, Button, Checkbox, ConnectedModal, DataColor, DataGrid, DocumentWrapper, Error, Form, GlobalState, HTTPClient, Heading, Icon, Input, Loader, Modal, PageWrapper, Radio, Select, SemanticColor, Sidebar, Spinner, StatsCard, Tag, Text, TextArea, Toaster, WhatsAppButton, splitURLasPath, useAppStore, useModalStore, usePageStore, useRouterStore, useToastStore };
502
- export type { AppState, AppWrapperProps, ButtonColor, ButtonProps, ButtonVariant, CheckboxProps, CheckboxVariant, ConnectedModalProps, DataGridColumn, DataGridProps, DocumentWrapperProps, ErrorProps, ExtendedStore, FormProps, GlobalStateProps, HeadingProps, HttpClientConfig, HttpRequestConfig, IconName, IconProps, InitPageFunction, InputProps, LoaderProps, ModalEntry, ModalProps, ModalState, PageState, PageWrapperProps, RadioProps, RouterState, SelectOption, SelectProps, SidebarItem, SidebarProps, SidebarSection, SidebarUser, Size, SpinnerProps, SpinnerVariant, StatsCardProps, TagProps, TextAreaProps, TextProps, TextVariant, Toast, ToastState, ToastType, WhatsAppButtonProps };
467
+ export { Button, Checkbox, ConnectedModal, DataColor, DataGrid, Error, Form, HTTPClient, Heading, Icon, Input, Loader, Modal, Radio, ReduxProvider, ReduxProviderProps, Select, SemanticColor, Sidebar, Spinner, StatsCard, Tag, Text, TextArea, Toaster, WhatsAppButton, splitURLasPath, useAppStore, useModalStore, usePageStore, useRouterStore, useToastStore };
468
+ export type { AppState, ButtonColor, ButtonProps, ButtonVariant, CheckboxProps, CheckboxVariant, ConnectedModalProps, DataGridColumn, DataGridProps, ErrorProps, FormProps, HeadingProps, HttpClientConfig, HttpRequestConfig, IconName, IconProps, InputProps, LoaderProps, ModalEntry, ModalProps, ModalState, PageState, RadioProps, ReduxStore, RouterState, SelectOption, SelectProps, SidebarItem, SidebarProps, SidebarSection, SidebarUser, Size, SpinnerProps, SpinnerVariant, StatsCardProps, TagProps, TextAreaProps, TextProps, TextVariant, Toast, ToastState, ToastType, WhatsAppButtonProps };