@alexandregme/zljs 1.17.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,34 @@ 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
+
11
39
  ## [1.17.0] - 2026-11-02
12
40
 
13
41
  ### Added
@@ -221,7 +249,8 @@ Replace the shade with the class of the `color` being tested, for example `/(^|\
221
249
 
222
250
  The message of `Error` is `text-zl-danger`. `none` and `ghost` didn't change.
223
251
 
224
- [Unreleased]: https://github.com/alexandregme/zljs/compare/v1.17.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
225
254
  [1.17.0]: https://github.com/alexandregme/zljs/compare/v1.16.0...v1.17.0
226
255
  [1.16.0]: https://github.com/alexandregme/zljs/compare/v1.15.0...v1.16.0
227
256
  [1.15.0]: https://github.com/alexandregme/zljs/compare/v1.14.0...v1.15.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 { Reducer, Middleware, Store, Dispatch } from '@reduxjs/toolkit';
7
+ import { Store, Reducer } from '@reduxjs/toolkit';
8
8
  import 'react-hook-form';
9
9
 
10
10
  /**
@@ -362,18 +362,10 @@ declare const useModalStore: zustand.UseBoundStore<Omit<zustand.StoreApi<ModalSt
362
362
  };
363
363
  }>;
364
364
 
365
- interface ReduxProviderProps {
366
- children: ReactNode;
367
- reducers?: Record<string, Reducer>;
368
- middlewares?: Middleware[];
369
- preloadedState?: Record<string, unknown>;
370
- coreReducers?: Record<string, Reducer>;
371
- }
372
-
373
365
  declare const ReduxProvider: ({ children, reducers, middlewares, preloadedState, coreReducers, }: ReduxProviderProps) => react.JSX.Element;
374
366
 
375
367
  interface ReduxStore extends Store {
376
- setDynamicReducers?: (reducers: Record<string, Reducer>) => void;
368
+ setDynamicReducers: (reducers: Record<string, Reducer>) => void;
377
369
  }
378
370
 
379
371
  interface RouterState {
@@ -425,16 +417,6 @@ declare const usePageStore: zustand.UseBoundStore<Omit<zustand.StoreApi<PageStat
425
417
  };
426
418
  }>;
427
419
 
428
- type InitPageFunction = (dispatch: Dispatch) => void;
429
- interface PageWrapperProps {
430
- title: string;
431
- children?: ReactNode;
432
- reducers?: Record<string, Reducer>;
433
- initPage?: InitPageFunction;
434
- }
435
-
436
- declare const PageWrapper: ({ reducers, title, initPage, children, }: PageWrapperProps) => react.JSX.Element;
437
-
438
420
  interface ConnectedModalProps {
439
421
  modalId: string;
440
422
  title: string;
@@ -459,23 +441,6 @@ interface LoaderProps {
459
441
 
460
442
  declare const Loader: ({ label }: LoaderProps) => react.JSX.Element | null;
461
443
 
462
- interface AppWrapperProps {
463
- children: ReactNode;
464
- reducers?: Record<string, Reducer>;
465
- middlewares?: Middleware[];
466
- coreReducers?: Record<string, Reducer>;
467
- }
468
-
469
- declare const AppWrapper: ({ children, reducers, middlewares, coreReducers, }: AppWrapperProps) => react.JSX.Element;
470
-
471
- interface DocumentWrapperProps {
472
- children: ReactNode;
473
- lang?: string;
474
- className?: string;
475
- }
476
-
477
- declare const DocumentWrapper: ({ children, lang, className, }: DocumentWrapperProps) => react.JSX.Element;
478
-
479
444
  interface HttpClientConfig {
480
445
  baseUrl?: string;
481
446
  headers?: Record<string, string>;
@@ -499,5 +464,5 @@ declare class HTTPClient {
499
464
  patch<T = unknown>(url: string, config?: HttpRequestConfig): Promise<T | undefined>;
500
465
  }
501
466
 
502
- export { AppWrapper, Button, Checkbox, ConnectedModal, DataColor, DataGrid, DocumentWrapper, Error, Form, HTTPClient, Heading, Icon, Input, Loader, Modal, PageWrapper, Radio, ReduxProvider, Select, SemanticColor, Sidebar, Spinner, StatsCard, Tag, Text, TextArea, Toaster, WhatsAppButton, splitURLasPath, useAppStore, useModalStore, usePageStore, useRouterStore, useToastStore };
503
- export type { AppState, AppWrapperProps, ButtonColor, ButtonProps, ButtonVariant, CheckboxProps, CheckboxVariant, ConnectedModalProps, DataGridColumn, DataGridProps, DocumentWrapperProps, ErrorProps, FormProps, HeadingProps, HttpClientConfig, HttpRequestConfig, IconName, IconProps, InitPageFunction, InputProps, LoaderProps, ModalEntry, ModalProps, ModalState, PageState, PageWrapperProps, RadioProps, ReduxProviderProps, ReduxStore, 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 };