@leduchai/vmeta-ui-shell 0.1.3

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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @leduchai/vmeta-ui-shell
2
+
3
+ Shell khong phu thuoc `react-router-dom`, `next/navigation`, Redux hay API cua san pham. Ung dung truyen metadata, menu, active path, permission resolver va navigation adapter qua props.
package/dist/index.cjs ADDED
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ "use client";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.tsx
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ VmetaAppShell: () => VmetaAppShell,
25
+ VmetaSidebar: () => VmetaSidebar,
26
+ isMenuItemActive: () => isMenuItemActive
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+ var import_react = require("react");
30
+ var import_jsx_runtime = require("react/jsx-runtime");
31
+ function isMenuItemActive(item, activePath) {
32
+ if (item.match === "exact") return activePath === item.href;
33
+ if (item.match === "prefix") return activePath === item.href || activePath.startsWith(`${item.href}/`);
34
+ if (!item.href.startsWith("/")) return activePath === item.href;
35
+ return activePath === item.href || activePath.startsWith(`${item.href}/`);
36
+ }
37
+ function readStoredBoolean(key, fallback) {
38
+ if (typeof window === "undefined") return fallback;
39
+ try {
40
+ const stored = window.localStorage.getItem(key);
41
+ return stored === null ? fallback : stored === "true";
42
+ } catch {
43
+ return fallback;
44
+ }
45
+ }
46
+ function writeStorage(key, value) {
47
+ if (typeof window === "undefined") return;
48
+ try {
49
+ window.localStorage.setItem(key, value);
50
+ } catch {
51
+ }
52
+ }
53
+ function initialsFromName(name) {
54
+ return name.trim().split(/\s+/).slice(-2).map((part) => part[0]?.toUpperCase() ?? "").join("");
55
+ }
56
+ function VmetaSidebar({
57
+ app,
58
+ groups,
59
+ activePath,
60
+ collapsed = false,
61
+ storageKey = `vmeta:${app.appId}:menu-groups`,
62
+ account,
63
+ footer,
64
+ showCollapse = false,
65
+ canAccess = () => true,
66
+ onNavigate,
67
+ onCollapsedChange
68
+ }) {
69
+ const visibleGroups = (0, import_react.useMemo)(
70
+ () => groups.map((group) => ({
71
+ ...group,
72
+ children: group.children.filter((item) => !item.permission || canAccess(item.permission))
73
+ })).filter((group) => group.children.length > 0).sort((left, right) => (left.order ?? 0) - (right.order ?? 0)),
74
+ [canAccess, groups]
75
+ );
76
+ const defaultOpenKeys = (0, import_react.useMemo)(() => visibleGroups.map((group) => group.key), [visibleGroups]);
77
+ const [openKeys, setOpenKeys] = (0, import_react.useState)(defaultOpenKeys);
78
+ (0, import_react.useEffect)(() => {
79
+ if (typeof window === "undefined") return;
80
+ try {
81
+ const stored = JSON.parse(window.localStorage.getItem(storageKey) ?? "null");
82
+ if (!Array.isArray(stored)) {
83
+ setOpenKeys(defaultOpenKeys);
84
+ return;
85
+ }
86
+ setOpenKeys(stored.filter((key) => typeof key === "string" && defaultOpenKeys.includes(key)));
87
+ } catch {
88
+ setOpenKeys(defaultOpenKeys);
89
+ }
90
+ }, [defaultOpenKeys, storageKey]);
91
+ const toggleGroup = (groupKey) => {
92
+ setOpenKeys((current) => {
93
+ const next = current.includes(groupKey) ? current.filter((key) => key !== groupKey) : [...current, groupKey];
94
+ writeStorage(storageKey, JSON.stringify(next));
95
+ return next;
96
+ });
97
+ };
98
+ const navigate = (item) => {
99
+ if (item.disabled) return;
100
+ if (onNavigate) {
101
+ onNavigate(item);
102
+ return;
103
+ }
104
+ if (typeof window === "undefined") return;
105
+ if (item.external) window.open(item.href, "_blank", "noopener,noreferrer");
106
+ else window.location.assign(item.href);
107
+ };
108
+ const menuItems = collapsed ? visibleGroups.flatMap((group) => group.children) : [];
109
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("aside", { className: `vmeta-sidebar ${collapsed ? "is-collapsed" : ""}`, "aria-label": `Dieu huong ${app.appName}`, children: [
110
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("a", { className: "vmeta-sidebar__brand", href: app.homeHref ?? "/", "aria-label": `${app.appName} - ${app.appSubtitle}`, children: [
111
+ app.logoSrc ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: app.logoSrc, alt: app.logoAlt ?? "", className: "vmeta-sidebar__logo" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "vmeta-sidebar__mark", "aria-hidden": "true", children: "V" }),
112
+ !collapsed && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "vmeta-sidebar__brand-copy", children: [
113
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: app.appName }),
114
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: app.appSubtitle })
115
+ ] })
116
+ ] }),
117
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", { className: "vmeta-sidebar__nav", children: collapsed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vmeta-sidebar__collapsed-items", children: menuItems.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VmetaMenuButton, { item, active: isMenuItemActive(item, activePath), collapsed: true, onNavigate: navigate }, item.key)) }) : visibleGroups.map((group) => {
118
+ const open = openKeys.includes(group.key);
119
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "vmeta-menu-group", children: [
120
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
121
+ "button",
122
+ {
123
+ type: "button",
124
+ className: "vmeta-menu-group__toggle",
125
+ onClick: () => toggleGroup(group.key),
126
+ "aria-expanded": open,
127
+ "aria-controls": `vmeta-menu-group-${group.key}`,
128
+ children: [
129
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: group.label }),
130
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `vmeta-menu-group__chevron ${open ? "is-open" : ""}`, "aria-hidden": "true", children: "\u2304" })
131
+ ]
132
+ }
133
+ ),
134
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { id: `vmeta-menu-group-${group.key}`, className: `vmeta-menu-group__items ${open ? "is-open" : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: group.children.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VmetaMenuButton, { item, active: isMenuItemActive(item, activePath), onNavigate: navigate }, item.key)) }) })
135
+ ] }, group.key);
136
+ }) }),
137
+ footer && !collapsed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vmeta-sidebar__footer", children: footer }),
138
+ account && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vmeta-sidebar__account", children: [
139
+ account.avatarSrc ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: account.avatarSrc, alt: "" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: account.initials ?? initialsFromName(account.name) }),
140
+ !collapsed && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
141
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: account.name }),
142
+ account.role && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: account.role })
143
+ ] }),
144
+ !collapsed && account.actions && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vmeta-sidebar__account-actions", children: account.actions })
145
+ ] }),
146
+ showCollapse && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
147
+ "button",
148
+ {
149
+ type: "button",
150
+ className: "vmeta-sidebar__collapse",
151
+ onClick: () => onCollapsedChange?.(!collapsed),
152
+ "aria-label": collapsed ? "Mo rong thanh dieu huong" : "Thu gon thanh dieu huong",
153
+ title: collapsed ? "Mo rong menu" : "Thu gon menu",
154
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: collapsed ? "\u203A" : "\u2039" })
155
+ }
156
+ )
157
+ ] });
158
+ }
159
+ function VmetaMenuButton({
160
+ item,
161
+ active,
162
+ collapsed = false,
163
+ onNavigate
164
+ }) {
165
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
166
+ "button",
167
+ {
168
+ type: "button",
169
+ className: `vmeta-menu-item ${active ? "is-active" : ""}`,
170
+ onClick: () => onNavigate(item),
171
+ disabled: item.disabled,
172
+ "aria-current": active ? "page" : void 0,
173
+ title: collapsed ? item.label : void 0,
174
+ children: [
175
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "vmeta-menu-item__icon", "aria-hidden": "true", children: item.icon ?? item.label.slice(0, 1) }),
176
+ !collapsed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "vmeta-menu-item__label", children: item.label }),
177
+ !collapsed && item.external && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "vmeta-menu-item__external", "aria-hidden": "true", children: "\u2197" })
178
+ ]
179
+ }
180
+ );
181
+ }
182
+ function VmetaAppShell({
183
+ app,
184
+ groups,
185
+ activePath,
186
+ header,
187
+ children,
188
+ account,
189
+ sidebarFooter,
190
+ contentClassName,
191
+ storageKey = `vmeta:${app.appId}:sidebar-collapsed`,
192
+ canAccess,
193
+ onNavigate
194
+ }) {
195
+ const [collapsed, setCollapsed] = (0, import_react.useState)(() => readStoredBoolean(storageKey, false));
196
+ const [menuOpen, setMenuOpen] = (0, import_react.useState)(false);
197
+ const updateCollapsed = (value) => {
198
+ setCollapsed(value);
199
+ writeStorage(storageKey, String(value));
200
+ };
201
+ const navigate = (item) => {
202
+ setMenuOpen(false);
203
+ onNavigate?.(item);
204
+ };
205
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("main", { className: `vmeta-app-shell ${collapsed ? "is-collapsed" : ""}`, children: [
206
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
207
+ "button",
208
+ {
209
+ type: "button",
210
+ className: `vmeta-app-shell__overlay ${menuOpen ? "is-open" : ""}`,
211
+ onClick: () => setMenuOpen(false),
212
+ "aria-label": "Dong menu dieu huong"
213
+ }
214
+ ),
215
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `vmeta-app-shell__sidebar ${menuOpen ? "is-open" : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
216
+ VmetaSidebar,
217
+ {
218
+ app,
219
+ groups,
220
+ activePath,
221
+ collapsed,
222
+ account,
223
+ footer: sidebarFooter,
224
+ showCollapse: true,
225
+ canAccess,
226
+ onNavigate: navigate,
227
+ onCollapsedChange: updateCollapsed
228
+ }
229
+ ) }),
230
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `vmeta-app-shell__mobile-sidebar ${menuOpen ? "is-open" : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
231
+ VmetaSidebar,
232
+ {
233
+ app,
234
+ groups,
235
+ activePath,
236
+ collapsed: false,
237
+ account,
238
+ footer: sidebarFooter,
239
+ canAccess,
240
+ onNavigate: navigate
241
+ }
242
+ ) }),
243
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "vmeta-app-shell__workspace", children: [
244
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { className: "vmeta-app-shell__header", children: [
245
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vmeta-app-shell__header-copy", children: [
246
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "vmeta-app-shell__menu-button", onClick: () => setMenuOpen(true), "aria-label": "Mo menu dieu huong", children: "\u2630" }),
247
+ header.leading,
248
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vmeta-app-shell__title", children: [
249
+ header.eyebrow && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: header.eyebrow }),
250
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { children: header.title }),
251
+ header.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: header.description })
252
+ ] })
253
+ ] }),
254
+ header.actions && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vmeta-app-shell__header-actions", children: header.actions })
255
+ ] }),
256
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: ["vmeta-app-shell__content", contentClassName].filter(Boolean).join(" "), children })
257
+ ] })
258
+ ] });
259
+ }
260
+ // Annotate the CommonJS export names for ESM import in node:
261
+ 0 && (module.exports = {
262
+ VmetaAppShell,
263
+ VmetaSidebar,
264
+ isMenuItemActive
265
+ });
@@ -0,0 +1,74 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ type VmetaMenuMatch = 'exact' | 'prefix';
5
+ type VmetaMenuGroup = {
6
+ key: string;
7
+ label: string;
8
+ order?: number;
9
+ children: VmetaMenuItem[];
10
+ };
11
+ type VmetaMenuItem = {
12
+ key: string;
13
+ label: string;
14
+ href: string;
15
+ icon?: ReactNode;
16
+ permission?: string | string[];
17
+ match?: VmetaMenuMatch;
18
+ external?: boolean;
19
+ disabled?: boolean;
20
+ };
21
+ type VmetaAppIdentity = {
22
+ appId: string;
23
+ appName: string;
24
+ appSubtitle: string;
25
+ homeHref?: string;
26
+ logoSrc?: string;
27
+ logoAlt?: string;
28
+ };
29
+ type VmetaAccount = {
30
+ name: string;
31
+ role?: string;
32
+ avatarSrc?: string;
33
+ initials?: string;
34
+ actions?: ReactNode;
35
+ };
36
+ type VmetaHeader = {
37
+ eyebrow?: ReactNode;
38
+ title: ReactNode;
39
+ description?: ReactNode;
40
+ actions?: ReactNode;
41
+ leading?: ReactNode;
42
+ };
43
+ type VmetaPermissionResolver = (permission: string | string[]) => boolean;
44
+ declare function isMenuItemActive(item: VmetaMenuItem, activePath: string): boolean;
45
+ interface VmetaSidebarProps {
46
+ app: VmetaAppIdentity;
47
+ groups: VmetaMenuGroup[];
48
+ activePath: string;
49
+ collapsed?: boolean;
50
+ storageKey?: string;
51
+ account?: VmetaAccount;
52
+ footer?: ReactNode;
53
+ showCollapse?: boolean;
54
+ canAccess?: VmetaPermissionResolver;
55
+ onNavigate?: (item: VmetaMenuItem) => void;
56
+ onCollapsedChange?: (collapsed: boolean) => void;
57
+ }
58
+ declare function VmetaSidebar({ app, groups, activePath, collapsed, storageKey, account, footer, showCollapse, canAccess, onNavigate, onCollapsedChange, }: VmetaSidebarProps): react_jsx_runtime.JSX.Element;
59
+ interface VmetaAppShellProps {
60
+ app: VmetaAppIdentity;
61
+ groups: VmetaMenuGroup[];
62
+ activePath: string;
63
+ header: VmetaHeader;
64
+ children: ReactNode;
65
+ account?: VmetaAccount;
66
+ sidebarFooter?: ReactNode;
67
+ contentClassName?: string;
68
+ storageKey?: string;
69
+ canAccess?: VmetaPermissionResolver;
70
+ onNavigate?: (item: VmetaMenuItem) => void;
71
+ }
72
+ declare function VmetaAppShell({ app, groups, activePath, header, children, account, sidebarFooter, contentClassName, storageKey, canAccess, onNavigate, }: VmetaAppShellProps): react_jsx_runtime.JSX.Element;
73
+
74
+ export { type VmetaAccount, type VmetaAppIdentity, VmetaAppShell, type VmetaAppShellProps, type VmetaHeader, type VmetaMenuGroup, type VmetaMenuItem, type VmetaMenuMatch, type VmetaPermissionResolver, VmetaSidebar, type VmetaSidebarProps, isMenuItemActive };
@@ -0,0 +1,74 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ type VmetaMenuMatch = 'exact' | 'prefix';
5
+ type VmetaMenuGroup = {
6
+ key: string;
7
+ label: string;
8
+ order?: number;
9
+ children: VmetaMenuItem[];
10
+ };
11
+ type VmetaMenuItem = {
12
+ key: string;
13
+ label: string;
14
+ href: string;
15
+ icon?: ReactNode;
16
+ permission?: string | string[];
17
+ match?: VmetaMenuMatch;
18
+ external?: boolean;
19
+ disabled?: boolean;
20
+ };
21
+ type VmetaAppIdentity = {
22
+ appId: string;
23
+ appName: string;
24
+ appSubtitle: string;
25
+ homeHref?: string;
26
+ logoSrc?: string;
27
+ logoAlt?: string;
28
+ };
29
+ type VmetaAccount = {
30
+ name: string;
31
+ role?: string;
32
+ avatarSrc?: string;
33
+ initials?: string;
34
+ actions?: ReactNode;
35
+ };
36
+ type VmetaHeader = {
37
+ eyebrow?: ReactNode;
38
+ title: ReactNode;
39
+ description?: ReactNode;
40
+ actions?: ReactNode;
41
+ leading?: ReactNode;
42
+ };
43
+ type VmetaPermissionResolver = (permission: string | string[]) => boolean;
44
+ declare function isMenuItemActive(item: VmetaMenuItem, activePath: string): boolean;
45
+ interface VmetaSidebarProps {
46
+ app: VmetaAppIdentity;
47
+ groups: VmetaMenuGroup[];
48
+ activePath: string;
49
+ collapsed?: boolean;
50
+ storageKey?: string;
51
+ account?: VmetaAccount;
52
+ footer?: ReactNode;
53
+ showCollapse?: boolean;
54
+ canAccess?: VmetaPermissionResolver;
55
+ onNavigate?: (item: VmetaMenuItem) => void;
56
+ onCollapsedChange?: (collapsed: boolean) => void;
57
+ }
58
+ declare function VmetaSidebar({ app, groups, activePath, collapsed, storageKey, account, footer, showCollapse, canAccess, onNavigate, onCollapsedChange, }: VmetaSidebarProps): react_jsx_runtime.JSX.Element;
59
+ interface VmetaAppShellProps {
60
+ app: VmetaAppIdentity;
61
+ groups: VmetaMenuGroup[];
62
+ activePath: string;
63
+ header: VmetaHeader;
64
+ children: ReactNode;
65
+ account?: VmetaAccount;
66
+ sidebarFooter?: ReactNode;
67
+ contentClassName?: string;
68
+ storageKey?: string;
69
+ canAccess?: VmetaPermissionResolver;
70
+ onNavigate?: (item: VmetaMenuItem) => void;
71
+ }
72
+ declare function VmetaAppShell({ app, groups, activePath, header, children, account, sidebarFooter, contentClassName, storageKey, canAccess, onNavigate, }: VmetaAppShellProps): react_jsx_runtime.JSX.Element;
73
+
74
+ export { type VmetaAccount, type VmetaAppIdentity, VmetaAppShell, type VmetaAppShellProps, type VmetaHeader, type VmetaMenuGroup, type VmetaMenuItem, type VmetaMenuMatch, type VmetaPermissionResolver, VmetaSidebar, type VmetaSidebarProps, isMenuItemActive };
package/dist/index.js ADDED
@@ -0,0 +1,239 @@
1
+ "use client";
2
+
3
+ // src/index.tsx
4
+ import { useEffect, useMemo, useState } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ function isMenuItemActive(item, activePath) {
7
+ if (item.match === "exact") return activePath === item.href;
8
+ if (item.match === "prefix") return activePath === item.href || activePath.startsWith(`${item.href}/`);
9
+ if (!item.href.startsWith("/")) return activePath === item.href;
10
+ return activePath === item.href || activePath.startsWith(`${item.href}/`);
11
+ }
12
+ function readStoredBoolean(key, fallback) {
13
+ if (typeof window === "undefined") return fallback;
14
+ try {
15
+ const stored = window.localStorage.getItem(key);
16
+ return stored === null ? fallback : stored === "true";
17
+ } catch {
18
+ return fallback;
19
+ }
20
+ }
21
+ function writeStorage(key, value) {
22
+ if (typeof window === "undefined") return;
23
+ try {
24
+ window.localStorage.setItem(key, value);
25
+ } catch {
26
+ }
27
+ }
28
+ function initialsFromName(name) {
29
+ return name.trim().split(/\s+/).slice(-2).map((part) => part[0]?.toUpperCase() ?? "").join("");
30
+ }
31
+ function VmetaSidebar({
32
+ app,
33
+ groups,
34
+ activePath,
35
+ collapsed = false,
36
+ storageKey = `vmeta:${app.appId}:menu-groups`,
37
+ account,
38
+ footer,
39
+ showCollapse = false,
40
+ canAccess = () => true,
41
+ onNavigate,
42
+ onCollapsedChange
43
+ }) {
44
+ const visibleGroups = useMemo(
45
+ () => groups.map((group) => ({
46
+ ...group,
47
+ children: group.children.filter((item) => !item.permission || canAccess(item.permission))
48
+ })).filter((group) => group.children.length > 0).sort((left, right) => (left.order ?? 0) - (right.order ?? 0)),
49
+ [canAccess, groups]
50
+ );
51
+ const defaultOpenKeys = useMemo(() => visibleGroups.map((group) => group.key), [visibleGroups]);
52
+ const [openKeys, setOpenKeys] = useState(defaultOpenKeys);
53
+ useEffect(() => {
54
+ if (typeof window === "undefined") return;
55
+ try {
56
+ const stored = JSON.parse(window.localStorage.getItem(storageKey) ?? "null");
57
+ if (!Array.isArray(stored)) {
58
+ setOpenKeys(defaultOpenKeys);
59
+ return;
60
+ }
61
+ setOpenKeys(stored.filter((key) => typeof key === "string" && defaultOpenKeys.includes(key)));
62
+ } catch {
63
+ setOpenKeys(defaultOpenKeys);
64
+ }
65
+ }, [defaultOpenKeys, storageKey]);
66
+ const toggleGroup = (groupKey) => {
67
+ setOpenKeys((current) => {
68
+ const next = current.includes(groupKey) ? current.filter((key) => key !== groupKey) : [...current, groupKey];
69
+ writeStorage(storageKey, JSON.stringify(next));
70
+ return next;
71
+ });
72
+ };
73
+ const navigate = (item) => {
74
+ if (item.disabled) return;
75
+ if (onNavigate) {
76
+ onNavigate(item);
77
+ return;
78
+ }
79
+ if (typeof window === "undefined") return;
80
+ if (item.external) window.open(item.href, "_blank", "noopener,noreferrer");
81
+ else window.location.assign(item.href);
82
+ };
83
+ const menuItems = collapsed ? visibleGroups.flatMap((group) => group.children) : [];
84
+ return /* @__PURE__ */ jsxs("aside", { className: `vmeta-sidebar ${collapsed ? "is-collapsed" : ""}`, "aria-label": `Dieu huong ${app.appName}`, children: [
85
+ /* @__PURE__ */ jsxs("a", { className: "vmeta-sidebar__brand", href: app.homeHref ?? "/", "aria-label": `${app.appName} - ${app.appSubtitle}`, children: [
86
+ app.logoSrc ? /* @__PURE__ */ jsx("img", { src: app.logoSrc, alt: app.logoAlt ?? "", className: "vmeta-sidebar__logo" }) : /* @__PURE__ */ jsx("span", { className: "vmeta-sidebar__mark", "aria-hidden": "true", children: "V" }),
87
+ !collapsed && /* @__PURE__ */ jsxs("span", { className: "vmeta-sidebar__brand-copy", children: [
88
+ /* @__PURE__ */ jsx("strong", { children: app.appName }),
89
+ /* @__PURE__ */ jsx("small", { children: app.appSubtitle })
90
+ ] })
91
+ ] }),
92
+ /* @__PURE__ */ jsx("nav", { className: "vmeta-sidebar__nav", children: collapsed ? /* @__PURE__ */ jsx("div", { className: "vmeta-sidebar__collapsed-items", children: menuItems.map((item) => /* @__PURE__ */ jsx(VmetaMenuButton, { item, active: isMenuItemActive(item, activePath), collapsed: true, onNavigate: navigate }, item.key)) }) : visibleGroups.map((group) => {
93
+ const open = openKeys.includes(group.key);
94
+ return /* @__PURE__ */ jsxs("section", { className: "vmeta-menu-group", children: [
95
+ /* @__PURE__ */ jsxs(
96
+ "button",
97
+ {
98
+ type: "button",
99
+ className: "vmeta-menu-group__toggle",
100
+ onClick: () => toggleGroup(group.key),
101
+ "aria-expanded": open,
102
+ "aria-controls": `vmeta-menu-group-${group.key}`,
103
+ children: [
104
+ /* @__PURE__ */ jsx("span", { children: group.label }),
105
+ /* @__PURE__ */ jsx("span", { className: `vmeta-menu-group__chevron ${open ? "is-open" : ""}`, "aria-hidden": "true", children: "\u2304" })
106
+ ]
107
+ }
108
+ ),
109
+ /* @__PURE__ */ jsx("div", { id: `vmeta-menu-group-${group.key}`, className: `vmeta-menu-group__items ${open ? "is-open" : ""}`, children: /* @__PURE__ */ jsx("div", { children: group.children.map((item) => /* @__PURE__ */ jsx(VmetaMenuButton, { item, active: isMenuItemActive(item, activePath), onNavigate: navigate }, item.key)) }) })
110
+ ] }, group.key);
111
+ }) }),
112
+ footer && !collapsed && /* @__PURE__ */ jsx("div", { className: "vmeta-sidebar__footer", children: footer }),
113
+ account && /* @__PURE__ */ jsxs("div", { className: "vmeta-sidebar__account", children: [
114
+ account.avatarSrc ? /* @__PURE__ */ jsx("img", { src: account.avatarSrc, alt: "" }) : /* @__PURE__ */ jsx("span", { children: account.initials ?? initialsFromName(account.name) }),
115
+ !collapsed && /* @__PURE__ */ jsxs("div", { children: [
116
+ /* @__PURE__ */ jsx("strong", { children: account.name }),
117
+ account.role && /* @__PURE__ */ jsx("small", { children: account.role })
118
+ ] }),
119
+ !collapsed && account.actions && /* @__PURE__ */ jsx("div", { className: "vmeta-sidebar__account-actions", children: account.actions })
120
+ ] }),
121
+ showCollapse && /* @__PURE__ */ jsx(
122
+ "button",
123
+ {
124
+ type: "button",
125
+ className: "vmeta-sidebar__collapse",
126
+ onClick: () => onCollapsedChange?.(!collapsed),
127
+ "aria-label": collapsed ? "Mo rong thanh dieu huong" : "Thu gon thanh dieu huong",
128
+ title: collapsed ? "Mo rong menu" : "Thu gon menu",
129
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: collapsed ? "\u203A" : "\u2039" })
130
+ }
131
+ )
132
+ ] });
133
+ }
134
+ function VmetaMenuButton({
135
+ item,
136
+ active,
137
+ collapsed = false,
138
+ onNavigate
139
+ }) {
140
+ return /* @__PURE__ */ jsxs(
141
+ "button",
142
+ {
143
+ type: "button",
144
+ className: `vmeta-menu-item ${active ? "is-active" : ""}`,
145
+ onClick: () => onNavigate(item),
146
+ disabled: item.disabled,
147
+ "aria-current": active ? "page" : void 0,
148
+ title: collapsed ? item.label : void 0,
149
+ children: [
150
+ /* @__PURE__ */ jsx("span", { className: "vmeta-menu-item__icon", "aria-hidden": "true", children: item.icon ?? item.label.slice(0, 1) }),
151
+ !collapsed && /* @__PURE__ */ jsx("span", { className: "vmeta-menu-item__label", children: item.label }),
152
+ !collapsed && item.external && /* @__PURE__ */ jsx("span", { className: "vmeta-menu-item__external", "aria-hidden": "true", children: "\u2197" })
153
+ ]
154
+ }
155
+ );
156
+ }
157
+ function VmetaAppShell({
158
+ app,
159
+ groups,
160
+ activePath,
161
+ header,
162
+ children,
163
+ account,
164
+ sidebarFooter,
165
+ contentClassName,
166
+ storageKey = `vmeta:${app.appId}:sidebar-collapsed`,
167
+ canAccess,
168
+ onNavigate
169
+ }) {
170
+ const [collapsed, setCollapsed] = useState(() => readStoredBoolean(storageKey, false));
171
+ const [menuOpen, setMenuOpen] = useState(false);
172
+ const updateCollapsed = (value) => {
173
+ setCollapsed(value);
174
+ writeStorage(storageKey, String(value));
175
+ };
176
+ const navigate = (item) => {
177
+ setMenuOpen(false);
178
+ onNavigate?.(item);
179
+ };
180
+ return /* @__PURE__ */ jsxs("main", { className: `vmeta-app-shell ${collapsed ? "is-collapsed" : ""}`, children: [
181
+ /* @__PURE__ */ jsx(
182
+ "button",
183
+ {
184
+ type: "button",
185
+ className: `vmeta-app-shell__overlay ${menuOpen ? "is-open" : ""}`,
186
+ onClick: () => setMenuOpen(false),
187
+ "aria-label": "Dong menu dieu huong"
188
+ }
189
+ ),
190
+ /* @__PURE__ */ jsx("div", { className: `vmeta-app-shell__sidebar ${menuOpen ? "is-open" : ""}`, children: /* @__PURE__ */ jsx(
191
+ VmetaSidebar,
192
+ {
193
+ app,
194
+ groups,
195
+ activePath,
196
+ collapsed,
197
+ account,
198
+ footer: sidebarFooter,
199
+ showCollapse: true,
200
+ canAccess,
201
+ onNavigate: navigate,
202
+ onCollapsedChange: updateCollapsed
203
+ }
204
+ ) }),
205
+ /* @__PURE__ */ jsx("div", { className: `vmeta-app-shell__mobile-sidebar ${menuOpen ? "is-open" : ""}`, children: /* @__PURE__ */ jsx(
206
+ VmetaSidebar,
207
+ {
208
+ app,
209
+ groups,
210
+ activePath,
211
+ collapsed: false,
212
+ account,
213
+ footer: sidebarFooter,
214
+ canAccess,
215
+ onNavigate: navigate
216
+ }
217
+ ) }),
218
+ /* @__PURE__ */ jsxs("section", { className: "vmeta-app-shell__workspace", children: [
219
+ /* @__PURE__ */ jsxs("header", { className: "vmeta-app-shell__header", children: [
220
+ /* @__PURE__ */ jsxs("div", { className: "vmeta-app-shell__header-copy", children: [
221
+ /* @__PURE__ */ jsx("button", { type: "button", className: "vmeta-app-shell__menu-button", onClick: () => setMenuOpen(true), "aria-label": "Mo menu dieu huong", children: "\u2630" }),
222
+ header.leading,
223
+ /* @__PURE__ */ jsxs("div", { className: "vmeta-app-shell__title", children: [
224
+ header.eyebrow && /* @__PURE__ */ jsx("p", { children: header.eyebrow }),
225
+ /* @__PURE__ */ jsx("h1", { children: header.title }),
226
+ header.description && /* @__PURE__ */ jsx("span", { children: header.description })
227
+ ] })
228
+ ] }),
229
+ header.actions && /* @__PURE__ */ jsx("div", { className: "vmeta-app-shell__header-actions", children: header.actions })
230
+ ] }),
231
+ /* @__PURE__ */ jsx("div", { className: ["vmeta-app-shell__content", contentClassName].filter(Boolean).join(" "), children })
232
+ ] })
233
+ ] });
234
+ }
235
+ export {
236
+ VmetaAppShell,
237
+ VmetaSidebar,
238
+ isMenuItemActive
239
+ };
@@ -0,0 +1,80 @@
1
+ .vmeta-app-shell { min-height: 100dvh; color: var(--color-ink); background: var(--color-canvas); font-family: var(--font-sans, sans-serif); }
2
+ .vmeta-app-shell__sidebar { position: fixed; inset: 0 auto 0 0; z-index: var(--z-sidebar, 50); width: var(--sidebar-width, 264px); transition: width var(--motion-normal) var(--motion-easing), transform var(--motion-normal) var(--motion-easing); }
3
+ .vmeta-app-shell__mobile-sidebar { display: none; }
4
+ .vmeta-app-shell.is-collapsed .vmeta-app-shell__sidebar { width: var(--sidebar-collapsed-width, 80px); }
5
+ .vmeta-app-shell__workspace { min-width: 0; min-height: 100dvh; margin-left: var(--sidebar-width, 264px); transition: margin-left var(--motion-normal) var(--motion-easing); }
6
+ .vmeta-app-shell.is-collapsed .vmeta-app-shell__workspace { margin-left: var(--sidebar-collapsed-width, 80px); }
7
+ .vmeta-app-shell__header { position: sticky; top: 0; z-index: var(--z-header, 40); min-height: 82px; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 14px var(--page-gutter-desktop, 28px); border-bottom: 1px solid var(--color-border); background: color-mix(in srgb, var(--color-surface) 94%, transparent); box-shadow: var(--shadow-sm); backdrop-filter: blur(16px); }
8
+ .vmeta-app-shell__header-copy { min-width: 0; display: flex; align-items: center; gap: 12px; }
9
+ .vmeta-app-shell__title { min-width: 0; }
10
+ .vmeta-app-shell__title p { margin: 0; color: var(--color-primary); font-size: 10px; font-weight: 750; letter-spacing: .13em; text-transform: uppercase; }
11
+ .vmeta-app-shell__title h1 { margin: 3px 0 0; color: var(--color-ink); font-size: clamp(20px, 2.5vw, 26px); line-height: 1.2; letter-spacing: -.035em; }
12
+ .vmeta-app-shell__title span { display: block; max-width: 740px; margin-top: 4px; overflow: hidden; color: var(--color-muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
13
+ .vmeta-app-shell__header-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }
14
+ .vmeta-app-shell__menu-button { display: none; width: 40px; height: 40px; place-items: center; border: 1px solid var(--color-border); border-radius: var(--radius-md); color: var(--color-ink); background: var(--color-surface); font-size: 18px; }
15
+ .vmeta-app-shell__content { min-width: 0; padding: var(--page-gutter-tablet, 20px) var(--page-gutter-desktop, 28px) 48px; }
16
+ .vmeta-app-shell__overlay { display: none; }
17
+ .vmeta-sidebar { height: 100%; min-height: 0; display: flex; flex-direction: column; overflow: hidden; border-right: 1px solid var(--color-border); color: var(--color-ink); background: var(--color-surface); box-shadow: var(--shadow-md); }
18
+ .vmeta-sidebar__brand { min-height: 82px; display: flex; align-items: center; gap: 11px; margin: 10px; padding: 10px 12px; border: 1px solid var(--color-border); border-radius: 20px; color: inherit; text-decoration: none; background: var(--color-surface); box-shadow: 0 5px 18px rgb(7 19 13 / 5%); transition: border-color var(--motion-fast), box-shadow var(--motion-fast), transform var(--motion-fast); }
19
+ .vmeta-sidebar__brand:hover { border-color: color-mix(in srgb, var(--color-primary) 25%, var(--color-border)); box-shadow: var(--shadow-sm); transform: translateY(-1px); }
20
+ .vmeta-sidebar__logo { width: 42px; height: 42px; flex: 0 0 42px; object-fit: contain; }
21
+ .vmeta-sidebar__mark { width: 42px; height: 42px; flex: 0 0 42px; display: grid; place-items: center; color: var(--color-primary); font-size: 34px; font-weight: 850; letter-spacing: -.13em; transform: translateX(-2px); }
22
+ .vmeta-sidebar__brand-copy { min-width: 0; line-height: 1.15; }
23
+ .vmeta-sidebar__brand-copy strong, .vmeta-sidebar__brand-copy small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
24
+ .vmeta-sidebar__brand-copy strong { color: var(--color-primary); font-size: 16px; font-weight: 800; letter-spacing: -.025em; }
25
+ .vmeta-sidebar__brand-copy small { margin-top: 5px; color: var(--color-muted); font-size: 8px; font-weight: 650; letter-spacing: .12em; text-transform: uppercase; }
26
+ .vmeta-sidebar__nav { min-height: 0; flex: 1; padding: 0 7px 16px 9px; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--color-border-strong) transparent; }
27
+ .vmeta-sidebar__nav::-webkit-scrollbar { width: 4px; }
28
+ .vmeta-sidebar__nav::-webkit-scrollbar-thumb { border-radius: var(--radius-pill); background: var(--color-border-strong); }
29
+ .vmeta-menu-group + .vmeta-menu-group { margin-top: 2px; }
30
+ .vmeta-menu-group__toggle { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 14px 10px 6px; border: 0; color: #82968f; background: transparent; font: 750 9px/1.35 var(--font-sans, sans-serif); letter-spacing: .15em; text-align: left; text-transform: uppercase; }
31
+ .vmeta-menu-group__toggle:hover { color: var(--color-primary); }
32
+ .vmeta-menu-group__chevron { font-size: 13px; transform: rotate(-90deg); transition: transform var(--motion-normal) var(--motion-easing); }
33
+ .vmeta-menu-group__chevron.is-open { transform: rotate(0); }
34
+ .vmeta-menu-group__items { display: grid; grid-template-rows: 0fr; opacity: 0; visibility: hidden; transition: grid-template-rows var(--motion-normal) var(--motion-easing), opacity var(--motion-fast) var(--motion-easing), visibility 0s linear var(--motion-normal); }
35
+ .vmeta-menu-group__items > div { min-height: 0; overflow: hidden; }
36
+ .vmeta-menu-group__items.is-open { grid-template-rows: 1fr; opacity: 1; visibility: visible; transition-delay: 0s; }
37
+ .vmeta-menu-item { width: 100%; min-height: 43px; display: flex; align-items: center; gap: 11px; margin: 2px 0; padding: 7px 11px; border: 0; border-radius: var(--radius-md); color: var(--color-muted); background: transparent; font: 600 12px/1.35 var(--font-sans, sans-serif); text-align: left; transition: color var(--motion-fast), background var(--motion-fast), box-shadow var(--motion-fast); }
38
+ .vmeta-menu-item:hover:not(:disabled) { color: var(--color-primary-dark); background: var(--color-muted-surface); }
39
+ .vmeta-menu-item.is-active { color: var(--color-primary-dark); background: var(--color-primary-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 9%, transparent); }
40
+ .vmeta-menu-item:focus-visible, .vmeta-menu-group__toggle:focus-visible, .vmeta-sidebar__collapse:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
41
+ .vmeta-menu-item:disabled { cursor: not-allowed; opacity: .48; }
42
+ .vmeta-menu-item__icon { width: 20px; height: 20px; flex: 0 0 20px; display: grid; place-items: center; color: var(--color-subtle); font-size: 16px; }
43
+ .vmeta-menu-item__icon > img, .vmeta-menu-item__icon > svg { width: 18px; height: 18px; object-fit: contain; }
44
+ .vmeta-menu-item.is-active .vmeta-menu-item__icon { color: var(--color-primary); }
45
+ .vmeta-menu-item__label { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
46
+ .vmeta-menu-item__external { color: var(--color-subtle); }
47
+ .vmeta-sidebar__footer { margin: 0 12px 10px; padding: 12px; border: 1px solid color-mix(in srgb, var(--color-primary) 16%, var(--color-border)); border-radius: var(--radius-md); color: var(--color-muted); background: color-mix(in srgb, var(--color-primary-soft) 65%, var(--color-surface)); font-size: 11px; line-height: 1.5; }
48
+ .vmeta-sidebar__account { min-height: 64px; display: flex; align-items: center; gap: 9px; margin: 0 12px; padding: 11px 2px; border-top: 1px solid var(--color-border); }
49
+ .vmeta-sidebar__account > img, .vmeta-sidebar__account > span { width: 34px; height: 34px; flex: 0 0 34px; display: grid; place-items: center; border-radius: 10px; color: var(--color-surface); background: var(--color-primary); object-fit: cover; font-size: 10px; font-weight: 700; }
50
+ .vmeta-sidebar__account > div:not(.vmeta-sidebar__account-actions) { min-width: 0; flex: 1; }
51
+ .vmeta-sidebar__account strong, .vmeta-sidebar__account small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
52
+ .vmeta-sidebar__account strong { font-size: 11px; }
53
+ .vmeta-sidebar__account small { margin-top: 2px; color: var(--color-muted); font-size: 9px; }
54
+ .vmeta-sidebar__account-actions { flex: 0 0 auto; }
55
+ .vmeta-sidebar__collapse { min-height: 46px; display: grid; place-items: center; border: 0; border-top: 1px solid var(--color-border); color: var(--color-muted); background: transparent; font-size: 22px; }
56
+ .vmeta-sidebar__collapse:hover { color: var(--color-primary); background: var(--color-primary-soft); }
57
+ .vmeta-sidebar.is-collapsed .vmeta-sidebar__brand { justify-content: center; min-height: 60px; margin: 9px; padding: 8px; }
58
+ .vmeta-sidebar.is-collapsed .vmeta-sidebar__logo, .vmeta-sidebar.is-collapsed .vmeta-sidebar__mark { width: 36px; height: 36px; flex-basis: 36px; }
59
+ .vmeta-sidebar.is-collapsed .vmeta-sidebar__nav { padding-inline: 9px; }
60
+ .vmeta-sidebar__collapsed-items { display: grid; gap: 4px; }
61
+ .vmeta-sidebar.is-collapsed .vmeta-menu-item { justify-content: center; padding-inline: 8px; }
62
+ .vmeta-sidebar.is-collapsed .vmeta-sidebar__account { justify-content: center; margin-inline: 9px; }
63
+ @media (max-width: 1023px) {
64
+ .vmeta-app-shell__sidebar { display: none; }
65
+ .vmeta-app-shell__mobile-sidebar { position: fixed; inset: 0 auto 0 0; z-index: var(--z-drawer, 80); width: min(320px, 88vw); display: block; transform: translateX(-105%); transition: transform var(--motion-normal) var(--motion-easing); }
66
+ .vmeta-app-shell__mobile-sidebar.is-open { transform: translateX(0); }
67
+ .vmeta-app-shell__workspace, .vmeta-app-shell.is-collapsed .vmeta-app-shell__workspace { margin-left: 0; }
68
+ .vmeta-app-shell__menu-button { display: grid; }
69
+ .vmeta-app-shell__header { padding-inline: var(--page-gutter-tablet, 20px); }
70
+ .vmeta-app-shell__overlay { position: fixed; inset: 0; z-index: var(--z-overlay, 70); display: block; border: 0; background: var(--color-overlay); opacity: 0; pointer-events: none; transition: opacity var(--motion-normal); }
71
+ .vmeta-app-shell__overlay.is-open { opacity: 1; pointer-events: auto; }
72
+ .vmeta-sidebar__collapse { display: none; }
73
+ }
74
+ @media (max-width: 639px) {
75
+ .vmeta-app-shell__header { min-height: 74px; align-items: center; padding: 11px var(--page-gutter-mobile, 12px); }
76
+ .vmeta-app-shell__title p, .vmeta-app-shell__title span { display: none; }
77
+ .vmeta-app-shell__title h1 { font-size: 19px; }
78
+ .vmeta-app-shell__header-actions { max-width: 46%; }
79
+ .vmeta-app-shell__content { padding: var(--page-gutter-mobile, 12px) var(--page-gutter-mobile, 12px) 36px; }
80
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@leduchai/vmeta-ui-shell",
3
+ "version": "0.1.3",
4
+ "description": "Application shell, sidebar, menu va header dung chung cua VMETA.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": ["./dist/styles.css"],
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ },
17
+ "./styles.css": "./dist/styles.css"
18
+ },
19
+ "files": ["dist", "README.md"],
20
+ "scripts": {
21
+ "build": "tsup src/index.tsx --format esm,cjs --dts --clean --external react --external react-dom && node ../../scripts/copy-file.mjs src/styles.css dist/styles.css"
22
+ },
23
+ "dependencies": {
24
+ "@leduchai/vmeta-ui-tokens": "0.1.3"
25
+ },
26
+ "peerDependencies": {
27
+ "react": ">=18 <20",
28
+ "react-dom": ">=18 <20"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ }
33
+ }