@leduchai/vmeta-ui-shell 0.1.3 → 0.1.5

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.cjs CHANGED
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  module.exports = __toCommonJS(index_exports);
29
29
  var import_react = require("react");
30
30
  var import_jsx_runtime = require("react/jsx-runtime");
31
+ var allowAllPermissions = () => true;
31
32
  function isMenuItemActive(item, activePath) {
32
33
  if (item.match === "exact") return activePath === item.href;
33
34
  if (item.match === "prefix") return activePath === item.href || activePath.startsWith(`${item.href}/`);
@@ -59,13 +60,16 @@ function VmetaSidebar({
59
60
  activePath,
60
61
  collapsed = false,
61
62
  storageKey = `vmeta:${app.appId}:menu-groups`,
63
+ idPrefix,
62
64
  account,
63
65
  footer,
64
66
  showCollapse = false,
65
- canAccess = () => true,
67
+ canAccess = allowAllPermissions,
66
68
  onNavigate,
67
69
  onCollapsedChange
68
70
  }) {
71
+ const generatedId = (0, import_react.useId)().replace(/:/g, "");
72
+ const groupIdPrefix = idPrefix ?? `vmeta-menu-${app.appId}-${generatedId}`;
69
73
  const visibleGroups = (0, import_react.useMemo)(
70
74
  () => groups.map((group) => ({
71
75
  ...group,
@@ -83,7 +87,10 @@ function VmetaSidebar({
83
87
  setOpenKeys(defaultOpenKeys);
84
88
  return;
85
89
  }
86
- setOpenKeys(stored.filter((key) => typeof key === "string" && defaultOpenKeys.includes(key)));
90
+ const nextOpenKeys = stored.filter(
91
+ (key) => typeof key === "string" && defaultOpenKeys.includes(key)
92
+ );
93
+ setOpenKeys((current) => current.length === nextOpenKeys.length && current.every((key, index) => key === nextOpenKeys[index]) ? current : nextOpenKeys);
87
94
  } catch {
88
95
  setOpenKeys(defaultOpenKeys);
89
96
  }
@@ -124,14 +131,14 @@ function VmetaSidebar({
124
131
  className: "vmeta-menu-group__toggle",
125
132
  onClick: () => toggleGroup(group.key),
126
133
  "aria-expanded": open,
127
- "aria-controls": `vmeta-menu-group-${group.key}`,
134
+ "aria-controls": `${groupIdPrefix}-${group.key}`,
128
135
  children: [
129
136
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: group.label }),
130
137
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `vmeta-menu-group__chevron ${open ? "is-open" : ""}`, "aria-hidden": "true", children: "\u2304" })
131
138
  ]
132
139
  }
133
140
  ),
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)) }) })
141
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { id: `${groupIdPrefix}-${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
142
  ] }, group.key);
136
143
  }) }),
137
144
  footer && !collapsed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vmeta-sidebar__footer", children: footer }),
@@ -192,15 +199,24 @@ function VmetaAppShell({
192
199
  canAccess,
193
200
  onNavigate
194
201
  }) {
195
- const [collapsed, setCollapsed] = (0, import_react.useState)(() => readStoredBoolean(storageKey, false));
202
+ const [collapsed, setCollapsed] = (0, import_react.useState)(false);
196
203
  const [menuOpen, setMenuOpen] = (0, import_react.useState)(false);
204
+ (0, import_react.useEffect)(() => {
205
+ setCollapsed(readStoredBoolean(storageKey, false));
206
+ }, [storageKey]);
197
207
  const updateCollapsed = (value) => {
198
208
  setCollapsed(value);
199
209
  writeStorage(storageKey, String(value));
200
210
  };
201
211
  const navigate = (item) => {
202
212
  setMenuOpen(false);
203
- onNavigate?.(item);
213
+ if (onNavigate) {
214
+ onNavigate(item);
215
+ return;
216
+ }
217
+ if (typeof window === "undefined") return;
218
+ if (item.external) window.open(item.href, "_blank", "noopener,noreferrer");
219
+ else window.location.assign(item.href);
204
220
  };
205
221
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("main", { className: `vmeta-app-shell ${collapsed ? "is-collapsed" : ""}`, children: [
206
222
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
package/dist/index.d.cts CHANGED
@@ -48,6 +48,7 @@ interface VmetaSidebarProps {
48
48
  activePath: string;
49
49
  collapsed?: boolean;
50
50
  storageKey?: string;
51
+ idPrefix?: string;
51
52
  account?: VmetaAccount;
52
53
  footer?: ReactNode;
53
54
  showCollapse?: boolean;
@@ -55,7 +56,7 @@ interface VmetaSidebarProps {
55
56
  onNavigate?: (item: VmetaMenuItem) => void;
56
57
  onCollapsedChange?: (collapsed: boolean) => void;
57
58
  }
58
- declare function VmetaSidebar({ app, groups, activePath, collapsed, storageKey, account, footer, showCollapse, canAccess, onNavigate, onCollapsedChange, }: VmetaSidebarProps): react_jsx_runtime.JSX.Element;
59
+ declare function VmetaSidebar({ app, groups, activePath, collapsed, storageKey, idPrefix, account, footer, showCollapse, canAccess, onNavigate, onCollapsedChange, }: VmetaSidebarProps): react_jsx_runtime.JSX.Element;
59
60
  interface VmetaAppShellProps {
60
61
  app: VmetaAppIdentity;
61
62
  groups: VmetaMenuGroup[];
package/dist/index.d.ts CHANGED
@@ -48,6 +48,7 @@ interface VmetaSidebarProps {
48
48
  activePath: string;
49
49
  collapsed?: boolean;
50
50
  storageKey?: string;
51
+ idPrefix?: string;
51
52
  account?: VmetaAccount;
52
53
  footer?: ReactNode;
53
54
  showCollapse?: boolean;
@@ -55,7 +56,7 @@ interface VmetaSidebarProps {
55
56
  onNavigate?: (item: VmetaMenuItem) => void;
56
57
  onCollapsedChange?: (collapsed: boolean) => void;
57
58
  }
58
- declare function VmetaSidebar({ app, groups, activePath, collapsed, storageKey, account, footer, showCollapse, canAccess, onNavigate, onCollapsedChange, }: VmetaSidebarProps): react_jsx_runtime.JSX.Element;
59
+ declare function VmetaSidebar({ app, groups, activePath, collapsed, storageKey, idPrefix, account, footer, showCollapse, canAccess, onNavigate, onCollapsedChange, }: VmetaSidebarProps): react_jsx_runtime.JSX.Element;
59
60
  interface VmetaAppShellProps {
60
61
  app: VmetaAppIdentity;
61
62
  groups: VmetaMenuGroup[];
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
 
3
3
  // src/index.tsx
4
- import { useEffect, useMemo, useState } from "react";
4
+ import { useEffect, useId, useMemo, useState } from "react";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
+ var allowAllPermissions = () => true;
6
7
  function isMenuItemActive(item, activePath) {
7
8
  if (item.match === "exact") return activePath === item.href;
8
9
  if (item.match === "prefix") return activePath === item.href || activePath.startsWith(`${item.href}/`);
@@ -34,13 +35,16 @@ function VmetaSidebar({
34
35
  activePath,
35
36
  collapsed = false,
36
37
  storageKey = `vmeta:${app.appId}:menu-groups`,
38
+ idPrefix,
37
39
  account,
38
40
  footer,
39
41
  showCollapse = false,
40
- canAccess = () => true,
42
+ canAccess = allowAllPermissions,
41
43
  onNavigate,
42
44
  onCollapsedChange
43
45
  }) {
46
+ const generatedId = useId().replace(/:/g, "");
47
+ const groupIdPrefix = idPrefix ?? `vmeta-menu-${app.appId}-${generatedId}`;
44
48
  const visibleGroups = useMemo(
45
49
  () => groups.map((group) => ({
46
50
  ...group,
@@ -58,7 +62,10 @@ function VmetaSidebar({
58
62
  setOpenKeys(defaultOpenKeys);
59
63
  return;
60
64
  }
61
- setOpenKeys(stored.filter((key) => typeof key === "string" && defaultOpenKeys.includes(key)));
65
+ const nextOpenKeys = stored.filter(
66
+ (key) => typeof key === "string" && defaultOpenKeys.includes(key)
67
+ );
68
+ setOpenKeys((current) => current.length === nextOpenKeys.length && current.every((key, index) => key === nextOpenKeys[index]) ? current : nextOpenKeys);
62
69
  } catch {
63
70
  setOpenKeys(defaultOpenKeys);
64
71
  }
@@ -99,14 +106,14 @@ function VmetaSidebar({
99
106
  className: "vmeta-menu-group__toggle",
100
107
  onClick: () => toggleGroup(group.key),
101
108
  "aria-expanded": open,
102
- "aria-controls": `vmeta-menu-group-${group.key}`,
109
+ "aria-controls": `${groupIdPrefix}-${group.key}`,
103
110
  children: [
104
111
  /* @__PURE__ */ jsx("span", { children: group.label }),
105
112
  /* @__PURE__ */ jsx("span", { className: `vmeta-menu-group__chevron ${open ? "is-open" : ""}`, "aria-hidden": "true", children: "\u2304" })
106
113
  ]
107
114
  }
108
115
  ),
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)) }) })
116
+ /* @__PURE__ */ jsx("div", { id: `${groupIdPrefix}-${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
117
  ] }, group.key);
111
118
  }) }),
112
119
  footer && !collapsed && /* @__PURE__ */ jsx("div", { className: "vmeta-sidebar__footer", children: footer }),
@@ -167,15 +174,24 @@ function VmetaAppShell({
167
174
  canAccess,
168
175
  onNavigate
169
176
  }) {
170
- const [collapsed, setCollapsed] = useState(() => readStoredBoolean(storageKey, false));
177
+ const [collapsed, setCollapsed] = useState(false);
171
178
  const [menuOpen, setMenuOpen] = useState(false);
179
+ useEffect(() => {
180
+ setCollapsed(readStoredBoolean(storageKey, false));
181
+ }, [storageKey]);
172
182
  const updateCollapsed = (value) => {
173
183
  setCollapsed(value);
174
184
  writeStorage(storageKey, String(value));
175
185
  };
176
186
  const navigate = (item) => {
177
187
  setMenuOpen(false);
178
- onNavigate?.(item);
188
+ if (onNavigate) {
189
+ onNavigate(item);
190
+ return;
191
+ }
192
+ if (typeof window === "undefined") return;
193
+ if (item.external) window.open(item.href, "_blank", "noopener,noreferrer");
194
+ else window.location.assign(item.href);
179
195
  };
180
196
  return /* @__PURE__ */ jsxs("main", { className: `vmeta-app-shell ${collapsed ? "is-collapsed" : ""}`, children: [
181
197
  /* @__PURE__ */ jsx(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leduchai/vmeta-ui-shell",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Application shell, sidebar, menu va header dung chung cua VMETA.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,7 +21,7 @@
21
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
22
  },
23
23
  "dependencies": {
24
- "@leduchai/vmeta-ui-tokens": "0.1.3"
24
+ "@leduchai/vmeta-ui-tokens": "0.1.5"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "react": ">=18 <20",