@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 +3 -0
- package/dist/index.cjs +265 -0
- package/dist/index.d.cts +74 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +239 -0
- package/dist/styles.css +80 -0
- package/package.json +33 -0
package/README.md
ADDED
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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/styles.css
ADDED
|
@@ -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
|
+
}
|