@dloizides/ui-nav 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +138 -4
- package/dist/index.d.ts +138 -4
- package/dist/index.js +193 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +191 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -82,6 +82,12 @@ declare const Sidebar: ({ items, pathname, onNavigate, title, regionLabel, navig
|
|
|
82
82
|
* wiring: a `left` logo slot, an optional language toggle, a notification slot,
|
|
83
83
|
* an optional user block, an optional account button, and a required logout
|
|
84
84
|
* action. Every colour comes from the UiProvider theme.
|
|
85
|
+
*
|
|
86
|
+
* Move 1c enriches this WITHOUT breaking the promoted contract: `account` now
|
|
87
|
+
* accepts EITHER the legacy `TopbarAction` (today's erevna/katalogos calls) OR a
|
|
88
|
+
* richer `AccountState` (displayName + muted tenant line + plan badge + upgrade
|
|
89
|
+
* action). The `logout` action stays required, so every existing call keeps
|
|
90
|
+
* compiling and the logout button keeps its caller-supplied testID.
|
|
85
91
|
*/
|
|
86
92
|
|
|
87
93
|
interface TopbarAction {
|
|
@@ -94,6 +100,26 @@ interface TopbarUser {
|
|
|
94
100
|
name: string;
|
|
95
101
|
email: string;
|
|
96
102
|
}
|
|
103
|
+
/** Plan tier shown as a small badge in the rich account header. */
|
|
104
|
+
interface AccountPlan {
|
|
105
|
+
label: string;
|
|
106
|
+
tone?: 'free' | 'pro' | 'enterprise';
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Richer account model for the header (Move 1c). Supplied via `account` as an
|
|
110
|
+
* alternative to the legacy `TopbarAction` — the Topbar renders the display name
|
|
111
|
+
* (tappable when `onAccount` is set), a muted tenant line, a plan badge, and an
|
|
112
|
+
* `upgrade` action beside logout. No FM/router/store — everything is a
|
|
113
|
+
* pre-localized string or a callback.
|
|
114
|
+
*/
|
|
115
|
+
interface AccountState {
|
|
116
|
+
displayName: string;
|
|
117
|
+
tenantName?: string;
|
|
118
|
+
plan?: AccountPlan;
|
|
119
|
+
onLogout: () => void;
|
|
120
|
+
onAccount?: () => void;
|
|
121
|
+
upgrade?: TopbarAction;
|
|
122
|
+
}
|
|
97
123
|
interface TopbarProps {
|
|
98
124
|
/** Left slot — typically the tenant logo. */
|
|
99
125
|
left?: React.ReactNode;
|
|
@@ -107,15 +133,109 @@ interface TopbarProps {
|
|
|
107
133
|
notificationSlot?: React.ReactNode;
|
|
108
134
|
/** Optional user identity block. */
|
|
109
135
|
user?: TopbarUser;
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Optional account slot. Legacy callers pass a `TopbarAction` (an account
|
|
138
|
+
* button). Move-1c callers pass an `AccountState` for the richer header.
|
|
139
|
+
*/
|
|
140
|
+
account?: TopbarAction | AccountState;
|
|
141
|
+
/** Required logout action (drives the stable logout button testID). */
|
|
113
142
|
logout: TopbarAction;
|
|
114
143
|
/** Extra container style overrides. */
|
|
115
144
|
containerStyle?: ViewStyle | ViewStyle[];
|
|
116
145
|
}
|
|
117
146
|
declare const Topbar: ({ left, language, notificationSlot, user, account, logout, containerStyle, }: TopbarProps) => React.ReactElement;
|
|
118
147
|
|
|
148
|
+
/** Content-width policy: a capped column, optionally wider past a viewport breakpoint, or full-bleed. */
|
|
149
|
+
type AppShellWidth = {
|
|
150
|
+
max: number;
|
|
151
|
+
wideMax?: number;
|
|
152
|
+
wideMinViewport?: number;
|
|
153
|
+
} | 'full';
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the content column's max width for a given viewport. `'full'` passes
|
|
156
|
+
* through (no cap); an object caps at `max`, widening to `wideMax` once the
|
|
157
|
+
* viewport reaches `wideMinViewport`.
|
|
158
|
+
*/
|
|
159
|
+
declare function resolveContentMaxWidth(width: AppShellWidth, viewport: number): number | 'full';
|
|
160
|
+
/** Hook wrapper: resolves the content max width against the live window width. */
|
|
161
|
+
declare function useContentMaxWidth(width: AppShellWidth): number | 'full';
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* AppShell — the shared dashboard-chrome composition (Move 1c). It hosts, top to
|
|
165
|
+
* bottom: an app-supplied header (full-bleed), an optional per-app nav region, an
|
|
166
|
+
* optional banner, and a width-disciplined scrollable content column. It also
|
|
167
|
+
* folds in the two things every product page re-implements: an auth gate
|
|
168
|
+
* (pending spinner / redirect-when-unauthenticated) and page state cards
|
|
169
|
+
* (loading / error / forbidden).
|
|
170
|
+
*
|
|
171
|
+
* Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM, router,
|
|
172
|
+
* or store imports. The app passes wired elements, pre-localized strings, and
|
|
173
|
+
* callbacks; every colour is routed through the `@dloizides/ui-feedback` theme.
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
/** A titled message card (error / forbidden). Colours come from the theme. */
|
|
177
|
+
interface ShellMessage {
|
|
178
|
+
titleText: string;
|
|
179
|
+
messageText: string;
|
|
180
|
+
}
|
|
181
|
+
interface AppShellProps {
|
|
182
|
+
/** App-supplied wired header (Topbar / AppHeader), rendered full-bleed. */
|
|
183
|
+
header: React.ReactNode;
|
|
184
|
+
/** Per-app nav region (kefi coral pills / erevna sidebar+drawer). */
|
|
185
|
+
nav?: React.ReactNode;
|
|
186
|
+
/** Banner slot (e.g. verification-pending). */
|
|
187
|
+
banner?: React.ReactNode;
|
|
188
|
+
/** Content-width policy. Defaults to full-bleed. */
|
|
189
|
+
width?: AppShellWidth;
|
|
190
|
+
/** Content padding inside the scroll column. Defaults to 24. */
|
|
191
|
+
contentPadding?: number;
|
|
192
|
+
/** Auth gate — spinner while pending, `onRedirect` when unauthenticated. */
|
|
193
|
+
gate?: {
|
|
194
|
+
pending: boolean;
|
|
195
|
+
authenticated: boolean;
|
|
196
|
+
onRedirect: () => void;
|
|
197
|
+
};
|
|
198
|
+
/** Page state — loading spinner card, or a titled error / forbidden card. */
|
|
199
|
+
state?: {
|
|
200
|
+
loading?: boolean;
|
|
201
|
+
error?: ShellMessage | null;
|
|
202
|
+
forbidden?: ShellMessage | null;
|
|
203
|
+
};
|
|
204
|
+
/** Root testID; the content region is exposed as `${testID}-content`. */
|
|
205
|
+
testID: string;
|
|
206
|
+
children: React.ReactNode;
|
|
207
|
+
}
|
|
208
|
+
declare const AppShell: ({ header, nav, banner, width, contentPadding, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Default testIDs for `@dloizides/ui-nav`. Kept as a small central map (mirrors
|
|
212
|
+
* `@dloizides/ui-layout`'s `LAYOUT_TEST_IDS`) so the app-agnostic chrome exposes
|
|
213
|
+
* stable Playwright selectors. The existing Topbar logout/account buttons keep
|
|
214
|
+
* their *caller-supplied* testIDs (via `TopbarAction.testID`) — these constants
|
|
215
|
+
* only name the NEW account-state bits and the AppShell region suffixes.
|
|
216
|
+
*/
|
|
217
|
+
declare const NAV_TEST_IDS: {
|
|
218
|
+
/** displayName line in the rich account header (tappable when `onAccount` set). */
|
|
219
|
+
readonly accountName: "topbar-account-name";
|
|
220
|
+
/** muted tenant-name line under the displayName. */
|
|
221
|
+
readonly accountTenant: "topbar-account-tenant";
|
|
222
|
+
/** small plan badge (free / pro / enterprise). */
|
|
223
|
+
readonly accountPlan: "topbar-account-plan";
|
|
224
|
+
/** default testID for the rich header's upgrade action (overridable per-action). */
|
|
225
|
+
readonly accountUpgrade: "topbar-account-upgrade";
|
|
226
|
+
};
|
|
227
|
+
/** Suffixes appended to an `AppShell`'s required `testID` to name its regions. */
|
|
228
|
+
declare const APP_SHELL_SUFFIX: {
|
|
229
|
+
readonly content: "-content";
|
|
230
|
+
readonly header: "-header";
|
|
231
|
+
readonly nav: "-nav";
|
|
232
|
+
readonly banner: "-banner";
|
|
233
|
+
readonly pending: "-pending";
|
|
234
|
+
readonly loading: "-loading";
|
|
235
|
+
readonly error: "-error";
|
|
236
|
+
readonly forbidden: "-forbidden";
|
|
237
|
+
};
|
|
238
|
+
|
|
119
239
|
/**
|
|
120
240
|
* NavExpandableItem — a single sidebar entry that is either a leaf (navigates)
|
|
121
241
|
* or an expandable section (toggles nested children). Ported from the twin
|
|
@@ -224,6 +344,20 @@ declare const navStyles: {
|
|
|
224
344
|
accountText: {
|
|
225
345
|
fontWeight: "600";
|
|
226
346
|
};
|
|
347
|
+
accountTenant: {
|
|
348
|
+
fontSize: number;
|
|
349
|
+
};
|
|
350
|
+
planBadge: {
|
|
351
|
+
marginTop: number;
|
|
352
|
+
paddingHorizontal: number;
|
|
353
|
+
paddingVertical: number;
|
|
354
|
+
borderRadius: number;
|
|
355
|
+
alignSelf: "flex-end";
|
|
356
|
+
};
|
|
357
|
+
planBadgeText: {
|
|
358
|
+
fontSize: number;
|
|
359
|
+
fontWeight: "700";
|
|
360
|
+
};
|
|
227
361
|
};
|
|
228
362
|
declare const expandableStyles: {
|
|
229
363
|
childItem: {
|
|
@@ -268,4 +402,4 @@ declare const NAV_ICON_SIZE = 14;
|
|
|
268
402
|
/** Chevron icon size for expandable sections. */
|
|
269
403
|
declare const CHEVRON_ICON_SIZE = 12;
|
|
270
404
|
|
|
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 };
|
|
405
|
+
export { ACTIVE_BORDER_RADIUS, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, NAV_ICON_SIZE, NAV_TEST_IDS, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavUser, type ShellMessage, Sidebar, type SidebarProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, expandableStyles, isRouteActive, navStyles, resolveContentMaxWidth, roleRoutesToNavItems, useContentMaxWidth };
|
package/dist/index.d.ts
CHANGED
|
@@ -82,6 +82,12 @@ declare const Sidebar: ({ items, pathname, onNavigate, title, regionLabel, navig
|
|
|
82
82
|
* wiring: a `left` logo slot, an optional language toggle, a notification slot,
|
|
83
83
|
* an optional user block, an optional account button, and a required logout
|
|
84
84
|
* action. Every colour comes from the UiProvider theme.
|
|
85
|
+
*
|
|
86
|
+
* Move 1c enriches this WITHOUT breaking the promoted contract: `account` now
|
|
87
|
+
* accepts EITHER the legacy `TopbarAction` (today's erevna/katalogos calls) OR a
|
|
88
|
+
* richer `AccountState` (displayName + muted tenant line + plan badge + upgrade
|
|
89
|
+
* action). The `logout` action stays required, so every existing call keeps
|
|
90
|
+
* compiling and the logout button keeps its caller-supplied testID.
|
|
85
91
|
*/
|
|
86
92
|
|
|
87
93
|
interface TopbarAction {
|
|
@@ -94,6 +100,26 @@ interface TopbarUser {
|
|
|
94
100
|
name: string;
|
|
95
101
|
email: string;
|
|
96
102
|
}
|
|
103
|
+
/** Plan tier shown as a small badge in the rich account header. */
|
|
104
|
+
interface AccountPlan {
|
|
105
|
+
label: string;
|
|
106
|
+
tone?: 'free' | 'pro' | 'enterprise';
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Richer account model for the header (Move 1c). Supplied via `account` as an
|
|
110
|
+
* alternative to the legacy `TopbarAction` — the Topbar renders the display name
|
|
111
|
+
* (tappable when `onAccount` is set), a muted tenant line, a plan badge, and an
|
|
112
|
+
* `upgrade` action beside logout. No FM/router/store — everything is a
|
|
113
|
+
* pre-localized string or a callback.
|
|
114
|
+
*/
|
|
115
|
+
interface AccountState {
|
|
116
|
+
displayName: string;
|
|
117
|
+
tenantName?: string;
|
|
118
|
+
plan?: AccountPlan;
|
|
119
|
+
onLogout: () => void;
|
|
120
|
+
onAccount?: () => void;
|
|
121
|
+
upgrade?: TopbarAction;
|
|
122
|
+
}
|
|
97
123
|
interface TopbarProps {
|
|
98
124
|
/** Left slot — typically the tenant logo. */
|
|
99
125
|
left?: React.ReactNode;
|
|
@@ -107,15 +133,109 @@ interface TopbarProps {
|
|
|
107
133
|
notificationSlot?: React.ReactNode;
|
|
108
134
|
/** Optional user identity block. */
|
|
109
135
|
user?: TopbarUser;
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Optional account slot. Legacy callers pass a `TopbarAction` (an account
|
|
138
|
+
* button). Move-1c callers pass an `AccountState` for the richer header.
|
|
139
|
+
*/
|
|
140
|
+
account?: TopbarAction | AccountState;
|
|
141
|
+
/** Required logout action (drives the stable logout button testID). */
|
|
113
142
|
logout: TopbarAction;
|
|
114
143
|
/** Extra container style overrides. */
|
|
115
144
|
containerStyle?: ViewStyle | ViewStyle[];
|
|
116
145
|
}
|
|
117
146
|
declare const Topbar: ({ left, language, notificationSlot, user, account, logout, containerStyle, }: TopbarProps) => React.ReactElement;
|
|
118
147
|
|
|
148
|
+
/** Content-width policy: a capped column, optionally wider past a viewport breakpoint, or full-bleed. */
|
|
149
|
+
type AppShellWidth = {
|
|
150
|
+
max: number;
|
|
151
|
+
wideMax?: number;
|
|
152
|
+
wideMinViewport?: number;
|
|
153
|
+
} | 'full';
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the content column's max width for a given viewport. `'full'` passes
|
|
156
|
+
* through (no cap); an object caps at `max`, widening to `wideMax` once the
|
|
157
|
+
* viewport reaches `wideMinViewport`.
|
|
158
|
+
*/
|
|
159
|
+
declare function resolveContentMaxWidth(width: AppShellWidth, viewport: number): number | 'full';
|
|
160
|
+
/** Hook wrapper: resolves the content max width against the live window width. */
|
|
161
|
+
declare function useContentMaxWidth(width: AppShellWidth): number | 'full';
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* AppShell — the shared dashboard-chrome composition (Move 1c). It hosts, top to
|
|
165
|
+
* bottom: an app-supplied header (full-bleed), an optional per-app nav region, an
|
|
166
|
+
* optional banner, and a width-disciplined scrollable content column. It also
|
|
167
|
+
* folds in the two things every product page re-implements: an auth gate
|
|
168
|
+
* (pending spinner / redirect-when-unauthenticated) and page state cards
|
|
169
|
+
* (loading / error / forbidden).
|
|
170
|
+
*
|
|
171
|
+
* Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM, router,
|
|
172
|
+
* or store imports. The app passes wired elements, pre-localized strings, and
|
|
173
|
+
* callbacks; every colour is routed through the `@dloizides/ui-feedback` theme.
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
/** A titled message card (error / forbidden). Colours come from the theme. */
|
|
177
|
+
interface ShellMessage {
|
|
178
|
+
titleText: string;
|
|
179
|
+
messageText: string;
|
|
180
|
+
}
|
|
181
|
+
interface AppShellProps {
|
|
182
|
+
/** App-supplied wired header (Topbar / AppHeader), rendered full-bleed. */
|
|
183
|
+
header: React.ReactNode;
|
|
184
|
+
/** Per-app nav region (kefi coral pills / erevna sidebar+drawer). */
|
|
185
|
+
nav?: React.ReactNode;
|
|
186
|
+
/** Banner slot (e.g. verification-pending). */
|
|
187
|
+
banner?: React.ReactNode;
|
|
188
|
+
/** Content-width policy. Defaults to full-bleed. */
|
|
189
|
+
width?: AppShellWidth;
|
|
190
|
+
/** Content padding inside the scroll column. Defaults to 24. */
|
|
191
|
+
contentPadding?: number;
|
|
192
|
+
/** Auth gate — spinner while pending, `onRedirect` when unauthenticated. */
|
|
193
|
+
gate?: {
|
|
194
|
+
pending: boolean;
|
|
195
|
+
authenticated: boolean;
|
|
196
|
+
onRedirect: () => void;
|
|
197
|
+
};
|
|
198
|
+
/** Page state — loading spinner card, or a titled error / forbidden card. */
|
|
199
|
+
state?: {
|
|
200
|
+
loading?: boolean;
|
|
201
|
+
error?: ShellMessage | null;
|
|
202
|
+
forbidden?: ShellMessage | null;
|
|
203
|
+
};
|
|
204
|
+
/** Root testID; the content region is exposed as `${testID}-content`. */
|
|
205
|
+
testID: string;
|
|
206
|
+
children: React.ReactNode;
|
|
207
|
+
}
|
|
208
|
+
declare const AppShell: ({ header, nav, banner, width, contentPadding, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Default testIDs for `@dloizides/ui-nav`. Kept as a small central map (mirrors
|
|
212
|
+
* `@dloizides/ui-layout`'s `LAYOUT_TEST_IDS`) so the app-agnostic chrome exposes
|
|
213
|
+
* stable Playwright selectors. The existing Topbar logout/account buttons keep
|
|
214
|
+
* their *caller-supplied* testIDs (via `TopbarAction.testID`) — these constants
|
|
215
|
+
* only name the NEW account-state bits and the AppShell region suffixes.
|
|
216
|
+
*/
|
|
217
|
+
declare const NAV_TEST_IDS: {
|
|
218
|
+
/** displayName line in the rich account header (tappable when `onAccount` set). */
|
|
219
|
+
readonly accountName: "topbar-account-name";
|
|
220
|
+
/** muted tenant-name line under the displayName. */
|
|
221
|
+
readonly accountTenant: "topbar-account-tenant";
|
|
222
|
+
/** small plan badge (free / pro / enterprise). */
|
|
223
|
+
readonly accountPlan: "topbar-account-plan";
|
|
224
|
+
/** default testID for the rich header's upgrade action (overridable per-action). */
|
|
225
|
+
readonly accountUpgrade: "topbar-account-upgrade";
|
|
226
|
+
};
|
|
227
|
+
/** Suffixes appended to an `AppShell`'s required `testID` to name its regions. */
|
|
228
|
+
declare const APP_SHELL_SUFFIX: {
|
|
229
|
+
readonly content: "-content";
|
|
230
|
+
readonly header: "-header";
|
|
231
|
+
readonly nav: "-nav";
|
|
232
|
+
readonly banner: "-banner";
|
|
233
|
+
readonly pending: "-pending";
|
|
234
|
+
readonly loading: "-loading";
|
|
235
|
+
readonly error: "-error";
|
|
236
|
+
readonly forbidden: "-forbidden";
|
|
237
|
+
};
|
|
238
|
+
|
|
119
239
|
/**
|
|
120
240
|
* NavExpandableItem — a single sidebar entry that is either a leaf (navigates)
|
|
121
241
|
* or an expandable section (toggles nested children). Ported from the twin
|
|
@@ -224,6 +344,20 @@ declare const navStyles: {
|
|
|
224
344
|
accountText: {
|
|
225
345
|
fontWeight: "600";
|
|
226
346
|
};
|
|
347
|
+
accountTenant: {
|
|
348
|
+
fontSize: number;
|
|
349
|
+
};
|
|
350
|
+
planBadge: {
|
|
351
|
+
marginTop: number;
|
|
352
|
+
paddingHorizontal: number;
|
|
353
|
+
paddingVertical: number;
|
|
354
|
+
borderRadius: number;
|
|
355
|
+
alignSelf: "flex-end";
|
|
356
|
+
};
|
|
357
|
+
planBadgeText: {
|
|
358
|
+
fontSize: number;
|
|
359
|
+
fontWeight: "700";
|
|
360
|
+
};
|
|
227
361
|
};
|
|
228
362
|
declare const expandableStyles: {
|
|
229
363
|
childItem: {
|
|
@@ -268,4 +402,4 @@ declare const NAV_ICON_SIZE = 14;
|
|
|
268
402
|
/** Chevron icon size for expandable sections. */
|
|
269
403
|
declare const CHEVRON_ICON_SIZE = 12;
|
|
270
404
|
|
|
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 };
|
|
405
|
+
export { ACTIVE_BORDER_RADIUS, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, NAV_ICON_SIZE, NAV_TEST_IDS, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavUser, type ShellMessage, Sidebar, type SidebarProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, expandableStyles, isRouteActive, navStyles, resolveContentMaxWidth, roleRoutesToNavItems, useContentMaxWidth };
|
package/dist/index.js
CHANGED
|
@@ -55,7 +55,11 @@ var navStyles = reactNative.StyleSheet.create({
|
|
|
55
55
|
userName: { fontWeight: "600" },
|
|
56
56
|
userEmail: { fontSize: 12 },
|
|
57
57
|
accountBtn: { marginLeft: 8, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 4 },
|
|
58
|
-
accountText: { fontWeight: "600" }
|
|
58
|
+
accountText: { fontWeight: "600" },
|
|
59
|
+
// --- Rich account-state header (Move 1c) ---
|
|
60
|
+
accountTenant: { fontSize: 12 },
|
|
61
|
+
planBadge: { marginTop: 2, paddingHorizontal: 8, paddingVertical: 2, borderRadius: 10, alignSelf: "flex-end" },
|
|
62
|
+
planBadgeText: { fontSize: 11, fontWeight: "700" }
|
|
59
63
|
});
|
|
60
64
|
var expandableStyles = reactNative.StyleSheet.create({
|
|
61
65
|
childItem: {
|
|
@@ -212,7 +216,38 @@ var Sidebar = ({
|
|
|
212
216
|
}
|
|
213
217
|
);
|
|
214
218
|
};
|
|
219
|
+
|
|
220
|
+
// src/constants.ts
|
|
221
|
+
var NAV_TEST_IDS = {
|
|
222
|
+
/** displayName line in the rich account header (tappable when `onAccount` set). */
|
|
223
|
+
accountName: "topbar-account-name",
|
|
224
|
+
/** muted tenant-name line under the displayName. */
|
|
225
|
+
accountTenant: "topbar-account-tenant",
|
|
226
|
+
/** small plan badge (free / pro / enterprise). */
|
|
227
|
+
accountPlan: "topbar-account-plan",
|
|
228
|
+
/** default testID for the rich header's upgrade action (overridable per-action). */
|
|
229
|
+
accountUpgrade: "topbar-account-upgrade"
|
|
230
|
+
};
|
|
231
|
+
var APP_SHELL_SUFFIX = {
|
|
232
|
+
content: "-content",
|
|
233
|
+
header: "-header",
|
|
234
|
+
nav: "-nav",
|
|
235
|
+
banner: "-banner",
|
|
236
|
+
pending: "-pending",
|
|
237
|
+
loading: "-loading",
|
|
238
|
+
error: "-error",
|
|
239
|
+
forbidden: "-forbidden"
|
|
240
|
+
};
|
|
215
241
|
var TEXT_ON_PRIMARY = "#ffffff";
|
|
242
|
+
function isAccountState(account) {
|
|
243
|
+
return "displayName" in account;
|
|
244
|
+
}
|
|
245
|
+
function planToneColor(theme, tone) {
|
|
246
|
+
const palette = theme.palette;
|
|
247
|
+
if (tone === "pro") return palette.primary["500"];
|
|
248
|
+
if (tone === "enterprise") return palette.accent?.["500"] ?? palette.primary["500"];
|
|
249
|
+
return theme.colors.textSecondary;
|
|
250
|
+
}
|
|
216
251
|
var Topbar = ({
|
|
217
252
|
left,
|
|
218
253
|
language,
|
|
@@ -225,6 +260,9 @@ var Topbar = ({
|
|
|
225
260
|
const { theme } = uiFeedback.useUi();
|
|
226
261
|
const colors = theme.colors;
|
|
227
262
|
const primaryColor = theme.palette.primary["500"];
|
|
263
|
+
const richAccount = account !== void 0 && isAccountState(account) ? account : void 0;
|
|
264
|
+
const legacyAccount = account !== void 0 && !isAccountState(account) ? account : void 0;
|
|
265
|
+
const upgrade = richAccount?.upgrade;
|
|
228
266
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
229
267
|
reactNative.View,
|
|
230
268
|
{
|
|
@@ -252,16 +290,57 @@ var Topbar = ({
|
|
|
252
290
|
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.userName, { color: colors.text }], children: user.name }),
|
|
253
291
|
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.userEmail, { color: colors.textSecondary }], children: user.email })
|
|
254
292
|
] }) : null,
|
|
255
|
-
|
|
293
|
+
richAccount ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: navStyles.userBlock, children: [
|
|
294
|
+
richAccount.onAccount ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
295
|
+
reactNative.TouchableOpacity,
|
|
296
|
+
{
|
|
297
|
+
accessibilityHint: richAccount.displayName,
|
|
298
|
+
accessibilityLabel: richAccount.displayName,
|
|
299
|
+
accessibilityRole: "button",
|
|
300
|
+
testID: NAV_TEST_IDS.accountName,
|
|
301
|
+
onPress: richAccount.onAccount,
|
|
302
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.userName, { color: colors.text }], children: richAccount.displayName })
|
|
303
|
+
}
|
|
304
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.userName, { color: colors.text }], testID: NAV_TEST_IDS.accountName, children: richAccount.displayName }),
|
|
305
|
+
richAccount.tenantName !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
306
|
+
reactNative.Text,
|
|
307
|
+
{
|
|
308
|
+
style: [navStyles.accountTenant, { color: colors.textSecondary }],
|
|
309
|
+
testID: NAV_TEST_IDS.accountTenant,
|
|
310
|
+
children: richAccount.tenantName
|
|
311
|
+
}
|
|
312
|
+
) : null,
|
|
313
|
+
richAccount.plan ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
314
|
+
reactNative.View,
|
|
315
|
+
{
|
|
316
|
+
style: [navStyles.planBadge, { backgroundColor: planToneColor(theme, richAccount.plan.tone) }],
|
|
317
|
+
testID: NAV_TEST_IDS.accountPlan,
|
|
318
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.planBadgeText, { color: TEXT_ON_PRIMARY }], children: richAccount.plan.label })
|
|
319
|
+
}
|
|
320
|
+
) : null
|
|
321
|
+
] }) : null,
|
|
322
|
+
legacyAccount ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
323
|
+
reactNative.TouchableOpacity,
|
|
324
|
+
{
|
|
325
|
+
accessibilityHint: legacyAccount.hint,
|
|
326
|
+
accessibilityLabel: legacyAccount.label,
|
|
327
|
+
accessibilityRole: "button",
|
|
328
|
+
style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
|
|
329
|
+
testID: legacyAccount.testID,
|
|
330
|
+
onPress: legacyAccount.onPress,
|
|
331
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children: legacyAccount.label })
|
|
332
|
+
}
|
|
333
|
+
) : null,
|
|
334
|
+
upgrade ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
256
335
|
reactNative.TouchableOpacity,
|
|
257
336
|
{
|
|
258
|
-
accessibilityHint:
|
|
259
|
-
accessibilityLabel:
|
|
337
|
+
accessibilityHint: upgrade.hint,
|
|
338
|
+
accessibilityLabel: upgrade.label,
|
|
260
339
|
accessibilityRole: "button",
|
|
261
340
|
style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
|
|
262
|
-
testID:
|
|
263
|
-
onPress:
|
|
264
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children:
|
|
341
|
+
testID: upgrade.testID ?? NAV_TEST_IDS.accountUpgrade,
|
|
342
|
+
onPress: upgrade.onPress,
|
|
343
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children: upgrade.label })
|
|
265
344
|
}
|
|
266
345
|
) : null,
|
|
267
346
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -273,7 +352,7 @@ var Topbar = ({
|
|
|
273
352
|
accessibilityRole: "button",
|
|
274
353
|
style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
|
|
275
354
|
testID: logout.testID,
|
|
276
|
-
onPress: logout.onPress,
|
|
355
|
+
onPress: richAccount ? richAccount.onLogout : logout.onPress,
|
|
277
356
|
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children: logout.label })
|
|
278
357
|
}
|
|
279
358
|
)
|
|
@@ -282,6 +361,107 @@ var Topbar = ({
|
|
|
282
361
|
}
|
|
283
362
|
);
|
|
284
363
|
};
|
|
364
|
+
function resolveContentMaxWidth(width, viewport) {
|
|
365
|
+
if (width === "full") return "full";
|
|
366
|
+
const wideMinViewport = width.wideMinViewport ?? Number.POSITIVE_INFINITY;
|
|
367
|
+
const isWide = viewport >= wideMinViewport;
|
|
368
|
+
return isWide ? width.wideMax ?? width.max : width.max;
|
|
369
|
+
}
|
|
370
|
+
function useContentMaxWidth(width) {
|
|
371
|
+
const { width: viewport } = reactNative.useWindowDimensions();
|
|
372
|
+
return resolveContentMaxWidth(width, viewport);
|
|
373
|
+
}
|
|
374
|
+
var DEFAULT_CONTENT_PADDING = 24;
|
|
375
|
+
var CARD_PADDING = 20;
|
|
376
|
+
var CARD_BORDER_RADIUS = 12;
|
|
377
|
+
var CARD_BORDER_WIDTH = 1;
|
|
378
|
+
var CARD_TITLE_FONT_SIZE = 16;
|
|
379
|
+
var CARD_MESSAGE_FONT_SIZE = 14;
|
|
380
|
+
var CARD_TITLE_MARGIN_BOTTOM = 8;
|
|
381
|
+
var styles = reactNative.StyleSheet.create({
|
|
382
|
+
root: { flex: 1 },
|
|
383
|
+
centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
|
|
384
|
+
scroll: { flex: 1 },
|
|
385
|
+
scrollContent: { flexGrow: 1 },
|
|
386
|
+
columnFull: { flex: 1 },
|
|
387
|
+
columnCapped: { flex: 1, width: "100%", alignSelf: "center" },
|
|
388
|
+
card: {
|
|
389
|
+
padding: CARD_PADDING,
|
|
390
|
+
borderRadius: CARD_BORDER_RADIUS,
|
|
391
|
+
borderWidth: CARD_BORDER_WIDTH
|
|
392
|
+
},
|
|
393
|
+
cardTitle: { fontSize: CARD_TITLE_FONT_SIZE, fontWeight: "700", marginBottom: CARD_TITLE_MARGIN_BOTTOM },
|
|
394
|
+
cardMessage: { fontSize: CARD_MESSAGE_FONT_SIZE }
|
|
395
|
+
});
|
|
396
|
+
function MessageCard({
|
|
397
|
+
message,
|
|
398
|
+
accentColor,
|
|
399
|
+
testID
|
|
400
|
+
}) {
|
|
401
|
+
const { theme } = uiFeedback.useUi();
|
|
402
|
+
const colors = theme.colors;
|
|
403
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
404
|
+
reactNative.View,
|
|
405
|
+
{
|
|
406
|
+
style: [styles.card, { backgroundColor: colors.surface, borderColor: accentColor }],
|
|
407
|
+
testID,
|
|
408
|
+
children: [
|
|
409
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.cardTitle, { color: accentColor }], children: message.titleText }),
|
|
410
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.cardMessage, { color: colors.textSecondary }], children: message.messageText })
|
|
411
|
+
]
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
function useContentBody(state, children, testID) {
|
|
416
|
+
const { theme } = uiFeedback.useUi();
|
|
417
|
+
const primary = theme.palette.primary["500"];
|
|
418
|
+
const errorColor = theme.semantic.error["500"];
|
|
419
|
+
if (state?.forbidden)
|
|
420
|
+
return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.forbidden, testID: `${testID}${APP_SHELL_SUFFIX.forbidden}` });
|
|
421
|
+
if (state?.error)
|
|
422
|
+
return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
|
|
423
|
+
if (state?.loading === true)
|
|
424
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.centerFill, testID: `${testID}${APP_SHELL_SUFFIX.loading}`, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" }) });
|
|
425
|
+
return children;
|
|
426
|
+
}
|
|
427
|
+
var AppShell = ({
|
|
428
|
+
header,
|
|
429
|
+
nav,
|
|
430
|
+
banner,
|
|
431
|
+
width = "full",
|
|
432
|
+
contentPadding = DEFAULT_CONTENT_PADDING,
|
|
433
|
+
gate,
|
|
434
|
+
state,
|
|
435
|
+
testID,
|
|
436
|
+
children
|
|
437
|
+
}) => {
|
|
438
|
+
const { theme } = uiFeedback.useUi();
|
|
439
|
+
const primary = theme.palette.primary["500"];
|
|
440
|
+
const maxWidth = useContentMaxWidth(width);
|
|
441
|
+
const body = useContentBody(state, children, testID);
|
|
442
|
+
const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
|
|
443
|
+
react.useEffect(() => {
|
|
444
|
+
if (isUnauthenticated && gate !== void 0) gate.onRedirect();
|
|
445
|
+
}, [isUnauthenticated, gate]);
|
|
446
|
+
if (gate?.pending === true)
|
|
447
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles.root, styles.centerFill, { backgroundColor: theme.colors.background }], testID: `${testID}${APP_SHELL_SUFFIX.pending}`, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" }) });
|
|
448
|
+
if (isUnauthenticated) return null;
|
|
449
|
+
const columnStyle = maxWidth === "full" ? styles.columnFull : [styles.columnCapped, { maxWidth }];
|
|
450
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.root, { backgroundColor: theme.colors.background }], testID, children: [
|
|
451
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header }),
|
|
452
|
+
nav !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.nav}`, children: nav }) : null,
|
|
453
|
+
banner !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: banner }) : null,
|
|
454
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
455
|
+
reactNative.ScrollView,
|
|
456
|
+
{
|
|
457
|
+
contentContainerStyle: [styles.scrollContent, { padding: contentPadding }],
|
|
458
|
+
style: styles.scroll,
|
|
459
|
+
testID: `${testID}${APP_SHELL_SUFFIX.content}`,
|
|
460
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: columnStyle, children: body })
|
|
461
|
+
}
|
|
462
|
+
)
|
|
463
|
+
] });
|
|
464
|
+
};
|
|
285
465
|
function roleRoutesToNavItems(routes, translate) {
|
|
286
466
|
return routes.map((entry) => ({
|
|
287
467
|
key: entry.role,
|
|
@@ -294,9 +474,12 @@ function accessibleNavItems(user, table, translate) {
|
|
|
294
474
|
}
|
|
295
475
|
|
|
296
476
|
exports.ACTIVE_BORDER_RADIUS = ACTIVE_BORDER_RADIUS;
|
|
477
|
+
exports.APP_SHELL_SUFFIX = APP_SHELL_SUFFIX;
|
|
478
|
+
exports.AppShell = AppShell;
|
|
297
479
|
exports.BASE_INDENT = BASE_INDENT;
|
|
298
480
|
exports.CHEVRON_ICON_SIZE = CHEVRON_ICON_SIZE;
|
|
299
481
|
exports.NAV_ICON_SIZE = NAV_ICON_SIZE;
|
|
482
|
+
exports.NAV_TEST_IDS = NAV_TEST_IDS;
|
|
300
483
|
exports.NavExpandableItem = NavExpandableItem;
|
|
301
484
|
exports.Sidebar = Sidebar;
|
|
302
485
|
exports.Topbar = Topbar;
|
|
@@ -304,6 +487,8 @@ exports.accessibleNavItems = accessibleNavItems;
|
|
|
304
487
|
exports.expandableStyles = expandableStyles;
|
|
305
488
|
exports.isRouteActive = isRouteActive;
|
|
306
489
|
exports.navStyles = navStyles;
|
|
490
|
+
exports.resolveContentMaxWidth = resolveContentMaxWidth;
|
|
307
491
|
exports.roleRoutesToNavItems = roleRoutesToNavItems;
|
|
492
|
+
exports.useContentMaxWidth = useContentMaxWidth;
|
|
308
493
|
//# sourceMappingURL=index.js.map
|
|
309
494
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../src/isRouteActive.ts","../src/styles.ts","../src/NavExpandableItem.tsx","../src/Sidebar.tsx","../src/constants.ts","../src/Topbar.tsx","../src/useContentMaxWidth.ts","../src/AppShell.tsx","../src/roleNav.ts"],"names":["StyleSheet","useState","useUi","useCallback","useMemo","jsxs","TouchableOpacity","jsx","View","Text","useWindowDimensions","ActivityIndicator","useEffect","ScrollView","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 // --- Rich account-state header (Move 1c) ---\n accountTenant: { fontSize: 12 },\n planBadge: { marginTop: 2, paddingHorizontal: 8, paddingVertical: 2, borderRadius: 10, alignSelf: 'flex-end' },\n planBadgeText: { fontSize: 11, fontWeight: '700' },\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 * Default testIDs for `@dloizides/ui-nav`. Kept as a small central map (mirrors\n * `@dloizides/ui-layout`'s `LAYOUT_TEST_IDS`) so the app-agnostic chrome exposes\n * stable Playwright selectors. The existing Topbar logout/account buttons keep\n * their *caller-supplied* testIDs (via `TopbarAction.testID`) — these constants\n * only name the NEW account-state bits and the AppShell region suffixes.\n */\nexport const NAV_TEST_IDS = {\n /** displayName line in the rich account header (tappable when `onAccount` set). */\n accountName: 'topbar-account-name',\n /** muted tenant-name line under the displayName. */\n accountTenant: 'topbar-account-tenant',\n /** small plan badge (free / pro / enterprise). */\n accountPlan: 'topbar-account-plan',\n /** default testID for the rich header's upgrade action (overridable per-action). */\n accountUpgrade: 'topbar-account-upgrade',\n} as const;\n\n/** Suffixes appended to an `AppShell`'s required `testID` to name its regions. */\nexport const APP_SHELL_SUFFIX = {\n content: '-content',\n header: '-header',\n nav: '-nav',\n banner: '-banner',\n pending: '-pending',\n loading: '-loading',\n error: '-error',\n forbidden: '-forbidden',\n} as const;\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 *\n * Move 1c enriches this WITHOUT breaking the promoted contract: `account` now\n * accepts EITHER the legacy `TopbarAction` (today's erevna/katalogos calls) OR a\n * richer `AccountState` (displayName + muted tenant line + plan badge + upgrade\n * action). The `logout` action stays required, so every existing call keeps\n * compiling and the logout button keeps its caller-supplied testID.\n */\nimport React from 'react';\n\nimport { Text, TouchableOpacity, View, type ViewStyle } from 'react-native';\n\nimport { useUi, type UiTheme } from '@dloizides/ui-feedback';\n\nimport { NAV_TEST_IDS } from './constants';\nimport { navStyles as styles } from './styles';\n\n/** Text color for elements on primary/coloured badge 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\n/** Plan tier shown as a small badge in the rich account header. */\nexport interface AccountPlan {\n label: string;\n tone?: 'free' | 'pro' | 'enterprise';\n}\n\n/**\n * Richer account model for the header (Move 1c). Supplied via `account` as an\n * alternative to the legacy `TopbarAction` — the Topbar renders the display name\n * (tappable when `onAccount` is set), a muted tenant line, a plan badge, and an\n * `upgrade` action beside logout. No FM/router/store — everything is a\n * pre-localized string or a callback.\n */\nexport interface AccountState {\n displayName: string;\n tenantName?: string;\n plan?: AccountPlan;\n onLogout: () => void;\n onAccount?: () => void;\n upgrade?: TopbarAction;\n}\n\n/** Type guard: distinguishes the rich `AccountState` from a legacy `TopbarAction`. */\nfunction isAccountState(account: TopbarAction | AccountState): account is AccountState {\n return 'displayName' in account;\n}\n\n/** Resolve a plan badge background from its tone, routed through the theme. */\nfunction planToneColor(theme: UiTheme, tone: AccountPlan['tone']): string {\n const palette = theme.palette;\n if (tone === 'pro') return palette.primary['500'];\n if (tone === 'enterprise') return palette.accent?.['500'] ?? palette.primary['500'];\n return theme.colors.textSecondary;\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 /**\n * Optional account slot. Legacy callers pass a `TopbarAction` (an account\n * button). Move-1c callers pass an `AccountState` for the richer header.\n */\n account?: TopbarAction | AccountState;\n /** Required logout action (drives the stable logout button testID). */\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 const richAccount = account !== undefined && isAccountState(account) ? account : undefined;\n const legacyAccount = account !== undefined && !isAccountState(account) ? account : undefined;\n const upgrade = richAccount?.upgrade;\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 {richAccount ? (\n <View style={styles.userBlock}>\n {richAccount.onAccount ? (\n <TouchableOpacity\n accessibilityHint={richAccount.displayName}\n accessibilityLabel={richAccount.displayName}\n accessibilityRole=\"button\"\n testID={NAV_TEST_IDS.accountName}\n onPress={richAccount.onAccount}\n >\n <Text style={[styles.userName, { color: colors.text }]}>{richAccount.displayName}</Text>\n </TouchableOpacity>\n ) : (\n <Text style={[styles.userName, { color: colors.text }]} testID={NAV_TEST_IDS.accountName}>\n {richAccount.displayName}\n </Text>\n )}\n\n {richAccount.tenantName !== undefined ? (\n <Text\n style={[styles.accountTenant, { color: colors.textSecondary }]}\n testID={NAV_TEST_IDS.accountTenant}\n >\n {richAccount.tenantName}\n </Text>\n ) : null}\n\n {richAccount.plan ? (\n <View\n style={[styles.planBadge, { backgroundColor: planToneColor(theme, richAccount.plan.tone) }]}\n testID={NAV_TEST_IDS.accountPlan}\n >\n <Text style={[styles.planBadgeText, { color: TEXT_ON_PRIMARY }]}>{richAccount.plan.label}</Text>\n </View>\n ) : null}\n </View>\n ) : null}\n\n {legacyAccount ? (\n <TouchableOpacity\n accessibilityHint={legacyAccount.hint}\n accessibilityLabel={legacyAccount.label}\n accessibilityRole=\"button\"\n style={[styles.accountBtn, { backgroundColor: primaryColor }]}\n testID={legacyAccount.testID}\n onPress={legacyAccount.onPress}\n >\n <Text style={[styles.accountText, { color: TEXT_ON_PRIMARY }]}>{legacyAccount.label}</Text>\n </TouchableOpacity>\n ) : null}\n\n {upgrade ? (\n <TouchableOpacity\n accessibilityHint={upgrade.hint}\n accessibilityLabel={upgrade.label}\n accessibilityRole=\"button\"\n style={[styles.accountBtn, { backgroundColor: primaryColor }]}\n testID={upgrade.testID ?? NAV_TEST_IDS.accountUpgrade}\n onPress={upgrade.onPress}\n >\n <Text style={[styles.accountText, { color: TEXT_ON_PRIMARY }]}>{upgrade.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={richAccount ? richAccount.onLogout : 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 * Width-discipline helper for `AppShell` (Move 1c). Split out from the component\n * so the (viewport → max-width) rule is a small, directly unit-testable pure\n * function, with a thin hook wrapper that feeds it the live viewport width.\n */\nimport { useWindowDimensions } from 'react-native';\n\n/** Content-width policy: a capped column, optionally wider past a viewport breakpoint, or full-bleed. */\nexport type AppShellWidth =\n | { max: number; wideMax?: number; wideMinViewport?: number }\n | 'full';\n\n/**\n * Resolve the content column's max width for a given viewport. `'full'` passes\n * through (no cap); an object caps at `max`, widening to `wideMax` once the\n * viewport reaches `wideMinViewport`.\n */\nexport function resolveContentMaxWidth(width: AppShellWidth, viewport: number): number | 'full' {\n if (width === 'full') return 'full';\n const wideMinViewport = width.wideMinViewport ?? Number.POSITIVE_INFINITY;\n const isWide = viewport >= wideMinViewport;\n return isWide ? (width.wideMax ?? width.max) : width.max;\n}\n\n/** Hook wrapper: resolves the content max width against the live window width. */\nexport function useContentMaxWidth(width: AppShellWidth): number | 'full' {\n const { width: viewport } = useWindowDimensions();\n return resolveContentMaxWidth(width, viewport);\n}\n","/**\n * AppShell — the shared dashboard-chrome composition (Move 1c). It hosts, top to\n * bottom: an app-supplied header (full-bleed), an optional per-app nav region, an\n * optional banner, and a width-disciplined scrollable content column. It also\n * folds in the two things every product page re-implements: an auth gate\n * (pending spinner / redirect-when-unauthenticated) and page state cards\n * (loading / error / forbidden).\n *\n * Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM, router,\n * or store imports. The app passes wired elements, pre-localized strings, and\n * callbacks; every colour is routed through the `@dloizides/ui-feedback` theme.\n */\nimport React, { useEffect } from 'react';\n\nimport { ActivityIndicator, ScrollView, StyleSheet, Text, View, type ViewStyle } from 'react-native';\n\nimport { useUi } from '@dloizides/ui-feedback';\n\nimport { APP_SHELL_SUFFIX } from './constants';\nimport { useContentMaxWidth, type AppShellWidth } from './useContentMaxWidth';\n\nconst DEFAULT_CONTENT_PADDING = 24;\nconst CARD_PADDING = 20;\nconst CARD_BORDER_RADIUS = 12;\nconst CARD_BORDER_WIDTH = 1;\nconst CARD_TITLE_FONT_SIZE = 16;\nconst CARD_MESSAGE_FONT_SIZE = 14;\nconst CARD_TITLE_MARGIN_BOTTOM = 8;\n\nconst styles = StyleSheet.create({\n root: { flex: 1 },\n centerFill: { flex: 1, justifyContent: 'center', alignItems: 'center' },\n scroll: { flex: 1 },\n scrollContent: { flexGrow: 1 },\n columnFull: { flex: 1 },\n columnCapped: { flex: 1, width: '100%', alignSelf: 'center' },\n card: {\n padding: CARD_PADDING,\n borderRadius: CARD_BORDER_RADIUS,\n borderWidth: CARD_BORDER_WIDTH,\n },\n cardTitle: { fontSize: CARD_TITLE_FONT_SIZE, fontWeight: '700', marginBottom: CARD_TITLE_MARGIN_BOTTOM },\n cardMessage: { fontSize: CARD_MESSAGE_FONT_SIZE },\n});\n\n/** A titled message card (error / forbidden). Colours come from the theme. */\nexport interface ShellMessage {\n titleText: string;\n messageText: string;\n}\n\nexport interface AppShellProps {\n /** App-supplied wired header (Topbar / AppHeader), rendered full-bleed. */\n header: React.ReactNode;\n /** Per-app nav region (kefi coral pills / erevna sidebar+drawer). */\n nav?: React.ReactNode;\n /** Banner slot (e.g. verification-pending). */\n banner?: React.ReactNode;\n /** Content-width policy. Defaults to full-bleed. */\n width?: AppShellWidth;\n /** Content padding inside the scroll column. Defaults to 24. */\n contentPadding?: number;\n /** Auth gate — spinner while pending, `onRedirect` when unauthenticated. */\n gate?: { pending: boolean; authenticated: boolean; onRedirect: () => void };\n /** Page state — loading spinner card, or a titled error / forbidden card. */\n state?: {\n loading?: boolean;\n error?: ShellMessage | null;\n forbidden?: ShellMessage | null;\n };\n /** Root testID; the content region is exposed as `${testID}-content`. */\n testID: string;\n children: React.ReactNode;\n}\n\nfunction MessageCard({\n message,\n accentColor,\n testID,\n}: {\n message: ShellMessage;\n accentColor: string;\n testID: string;\n}): React.ReactElement {\n const { theme } = useUi();\n const colors = theme.colors;\n return (\n <View\n style={[styles.card, { backgroundColor: colors.surface, borderColor: accentColor }]}\n testID={testID}\n >\n <Text style={[styles.cardTitle, { color: accentColor }]}>{message.titleText}</Text>\n <Text style={[styles.cardMessage, { color: colors.textSecondary }]}>{message.messageText}</Text>\n </View>\n );\n}\n\n/** Pick what renders inside the content column, honouring state precedence. */\nfunction useContentBody(\n state: AppShellProps['state'],\n children: React.ReactNode,\n testID: string,\n): React.ReactNode {\n const { theme } = useUi();\n const primary = theme.palette.primary['500'];\n const errorColor = theme.semantic.error['500'];\n\n if (state?.forbidden)\n return <MessageCard accentColor={errorColor} message={state.forbidden} testID={`${testID}${APP_SHELL_SUFFIX.forbidden}`} />;\n if (state?.error)\n return <MessageCard accentColor={errorColor} message={state.error} testID={`${testID}${APP_SHELL_SUFFIX.error}`} />;\n if (state?.loading === true)\n return (\n <View style={styles.centerFill} testID={`${testID}${APP_SHELL_SUFFIX.loading}`}>\n <ActivityIndicator color={primary} size=\"large\" />\n </View>\n );\n return children;\n}\n\nexport const AppShell = ({\n header,\n nav,\n banner,\n width = 'full',\n contentPadding = DEFAULT_CONTENT_PADDING,\n gate,\n state,\n testID,\n children,\n}: AppShellProps): React.ReactElement | null => {\n const { theme } = useUi();\n const primary = theme.palette.primary['500'];\n const maxWidth = useContentMaxWidth(width);\n const body = useContentBody(state, children, testID);\n\n const isUnauthenticated = gate !== undefined && !gate.pending && !gate.authenticated;\n useEffect(() => {\n if (isUnauthenticated && gate !== undefined) gate.onRedirect();\n }, [isUnauthenticated, gate]);\n\n if (gate?.pending === true)\n return (\n <View style={[styles.root, styles.centerFill, { backgroundColor: theme.colors.background }]} testID={`${testID}${APP_SHELL_SUFFIX.pending}`}>\n <ActivityIndicator color={primary} size=\"large\" />\n </View>\n );\n\n if (isUnauthenticated) return null;\n\n const columnStyle: ViewStyle | ViewStyle[] =\n maxWidth === 'full' ? styles.columnFull : [styles.columnCapped, { maxWidth }];\n\n return (\n <View style={[styles.root, { backgroundColor: theme.colors.background }]} testID={testID}>\n <View testID={`${testID}${APP_SHELL_SUFFIX.header}`}>{header}</View>\n {nav !== undefined ? <View testID={`${testID}${APP_SHELL_SUFFIX.nav}`}>{nav}</View> : null}\n {banner !== undefined ? <View testID={`${testID}${APP_SHELL_SUFFIX.banner}`}>{banner}</View> : null}\n <ScrollView\n contentContainerStyle={[styles.scrollContent, { padding: contentPadding }]}\n style={styles.scroll}\n testID={`${testID}${APP_SHELL_SUFFIX.content}`}\n >\n <View style={columnStyle}>{body}</View>\n </ScrollView>\n </View>\n );\n};\n\nexport default AppShell;\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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { StyleSheet, TouchableOpacity, View, Text } from 'react-native';
|
|
1
|
+
import { StyleSheet, TouchableOpacity, View, Text, useWindowDimensions, ActivityIndicator, ScrollView } from 'react-native';
|
|
2
2
|
import { useUi } from '@dloizides/ui-feedback';
|
|
3
|
-
import { useState, useCallback, useMemo } from 'react';
|
|
3
|
+
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
4
4
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
5
|
import { resolveAccessibleRoutes } from '@dloizides/auth-web';
|
|
6
6
|
|
|
@@ -53,7 +53,11 @@ var navStyles = StyleSheet.create({
|
|
|
53
53
|
userName: { fontWeight: "600" },
|
|
54
54
|
userEmail: { fontSize: 12 },
|
|
55
55
|
accountBtn: { marginLeft: 8, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 4 },
|
|
56
|
-
accountText: { fontWeight: "600" }
|
|
56
|
+
accountText: { fontWeight: "600" },
|
|
57
|
+
// --- Rich account-state header (Move 1c) ---
|
|
58
|
+
accountTenant: { fontSize: 12 },
|
|
59
|
+
planBadge: { marginTop: 2, paddingHorizontal: 8, paddingVertical: 2, borderRadius: 10, alignSelf: "flex-end" },
|
|
60
|
+
planBadgeText: { fontSize: 11, fontWeight: "700" }
|
|
57
61
|
});
|
|
58
62
|
var expandableStyles = StyleSheet.create({
|
|
59
63
|
childItem: {
|
|
@@ -210,7 +214,38 @@ var Sidebar = ({
|
|
|
210
214
|
}
|
|
211
215
|
);
|
|
212
216
|
};
|
|
217
|
+
|
|
218
|
+
// src/constants.ts
|
|
219
|
+
var NAV_TEST_IDS = {
|
|
220
|
+
/** displayName line in the rich account header (tappable when `onAccount` set). */
|
|
221
|
+
accountName: "topbar-account-name",
|
|
222
|
+
/** muted tenant-name line under the displayName. */
|
|
223
|
+
accountTenant: "topbar-account-tenant",
|
|
224
|
+
/** small plan badge (free / pro / enterprise). */
|
|
225
|
+
accountPlan: "topbar-account-plan",
|
|
226
|
+
/** default testID for the rich header's upgrade action (overridable per-action). */
|
|
227
|
+
accountUpgrade: "topbar-account-upgrade"
|
|
228
|
+
};
|
|
229
|
+
var APP_SHELL_SUFFIX = {
|
|
230
|
+
content: "-content",
|
|
231
|
+
header: "-header",
|
|
232
|
+
nav: "-nav",
|
|
233
|
+
banner: "-banner",
|
|
234
|
+
pending: "-pending",
|
|
235
|
+
loading: "-loading",
|
|
236
|
+
error: "-error",
|
|
237
|
+
forbidden: "-forbidden"
|
|
238
|
+
};
|
|
213
239
|
var TEXT_ON_PRIMARY = "#ffffff";
|
|
240
|
+
function isAccountState(account) {
|
|
241
|
+
return "displayName" in account;
|
|
242
|
+
}
|
|
243
|
+
function planToneColor(theme, tone) {
|
|
244
|
+
const palette = theme.palette;
|
|
245
|
+
if (tone === "pro") return palette.primary["500"];
|
|
246
|
+
if (tone === "enterprise") return palette.accent?.["500"] ?? palette.primary["500"];
|
|
247
|
+
return theme.colors.textSecondary;
|
|
248
|
+
}
|
|
214
249
|
var Topbar = ({
|
|
215
250
|
left,
|
|
216
251
|
language,
|
|
@@ -223,6 +258,9 @@ var Topbar = ({
|
|
|
223
258
|
const { theme } = useUi();
|
|
224
259
|
const colors = theme.colors;
|
|
225
260
|
const primaryColor = theme.palette.primary["500"];
|
|
261
|
+
const richAccount = account !== void 0 && isAccountState(account) ? account : void 0;
|
|
262
|
+
const legacyAccount = account !== void 0 && !isAccountState(account) ? account : void 0;
|
|
263
|
+
const upgrade = richAccount?.upgrade;
|
|
226
264
|
return /* @__PURE__ */ jsxs(
|
|
227
265
|
View,
|
|
228
266
|
{
|
|
@@ -250,16 +288,57 @@ var Topbar = ({
|
|
|
250
288
|
/* @__PURE__ */ jsx(Text, { style: [navStyles.userName, { color: colors.text }], children: user.name }),
|
|
251
289
|
/* @__PURE__ */ jsx(Text, { style: [navStyles.userEmail, { color: colors.textSecondary }], children: user.email })
|
|
252
290
|
] }) : null,
|
|
253
|
-
|
|
291
|
+
richAccount ? /* @__PURE__ */ jsxs(View, { style: navStyles.userBlock, children: [
|
|
292
|
+
richAccount.onAccount ? /* @__PURE__ */ jsx(
|
|
293
|
+
TouchableOpacity,
|
|
294
|
+
{
|
|
295
|
+
accessibilityHint: richAccount.displayName,
|
|
296
|
+
accessibilityLabel: richAccount.displayName,
|
|
297
|
+
accessibilityRole: "button",
|
|
298
|
+
testID: NAV_TEST_IDS.accountName,
|
|
299
|
+
onPress: richAccount.onAccount,
|
|
300
|
+
children: /* @__PURE__ */ jsx(Text, { style: [navStyles.userName, { color: colors.text }], children: richAccount.displayName })
|
|
301
|
+
}
|
|
302
|
+
) : /* @__PURE__ */ jsx(Text, { style: [navStyles.userName, { color: colors.text }], testID: NAV_TEST_IDS.accountName, children: richAccount.displayName }),
|
|
303
|
+
richAccount.tenantName !== void 0 ? /* @__PURE__ */ jsx(
|
|
304
|
+
Text,
|
|
305
|
+
{
|
|
306
|
+
style: [navStyles.accountTenant, { color: colors.textSecondary }],
|
|
307
|
+
testID: NAV_TEST_IDS.accountTenant,
|
|
308
|
+
children: richAccount.tenantName
|
|
309
|
+
}
|
|
310
|
+
) : null,
|
|
311
|
+
richAccount.plan ? /* @__PURE__ */ jsx(
|
|
312
|
+
View,
|
|
313
|
+
{
|
|
314
|
+
style: [navStyles.planBadge, { backgroundColor: planToneColor(theme, richAccount.plan.tone) }],
|
|
315
|
+
testID: NAV_TEST_IDS.accountPlan,
|
|
316
|
+
children: /* @__PURE__ */ jsx(Text, { style: [navStyles.planBadgeText, { color: TEXT_ON_PRIMARY }], children: richAccount.plan.label })
|
|
317
|
+
}
|
|
318
|
+
) : null
|
|
319
|
+
] }) : null,
|
|
320
|
+
legacyAccount ? /* @__PURE__ */ jsx(
|
|
321
|
+
TouchableOpacity,
|
|
322
|
+
{
|
|
323
|
+
accessibilityHint: legacyAccount.hint,
|
|
324
|
+
accessibilityLabel: legacyAccount.label,
|
|
325
|
+
accessibilityRole: "button",
|
|
326
|
+
style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
|
|
327
|
+
testID: legacyAccount.testID,
|
|
328
|
+
onPress: legacyAccount.onPress,
|
|
329
|
+
children: /* @__PURE__ */ jsx(Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children: legacyAccount.label })
|
|
330
|
+
}
|
|
331
|
+
) : null,
|
|
332
|
+
upgrade ? /* @__PURE__ */ jsx(
|
|
254
333
|
TouchableOpacity,
|
|
255
334
|
{
|
|
256
|
-
accessibilityHint:
|
|
257
|
-
accessibilityLabel:
|
|
335
|
+
accessibilityHint: upgrade.hint,
|
|
336
|
+
accessibilityLabel: upgrade.label,
|
|
258
337
|
accessibilityRole: "button",
|
|
259
338
|
style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
|
|
260
|
-
testID:
|
|
261
|
-
onPress:
|
|
262
|
-
children: /* @__PURE__ */ jsx(Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children:
|
|
339
|
+
testID: upgrade.testID ?? NAV_TEST_IDS.accountUpgrade,
|
|
340
|
+
onPress: upgrade.onPress,
|
|
341
|
+
children: /* @__PURE__ */ jsx(Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children: upgrade.label })
|
|
263
342
|
}
|
|
264
343
|
) : null,
|
|
265
344
|
/* @__PURE__ */ jsx(
|
|
@@ -271,7 +350,7 @@ var Topbar = ({
|
|
|
271
350
|
accessibilityRole: "button",
|
|
272
351
|
style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
|
|
273
352
|
testID: logout.testID,
|
|
274
|
-
onPress: logout.onPress,
|
|
353
|
+
onPress: richAccount ? richAccount.onLogout : logout.onPress,
|
|
275
354
|
children: /* @__PURE__ */ jsx(Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children: logout.label })
|
|
276
355
|
}
|
|
277
356
|
)
|
|
@@ -280,6 +359,107 @@ var Topbar = ({
|
|
|
280
359
|
}
|
|
281
360
|
);
|
|
282
361
|
};
|
|
362
|
+
function resolveContentMaxWidth(width, viewport) {
|
|
363
|
+
if (width === "full") return "full";
|
|
364
|
+
const wideMinViewport = width.wideMinViewport ?? Number.POSITIVE_INFINITY;
|
|
365
|
+
const isWide = viewport >= wideMinViewport;
|
|
366
|
+
return isWide ? width.wideMax ?? width.max : width.max;
|
|
367
|
+
}
|
|
368
|
+
function useContentMaxWidth(width) {
|
|
369
|
+
const { width: viewport } = useWindowDimensions();
|
|
370
|
+
return resolveContentMaxWidth(width, viewport);
|
|
371
|
+
}
|
|
372
|
+
var DEFAULT_CONTENT_PADDING = 24;
|
|
373
|
+
var CARD_PADDING = 20;
|
|
374
|
+
var CARD_BORDER_RADIUS = 12;
|
|
375
|
+
var CARD_BORDER_WIDTH = 1;
|
|
376
|
+
var CARD_TITLE_FONT_SIZE = 16;
|
|
377
|
+
var CARD_MESSAGE_FONT_SIZE = 14;
|
|
378
|
+
var CARD_TITLE_MARGIN_BOTTOM = 8;
|
|
379
|
+
var styles = StyleSheet.create({
|
|
380
|
+
root: { flex: 1 },
|
|
381
|
+
centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
|
|
382
|
+
scroll: { flex: 1 },
|
|
383
|
+
scrollContent: { flexGrow: 1 },
|
|
384
|
+
columnFull: { flex: 1 },
|
|
385
|
+
columnCapped: { flex: 1, width: "100%", alignSelf: "center" },
|
|
386
|
+
card: {
|
|
387
|
+
padding: CARD_PADDING,
|
|
388
|
+
borderRadius: CARD_BORDER_RADIUS,
|
|
389
|
+
borderWidth: CARD_BORDER_WIDTH
|
|
390
|
+
},
|
|
391
|
+
cardTitle: { fontSize: CARD_TITLE_FONT_SIZE, fontWeight: "700", marginBottom: CARD_TITLE_MARGIN_BOTTOM },
|
|
392
|
+
cardMessage: { fontSize: CARD_MESSAGE_FONT_SIZE }
|
|
393
|
+
});
|
|
394
|
+
function MessageCard({
|
|
395
|
+
message,
|
|
396
|
+
accentColor,
|
|
397
|
+
testID
|
|
398
|
+
}) {
|
|
399
|
+
const { theme } = useUi();
|
|
400
|
+
const colors = theme.colors;
|
|
401
|
+
return /* @__PURE__ */ jsxs(
|
|
402
|
+
View,
|
|
403
|
+
{
|
|
404
|
+
style: [styles.card, { backgroundColor: colors.surface, borderColor: accentColor }],
|
|
405
|
+
testID,
|
|
406
|
+
children: [
|
|
407
|
+
/* @__PURE__ */ jsx(Text, { style: [styles.cardTitle, { color: accentColor }], children: message.titleText }),
|
|
408
|
+
/* @__PURE__ */ jsx(Text, { style: [styles.cardMessage, { color: colors.textSecondary }], children: message.messageText })
|
|
409
|
+
]
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
function useContentBody(state, children, testID) {
|
|
414
|
+
const { theme } = useUi();
|
|
415
|
+
const primary = theme.palette.primary["500"];
|
|
416
|
+
const errorColor = theme.semantic.error["500"];
|
|
417
|
+
if (state?.forbidden)
|
|
418
|
+
return /* @__PURE__ */ jsx(MessageCard, { accentColor: errorColor, message: state.forbidden, testID: `${testID}${APP_SHELL_SUFFIX.forbidden}` });
|
|
419
|
+
if (state?.error)
|
|
420
|
+
return /* @__PURE__ */ jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
|
|
421
|
+
if (state?.loading === true)
|
|
422
|
+
return /* @__PURE__ */ jsx(View, { style: styles.centerFill, testID: `${testID}${APP_SHELL_SUFFIX.loading}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
|
|
423
|
+
return children;
|
|
424
|
+
}
|
|
425
|
+
var AppShell = ({
|
|
426
|
+
header,
|
|
427
|
+
nav,
|
|
428
|
+
banner,
|
|
429
|
+
width = "full",
|
|
430
|
+
contentPadding = DEFAULT_CONTENT_PADDING,
|
|
431
|
+
gate,
|
|
432
|
+
state,
|
|
433
|
+
testID,
|
|
434
|
+
children
|
|
435
|
+
}) => {
|
|
436
|
+
const { theme } = useUi();
|
|
437
|
+
const primary = theme.palette.primary["500"];
|
|
438
|
+
const maxWidth = useContentMaxWidth(width);
|
|
439
|
+
const body = useContentBody(state, children, testID);
|
|
440
|
+
const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
|
|
441
|
+
useEffect(() => {
|
|
442
|
+
if (isUnauthenticated && gate !== void 0) gate.onRedirect();
|
|
443
|
+
}, [isUnauthenticated, gate]);
|
|
444
|
+
if (gate?.pending === true)
|
|
445
|
+
return /* @__PURE__ */ jsx(View, { style: [styles.root, styles.centerFill, { backgroundColor: theme.colors.background }], testID: `${testID}${APP_SHELL_SUFFIX.pending}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
|
|
446
|
+
if (isUnauthenticated) return null;
|
|
447
|
+
const columnStyle = maxWidth === "full" ? styles.columnFull : [styles.columnCapped, { maxWidth }];
|
|
448
|
+
return /* @__PURE__ */ jsxs(View, { style: [styles.root, { backgroundColor: theme.colors.background }], testID, children: [
|
|
449
|
+
/* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header }),
|
|
450
|
+
nav !== void 0 ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.nav}`, children: nav }) : null,
|
|
451
|
+
banner !== void 0 ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: banner }) : null,
|
|
452
|
+
/* @__PURE__ */ jsx(
|
|
453
|
+
ScrollView,
|
|
454
|
+
{
|
|
455
|
+
contentContainerStyle: [styles.scrollContent, { padding: contentPadding }],
|
|
456
|
+
style: styles.scroll,
|
|
457
|
+
testID: `${testID}${APP_SHELL_SUFFIX.content}`,
|
|
458
|
+
children: /* @__PURE__ */ jsx(View, { style: columnStyle, children: body })
|
|
459
|
+
}
|
|
460
|
+
)
|
|
461
|
+
] });
|
|
462
|
+
};
|
|
283
463
|
function roleRoutesToNavItems(routes, translate) {
|
|
284
464
|
return routes.map((entry) => ({
|
|
285
465
|
key: entry.role,
|
|
@@ -291,6 +471,6 @@ function accessibleNavItems(user, table, translate) {
|
|
|
291
471
|
return roleRoutesToNavItems(resolveAccessibleRoutes(user, table), translate);
|
|
292
472
|
}
|
|
293
473
|
|
|
294
|
-
export { ACTIVE_BORDER_RADIUS, BASE_INDENT, CHEVRON_ICON_SIZE, NAV_ICON_SIZE, NavExpandableItem, Sidebar, Topbar, accessibleNavItems, expandableStyles, isRouteActive, navStyles, roleRoutesToNavItems };
|
|
474
|
+
export { ACTIVE_BORDER_RADIUS, APP_SHELL_SUFFIX, AppShell, BASE_INDENT, CHEVRON_ICON_SIZE, NAV_ICON_SIZE, NAV_TEST_IDS, NavExpandableItem, Sidebar, Topbar, accessibleNavItems, expandableStyles, isRouteActive, navStyles, resolveContentMaxWidth, roleRoutesToNavItems, useContentMaxWidth };
|
|
295
475
|
//# sourceMappingURL=index.mjs.map
|
|
296
476
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/isRouteActive.ts","../src/styles.ts","../src/NavExpandableItem.tsx","../src/Sidebar.tsx","../src/Topbar.tsx","../src/roleNav.ts"],"names":["useUi","jsxs","View","jsx","Text","TouchableOpacity"],"mappings":"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","file":"index.mjs","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"]}
|
|
1
|
+
{"version":3,"sources":["../src/isRouteActive.ts","../src/styles.ts","../src/NavExpandableItem.tsx","../src/Sidebar.tsx","../src/constants.ts","../src/Topbar.tsx","../src/useContentMaxWidth.ts","../src/AppShell.tsx","../src/roleNav.ts"],"names":["useUi","jsxs","View","jsx","Text","TouchableOpacity","StyleSheet"],"mappings":"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","file":"index.mjs","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 // --- Rich account-state header (Move 1c) ---\n accountTenant: { fontSize: 12 },\n planBadge: { marginTop: 2, paddingHorizontal: 8, paddingVertical: 2, borderRadius: 10, alignSelf: 'flex-end' },\n planBadgeText: { fontSize: 11, fontWeight: '700' },\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 * Default testIDs for `@dloizides/ui-nav`. Kept as a small central map (mirrors\n * `@dloizides/ui-layout`'s `LAYOUT_TEST_IDS`) so the app-agnostic chrome exposes\n * stable Playwright selectors. The existing Topbar logout/account buttons keep\n * their *caller-supplied* testIDs (via `TopbarAction.testID`) — these constants\n * only name the NEW account-state bits and the AppShell region suffixes.\n */\nexport const NAV_TEST_IDS = {\n /** displayName line in the rich account header (tappable when `onAccount` set). */\n accountName: 'topbar-account-name',\n /** muted tenant-name line under the displayName. */\n accountTenant: 'topbar-account-tenant',\n /** small plan badge (free / pro / enterprise). */\n accountPlan: 'topbar-account-plan',\n /** default testID for the rich header's upgrade action (overridable per-action). */\n accountUpgrade: 'topbar-account-upgrade',\n} as const;\n\n/** Suffixes appended to an `AppShell`'s required `testID` to name its regions. */\nexport const APP_SHELL_SUFFIX = {\n content: '-content',\n header: '-header',\n nav: '-nav',\n banner: '-banner',\n pending: '-pending',\n loading: '-loading',\n error: '-error',\n forbidden: '-forbidden',\n} as const;\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 *\n * Move 1c enriches this WITHOUT breaking the promoted contract: `account` now\n * accepts EITHER the legacy `TopbarAction` (today's erevna/katalogos calls) OR a\n * richer `AccountState` (displayName + muted tenant line + plan badge + upgrade\n * action). The `logout` action stays required, so every existing call keeps\n * compiling and the logout button keeps its caller-supplied testID.\n */\nimport React from 'react';\n\nimport { Text, TouchableOpacity, View, type ViewStyle } from 'react-native';\n\nimport { useUi, type UiTheme } from '@dloizides/ui-feedback';\n\nimport { NAV_TEST_IDS } from './constants';\nimport { navStyles as styles } from './styles';\n\n/** Text color for elements on primary/coloured badge 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\n/** Plan tier shown as a small badge in the rich account header. */\nexport interface AccountPlan {\n label: string;\n tone?: 'free' | 'pro' | 'enterprise';\n}\n\n/**\n * Richer account model for the header (Move 1c). Supplied via `account` as an\n * alternative to the legacy `TopbarAction` — the Topbar renders the display name\n * (tappable when `onAccount` is set), a muted tenant line, a plan badge, and an\n * `upgrade` action beside logout. No FM/router/store — everything is a\n * pre-localized string or a callback.\n */\nexport interface AccountState {\n displayName: string;\n tenantName?: string;\n plan?: AccountPlan;\n onLogout: () => void;\n onAccount?: () => void;\n upgrade?: TopbarAction;\n}\n\n/** Type guard: distinguishes the rich `AccountState` from a legacy `TopbarAction`. */\nfunction isAccountState(account: TopbarAction | AccountState): account is AccountState {\n return 'displayName' in account;\n}\n\n/** Resolve a plan badge background from its tone, routed through the theme. */\nfunction planToneColor(theme: UiTheme, tone: AccountPlan['tone']): string {\n const palette = theme.palette;\n if (tone === 'pro') return palette.primary['500'];\n if (tone === 'enterprise') return palette.accent?.['500'] ?? palette.primary['500'];\n return theme.colors.textSecondary;\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 /**\n * Optional account slot. Legacy callers pass a `TopbarAction` (an account\n * button). Move-1c callers pass an `AccountState` for the richer header.\n */\n account?: TopbarAction | AccountState;\n /** Required logout action (drives the stable logout button testID). */\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 const richAccount = account !== undefined && isAccountState(account) ? account : undefined;\n const legacyAccount = account !== undefined && !isAccountState(account) ? account : undefined;\n const upgrade = richAccount?.upgrade;\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 {richAccount ? (\n <View style={styles.userBlock}>\n {richAccount.onAccount ? (\n <TouchableOpacity\n accessibilityHint={richAccount.displayName}\n accessibilityLabel={richAccount.displayName}\n accessibilityRole=\"button\"\n testID={NAV_TEST_IDS.accountName}\n onPress={richAccount.onAccount}\n >\n <Text style={[styles.userName, { color: colors.text }]}>{richAccount.displayName}</Text>\n </TouchableOpacity>\n ) : (\n <Text style={[styles.userName, { color: colors.text }]} testID={NAV_TEST_IDS.accountName}>\n {richAccount.displayName}\n </Text>\n )}\n\n {richAccount.tenantName !== undefined ? (\n <Text\n style={[styles.accountTenant, { color: colors.textSecondary }]}\n testID={NAV_TEST_IDS.accountTenant}\n >\n {richAccount.tenantName}\n </Text>\n ) : null}\n\n {richAccount.plan ? (\n <View\n style={[styles.planBadge, { backgroundColor: planToneColor(theme, richAccount.plan.tone) }]}\n testID={NAV_TEST_IDS.accountPlan}\n >\n <Text style={[styles.planBadgeText, { color: TEXT_ON_PRIMARY }]}>{richAccount.plan.label}</Text>\n </View>\n ) : null}\n </View>\n ) : null}\n\n {legacyAccount ? (\n <TouchableOpacity\n accessibilityHint={legacyAccount.hint}\n accessibilityLabel={legacyAccount.label}\n accessibilityRole=\"button\"\n style={[styles.accountBtn, { backgroundColor: primaryColor }]}\n testID={legacyAccount.testID}\n onPress={legacyAccount.onPress}\n >\n <Text style={[styles.accountText, { color: TEXT_ON_PRIMARY }]}>{legacyAccount.label}</Text>\n </TouchableOpacity>\n ) : null}\n\n {upgrade ? (\n <TouchableOpacity\n accessibilityHint={upgrade.hint}\n accessibilityLabel={upgrade.label}\n accessibilityRole=\"button\"\n style={[styles.accountBtn, { backgroundColor: primaryColor }]}\n testID={upgrade.testID ?? NAV_TEST_IDS.accountUpgrade}\n onPress={upgrade.onPress}\n >\n <Text style={[styles.accountText, { color: TEXT_ON_PRIMARY }]}>{upgrade.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={richAccount ? richAccount.onLogout : 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 * Width-discipline helper for `AppShell` (Move 1c). Split out from the component\n * so the (viewport → max-width) rule is a small, directly unit-testable pure\n * function, with a thin hook wrapper that feeds it the live viewport width.\n */\nimport { useWindowDimensions } from 'react-native';\n\n/** Content-width policy: a capped column, optionally wider past a viewport breakpoint, or full-bleed. */\nexport type AppShellWidth =\n | { max: number; wideMax?: number; wideMinViewport?: number }\n | 'full';\n\n/**\n * Resolve the content column's max width for a given viewport. `'full'` passes\n * through (no cap); an object caps at `max`, widening to `wideMax` once the\n * viewport reaches `wideMinViewport`.\n */\nexport function resolveContentMaxWidth(width: AppShellWidth, viewport: number): number | 'full' {\n if (width === 'full') return 'full';\n const wideMinViewport = width.wideMinViewport ?? Number.POSITIVE_INFINITY;\n const isWide = viewport >= wideMinViewport;\n return isWide ? (width.wideMax ?? width.max) : width.max;\n}\n\n/** Hook wrapper: resolves the content max width against the live window width. */\nexport function useContentMaxWidth(width: AppShellWidth): number | 'full' {\n const { width: viewport } = useWindowDimensions();\n return resolveContentMaxWidth(width, viewport);\n}\n","/**\n * AppShell — the shared dashboard-chrome composition (Move 1c). It hosts, top to\n * bottom: an app-supplied header (full-bleed), an optional per-app nav region, an\n * optional banner, and a width-disciplined scrollable content column. It also\n * folds in the two things every product page re-implements: an auth gate\n * (pending spinner / redirect-when-unauthenticated) and page state cards\n * (loading / error / forbidden).\n *\n * Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM, router,\n * or store imports. The app passes wired elements, pre-localized strings, and\n * callbacks; every colour is routed through the `@dloizides/ui-feedback` theme.\n */\nimport React, { useEffect } from 'react';\n\nimport { ActivityIndicator, ScrollView, StyleSheet, Text, View, type ViewStyle } from 'react-native';\n\nimport { useUi } from '@dloizides/ui-feedback';\n\nimport { APP_SHELL_SUFFIX } from './constants';\nimport { useContentMaxWidth, type AppShellWidth } from './useContentMaxWidth';\n\nconst DEFAULT_CONTENT_PADDING = 24;\nconst CARD_PADDING = 20;\nconst CARD_BORDER_RADIUS = 12;\nconst CARD_BORDER_WIDTH = 1;\nconst CARD_TITLE_FONT_SIZE = 16;\nconst CARD_MESSAGE_FONT_SIZE = 14;\nconst CARD_TITLE_MARGIN_BOTTOM = 8;\n\nconst styles = StyleSheet.create({\n root: { flex: 1 },\n centerFill: { flex: 1, justifyContent: 'center', alignItems: 'center' },\n scroll: { flex: 1 },\n scrollContent: { flexGrow: 1 },\n columnFull: { flex: 1 },\n columnCapped: { flex: 1, width: '100%', alignSelf: 'center' },\n card: {\n padding: CARD_PADDING,\n borderRadius: CARD_BORDER_RADIUS,\n borderWidth: CARD_BORDER_WIDTH,\n },\n cardTitle: { fontSize: CARD_TITLE_FONT_SIZE, fontWeight: '700', marginBottom: CARD_TITLE_MARGIN_BOTTOM },\n cardMessage: { fontSize: CARD_MESSAGE_FONT_SIZE },\n});\n\n/** A titled message card (error / forbidden). Colours come from the theme. */\nexport interface ShellMessage {\n titleText: string;\n messageText: string;\n}\n\nexport interface AppShellProps {\n /** App-supplied wired header (Topbar / AppHeader), rendered full-bleed. */\n header: React.ReactNode;\n /** Per-app nav region (kefi coral pills / erevna sidebar+drawer). */\n nav?: React.ReactNode;\n /** Banner slot (e.g. verification-pending). */\n banner?: React.ReactNode;\n /** Content-width policy. Defaults to full-bleed. */\n width?: AppShellWidth;\n /** Content padding inside the scroll column. Defaults to 24. */\n contentPadding?: number;\n /** Auth gate — spinner while pending, `onRedirect` when unauthenticated. */\n gate?: { pending: boolean; authenticated: boolean; onRedirect: () => void };\n /** Page state — loading spinner card, or a titled error / forbidden card. */\n state?: {\n loading?: boolean;\n error?: ShellMessage | null;\n forbidden?: ShellMessage | null;\n };\n /** Root testID; the content region is exposed as `${testID}-content`. */\n testID: string;\n children: React.ReactNode;\n}\n\nfunction MessageCard({\n message,\n accentColor,\n testID,\n}: {\n message: ShellMessage;\n accentColor: string;\n testID: string;\n}): React.ReactElement {\n const { theme } = useUi();\n const colors = theme.colors;\n return (\n <View\n style={[styles.card, { backgroundColor: colors.surface, borderColor: accentColor }]}\n testID={testID}\n >\n <Text style={[styles.cardTitle, { color: accentColor }]}>{message.titleText}</Text>\n <Text style={[styles.cardMessage, { color: colors.textSecondary }]}>{message.messageText}</Text>\n </View>\n );\n}\n\n/** Pick what renders inside the content column, honouring state precedence. */\nfunction useContentBody(\n state: AppShellProps['state'],\n children: React.ReactNode,\n testID: string,\n): React.ReactNode {\n const { theme } = useUi();\n const primary = theme.palette.primary['500'];\n const errorColor = theme.semantic.error['500'];\n\n if (state?.forbidden)\n return <MessageCard accentColor={errorColor} message={state.forbidden} testID={`${testID}${APP_SHELL_SUFFIX.forbidden}`} />;\n if (state?.error)\n return <MessageCard accentColor={errorColor} message={state.error} testID={`${testID}${APP_SHELL_SUFFIX.error}`} />;\n if (state?.loading === true)\n return (\n <View style={styles.centerFill} testID={`${testID}${APP_SHELL_SUFFIX.loading}`}>\n <ActivityIndicator color={primary} size=\"large\" />\n </View>\n );\n return children;\n}\n\nexport const AppShell = ({\n header,\n nav,\n banner,\n width = 'full',\n contentPadding = DEFAULT_CONTENT_PADDING,\n gate,\n state,\n testID,\n children,\n}: AppShellProps): React.ReactElement | null => {\n const { theme } = useUi();\n const primary = theme.palette.primary['500'];\n const maxWidth = useContentMaxWidth(width);\n const body = useContentBody(state, children, testID);\n\n const isUnauthenticated = gate !== undefined && !gate.pending && !gate.authenticated;\n useEffect(() => {\n if (isUnauthenticated && gate !== undefined) gate.onRedirect();\n }, [isUnauthenticated, gate]);\n\n if (gate?.pending === true)\n return (\n <View style={[styles.root, styles.centerFill, { backgroundColor: theme.colors.background }]} testID={`${testID}${APP_SHELL_SUFFIX.pending}`}>\n <ActivityIndicator color={primary} size=\"large\" />\n </View>\n );\n\n if (isUnauthenticated) return null;\n\n const columnStyle: ViewStyle | ViewStyle[] =\n maxWidth === 'full' ? styles.columnFull : [styles.columnCapped, { maxWidth }];\n\n return (\n <View style={[styles.root, { backgroundColor: theme.colors.background }]} testID={testID}>\n <View testID={`${testID}${APP_SHELL_SUFFIX.header}`}>{header}</View>\n {nav !== undefined ? <View testID={`${testID}${APP_SHELL_SUFFIX.nav}`}>{nav}</View> : null}\n {banner !== undefined ? <View testID={`${testID}${APP_SHELL_SUFFIX.banner}`}>{banner}</View> : null}\n <ScrollView\n contentContainerStyle={[styles.scrollContent, { padding: contentPadding }]}\n style={styles.scroll}\n testID={`${testID}${APP_SHELL_SUFFIX.content}`}\n >\n <View style={columnStyle}>{body}</View>\n </ScrollView>\n </View>\n );\n};\n\nexport default AppShell;\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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dloizides/ui-nav",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Config-driven, brand-agnostic React Native (RN-web) navigation shell — Sidebar + Topbar promoted from the byte-identical erevna-web/katalogos-web nav twins. Renders a caller-supplied NavItem[] (pre-localized labels, icon render slots), routes colours through the @dloizides/ui-feedback UiProvider, and gates items by role via resolveAccessibleRoutes from @dloizides/auth-web. Part of the dloizides.com shared UI kit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|