@dloizides/ui-nav 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/index.d.mts +271 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.js +309 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +296 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +111 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var reactNative = require('react-native');
|
|
4
|
+
var uiFeedback = require('@dloizides/ui-feedback');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
var authWeb = require('@dloizides/auth-web');
|
|
8
|
+
|
|
9
|
+
// src/Sidebar.tsx
|
|
10
|
+
|
|
11
|
+
// src/isRouteActive.ts
|
|
12
|
+
function isRouteActive(pathname, route) {
|
|
13
|
+
if (route === "/") return pathname === "/";
|
|
14
|
+
return pathname === route || pathname.startsWith(`${route}/`);
|
|
15
|
+
}
|
|
16
|
+
var ACTIVE_BORDER_RADIUS = 4;
|
|
17
|
+
var navStyles = reactNative.StyleSheet.create({
|
|
18
|
+
// --- Sidebar ---
|
|
19
|
+
sidebarContainer: {
|
|
20
|
+
width: 220,
|
|
21
|
+
paddingTop: 24,
|
|
22
|
+
paddingHorizontal: 12,
|
|
23
|
+
borderRightWidth: 1,
|
|
24
|
+
height: "100%"
|
|
25
|
+
},
|
|
26
|
+
sidebarTitle: {
|
|
27
|
+
fontWeight: "700",
|
|
28
|
+
marginBottom: 12
|
|
29
|
+
},
|
|
30
|
+
sidebarItem: {
|
|
31
|
+
paddingVertical: 10,
|
|
32
|
+
paddingHorizontal: 6,
|
|
33
|
+
borderRadius: 4
|
|
34
|
+
},
|
|
35
|
+
sidebarItemText: {
|
|
36
|
+
fontSize: 16
|
|
37
|
+
},
|
|
38
|
+
sidebarSpacer: {
|
|
39
|
+
flex: 1
|
|
40
|
+
},
|
|
41
|
+
// --- Topbar ---
|
|
42
|
+
topbarContainer: {
|
|
43
|
+
height: 64,
|
|
44
|
+
paddingHorizontal: 12,
|
|
45
|
+
borderBottomWidth: 1,
|
|
46
|
+
flexDirection: "row",
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
justifyContent: "space-between"
|
|
49
|
+
},
|
|
50
|
+
topbarLeft: { flex: 1 },
|
|
51
|
+
topbarRight: { flexDirection: "row", alignItems: "center" },
|
|
52
|
+
topbarRowItem: { marginHorizontal: 8, alignItems: "center" },
|
|
53
|
+
topbarLabel: { fontSize: 14 },
|
|
54
|
+
userBlock: { marginHorizontal: 12, alignItems: "flex-end" },
|
|
55
|
+
userName: { fontWeight: "600" },
|
|
56
|
+
userEmail: { fontSize: 12 },
|
|
57
|
+
accountBtn: { marginLeft: 8, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 4 },
|
|
58
|
+
accountText: { fontWeight: "600" }
|
|
59
|
+
});
|
|
60
|
+
var expandableStyles = reactNative.StyleSheet.create({
|
|
61
|
+
childItem: {
|
|
62
|
+
borderRadius: 4,
|
|
63
|
+
flexDirection: "row",
|
|
64
|
+
alignItems: "center",
|
|
65
|
+
paddingVertical: 6
|
|
66
|
+
},
|
|
67
|
+
childItemText: { fontSize: 14 },
|
|
68
|
+
childItemTextWithIcon: { fontSize: 14, marginLeft: 6 },
|
|
69
|
+
chevron: { marginLeft: "auto" },
|
|
70
|
+
childrenContainer: { overflow: "hidden" },
|
|
71
|
+
header: {
|
|
72
|
+
borderRadius: 4,
|
|
73
|
+
flexDirection: "row",
|
|
74
|
+
alignItems: "center",
|
|
75
|
+
paddingVertical: 8
|
|
76
|
+
},
|
|
77
|
+
headerText: { fontSize: 15, fontWeight: "600", marginLeft: 6 },
|
|
78
|
+
iconWrapper: { width: 20, alignItems: "center" }
|
|
79
|
+
});
|
|
80
|
+
var BASE_INDENT = 12;
|
|
81
|
+
var NAV_ICON_SIZE = 14;
|
|
82
|
+
var CHEVRON_ICON_SIZE = 12;
|
|
83
|
+
var NavExpandableItem = ({
|
|
84
|
+
item,
|
|
85
|
+
pathname,
|
|
86
|
+
onNavigate,
|
|
87
|
+
navigateHint,
|
|
88
|
+
expandHint,
|
|
89
|
+
collapseHint,
|
|
90
|
+
renderChevron,
|
|
91
|
+
depth = 0
|
|
92
|
+
}) => {
|
|
93
|
+
const [expanded, setExpanded] = react.useState(false);
|
|
94
|
+
const { theme } = uiFeedback.useUi();
|
|
95
|
+
const colors = theme.colors;
|
|
96
|
+
const primaryColor = theme.palette.primary["500"];
|
|
97
|
+
const toggle = react.useCallback(() => setExpanded((v) => !v), []);
|
|
98
|
+
const indent = depth * BASE_INDENT;
|
|
99
|
+
const hasChildren = Array.isArray(item.children) && item.children.length > 0;
|
|
100
|
+
const isActive = isRouteActive(pathname, item.route);
|
|
101
|
+
const activeItemStyle = react.useMemo(
|
|
102
|
+
() => ({ backgroundColor: colors.border, borderRadius: ACTIVE_BORDER_RADIUS }),
|
|
103
|
+
[colors.border]
|
|
104
|
+
);
|
|
105
|
+
const paddingStyle = react.useMemo(() => ({ paddingLeft: indent + BASE_INDENT }), [indent]);
|
|
106
|
+
const headerPaddingStyle = react.useMemo(() => ({ paddingLeft: indent }), [indent]);
|
|
107
|
+
if (!hasChildren)
|
|
108
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
109
|
+
reactNative.TouchableOpacity,
|
|
110
|
+
{
|
|
111
|
+
accessibilityHint: navigateHint(item.label),
|
|
112
|
+
accessibilityLabel: item.label,
|
|
113
|
+
accessibilityRole: "button",
|
|
114
|
+
style: [expandableStyles.childItem, paddingStyle, isActive ? activeItemStyle : void 0],
|
|
115
|
+
testID: item.testID ?? item.key,
|
|
116
|
+
onPress: () => onNavigate(item.route),
|
|
117
|
+
children: [
|
|
118
|
+
typeof item.renderIcon === "function" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: expandableStyles.iconWrapper, children: item.renderIcon(colors.textSecondary, NAV_ICON_SIZE) }) : null,
|
|
119
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
120
|
+
reactNative.Text,
|
|
121
|
+
{
|
|
122
|
+
style: [
|
|
123
|
+
typeof item.renderIcon === "function" ? expandableStyles.childItemTextWithIcon : expandableStyles.childItemText,
|
|
124
|
+
{ color: isActive ? primaryColor : colors.text }
|
|
125
|
+
],
|
|
126
|
+
children: item.label
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { children: [
|
|
133
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
134
|
+
reactNative.TouchableOpacity,
|
|
135
|
+
{
|
|
136
|
+
accessibilityHint: expanded ? collapseHint : expandHint,
|
|
137
|
+
accessibilityLabel: item.label,
|
|
138
|
+
accessibilityRole: "button",
|
|
139
|
+
accessibilityState: { expanded },
|
|
140
|
+
style: [expandableStyles.header, headerPaddingStyle],
|
|
141
|
+
testID: item.testID ?? item.key,
|
|
142
|
+
onPress: toggle,
|
|
143
|
+
children: [
|
|
144
|
+
typeof item.renderIcon === "function" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: expandableStyles.iconWrapper, children: item.renderIcon(colors.text, NAV_ICON_SIZE) }) : null,
|
|
145
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [expandableStyles.headerText, { color: colors.text }], children: item.label }),
|
|
146
|
+
typeof renderChevron === "function" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: expandableStyles.chevron, children: renderChevron(expanded, colors.textSecondary, CHEVRON_ICON_SIZE) }) : null
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
),
|
|
150
|
+
expanded ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: expandableStyles.childrenContainer, children: item.children?.map((child) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
151
|
+
NavExpandableItem,
|
|
152
|
+
{
|
|
153
|
+
collapseHint,
|
|
154
|
+
depth: depth + 1,
|
|
155
|
+
expandHint,
|
|
156
|
+
item: child,
|
|
157
|
+
navigateHint,
|
|
158
|
+
pathname,
|
|
159
|
+
renderChevron,
|
|
160
|
+
onNavigate
|
|
161
|
+
},
|
|
162
|
+
child.key
|
|
163
|
+
)) }) : null
|
|
164
|
+
] });
|
|
165
|
+
};
|
|
166
|
+
var Sidebar = ({
|
|
167
|
+
items,
|
|
168
|
+
pathname,
|
|
169
|
+
onNavigate,
|
|
170
|
+
title,
|
|
171
|
+
regionLabel,
|
|
172
|
+
navigateHint = (label) => label,
|
|
173
|
+
expandHint = "",
|
|
174
|
+
collapseHint = "",
|
|
175
|
+
renderChevron,
|
|
176
|
+
header,
|
|
177
|
+
footer,
|
|
178
|
+
containerStyle
|
|
179
|
+
}) => {
|
|
180
|
+
const { theme } = uiFeedback.useUi();
|
|
181
|
+
const colors = theme.colors;
|
|
182
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
183
|
+
reactNative.View,
|
|
184
|
+
{
|
|
185
|
+
accessibilityRole: "none",
|
|
186
|
+
"aria-label": regionLabel,
|
|
187
|
+
role: "navigation",
|
|
188
|
+
style: [
|
|
189
|
+
navStyles.sidebarContainer,
|
|
190
|
+
{ backgroundColor: colors.surface, borderRightColor: colors.border },
|
|
191
|
+
containerStyle
|
|
192
|
+
],
|
|
193
|
+
children: [
|
|
194
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { accessibilityRole: "header", style: [navStyles.sidebarTitle, { color: colors.text }], children: title }),
|
|
195
|
+
header,
|
|
196
|
+
items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
197
|
+
NavExpandableItem,
|
|
198
|
+
{
|
|
199
|
+
collapseHint,
|
|
200
|
+
expandHint,
|
|
201
|
+
item,
|
|
202
|
+
navigateHint,
|
|
203
|
+
pathname,
|
|
204
|
+
renderChevron,
|
|
205
|
+
onNavigate
|
|
206
|
+
},
|
|
207
|
+
item.key
|
|
208
|
+
)),
|
|
209
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navStyles.sidebarSpacer }),
|
|
210
|
+
footer
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
var TEXT_ON_PRIMARY = "#ffffff";
|
|
216
|
+
var Topbar = ({
|
|
217
|
+
left,
|
|
218
|
+
language,
|
|
219
|
+
notificationSlot,
|
|
220
|
+
user,
|
|
221
|
+
account,
|
|
222
|
+
logout,
|
|
223
|
+
containerStyle
|
|
224
|
+
}) => {
|
|
225
|
+
const { theme } = uiFeedback.useUi();
|
|
226
|
+
const colors = theme.colors;
|
|
227
|
+
const primaryColor = theme.palette.primary["500"];
|
|
228
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
229
|
+
reactNative.View,
|
|
230
|
+
{
|
|
231
|
+
style: [
|
|
232
|
+
navStyles.topbarContainer,
|
|
233
|
+
{ borderBottomColor: colors.border, backgroundColor: colors.background },
|
|
234
|
+
containerStyle
|
|
235
|
+
],
|
|
236
|
+
children: [
|
|
237
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navStyles.topbarLeft, children: left }),
|
|
238
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: navStyles.topbarRight, children: [
|
|
239
|
+
language ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
240
|
+
reactNative.TouchableOpacity,
|
|
241
|
+
{
|
|
242
|
+
accessibilityHint: language.hint,
|
|
243
|
+
accessibilityLabel: language.label,
|
|
244
|
+
accessibilityRole: "button",
|
|
245
|
+
style: navStyles.topbarRowItem,
|
|
246
|
+
onPress: language.onPress,
|
|
247
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.topbarLabel, { color: colors.text }], children: language.label })
|
|
248
|
+
}
|
|
249
|
+
) : null,
|
|
250
|
+
notificationSlot,
|
|
251
|
+
user ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: navStyles.userBlock, children: [
|
|
252
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.userName, { color: colors.text }], children: user.name }),
|
|
253
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.userEmail, { color: colors.textSecondary }], children: user.email })
|
|
254
|
+
] }) : null,
|
|
255
|
+
account ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
256
|
+
reactNative.TouchableOpacity,
|
|
257
|
+
{
|
|
258
|
+
accessibilityHint: account.hint,
|
|
259
|
+
accessibilityLabel: account.label,
|
|
260
|
+
accessibilityRole: "button",
|
|
261
|
+
style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
|
|
262
|
+
testID: account.testID,
|
|
263
|
+
onPress: account.onPress,
|
|
264
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children: account.label })
|
|
265
|
+
}
|
|
266
|
+
) : null,
|
|
267
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
268
|
+
reactNative.TouchableOpacity,
|
|
269
|
+
{
|
|
270
|
+
accessible: true,
|
|
271
|
+
accessibilityHint: logout.hint,
|
|
272
|
+
accessibilityLabel: logout.label,
|
|
273
|
+
accessibilityRole: "button",
|
|
274
|
+
style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
|
|
275
|
+
testID: logout.testID,
|
|
276
|
+
onPress: logout.onPress,
|
|
277
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children: logout.label })
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
] })
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
function roleRoutesToNavItems(routes, translate) {
|
|
286
|
+
return routes.map((entry) => ({
|
|
287
|
+
key: entry.role,
|
|
288
|
+
route: entry.route,
|
|
289
|
+
label: entry.labelKey !== void 0 ? translate(entry.labelKey) : entry.route
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
function accessibleNavItems(user, table, translate) {
|
|
293
|
+
return roleRoutesToNavItems(authWeb.resolveAccessibleRoutes(user, table), translate);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
exports.ACTIVE_BORDER_RADIUS = ACTIVE_BORDER_RADIUS;
|
|
297
|
+
exports.BASE_INDENT = BASE_INDENT;
|
|
298
|
+
exports.CHEVRON_ICON_SIZE = CHEVRON_ICON_SIZE;
|
|
299
|
+
exports.NAV_ICON_SIZE = NAV_ICON_SIZE;
|
|
300
|
+
exports.NavExpandableItem = NavExpandableItem;
|
|
301
|
+
exports.Sidebar = Sidebar;
|
|
302
|
+
exports.Topbar = Topbar;
|
|
303
|
+
exports.accessibleNavItems = accessibleNavItems;
|
|
304
|
+
exports.expandableStyles = expandableStyles;
|
|
305
|
+
exports.isRouteActive = isRouteActive;
|
|
306
|
+
exports.navStyles = navStyles;
|
|
307
|
+
exports.roleRoutesToNavItems = roleRoutesToNavItems;
|
|
308
|
+
//# sourceMappingURL=index.js.map
|
|
309
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/isRouteActive.ts","../src/styles.ts","../src/NavExpandableItem.tsx","../src/Sidebar.tsx","../src/Topbar.tsx","../src/roleNav.ts"],"names":["StyleSheet","useState","useUi","useCallback","useMemo","jsxs","TouchableOpacity","jsx","View","Text","resolveAccessibleRoutes"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Active-route matcher shared by the sidebar entries. Ported verbatim from the\n * twin app sidebars: an item is active when the current pathname equals its\n * route or is nested under it (`/foo` matches `/foo` and `/foo/bar`, but `/`\n * only matches `/`).\n */\nexport function isRouteActive(pathname: string, route: string): boolean {\n if (route === '/') return pathname === '/';\n return pathname === route || pathname.startsWith(`${route}/`);\n}\n","/**\n * Layout styles for the nav shell — ported verbatim (dimensions unchanged) from\n * erevna-web / katalogos-web `theme/utils/layoutSidebar.ts` + `layoutTopbar.ts`,\n * so a migrated app's sidebar/topbar keep the exact same metrics. Colours are\n * NOT baked in here; they are applied at render time from the UiProvider theme.\n */\nimport { StyleSheet } from 'react-native';\n\nexport const ACTIVE_BORDER_RADIUS = 4;\n\nexport const navStyles = StyleSheet.create({\n // --- Sidebar ---\n sidebarContainer: {\n width: 220,\n paddingTop: 24,\n paddingHorizontal: 12,\n borderRightWidth: 1,\n height: '100%',\n },\n sidebarTitle: {\n fontWeight: '700',\n marginBottom: 12,\n },\n sidebarItem: {\n paddingVertical: 10,\n paddingHorizontal: 6,\n borderRadius: 4,\n },\n sidebarItemText: {\n fontSize: 16,\n },\n sidebarSpacer: {\n flex: 1,\n },\n\n // --- Topbar ---\n topbarContainer: {\n height: 64,\n paddingHorizontal: 12,\n borderBottomWidth: 1,\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'space-between',\n },\n topbarLeft: { flex: 1 },\n topbarRight: { flexDirection: 'row', alignItems: 'center' },\n topbarRowItem: { marginHorizontal: 8, alignItems: 'center' },\n topbarLabel: { fontSize: 14 },\n userBlock: { marginHorizontal: 12, alignItems: 'flex-end' },\n userName: { fontWeight: '600' },\n userEmail: { fontSize: 12 },\n accountBtn: { marginLeft: 8, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 4 },\n accountText: { fontWeight: '600' },\n});\n\nexport const expandableStyles = StyleSheet.create({\n childItem: {\n borderRadius: 4,\n flexDirection: 'row',\n alignItems: 'center',\n paddingVertical: 6,\n },\n childItemText: { fontSize: 14 },\n childItemTextWithIcon: { fontSize: 14, marginLeft: 6 },\n chevron: { marginLeft: 'auto' },\n childrenContainer: { overflow: 'hidden' },\n header: {\n borderRadius: 4,\n flexDirection: 'row',\n alignItems: 'center',\n paddingVertical: 8,\n },\n headerText: { fontSize: 15, fontWeight: '600', marginLeft: 6 },\n iconWrapper: { width: 20, alignItems: 'center' },\n});\n\n/** Base indent applied per nesting depth in the expandable item. */\nexport const BASE_INDENT = 12;\n/** Default icon size for nav item icons. */\nexport const NAV_ICON_SIZE = 14;\n/** Chevron icon size for expandable sections. */\nexport const CHEVRON_ICON_SIZE = 12;\n","/**\n * NavExpandableItem — a single sidebar entry that is either a leaf (navigates)\n * or an expandable section (toggles nested children). Ported from the twin\n * erevna/katalogos `Sidebar/NavExpandableItem`, made config-driven: labels are\n * pre-localized strings, icons are render slots, colours come from `useUi`.\n */\nimport React, { useCallback, useMemo, useState } from 'react';\n\nimport { Text, TouchableOpacity, View } from 'react-native';\n\nimport { useUi } from '@dloizides/ui-feedback';\n\nimport {\n BASE_INDENT,\n CHEVRON_ICON_SIZE,\n NAV_ICON_SIZE,\n ACTIVE_BORDER_RADIUS,\n expandableStyles as styles,\n} from './styles';\nimport { isRouteActive } from './isRouteActive';\nimport type { NavItem } from './types';\n\nexport interface NavExpandableItemProps {\n item: NavItem;\n pathname: string;\n onNavigate: (route: string) => void;\n /** a11y hint for a leaf item, given its label. */\n navigateHint: (label: string) => string;\n /** a11y hint shown when the section is collapsed (press expands). */\n expandHint: string;\n /** a11y hint shown when the section is expanded (press collapses). */\n collapseHint: string;\n /** Optional chevron icon renderer for expandable sections. */\n renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;\n depth?: number;\n}\n\nexport const NavExpandableItem = ({\n item,\n pathname,\n onNavigate,\n navigateHint,\n expandHint,\n collapseHint,\n renderChevron,\n depth = 0,\n}: NavExpandableItemProps): React.ReactElement => {\n const [expanded, setExpanded] = useState(false);\n const { theme } = useUi();\n const colors = theme.colors;\n const primaryColor = theme.palette.primary['500'];\n\n const toggle = useCallback(() => setExpanded((v) => !v), []);\n\n const indent = depth * BASE_INDENT;\n const hasChildren = Array.isArray(item.children) && item.children.length > 0;\n const isActive = isRouteActive(pathname, item.route);\n\n const activeItemStyle = useMemo(\n () => ({ backgroundColor: colors.border, borderRadius: ACTIVE_BORDER_RADIUS }),\n [colors.border],\n );\n const paddingStyle = useMemo(() => ({ paddingLeft: indent + BASE_INDENT }), [indent]);\n const headerPaddingStyle = useMemo(() => ({ paddingLeft: indent }), [indent]);\n\n if (!hasChildren)\n return (\n <TouchableOpacity\n accessibilityHint={navigateHint(item.label)}\n accessibilityLabel={item.label}\n accessibilityRole=\"button\"\n style={[styles.childItem, paddingStyle, isActive ? activeItemStyle : undefined]}\n testID={item.testID ?? item.key}\n onPress={() => onNavigate(item.route)}\n >\n {typeof item.renderIcon === 'function' ? (\n <View style={styles.iconWrapper}>{item.renderIcon(colors.textSecondary, NAV_ICON_SIZE)}</View>\n ) : null}\n <Text\n style={[\n typeof item.renderIcon === 'function' ? styles.childItemTextWithIcon : styles.childItemText,\n { color: isActive ? primaryColor : colors.text },\n ]}\n >\n {item.label}\n </Text>\n </TouchableOpacity>\n );\n\n return (\n <View>\n <TouchableOpacity\n accessibilityHint={expanded ? collapseHint : expandHint}\n accessibilityLabel={item.label}\n accessibilityRole=\"button\"\n accessibilityState={{ expanded }}\n style={[styles.header, headerPaddingStyle]}\n testID={item.testID ?? item.key}\n onPress={toggle}\n >\n {typeof item.renderIcon === 'function' ? (\n <View style={styles.iconWrapper}>{item.renderIcon(colors.text, NAV_ICON_SIZE)}</View>\n ) : null}\n <Text style={[styles.headerText, { color: colors.text }]}>{item.label}</Text>\n {typeof renderChevron === 'function' ? (\n <View style={styles.chevron}>{renderChevron(expanded, colors.textSecondary, CHEVRON_ICON_SIZE)}</View>\n ) : null}\n </TouchableOpacity>\n\n {expanded ? (\n <View style={styles.childrenContainer}>\n {item.children?.map((child) => (\n <NavExpandableItem\n key={child.key}\n collapseHint={collapseHint}\n depth={depth + 1}\n expandHint={expandHint}\n item={child}\n navigateHint={navigateHint}\n pathname={pathname}\n renderChevron={renderChevron}\n onNavigate={onNavigate}\n />\n ))}\n </View>\n ) : null}\n </View>\n );\n};\n\nexport default NavExpandableItem;\n","/**\n * Sidebar — the config-driven left navigation shell promoted from the\n * byte-identical erevna-web / katalogos-web `Sidebar`. It renders a caller\n * supplied `NavItem[]` (leaf + expandable), highlights the active route, and\n * exposes header/footer slots for app-specific chrome (title, dark-mode toggle,\n * logout, notification bell). Every colour is read from the UiProvider theme.\n */\nimport React from 'react';\n\nimport { Text, View, type ViewStyle } from 'react-native';\n\nimport { useUi } from '@dloizides/ui-feedback';\n\nimport { isRouteActive } from './isRouteActive';\nimport { NavExpandableItem } from './NavExpandableItem';\nimport { navStyles as styles } from './styles';\nimport type { NavItem } from './types';\n\nexport interface SidebarProps {\n /** Nav entries — already role-filtered / grouped by the app. */\n items: NavItem[];\n /** Current active route/path. */\n pathname: string;\n /** Navigation callback — receives a `NavItem.route`. */\n onNavigate: (route: string) => void;\n /** Localized menu title (heading). */\n title: string;\n /** Localized accessibility label for the navigation landmark. */\n regionLabel: string;\n /** a11y hint for a leaf item, given its label. Defaults to the label. */\n navigateHint?: (label: string) => string;\n /** a11y hint shown when an expandable section is collapsed. */\n expandHint?: string;\n /** a11y hint shown when an expandable section is expanded. */\n collapseHint?: string;\n /** Optional chevron renderer for expandable sections. */\n renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;\n /** Optional header slot rendered above the items (e.g. a Home shortcut). */\n header?: React.ReactNode;\n /** Optional footer slot rendered after a flex spacer (dark-mode toggle, logout). */\n footer?: React.ReactNode;\n /** Extra container style overrides. */\n containerStyle?: ViewStyle | ViewStyle[];\n}\n\nexport const Sidebar = ({\n items,\n pathname,\n onNavigate,\n title,\n regionLabel,\n navigateHint = (label) => label,\n expandHint = '',\n collapseHint = '',\n renderChevron,\n header,\n footer,\n containerStyle,\n}: SidebarProps): React.ReactElement => {\n const { theme } = useUi();\n const colors = theme.colors;\n\n return (\n <View\n accessibilityRole=\"none\"\n aria-label={regionLabel}\n role=\"navigation\"\n style={[\n styles.sidebarContainer,\n { backgroundColor: colors.surface, borderRightColor: colors.border },\n containerStyle,\n ]}\n >\n <Text accessibilityRole=\"header\" style={[styles.sidebarTitle, { color: colors.text }]}>\n {title}\n </Text>\n\n {header}\n\n {items.map((item) => (\n <NavExpandableItem\n key={item.key}\n collapseHint={collapseHint}\n expandHint={expandHint}\n item={item}\n navigateHint={navigateHint}\n pathname={pathname}\n renderChevron={renderChevron}\n onNavigate={onNavigate}\n />\n ))}\n\n <View style={styles.sidebarSpacer} />\n\n {footer}\n </View>\n );\n};\n\nexport { isRouteActive };\nexport default Sidebar;\n","/**\n * Topbar — the config-driven top navigation bar promoted from the byte-identical\n * erevna-web / katalogos-web `Topbar`. Structured slots replace the app-specific\n * wiring: a `left` logo slot, an optional language toggle, a notification slot,\n * an optional user block, an optional account button, and a required logout\n * action. Every colour comes from the UiProvider theme.\n */\nimport React from 'react';\n\nimport { Text, TouchableOpacity, View, type ViewStyle } from 'react-native';\n\nimport { useUi } from '@dloizides/ui-feedback';\n\nimport { navStyles as styles } from './styles';\n\n/** Text color for elements on primary-colored backgrounds. */\nconst TEXT_ON_PRIMARY = '#ffffff';\n\nexport interface TopbarAction {\n label: string;\n hint: string;\n onPress: () => void;\n testID?: string;\n}\n\nexport interface TopbarUser {\n name: string;\n email: string;\n}\n\nexport interface TopbarProps {\n /** Left slot — typically the tenant logo. */\n left?: React.ReactNode;\n /** Optional language toggle. */\n language?: { label: string; hint: string; onPress: () => void };\n /** Optional notification slot (e.g. a notification bell). */\n notificationSlot?: React.ReactNode;\n /** Optional user identity block. */\n user?: TopbarUser;\n /** Optional account button. */\n account?: TopbarAction;\n /** Required logout action. */\n logout: TopbarAction;\n /** Extra container style overrides. */\n containerStyle?: ViewStyle | ViewStyle[];\n}\n\nexport const Topbar = ({\n left,\n language,\n notificationSlot,\n user,\n account,\n logout,\n containerStyle,\n}: TopbarProps): React.ReactElement => {\n const { theme } = useUi();\n const colors = theme.colors;\n const primaryColor = theme.palette.primary['500'];\n\n return (\n <View\n style={[\n styles.topbarContainer,\n { borderBottomColor: colors.border, backgroundColor: colors.background },\n containerStyle,\n ]}\n >\n <View style={styles.topbarLeft}>{left}</View>\n <View style={styles.topbarRight}>\n {language ? (\n <TouchableOpacity\n accessibilityHint={language.hint}\n accessibilityLabel={language.label}\n accessibilityRole=\"button\"\n style={styles.topbarRowItem}\n onPress={language.onPress}\n >\n <Text style={[styles.topbarLabel, { color: colors.text }]}>{language.label}</Text>\n </TouchableOpacity>\n ) : null}\n\n {notificationSlot}\n\n {user ? (\n <View style={styles.userBlock}>\n <Text style={[styles.userName, { color: colors.text }]}>{user.name}</Text>\n <Text style={[styles.userEmail, { color: colors.textSecondary }]}>{user.email}</Text>\n </View>\n ) : null}\n\n {account ? (\n <TouchableOpacity\n accessibilityHint={account.hint}\n accessibilityLabel={account.label}\n accessibilityRole=\"button\"\n style={[styles.accountBtn, { backgroundColor: primaryColor }]}\n testID={account.testID}\n onPress={account.onPress}\n >\n <Text style={[styles.accountText, { color: TEXT_ON_PRIMARY }]}>{account.label}</Text>\n </TouchableOpacity>\n ) : null}\n\n <TouchableOpacity\n accessible\n accessibilityHint={logout.hint}\n accessibilityLabel={logout.label}\n accessibilityRole=\"button\"\n style={[styles.accountBtn, { backgroundColor: primaryColor }]}\n testID={logout.testID}\n onPress={logout.onPress}\n >\n <Text style={[styles.accountText, { color: TEXT_ON_PRIMARY }]}>{logout.label}</Text>\n </TouchableOpacity>\n </View>\n </View>\n );\n};\n\nexport default Topbar;\n","/**\n * Role-gated nav helpers — build a `NavItem[]` from a user's roles, reusing the\n * existing `resolveAccessibleRoutes` from `@dloizides/auth-web` (the same helper\n * the multi-role dashboard nav uses) rather than reimplementing role filtering.\n *\n * The app supplies its ordered `RoleRouteTable` (role → route + optional\n * `labelKey`) and a `translate` fn (its `FM`); this returns the ordered set of\n * `NavItem`s the user's roles unlock — most privileged first, empty when none.\n */\nimport { resolveAccessibleRoutes } from '@dloizides/auth-web';\nimport type { RoleRoute, RoleRouteTable } from '@dloizides/auth-web';\n\nimport type { NavItem } from './types';\n\n/** The user shape `resolveAccessibleRoutes` accepts (derived — no extra dep). */\nexport type NavUser = Parameters<typeof resolveAccessibleRoutes>[0];\n\n/** Map already-resolved role routes to nav items, localizing each `labelKey`. */\nexport function roleRoutesToNavItems(\n routes: RoleRoute[],\n translate: (key: string) => string,\n): NavItem[] {\n return routes.map((entry) => ({\n key: entry.role,\n route: entry.route,\n label: entry.labelKey !== undefined ? translate(entry.labelKey) : entry.route,\n }));\n}\n\n/**\n * Resolve the nav items a user can reach, in table (priority) order. Wraps\n * `resolveAccessibleRoutes` so role gating lives in one place.\n */\nexport function accessibleNavItems(\n user: NavUser,\n table: RoleRouteTable,\n translate: (key: string) => string,\n): NavItem[] {\n return roleRoutesToNavItems(resolveAccessibleRoutes(user, table), translate);\n}\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { StyleSheet, TouchableOpacity, View, Text } from 'react-native';
|
|
2
|
+
import { useUi } from '@dloizides/ui-feedback';
|
|
3
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
4
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
+
import { resolveAccessibleRoutes } from '@dloizides/auth-web';
|
|
6
|
+
|
|
7
|
+
// src/Sidebar.tsx
|
|
8
|
+
|
|
9
|
+
// src/isRouteActive.ts
|
|
10
|
+
function isRouteActive(pathname, route) {
|
|
11
|
+
if (route === "/") return pathname === "/";
|
|
12
|
+
return pathname === route || pathname.startsWith(`${route}/`);
|
|
13
|
+
}
|
|
14
|
+
var ACTIVE_BORDER_RADIUS = 4;
|
|
15
|
+
var navStyles = StyleSheet.create({
|
|
16
|
+
// --- Sidebar ---
|
|
17
|
+
sidebarContainer: {
|
|
18
|
+
width: 220,
|
|
19
|
+
paddingTop: 24,
|
|
20
|
+
paddingHorizontal: 12,
|
|
21
|
+
borderRightWidth: 1,
|
|
22
|
+
height: "100%"
|
|
23
|
+
},
|
|
24
|
+
sidebarTitle: {
|
|
25
|
+
fontWeight: "700",
|
|
26
|
+
marginBottom: 12
|
|
27
|
+
},
|
|
28
|
+
sidebarItem: {
|
|
29
|
+
paddingVertical: 10,
|
|
30
|
+
paddingHorizontal: 6,
|
|
31
|
+
borderRadius: 4
|
|
32
|
+
},
|
|
33
|
+
sidebarItemText: {
|
|
34
|
+
fontSize: 16
|
|
35
|
+
},
|
|
36
|
+
sidebarSpacer: {
|
|
37
|
+
flex: 1
|
|
38
|
+
},
|
|
39
|
+
// --- Topbar ---
|
|
40
|
+
topbarContainer: {
|
|
41
|
+
height: 64,
|
|
42
|
+
paddingHorizontal: 12,
|
|
43
|
+
borderBottomWidth: 1,
|
|
44
|
+
flexDirection: "row",
|
|
45
|
+
alignItems: "center",
|
|
46
|
+
justifyContent: "space-between"
|
|
47
|
+
},
|
|
48
|
+
topbarLeft: { flex: 1 },
|
|
49
|
+
topbarRight: { flexDirection: "row", alignItems: "center" },
|
|
50
|
+
topbarRowItem: { marginHorizontal: 8, alignItems: "center" },
|
|
51
|
+
topbarLabel: { fontSize: 14 },
|
|
52
|
+
userBlock: { marginHorizontal: 12, alignItems: "flex-end" },
|
|
53
|
+
userName: { fontWeight: "600" },
|
|
54
|
+
userEmail: { fontSize: 12 },
|
|
55
|
+
accountBtn: { marginLeft: 8, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 4 },
|
|
56
|
+
accountText: { fontWeight: "600" }
|
|
57
|
+
});
|
|
58
|
+
var expandableStyles = StyleSheet.create({
|
|
59
|
+
childItem: {
|
|
60
|
+
borderRadius: 4,
|
|
61
|
+
flexDirection: "row",
|
|
62
|
+
alignItems: "center",
|
|
63
|
+
paddingVertical: 6
|
|
64
|
+
},
|
|
65
|
+
childItemText: { fontSize: 14 },
|
|
66
|
+
childItemTextWithIcon: { fontSize: 14, marginLeft: 6 },
|
|
67
|
+
chevron: { marginLeft: "auto" },
|
|
68
|
+
childrenContainer: { overflow: "hidden" },
|
|
69
|
+
header: {
|
|
70
|
+
borderRadius: 4,
|
|
71
|
+
flexDirection: "row",
|
|
72
|
+
alignItems: "center",
|
|
73
|
+
paddingVertical: 8
|
|
74
|
+
},
|
|
75
|
+
headerText: { fontSize: 15, fontWeight: "600", marginLeft: 6 },
|
|
76
|
+
iconWrapper: { width: 20, alignItems: "center" }
|
|
77
|
+
});
|
|
78
|
+
var BASE_INDENT = 12;
|
|
79
|
+
var NAV_ICON_SIZE = 14;
|
|
80
|
+
var CHEVRON_ICON_SIZE = 12;
|
|
81
|
+
var NavExpandableItem = ({
|
|
82
|
+
item,
|
|
83
|
+
pathname,
|
|
84
|
+
onNavigate,
|
|
85
|
+
navigateHint,
|
|
86
|
+
expandHint,
|
|
87
|
+
collapseHint,
|
|
88
|
+
renderChevron,
|
|
89
|
+
depth = 0
|
|
90
|
+
}) => {
|
|
91
|
+
const [expanded, setExpanded] = useState(false);
|
|
92
|
+
const { theme } = useUi();
|
|
93
|
+
const colors = theme.colors;
|
|
94
|
+
const primaryColor = theme.palette.primary["500"];
|
|
95
|
+
const toggle = useCallback(() => setExpanded((v) => !v), []);
|
|
96
|
+
const indent = depth * BASE_INDENT;
|
|
97
|
+
const hasChildren = Array.isArray(item.children) && item.children.length > 0;
|
|
98
|
+
const isActive = isRouteActive(pathname, item.route);
|
|
99
|
+
const activeItemStyle = useMemo(
|
|
100
|
+
() => ({ backgroundColor: colors.border, borderRadius: ACTIVE_BORDER_RADIUS }),
|
|
101
|
+
[colors.border]
|
|
102
|
+
);
|
|
103
|
+
const paddingStyle = useMemo(() => ({ paddingLeft: indent + BASE_INDENT }), [indent]);
|
|
104
|
+
const headerPaddingStyle = useMemo(() => ({ paddingLeft: indent }), [indent]);
|
|
105
|
+
if (!hasChildren)
|
|
106
|
+
return /* @__PURE__ */ jsxs(
|
|
107
|
+
TouchableOpacity,
|
|
108
|
+
{
|
|
109
|
+
accessibilityHint: navigateHint(item.label),
|
|
110
|
+
accessibilityLabel: item.label,
|
|
111
|
+
accessibilityRole: "button",
|
|
112
|
+
style: [expandableStyles.childItem, paddingStyle, isActive ? activeItemStyle : void 0],
|
|
113
|
+
testID: item.testID ?? item.key,
|
|
114
|
+
onPress: () => onNavigate(item.route),
|
|
115
|
+
children: [
|
|
116
|
+
typeof item.renderIcon === "function" ? /* @__PURE__ */ jsx(View, { style: expandableStyles.iconWrapper, children: item.renderIcon(colors.textSecondary, NAV_ICON_SIZE) }) : null,
|
|
117
|
+
/* @__PURE__ */ jsx(
|
|
118
|
+
Text,
|
|
119
|
+
{
|
|
120
|
+
style: [
|
|
121
|
+
typeof item.renderIcon === "function" ? expandableStyles.childItemTextWithIcon : expandableStyles.childItemText,
|
|
122
|
+
{ color: isActive ? primaryColor : colors.text }
|
|
123
|
+
],
|
|
124
|
+
children: item.label
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
return /* @__PURE__ */ jsxs(View, { children: [
|
|
131
|
+
/* @__PURE__ */ jsxs(
|
|
132
|
+
TouchableOpacity,
|
|
133
|
+
{
|
|
134
|
+
accessibilityHint: expanded ? collapseHint : expandHint,
|
|
135
|
+
accessibilityLabel: item.label,
|
|
136
|
+
accessibilityRole: "button",
|
|
137
|
+
accessibilityState: { expanded },
|
|
138
|
+
style: [expandableStyles.header, headerPaddingStyle],
|
|
139
|
+
testID: item.testID ?? item.key,
|
|
140
|
+
onPress: toggle,
|
|
141
|
+
children: [
|
|
142
|
+
typeof item.renderIcon === "function" ? /* @__PURE__ */ jsx(View, { style: expandableStyles.iconWrapper, children: item.renderIcon(colors.text, NAV_ICON_SIZE) }) : null,
|
|
143
|
+
/* @__PURE__ */ jsx(Text, { style: [expandableStyles.headerText, { color: colors.text }], children: item.label }),
|
|
144
|
+
typeof renderChevron === "function" ? /* @__PURE__ */ jsx(View, { style: expandableStyles.chevron, children: renderChevron(expanded, colors.textSecondary, CHEVRON_ICON_SIZE) }) : null
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
),
|
|
148
|
+
expanded ? /* @__PURE__ */ jsx(View, { style: expandableStyles.childrenContainer, children: item.children?.map((child) => /* @__PURE__ */ jsx(
|
|
149
|
+
NavExpandableItem,
|
|
150
|
+
{
|
|
151
|
+
collapseHint,
|
|
152
|
+
depth: depth + 1,
|
|
153
|
+
expandHint,
|
|
154
|
+
item: child,
|
|
155
|
+
navigateHint,
|
|
156
|
+
pathname,
|
|
157
|
+
renderChevron,
|
|
158
|
+
onNavigate
|
|
159
|
+
},
|
|
160
|
+
child.key
|
|
161
|
+
)) }) : null
|
|
162
|
+
] });
|
|
163
|
+
};
|
|
164
|
+
var Sidebar = ({
|
|
165
|
+
items,
|
|
166
|
+
pathname,
|
|
167
|
+
onNavigate,
|
|
168
|
+
title,
|
|
169
|
+
regionLabel,
|
|
170
|
+
navigateHint = (label) => label,
|
|
171
|
+
expandHint = "",
|
|
172
|
+
collapseHint = "",
|
|
173
|
+
renderChevron,
|
|
174
|
+
header,
|
|
175
|
+
footer,
|
|
176
|
+
containerStyle
|
|
177
|
+
}) => {
|
|
178
|
+
const { theme } = useUi();
|
|
179
|
+
const colors = theme.colors;
|
|
180
|
+
return /* @__PURE__ */ jsxs(
|
|
181
|
+
View,
|
|
182
|
+
{
|
|
183
|
+
accessibilityRole: "none",
|
|
184
|
+
"aria-label": regionLabel,
|
|
185
|
+
role: "navigation",
|
|
186
|
+
style: [
|
|
187
|
+
navStyles.sidebarContainer,
|
|
188
|
+
{ backgroundColor: colors.surface, borderRightColor: colors.border },
|
|
189
|
+
containerStyle
|
|
190
|
+
],
|
|
191
|
+
children: [
|
|
192
|
+
/* @__PURE__ */ jsx(Text, { accessibilityRole: "header", style: [navStyles.sidebarTitle, { color: colors.text }], children: title }),
|
|
193
|
+
header,
|
|
194
|
+
items.map((item) => /* @__PURE__ */ jsx(
|
|
195
|
+
NavExpandableItem,
|
|
196
|
+
{
|
|
197
|
+
collapseHint,
|
|
198
|
+
expandHint,
|
|
199
|
+
item,
|
|
200
|
+
navigateHint,
|
|
201
|
+
pathname,
|
|
202
|
+
renderChevron,
|
|
203
|
+
onNavigate
|
|
204
|
+
},
|
|
205
|
+
item.key
|
|
206
|
+
)),
|
|
207
|
+
/* @__PURE__ */ jsx(View, { style: navStyles.sidebarSpacer }),
|
|
208
|
+
footer
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
var TEXT_ON_PRIMARY = "#ffffff";
|
|
214
|
+
var Topbar = ({
|
|
215
|
+
left,
|
|
216
|
+
language,
|
|
217
|
+
notificationSlot,
|
|
218
|
+
user,
|
|
219
|
+
account,
|
|
220
|
+
logout,
|
|
221
|
+
containerStyle
|
|
222
|
+
}) => {
|
|
223
|
+
const { theme } = useUi();
|
|
224
|
+
const colors = theme.colors;
|
|
225
|
+
const primaryColor = theme.palette.primary["500"];
|
|
226
|
+
return /* @__PURE__ */ jsxs(
|
|
227
|
+
View,
|
|
228
|
+
{
|
|
229
|
+
style: [
|
|
230
|
+
navStyles.topbarContainer,
|
|
231
|
+
{ borderBottomColor: colors.border, backgroundColor: colors.background },
|
|
232
|
+
containerStyle
|
|
233
|
+
],
|
|
234
|
+
children: [
|
|
235
|
+
/* @__PURE__ */ jsx(View, { style: navStyles.topbarLeft, children: left }),
|
|
236
|
+
/* @__PURE__ */ jsxs(View, { style: navStyles.topbarRight, children: [
|
|
237
|
+
language ? /* @__PURE__ */ jsx(
|
|
238
|
+
TouchableOpacity,
|
|
239
|
+
{
|
|
240
|
+
accessibilityHint: language.hint,
|
|
241
|
+
accessibilityLabel: language.label,
|
|
242
|
+
accessibilityRole: "button",
|
|
243
|
+
style: navStyles.topbarRowItem,
|
|
244
|
+
onPress: language.onPress,
|
|
245
|
+
children: /* @__PURE__ */ jsx(Text, { style: [navStyles.topbarLabel, { color: colors.text }], children: language.label })
|
|
246
|
+
}
|
|
247
|
+
) : null,
|
|
248
|
+
notificationSlot,
|
|
249
|
+
user ? /* @__PURE__ */ jsxs(View, { style: navStyles.userBlock, children: [
|
|
250
|
+
/* @__PURE__ */ jsx(Text, { style: [navStyles.userName, { color: colors.text }], children: user.name }),
|
|
251
|
+
/* @__PURE__ */ jsx(Text, { style: [navStyles.userEmail, { color: colors.textSecondary }], children: user.email })
|
|
252
|
+
] }) : null,
|
|
253
|
+
account ? /* @__PURE__ */ jsx(
|
|
254
|
+
TouchableOpacity,
|
|
255
|
+
{
|
|
256
|
+
accessibilityHint: account.hint,
|
|
257
|
+
accessibilityLabel: account.label,
|
|
258
|
+
accessibilityRole: "button",
|
|
259
|
+
style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
|
|
260
|
+
testID: account.testID,
|
|
261
|
+
onPress: account.onPress,
|
|
262
|
+
children: /* @__PURE__ */ jsx(Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children: account.label })
|
|
263
|
+
}
|
|
264
|
+
) : null,
|
|
265
|
+
/* @__PURE__ */ jsx(
|
|
266
|
+
TouchableOpacity,
|
|
267
|
+
{
|
|
268
|
+
accessible: true,
|
|
269
|
+
accessibilityHint: logout.hint,
|
|
270
|
+
accessibilityLabel: logout.label,
|
|
271
|
+
accessibilityRole: "button",
|
|
272
|
+
style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
|
|
273
|
+
testID: logout.testID,
|
|
274
|
+
onPress: logout.onPress,
|
|
275
|
+
children: /* @__PURE__ */ jsx(Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children: logout.label })
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
] })
|
|
279
|
+
]
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
};
|
|
283
|
+
function roleRoutesToNavItems(routes, translate) {
|
|
284
|
+
return routes.map((entry) => ({
|
|
285
|
+
key: entry.role,
|
|
286
|
+
route: entry.route,
|
|
287
|
+
label: entry.labelKey !== void 0 ? translate(entry.labelKey) : entry.route
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
function accessibleNavItems(user, table, translate) {
|
|
291
|
+
return roleRoutesToNavItems(resolveAccessibleRoutes(user, table), translate);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export { ACTIVE_BORDER_RADIUS, BASE_INDENT, CHEVRON_ICON_SIZE, NAV_ICON_SIZE, NavExpandableItem, Sidebar, Topbar, accessibleNavItems, expandableStyles, isRouteActive, navStyles, roleRoutesToNavItems };
|
|
295
|
+
//# sourceMappingURL=index.mjs.map
|
|
296
|
+
//# sourceMappingURL=index.mjs.map
|