@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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
Initial release. `Sidebar` (leaf + expandable items, active-route highlight, header/footer
|
|
6
|
+
slots), `Topbar` (logo/language/notification/user/account/logout slots), and the
|
|
7
|
+
`accessibleNavItems` / `roleRoutesToNavItems` role-gating helpers (reusing
|
|
8
|
+
`resolveAccessibleRoutes` from `@dloizides/auth-web`). Chrome + metrics ported verbatim from
|
|
9
|
+
the byte-identical erevna-web / katalogos-web nav twins; every colour reads from the
|
|
10
|
+
`@dloizides/ui-feedback` UiProvider theme.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 dloizides
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @dloizides/ui-nav
|
|
2
|
+
|
|
3
|
+
Config-driven, brand-agnostic React Native (RN-web) **navigation shell** for the
|
|
4
|
+
dloizides.com portfolio: `Sidebar` + `Topbar`, promoted from the byte-identical
|
|
5
|
+
erevna-web / katalogos-web nav twins. The *chrome* is shared; the *item data* stays with
|
|
6
|
+
your app — the components render a caller-supplied `NavItem[]` (labels already localized,
|
|
7
|
+
icons as render slots) and route every colour through the shared `@dloizides/ui-feedback`
|
|
8
|
+
UiProvider theme.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @dloizides/ui-nav @dloizides/ui-feedback @dloizides/auth-web
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Peer deps: `@dloizides/ui-feedback >= 1.2.0`, `@dloizides/auth-web >= 1.5.0`, `react >= 18`,
|
|
17
|
+
`react-native >= 0.74` (use `react-native-web` on web).
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { Sidebar, Topbar, accessibleNavItems, type NavItem } from '@dloizides/ui-nav';
|
|
23
|
+
|
|
24
|
+
const items: NavItem[] = buildItems(); // your role-filtered / grouped items
|
|
25
|
+
|
|
26
|
+
<Sidebar
|
|
27
|
+
items={items}
|
|
28
|
+
pathname={usePathname()}
|
|
29
|
+
onNavigate={(route) => router.push(route)}
|
|
30
|
+
title={FM('menu.title')}
|
|
31
|
+
regionLabel={FM('accessibility.navigationRegion')}
|
|
32
|
+
navigateHint={(label) => FM('menu.navigateToHint', label)}
|
|
33
|
+
expandHint={FM('menu.expandSection')}
|
|
34
|
+
collapseHint={FM('menu.collapseSection')}
|
|
35
|
+
renderChevron={(open, color, size) => <SvgIcon name={open ? 'chevronUp' : 'chevronDown'} color={color} size={size} />}
|
|
36
|
+
header={<HomeShortcut />}
|
|
37
|
+
footer={<><DarkModeToggle /><LogoutButton /></>}
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
<Topbar
|
|
41
|
+
left={<TenantLogo />}
|
|
42
|
+
language={{ label: FM(`topbar.lang.${locale}`), hint: FM('topbar.langHint'), onPress: toggleLocale }}
|
|
43
|
+
notificationSlot={<NotificationBell />}
|
|
44
|
+
user={{ name: displayName, email }}
|
|
45
|
+
logout={{ label: FM('topbar.logout'), hint: FM('topbar.logoutHint'), onPress: logout, testID: 'logout-button' }}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Role gating
|
|
50
|
+
|
|
51
|
+
`accessibleNavItems(user, roleRouteTable, translate)` builds the `NavItem[]` a user's roles
|
|
52
|
+
unlock, reusing `resolveAccessibleRoutes` from `@dloizides/auth-web` (most privileged first,
|
|
53
|
+
empty when none) — no duplicated role logic.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ViewStyle } from 'react-native';
|
|
3
|
+
import { resolveAccessibleRoutes, RoleRouteTable, RoleRoute } from '@dloizides/auth-web';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Active-route matcher shared by the sidebar entries. Ported verbatim from the
|
|
7
|
+
* twin app sidebars: an item is active when the current pathname equals its
|
|
8
|
+
* route or is nested under it (`/foo` matches `/foo` and `/foo/bar`, but `/`
|
|
9
|
+
* only matches `/`).
|
|
10
|
+
*/
|
|
11
|
+
declare function isRouteActive(pathname: string, route: string): boolean;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Public prop types for the `@dloizides/ui-nav` config-driven navigation shell.
|
|
15
|
+
*
|
|
16
|
+
* The rendering chrome (sidebar + topbar) was byte-identical across erevna-web
|
|
17
|
+
* and katalogos-web; only the *item data* (which routes, how they group) differs
|
|
18
|
+
* per app. So this package renders a caller-supplied `NavItem[]` — labels are
|
|
19
|
+
* pre-localized strings and icons are render slots, keeping the package free of
|
|
20
|
+
* any app's i18n helper, icon set, router, or store.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** One navigation entry. Labels are already localized by the caller. */
|
|
24
|
+
interface NavItem {
|
|
25
|
+
/** Stable key + default testID. */
|
|
26
|
+
key: string;
|
|
27
|
+
/** Localized display label. */
|
|
28
|
+
label: string;
|
|
29
|
+
/** Route/path this item navigates to (passed back to `onNavigate`). */
|
|
30
|
+
route: string;
|
|
31
|
+
/** Optional testID override (defaults to `key`). */
|
|
32
|
+
testID?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Optional leading icon. Receives the resolved foreground colour and a size,
|
|
35
|
+
* so the app supplies its own icon component without this package importing
|
|
36
|
+
* an icon set.
|
|
37
|
+
*/
|
|
38
|
+
renderIcon?: (color: string, size: number) => React.ReactNode;
|
|
39
|
+
/** Optional nested items — rendered as an expandable section. */
|
|
40
|
+
children?: NavItem[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sidebar — the config-driven left navigation shell promoted from the
|
|
45
|
+
* byte-identical erevna-web / katalogos-web `Sidebar`. It renders a caller
|
|
46
|
+
* supplied `NavItem[]` (leaf + expandable), highlights the active route, and
|
|
47
|
+
* exposes header/footer slots for app-specific chrome (title, dark-mode toggle,
|
|
48
|
+
* logout, notification bell). Every colour is read from the UiProvider theme.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
interface SidebarProps {
|
|
52
|
+
/** Nav entries — already role-filtered / grouped by the app. */
|
|
53
|
+
items: NavItem[];
|
|
54
|
+
/** Current active route/path. */
|
|
55
|
+
pathname: string;
|
|
56
|
+
/** Navigation callback — receives a `NavItem.route`. */
|
|
57
|
+
onNavigate: (route: string) => void;
|
|
58
|
+
/** Localized menu title (heading). */
|
|
59
|
+
title: string;
|
|
60
|
+
/** Localized accessibility label for the navigation landmark. */
|
|
61
|
+
regionLabel: string;
|
|
62
|
+
/** a11y hint for a leaf item, given its label. Defaults to the label. */
|
|
63
|
+
navigateHint?: (label: string) => string;
|
|
64
|
+
/** a11y hint shown when an expandable section is collapsed. */
|
|
65
|
+
expandHint?: string;
|
|
66
|
+
/** a11y hint shown when an expandable section is expanded. */
|
|
67
|
+
collapseHint?: string;
|
|
68
|
+
/** Optional chevron renderer for expandable sections. */
|
|
69
|
+
renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
|
|
70
|
+
/** Optional header slot rendered above the items (e.g. a Home shortcut). */
|
|
71
|
+
header?: React.ReactNode;
|
|
72
|
+
/** Optional footer slot rendered after a flex spacer (dark-mode toggle, logout). */
|
|
73
|
+
footer?: React.ReactNode;
|
|
74
|
+
/** Extra container style overrides. */
|
|
75
|
+
containerStyle?: ViewStyle | ViewStyle[];
|
|
76
|
+
}
|
|
77
|
+
declare const Sidebar: ({ items, pathname, onNavigate, title, regionLabel, navigateHint, expandHint, collapseHint, renderChevron, header, footer, containerStyle, }: SidebarProps) => React.ReactElement;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Topbar — the config-driven top navigation bar promoted from the byte-identical
|
|
81
|
+
* erevna-web / katalogos-web `Topbar`. Structured slots replace the app-specific
|
|
82
|
+
* wiring: a `left` logo slot, an optional language toggle, a notification slot,
|
|
83
|
+
* an optional user block, an optional account button, and a required logout
|
|
84
|
+
* action. Every colour comes from the UiProvider theme.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
interface TopbarAction {
|
|
88
|
+
label: string;
|
|
89
|
+
hint: string;
|
|
90
|
+
onPress: () => void;
|
|
91
|
+
testID?: string;
|
|
92
|
+
}
|
|
93
|
+
interface TopbarUser {
|
|
94
|
+
name: string;
|
|
95
|
+
email: string;
|
|
96
|
+
}
|
|
97
|
+
interface TopbarProps {
|
|
98
|
+
/** Left slot — typically the tenant logo. */
|
|
99
|
+
left?: React.ReactNode;
|
|
100
|
+
/** Optional language toggle. */
|
|
101
|
+
language?: {
|
|
102
|
+
label: string;
|
|
103
|
+
hint: string;
|
|
104
|
+
onPress: () => void;
|
|
105
|
+
};
|
|
106
|
+
/** Optional notification slot (e.g. a notification bell). */
|
|
107
|
+
notificationSlot?: React.ReactNode;
|
|
108
|
+
/** Optional user identity block. */
|
|
109
|
+
user?: TopbarUser;
|
|
110
|
+
/** Optional account button. */
|
|
111
|
+
account?: TopbarAction;
|
|
112
|
+
/** Required logout action. */
|
|
113
|
+
logout: TopbarAction;
|
|
114
|
+
/** Extra container style overrides. */
|
|
115
|
+
containerStyle?: ViewStyle | ViewStyle[];
|
|
116
|
+
}
|
|
117
|
+
declare const Topbar: ({ left, language, notificationSlot, user, account, logout, containerStyle, }: TopbarProps) => React.ReactElement;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* NavExpandableItem — a single sidebar entry that is either a leaf (navigates)
|
|
121
|
+
* or an expandable section (toggles nested children). Ported from the twin
|
|
122
|
+
* erevna/katalogos `Sidebar/NavExpandableItem`, made config-driven: labels are
|
|
123
|
+
* pre-localized strings, icons are render slots, colours come from `useUi`.
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
interface NavExpandableItemProps {
|
|
127
|
+
item: NavItem;
|
|
128
|
+
pathname: string;
|
|
129
|
+
onNavigate: (route: string) => void;
|
|
130
|
+
/** a11y hint for a leaf item, given its label. */
|
|
131
|
+
navigateHint: (label: string) => string;
|
|
132
|
+
/** a11y hint shown when the section is collapsed (press expands). */
|
|
133
|
+
expandHint: string;
|
|
134
|
+
/** a11y hint shown when the section is expanded (press collapses). */
|
|
135
|
+
collapseHint: string;
|
|
136
|
+
/** Optional chevron icon renderer for expandable sections. */
|
|
137
|
+
renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
|
|
138
|
+
depth?: number;
|
|
139
|
+
}
|
|
140
|
+
declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, depth, }: NavExpandableItemProps) => React.ReactElement;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Role-gated nav helpers — build a `NavItem[]` from a user's roles, reusing the
|
|
144
|
+
* existing `resolveAccessibleRoutes` from `@dloizides/auth-web` (the same helper
|
|
145
|
+
* the multi-role dashboard nav uses) rather than reimplementing role filtering.
|
|
146
|
+
*
|
|
147
|
+
* The app supplies its ordered `RoleRouteTable` (role → route + optional
|
|
148
|
+
* `labelKey`) and a `translate` fn (its `FM`); this returns the ordered set of
|
|
149
|
+
* `NavItem`s the user's roles unlock — most privileged first, empty when none.
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
/** The user shape `resolveAccessibleRoutes` accepts (derived — no extra dep). */
|
|
153
|
+
type NavUser = Parameters<typeof resolveAccessibleRoutes>[0];
|
|
154
|
+
/** Map already-resolved role routes to nav items, localizing each `labelKey`. */
|
|
155
|
+
declare function roleRoutesToNavItems(routes: RoleRoute[], translate: (key: string) => string): NavItem[];
|
|
156
|
+
/**
|
|
157
|
+
* Resolve the nav items a user can reach, in table (priority) order. Wraps
|
|
158
|
+
* `resolveAccessibleRoutes` so role gating lives in one place.
|
|
159
|
+
*/
|
|
160
|
+
declare function accessibleNavItems(user: NavUser, table: RoleRouteTable, translate: (key: string) => string): NavItem[];
|
|
161
|
+
|
|
162
|
+
declare const ACTIVE_BORDER_RADIUS = 4;
|
|
163
|
+
declare const navStyles: {
|
|
164
|
+
sidebarContainer: {
|
|
165
|
+
width: number;
|
|
166
|
+
paddingTop: number;
|
|
167
|
+
paddingHorizontal: number;
|
|
168
|
+
borderRightWidth: number;
|
|
169
|
+
height: "100%";
|
|
170
|
+
};
|
|
171
|
+
sidebarTitle: {
|
|
172
|
+
fontWeight: "700";
|
|
173
|
+
marginBottom: number;
|
|
174
|
+
};
|
|
175
|
+
sidebarItem: {
|
|
176
|
+
paddingVertical: number;
|
|
177
|
+
paddingHorizontal: number;
|
|
178
|
+
borderRadius: number;
|
|
179
|
+
};
|
|
180
|
+
sidebarItemText: {
|
|
181
|
+
fontSize: number;
|
|
182
|
+
};
|
|
183
|
+
sidebarSpacer: {
|
|
184
|
+
flex: number;
|
|
185
|
+
};
|
|
186
|
+
topbarContainer: {
|
|
187
|
+
height: number;
|
|
188
|
+
paddingHorizontal: number;
|
|
189
|
+
borderBottomWidth: number;
|
|
190
|
+
flexDirection: "row";
|
|
191
|
+
alignItems: "center";
|
|
192
|
+
justifyContent: "space-between";
|
|
193
|
+
};
|
|
194
|
+
topbarLeft: {
|
|
195
|
+
flex: number;
|
|
196
|
+
};
|
|
197
|
+
topbarRight: {
|
|
198
|
+
flexDirection: "row";
|
|
199
|
+
alignItems: "center";
|
|
200
|
+
};
|
|
201
|
+
topbarRowItem: {
|
|
202
|
+
marginHorizontal: number;
|
|
203
|
+
alignItems: "center";
|
|
204
|
+
};
|
|
205
|
+
topbarLabel: {
|
|
206
|
+
fontSize: number;
|
|
207
|
+
};
|
|
208
|
+
userBlock: {
|
|
209
|
+
marginHorizontal: number;
|
|
210
|
+
alignItems: "flex-end";
|
|
211
|
+
};
|
|
212
|
+
userName: {
|
|
213
|
+
fontWeight: "600";
|
|
214
|
+
};
|
|
215
|
+
userEmail: {
|
|
216
|
+
fontSize: number;
|
|
217
|
+
};
|
|
218
|
+
accountBtn: {
|
|
219
|
+
marginLeft: number;
|
|
220
|
+
paddingHorizontal: number;
|
|
221
|
+
paddingVertical: number;
|
|
222
|
+
borderRadius: number;
|
|
223
|
+
};
|
|
224
|
+
accountText: {
|
|
225
|
+
fontWeight: "600";
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
declare const expandableStyles: {
|
|
229
|
+
childItem: {
|
|
230
|
+
borderRadius: number;
|
|
231
|
+
flexDirection: "row";
|
|
232
|
+
alignItems: "center";
|
|
233
|
+
paddingVertical: number;
|
|
234
|
+
};
|
|
235
|
+
childItemText: {
|
|
236
|
+
fontSize: number;
|
|
237
|
+
};
|
|
238
|
+
childItemTextWithIcon: {
|
|
239
|
+
fontSize: number;
|
|
240
|
+
marginLeft: number;
|
|
241
|
+
};
|
|
242
|
+
chevron: {
|
|
243
|
+
marginLeft: "auto";
|
|
244
|
+
};
|
|
245
|
+
childrenContainer: {
|
|
246
|
+
overflow: "hidden";
|
|
247
|
+
};
|
|
248
|
+
header: {
|
|
249
|
+
borderRadius: number;
|
|
250
|
+
flexDirection: "row";
|
|
251
|
+
alignItems: "center";
|
|
252
|
+
paddingVertical: number;
|
|
253
|
+
};
|
|
254
|
+
headerText: {
|
|
255
|
+
fontSize: number;
|
|
256
|
+
fontWeight: "600";
|
|
257
|
+
marginLeft: number;
|
|
258
|
+
};
|
|
259
|
+
iconWrapper: {
|
|
260
|
+
width: number;
|
|
261
|
+
alignItems: "center";
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
/** Base indent applied per nesting depth in the expandable item. */
|
|
265
|
+
declare const BASE_INDENT = 12;
|
|
266
|
+
/** Default icon size for nav item icons. */
|
|
267
|
+
declare const NAV_ICON_SIZE = 14;
|
|
268
|
+
/** Chevron icon size for expandable sections. */
|
|
269
|
+
declare const CHEVRON_ICON_SIZE = 12;
|
|
270
|
+
|
|
271
|
+
export { ACTIVE_BORDER_RADIUS, BASE_INDENT, CHEVRON_ICON_SIZE, NAV_ICON_SIZE, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavUser, Sidebar, type SidebarProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, expandableStyles, isRouteActive, navStyles, roleRoutesToNavItems };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ViewStyle } from 'react-native';
|
|
3
|
+
import { resolveAccessibleRoutes, RoleRouteTable, RoleRoute } from '@dloizides/auth-web';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Active-route matcher shared by the sidebar entries. Ported verbatim from the
|
|
7
|
+
* twin app sidebars: an item is active when the current pathname equals its
|
|
8
|
+
* route or is nested under it (`/foo` matches `/foo` and `/foo/bar`, but `/`
|
|
9
|
+
* only matches `/`).
|
|
10
|
+
*/
|
|
11
|
+
declare function isRouteActive(pathname: string, route: string): boolean;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Public prop types for the `@dloizides/ui-nav` config-driven navigation shell.
|
|
15
|
+
*
|
|
16
|
+
* The rendering chrome (sidebar + topbar) was byte-identical across erevna-web
|
|
17
|
+
* and katalogos-web; only the *item data* (which routes, how they group) differs
|
|
18
|
+
* per app. So this package renders a caller-supplied `NavItem[]` — labels are
|
|
19
|
+
* pre-localized strings and icons are render slots, keeping the package free of
|
|
20
|
+
* any app's i18n helper, icon set, router, or store.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** One navigation entry. Labels are already localized by the caller. */
|
|
24
|
+
interface NavItem {
|
|
25
|
+
/** Stable key + default testID. */
|
|
26
|
+
key: string;
|
|
27
|
+
/** Localized display label. */
|
|
28
|
+
label: string;
|
|
29
|
+
/** Route/path this item navigates to (passed back to `onNavigate`). */
|
|
30
|
+
route: string;
|
|
31
|
+
/** Optional testID override (defaults to `key`). */
|
|
32
|
+
testID?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Optional leading icon. Receives the resolved foreground colour and a size,
|
|
35
|
+
* so the app supplies its own icon component without this package importing
|
|
36
|
+
* an icon set.
|
|
37
|
+
*/
|
|
38
|
+
renderIcon?: (color: string, size: number) => React.ReactNode;
|
|
39
|
+
/** Optional nested items — rendered as an expandable section. */
|
|
40
|
+
children?: NavItem[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sidebar — the config-driven left navigation shell promoted from the
|
|
45
|
+
* byte-identical erevna-web / katalogos-web `Sidebar`. It renders a caller
|
|
46
|
+
* supplied `NavItem[]` (leaf + expandable), highlights the active route, and
|
|
47
|
+
* exposes header/footer slots for app-specific chrome (title, dark-mode toggle,
|
|
48
|
+
* logout, notification bell). Every colour is read from the UiProvider theme.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
interface SidebarProps {
|
|
52
|
+
/** Nav entries — already role-filtered / grouped by the app. */
|
|
53
|
+
items: NavItem[];
|
|
54
|
+
/** Current active route/path. */
|
|
55
|
+
pathname: string;
|
|
56
|
+
/** Navigation callback — receives a `NavItem.route`. */
|
|
57
|
+
onNavigate: (route: string) => void;
|
|
58
|
+
/** Localized menu title (heading). */
|
|
59
|
+
title: string;
|
|
60
|
+
/** Localized accessibility label for the navigation landmark. */
|
|
61
|
+
regionLabel: string;
|
|
62
|
+
/** a11y hint for a leaf item, given its label. Defaults to the label. */
|
|
63
|
+
navigateHint?: (label: string) => string;
|
|
64
|
+
/** a11y hint shown when an expandable section is collapsed. */
|
|
65
|
+
expandHint?: string;
|
|
66
|
+
/** a11y hint shown when an expandable section is expanded. */
|
|
67
|
+
collapseHint?: string;
|
|
68
|
+
/** Optional chevron renderer for expandable sections. */
|
|
69
|
+
renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
|
|
70
|
+
/** Optional header slot rendered above the items (e.g. a Home shortcut). */
|
|
71
|
+
header?: React.ReactNode;
|
|
72
|
+
/** Optional footer slot rendered after a flex spacer (dark-mode toggle, logout). */
|
|
73
|
+
footer?: React.ReactNode;
|
|
74
|
+
/** Extra container style overrides. */
|
|
75
|
+
containerStyle?: ViewStyle | ViewStyle[];
|
|
76
|
+
}
|
|
77
|
+
declare const Sidebar: ({ items, pathname, onNavigate, title, regionLabel, navigateHint, expandHint, collapseHint, renderChevron, header, footer, containerStyle, }: SidebarProps) => React.ReactElement;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Topbar — the config-driven top navigation bar promoted from the byte-identical
|
|
81
|
+
* erevna-web / katalogos-web `Topbar`. Structured slots replace the app-specific
|
|
82
|
+
* wiring: a `left` logo slot, an optional language toggle, a notification slot,
|
|
83
|
+
* an optional user block, an optional account button, and a required logout
|
|
84
|
+
* action. Every colour comes from the UiProvider theme.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
interface TopbarAction {
|
|
88
|
+
label: string;
|
|
89
|
+
hint: string;
|
|
90
|
+
onPress: () => void;
|
|
91
|
+
testID?: string;
|
|
92
|
+
}
|
|
93
|
+
interface TopbarUser {
|
|
94
|
+
name: string;
|
|
95
|
+
email: string;
|
|
96
|
+
}
|
|
97
|
+
interface TopbarProps {
|
|
98
|
+
/** Left slot — typically the tenant logo. */
|
|
99
|
+
left?: React.ReactNode;
|
|
100
|
+
/** Optional language toggle. */
|
|
101
|
+
language?: {
|
|
102
|
+
label: string;
|
|
103
|
+
hint: string;
|
|
104
|
+
onPress: () => void;
|
|
105
|
+
};
|
|
106
|
+
/** Optional notification slot (e.g. a notification bell). */
|
|
107
|
+
notificationSlot?: React.ReactNode;
|
|
108
|
+
/** Optional user identity block. */
|
|
109
|
+
user?: TopbarUser;
|
|
110
|
+
/** Optional account button. */
|
|
111
|
+
account?: TopbarAction;
|
|
112
|
+
/** Required logout action. */
|
|
113
|
+
logout: TopbarAction;
|
|
114
|
+
/** Extra container style overrides. */
|
|
115
|
+
containerStyle?: ViewStyle | ViewStyle[];
|
|
116
|
+
}
|
|
117
|
+
declare const Topbar: ({ left, language, notificationSlot, user, account, logout, containerStyle, }: TopbarProps) => React.ReactElement;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* NavExpandableItem — a single sidebar entry that is either a leaf (navigates)
|
|
121
|
+
* or an expandable section (toggles nested children). Ported from the twin
|
|
122
|
+
* erevna/katalogos `Sidebar/NavExpandableItem`, made config-driven: labels are
|
|
123
|
+
* pre-localized strings, icons are render slots, colours come from `useUi`.
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
interface NavExpandableItemProps {
|
|
127
|
+
item: NavItem;
|
|
128
|
+
pathname: string;
|
|
129
|
+
onNavigate: (route: string) => void;
|
|
130
|
+
/** a11y hint for a leaf item, given its label. */
|
|
131
|
+
navigateHint: (label: string) => string;
|
|
132
|
+
/** a11y hint shown when the section is collapsed (press expands). */
|
|
133
|
+
expandHint: string;
|
|
134
|
+
/** a11y hint shown when the section is expanded (press collapses). */
|
|
135
|
+
collapseHint: string;
|
|
136
|
+
/** Optional chevron icon renderer for expandable sections. */
|
|
137
|
+
renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
|
|
138
|
+
depth?: number;
|
|
139
|
+
}
|
|
140
|
+
declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, depth, }: NavExpandableItemProps) => React.ReactElement;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Role-gated nav helpers — build a `NavItem[]` from a user's roles, reusing the
|
|
144
|
+
* existing `resolveAccessibleRoutes` from `@dloizides/auth-web` (the same helper
|
|
145
|
+
* the multi-role dashboard nav uses) rather than reimplementing role filtering.
|
|
146
|
+
*
|
|
147
|
+
* The app supplies its ordered `RoleRouteTable` (role → route + optional
|
|
148
|
+
* `labelKey`) and a `translate` fn (its `FM`); this returns the ordered set of
|
|
149
|
+
* `NavItem`s the user's roles unlock — most privileged first, empty when none.
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
/** The user shape `resolveAccessibleRoutes` accepts (derived — no extra dep). */
|
|
153
|
+
type NavUser = Parameters<typeof resolveAccessibleRoutes>[0];
|
|
154
|
+
/** Map already-resolved role routes to nav items, localizing each `labelKey`. */
|
|
155
|
+
declare function roleRoutesToNavItems(routes: RoleRoute[], translate: (key: string) => string): NavItem[];
|
|
156
|
+
/**
|
|
157
|
+
* Resolve the nav items a user can reach, in table (priority) order. Wraps
|
|
158
|
+
* `resolveAccessibleRoutes` so role gating lives in one place.
|
|
159
|
+
*/
|
|
160
|
+
declare function accessibleNavItems(user: NavUser, table: RoleRouteTable, translate: (key: string) => string): NavItem[];
|
|
161
|
+
|
|
162
|
+
declare const ACTIVE_BORDER_RADIUS = 4;
|
|
163
|
+
declare const navStyles: {
|
|
164
|
+
sidebarContainer: {
|
|
165
|
+
width: number;
|
|
166
|
+
paddingTop: number;
|
|
167
|
+
paddingHorizontal: number;
|
|
168
|
+
borderRightWidth: number;
|
|
169
|
+
height: "100%";
|
|
170
|
+
};
|
|
171
|
+
sidebarTitle: {
|
|
172
|
+
fontWeight: "700";
|
|
173
|
+
marginBottom: number;
|
|
174
|
+
};
|
|
175
|
+
sidebarItem: {
|
|
176
|
+
paddingVertical: number;
|
|
177
|
+
paddingHorizontal: number;
|
|
178
|
+
borderRadius: number;
|
|
179
|
+
};
|
|
180
|
+
sidebarItemText: {
|
|
181
|
+
fontSize: number;
|
|
182
|
+
};
|
|
183
|
+
sidebarSpacer: {
|
|
184
|
+
flex: number;
|
|
185
|
+
};
|
|
186
|
+
topbarContainer: {
|
|
187
|
+
height: number;
|
|
188
|
+
paddingHorizontal: number;
|
|
189
|
+
borderBottomWidth: number;
|
|
190
|
+
flexDirection: "row";
|
|
191
|
+
alignItems: "center";
|
|
192
|
+
justifyContent: "space-between";
|
|
193
|
+
};
|
|
194
|
+
topbarLeft: {
|
|
195
|
+
flex: number;
|
|
196
|
+
};
|
|
197
|
+
topbarRight: {
|
|
198
|
+
flexDirection: "row";
|
|
199
|
+
alignItems: "center";
|
|
200
|
+
};
|
|
201
|
+
topbarRowItem: {
|
|
202
|
+
marginHorizontal: number;
|
|
203
|
+
alignItems: "center";
|
|
204
|
+
};
|
|
205
|
+
topbarLabel: {
|
|
206
|
+
fontSize: number;
|
|
207
|
+
};
|
|
208
|
+
userBlock: {
|
|
209
|
+
marginHorizontal: number;
|
|
210
|
+
alignItems: "flex-end";
|
|
211
|
+
};
|
|
212
|
+
userName: {
|
|
213
|
+
fontWeight: "600";
|
|
214
|
+
};
|
|
215
|
+
userEmail: {
|
|
216
|
+
fontSize: number;
|
|
217
|
+
};
|
|
218
|
+
accountBtn: {
|
|
219
|
+
marginLeft: number;
|
|
220
|
+
paddingHorizontal: number;
|
|
221
|
+
paddingVertical: number;
|
|
222
|
+
borderRadius: number;
|
|
223
|
+
};
|
|
224
|
+
accountText: {
|
|
225
|
+
fontWeight: "600";
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
declare const expandableStyles: {
|
|
229
|
+
childItem: {
|
|
230
|
+
borderRadius: number;
|
|
231
|
+
flexDirection: "row";
|
|
232
|
+
alignItems: "center";
|
|
233
|
+
paddingVertical: number;
|
|
234
|
+
};
|
|
235
|
+
childItemText: {
|
|
236
|
+
fontSize: number;
|
|
237
|
+
};
|
|
238
|
+
childItemTextWithIcon: {
|
|
239
|
+
fontSize: number;
|
|
240
|
+
marginLeft: number;
|
|
241
|
+
};
|
|
242
|
+
chevron: {
|
|
243
|
+
marginLeft: "auto";
|
|
244
|
+
};
|
|
245
|
+
childrenContainer: {
|
|
246
|
+
overflow: "hidden";
|
|
247
|
+
};
|
|
248
|
+
header: {
|
|
249
|
+
borderRadius: number;
|
|
250
|
+
flexDirection: "row";
|
|
251
|
+
alignItems: "center";
|
|
252
|
+
paddingVertical: number;
|
|
253
|
+
};
|
|
254
|
+
headerText: {
|
|
255
|
+
fontSize: number;
|
|
256
|
+
fontWeight: "600";
|
|
257
|
+
marginLeft: number;
|
|
258
|
+
};
|
|
259
|
+
iconWrapper: {
|
|
260
|
+
width: number;
|
|
261
|
+
alignItems: "center";
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
/** Base indent applied per nesting depth in the expandable item. */
|
|
265
|
+
declare const BASE_INDENT = 12;
|
|
266
|
+
/** Default icon size for nav item icons. */
|
|
267
|
+
declare const NAV_ICON_SIZE = 14;
|
|
268
|
+
/** Chevron icon size for expandable sections. */
|
|
269
|
+
declare const CHEVRON_ICON_SIZE = 12;
|
|
270
|
+
|
|
271
|
+
export { ACTIVE_BORDER_RADIUS, BASE_INDENT, CHEVRON_ICON_SIZE, NAV_ICON_SIZE, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavUser, Sidebar, type SidebarProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, expandableStyles, isRouteActive, navStyles, roleRoutesToNavItems };
|