@dloizides/ui-nav 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,115 @@ interface TopbarProps {
107
133
  notificationSlot?: React.ReactNode;
108
134
  /** Optional user identity block. */
109
135
  user?: TopbarUser;
110
- /** Optional account button. */
111
- account?: TopbarAction;
112
- /** Required logout action. */
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
+ /**
193
+ * Whether the nav strip aligns with the width-capped content column
194
+ * (`'content'`) or runs full-bleed (`'full'`, the default). Only has an effect
195
+ * when `width` caps the content. The header always renders full-bleed.
196
+ */
197
+ chromeAlignment?: 'full' | 'content';
198
+ /** Auth gate — spinner while pending, `onRedirect` when unauthenticated. */
199
+ gate?: {
200
+ pending: boolean;
201
+ authenticated: boolean;
202
+ onRedirect: () => void;
203
+ };
204
+ /** Page state — loading spinner card, or a titled error / forbidden card. */
205
+ state?: {
206
+ loading?: boolean;
207
+ error?: ShellMessage | null;
208
+ forbidden?: ShellMessage | null;
209
+ };
210
+ /** Root testID; the content region is exposed as `${testID}-content`. */
211
+ testID: string;
212
+ children: React.ReactNode;
213
+ }
214
+ declare const AppShell: ({ header, nav, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
215
+
216
+ /**
217
+ * Default testIDs for `@dloizides/ui-nav`. Kept as a small central map (mirrors
218
+ * `@dloizides/ui-layout`'s `LAYOUT_TEST_IDS`) so the app-agnostic chrome exposes
219
+ * stable Playwright selectors. The existing Topbar logout/account buttons keep
220
+ * their *caller-supplied* testIDs (via `TopbarAction.testID`) — these constants
221
+ * only name the NEW account-state bits and the AppShell region suffixes.
222
+ */
223
+ declare const NAV_TEST_IDS: {
224
+ /** displayName line in the rich account header (tappable when `onAccount` set). */
225
+ readonly accountName: "topbar-account-name";
226
+ /** muted tenant-name line under the displayName. */
227
+ readonly accountTenant: "topbar-account-tenant";
228
+ /** small plan badge (free / pro / enterprise). */
229
+ readonly accountPlan: "topbar-account-plan";
230
+ /** default testID for the rich header's upgrade action (overridable per-action). */
231
+ readonly accountUpgrade: "topbar-account-upgrade";
232
+ };
233
+ /** Suffixes appended to an `AppShell`'s required `testID` to name its regions. */
234
+ declare const APP_SHELL_SUFFIX: {
235
+ readonly content: "-content";
236
+ readonly header: "-header";
237
+ readonly nav: "-nav";
238
+ readonly banner: "-banner";
239
+ readonly pending: "-pending";
240
+ readonly loading: "-loading";
241
+ readonly error: "-error";
242
+ readonly forbidden: "-forbidden";
243
+ };
244
+
119
245
  /**
120
246
  * NavExpandableItem — a single sidebar entry that is either a leaf (navigates)
121
247
  * or an expandable section (toggles nested children). Ported from the twin
@@ -224,6 +350,20 @@ declare const navStyles: {
224
350
  accountText: {
225
351
  fontWeight: "600";
226
352
  };
353
+ accountTenant: {
354
+ fontSize: number;
355
+ };
356
+ planBadge: {
357
+ marginTop: number;
358
+ paddingHorizontal: number;
359
+ paddingVertical: number;
360
+ borderRadius: number;
361
+ alignSelf: "flex-end";
362
+ };
363
+ planBadgeText: {
364
+ fontSize: number;
365
+ fontWeight: "700";
366
+ };
227
367
  };
228
368
  declare const expandableStyles: {
229
369
  childItem: {
@@ -268,4 +408,4 @@ declare const NAV_ICON_SIZE = 14;
268
408
  /** Chevron icon size for expandable sections. */
269
409
  declare const CHEVRON_ICON_SIZE = 12;
270
410
 
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 };
411
+ 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,115 @@ interface TopbarProps {
107
133
  notificationSlot?: React.ReactNode;
108
134
  /** Optional user identity block. */
109
135
  user?: TopbarUser;
110
- /** Optional account button. */
111
- account?: TopbarAction;
112
- /** Required logout action. */
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
+ /**
193
+ * Whether the nav strip aligns with the width-capped content column
194
+ * (`'content'`) or runs full-bleed (`'full'`, the default). Only has an effect
195
+ * when `width` caps the content. The header always renders full-bleed.
196
+ */
197
+ chromeAlignment?: 'full' | 'content';
198
+ /** Auth gate — spinner while pending, `onRedirect` when unauthenticated. */
199
+ gate?: {
200
+ pending: boolean;
201
+ authenticated: boolean;
202
+ onRedirect: () => void;
203
+ };
204
+ /** Page state — loading spinner card, or a titled error / forbidden card. */
205
+ state?: {
206
+ loading?: boolean;
207
+ error?: ShellMessage | null;
208
+ forbidden?: ShellMessage | null;
209
+ };
210
+ /** Root testID; the content region is exposed as `${testID}-content`. */
211
+ testID: string;
212
+ children: React.ReactNode;
213
+ }
214
+ declare const AppShell: ({ header, nav, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
215
+
216
+ /**
217
+ * Default testIDs for `@dloizides/ui-nav`. Kept as a small central map (mirrors
218
+ * `@dloizides/ui-layout`'s `LAYOUT_TEST_IDS`) so the app-agnostic chrome exposes
219
+ * stable Playwright selectors. The existing Topbar logout/account buttons keep
220
+ * their *caller-supplied* testIDs (via `TopbarAction.testID`) — these constants
221
+ * only name the NEW account-state bits and the AppShell region suffixes.
222
+ */
223
+ declare const NAV_TEST_IDS: {
224
+ /** displayName line in the rich account header (tappable when `onAccount` set). */
225
+ readonly accountName: "topbar-account-name";
226
+ /** muted tenant-name line under the displayName. */
227
+ readonly accountTenant: "topbar-account-tenant";
228
+ /** small plan badge (free / pro / enterprise). */
229
+ readonly accountPlan: "topbar-account-plan";
230
+ /** default testID for the rich header's upgrade action (overridable per-action). */
231
+ readonly accountUpgrade: "topbar-account-upgrade";
232
+ };
233
+ /** Suffixes appended to an `AppShell`'s required `testID` to name its regions. */
234
+ declare const APP_SHELL_SUFFIX: {
235
+ readonly content: "-content";
236
+ readonly header: "-header";
237
+ readonly nav: "-nav";
238
+ readonly banner: "-banner";
239
+ readonly pending: "-pending";
240
+ readonly loading: "-loading";
241
+ readonly error: "-error";
242
+ readonly forbidden: "-forbidden";
243
+ };
244
+
119
245
  /**
120
246
  * NavExpandableItem — a single sidebar entry that is either a leaf (navigates)
121
247
  * or an expandable section (toggles nested children). Ported from the twin
@@ -224,6 +350,20 @@ declare const navStyles: {
224
350
  accountText: {
225
351
  fontWeight: "600";
226
352
  };
353
+ accountTenant: {
354
+ fontSize: number;
355
+ };
356
+ planBadge: {
357
+ marginTop: number;
358
+ paddingHorizontal: number;
359
+ paddingVertical: number;
360
+ borderRadius: number;
361
+ alignSelf: "flex-end";
362
+ };
363
+ planBadgeText: {
364
+ fontSize: number;
365
+ fontWeight: "700";
366
+ };
227
367
  };
228
368
  declare const expandableStyles: {
229
369
  childItem: {
@@ -268,4 +408,4 @@ declare const NAV_ICON_SIZE = 14;
268
408
  /** Chevron icon size for expandable sections. */
269
409
  declare const CHEVRON_ICON_SIZE = 12;
270
410
 
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 };
411
+ 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
- account ? /* @__PURE__ */ jsxRuntime.jsx(
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: account.hint,
259
- accessibilityLabel: account.label,
337
+ accessibilityHint: upgrade.hint,
338
+ accessibilityLabel: upgrade.label,
260
339
  accessibilityRole: "button",
261
340
  style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
262
- testID: account.testID,
263
- onPress: account.onPress,
264
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children: account.label })
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,112 @@ 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
+ // Non-flex variant used to cap the nav strip's inner content to the content
389
+ // column so the nav aligns with the page content instead of running full-bleed.
390
+ chromeCapped: { width: "100%", alignSelf: "center" },
391
+ card: {
392
+ padding: CARD_PADDING,
393
+ borderRadius: CARD_BORDER_RADIUS,
394
+ borderWidth: CARD_BORDER_WIDTH
395
+ },
396
+ cardTitle: { fontSize: CARD_TITLE_FONT_SIZE, fontWeight: "700", marginBottom: CARD_TITLE_MARGIN_BOTTOM },
397
+ cardMessage: { fontSize: CARD_MESSAGE_FONT_SIZE }
398
+ });
399
+ function MessageCard({
400
+ message,
401
+ accentColor,
402
+ testID
403
+ }) {
404
+ const { theme } = uiFeedback.useUi();
405
+ const colors = theme.colors;
406
+ return /* @__PURE__ */ jsxRuntime.jsxs(
407
+ reactNative.View,
408
+ {
409
+ style: [styles.card, { backgroundColor: colors.surface, borderColor: accentColor }],
410
+ testID,
411
+ children: [
412
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.cardTitle, { color: accentColor }], children: message.titleText }),
413
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.cardMessage, { color: colors.textSecondary }], children: message.messageText })
414
+ ]
415
+ }
416
+ );
417
+ }
418
+ function useContentBody(state, children, testID) {
419
+ const { theme } = uiFeedback.useUi();
420
+ const primary = theme.palette.primary["500"];
421
+ const errorColor = theme.semantic.error["500"];
422
+ if (state?.forbidden)
423
+ return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.forbidden, testID: `${testID}${APP_SHELL_SUFFIX.forbidden}` });
424
+ if (state?.error)
425
+ return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
426
+ if (state?.loading === true)
427
+ 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" }) });
428
+ return children;
429
+ }
430
+ var AppShell = ({
431
+ header,
432
+ nav,
433
+ banner,
434
+ width = "full",
435
+ contentPadding = DEFAULT_CONTENT_PADDING,
436
+ chromeAlignment = "full",
437
+ gate,
438
+ state,
439
+ testID,
440
+ children
441
+ }) => {
442
+ const { theme } = uiFeedback.useUi();
443
+ const primary = theme.palette.primary["500"];
444
+ const maxWidth = useContentMaxWidth(width);
445
+ const body = useContentBody(state, children, testID);
446
+ const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
447
+ react.useEffect(() => {
448
+ if (isUnauthenticated && gate !== void 0) gate.onRedirect();
449
+ }, [isUnauthenticated, gate]);
450
+ if (gate?.pending === true)
451
+ 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" }) });
452
+ if (isUnauthenticated) return null;
453
+ const columnStyle = maxWidth === "full" ? styles.columnFull : [styles.columnCapped, { maxWidth }];
454
+ const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
455
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.root, { backgroundColor: theme.colors.background }], testID, children: [
456
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header }),
457
+ nav !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.nav}`, children: navInnerStyle !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: navInnerStyle, children: nav }) : nav }) : null,
458
+ banner !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: banner }) : null,
459
+ /* @__PURE__ */ jsxRuntime.jsx(
460
+ reactNative.ScrollView,
461
+ {
462
+ contentContainerStyle: [styles.scrollContent, { padding: contentPadding }],
463
+ style: styles.scroll,
464
+ testID: `${testID}${APP_SHELL_SUFFIX.content}`,
465
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: columnStyle, children: body })
466
+ }
467
+ )
468
+ ] });
469
+ };
285
470
  function roleRoutesToNavItems(routes, translate) {
286
471
  return routes.map((entry) => ({
287
472
  key: entry.role,
@@ -294,9 +479,12 @@ function accessibleNavItems(user, table, translate) {
294
479
  }
295
480
 
296
481
  exports.ACTIVE_BORDER_RADIUS = ACTIVE_BORDER_RADIUS;
482
+ exports.APP_SHELL_SUFFIX = APP_SHELL_SUFFIX;
483
+ exports.AppShell = AppShell;
297
484
  exports.BASE_INDENT = BASE_INDENT;
298
485
  exports.CHEVRON_ICON_SIZE = CHEVRON_ICON_SIZE;
299
486
  exports.NAV_ICON_SIZE = NAV_ICON_SIZE;
487
+ exports.NAV_TEST_IDS = NAV_TEST_IDS;
300
488
  exports.NavExpandableItem = NavExpandableItem;
301
489
  exports.Sidebar = Sidebar;
302
490
  exports.Topbar = Topbar;
@@ -304,6 +492,8 @@ exports.accessibleNavItems = accessibleNavItems;
304
492
  exports.expandableStyles = expandableStyles;
305
493
  exports.isRouteActive = isRouteActive;
306
494
  exports.navStyles = navStyles;
495
+ exports.resolveContentMaxWidth = resolveContentMaxWidth;
307
496
  exports.roleRoutesToNavItems = roleRoutesToNavItems;
497
+ exports.useContentMaxWidth = useContentMaxWidth;
308
498
  //# sourceMappingURL=index.js.map
309
499
  //# 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 // Non-flex variant used to cap the nav strip's inner content to the content\n // column so the nav aligns with the page content instead of running full-bleed.\n chromeCapped: { 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 /**\n * Whether the nav strip aligns with the width-capped content column\n * (`'content'`) or runs full-bleed (`'full'`, the default). Only has an effect\n * when `width` caps the content. The header always renders full-bleed.\n */\n chromeAlignment?: 'full' | 'content';\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 chromeAlignment = 'full',\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 // Align the nav strip's inner content with the capped content column when asked\n // (and only when the content is actually capped); otherwise it runs full-bleed.\n const navInnerStyle: ViewStyle | ViewStyle[] | undefined =\n chromeAlignment === 'content' && maxWidth !== 'full'\n ? [styles.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }]\n : undefined;\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 ? (\n <View testID={`${testID}${APP_SHELL_SUFFIX.nav}`}>\n {navInnerStyle !== undefined ? <View style={navInnerStyle}>{nav}</View> : nav}\n </View>\n ) : 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
- account ? /* @__PURE__ */ jsx(
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: account.hint,
257
- accessibilityLabel: account.label,
335
+ accessibilityHint: upgrade.hint,
336
+ accessibilityLabel: upgrade.label,
258
337
  accessibilityRole: "button",
259
338
  style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
260
- testID: account.testID,
261
- onPress: account.onPress,
262
- children: /* @__PURE__ */ jsx(Text, { style: [navStyles.accountText, { color: TEXT_ON_PRIMARY }], children: account.label })
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,112 @@ 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
+ // Non-flex variant used to cap the nav strip's inner content to the content
387
+ // column so the nav aligns with the page content instead of running full-bleed.
388
+ chromeCapped: { width: "100%", alignSelf: "center" },
389
+ card: {
390
+ padding: CARD_PADDING,
391
+ borderRadius: CARD_BORDER_RADIUS,
392
+ borderWidth: CARD_BORDER_WIDTH
393
+ },
394
+ cardTitle: { fontSize: CARD_TITLE_FONT_SIZE, fontWeight: "700", marginBottom: CARD_TITLE_MARGIN_BOTTOM },
395
+ cardMessage: { fontSize: CARD_MESSAGE_FONT_SIZE }
396
+ });
397
+ function MessageCard({
398
+ message,
399
+ accentColor,
400
+ testID
401
+ }) {
402
+ const { theme } = useUi();
403
+ const colors = theme.colors;
404
+ return /* @__PURE__ */ jsxs(
405
+ View,
406
+ {
407
+ style: [styles.card, { backgroundColor: colors.surface, borderColor: accentColor }],
408
+ testID,
409
+ children: [
410
+ /* @__PURE__ */ jsx(Text, { style: [styles.cardTitle, { color: accentColor }], children: message.titleText }),
411
+ /* @__PURE__ */ jsx(Text, { style: [styles.cardMessage, { color: colors.textSecondary }], children: message.messageText })
412
+ ]
413
+ }
414
+ );
415
+ }
416
+ function useContentBody(state, children, testID) {
417
+ const { theme } = useUi();
418
+ const primary = theme.palette.primary["500"];
419
+ const errorColor = theme.semantic.error["500"];
420
+ if (state?.forbidden)
421
+ return /* @__PURE__ */ jsx(MessageCard, { accentColor: errorColor, message: state.forbidden, testID: `${testID}${APP_SHELL_SUFFIX.forbidden}` });
422
+ if (state?.error)
423
+ return /* @__PURE__ */ jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
424
+ if (state?.loading === true)
425
+ return /* @__PURE__ */ jsx(View, { style: styles.centerFill, testID: `${testID}${APP_SHELL_SUFFIX.loading}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
426
+ return children;
427
+ }
428
+ var AppShell = ({
429
+ header,
430
+ nav,
431
+ banner,
432
+ width = "full",
433
+ contentPadding = DEFAULT_CONTENT_PADDING,
434
+ chromeAlignment = "full",
435
+ gate,
436
+ state,
437
+ testID,
438
+ children
439
+ }) => {
440
+ const { theme } = useUi();
441
+ const primary = theme.palette.primary["500"];
442
+ const maxWidth = useContentMaxWidth(width);
443
+ const body = useContentBody(state, children, testID);
444
+ const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
445
+ useEffect(() => {
446
+ if (isUnauthenticated && gate !== void 0) gate.onRedirect();
447
+ }, [isUnauthenticated, gate]);
448
+ if (gate?.pending === true)
449
+ 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" }) });
450
+ if (isUnauthenticated) return null;
451
+ const columnStyle = maxWidth === "full" ? styles.columnFull : [styles.columnCapped, { maxWidth }];
452
+ const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
453
+ return /* @__PURE__ */ jsxs(View, { style: [styles.root, { backgroundColor: theme.colors.background }], testID, children: [
454
+ /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header }),
455
+ nav !== void 0 ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.nav}`, children: navInnerStyle !== void 0 ? /* @__PURE__ */ jsx(View, { style: navInnerStyle, children: nav }) : nav }) : null,
456
+ banner !== void 0 ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: banner }) : null,
457
+ /* @__PURE__ */ jsx(
458
+ ScrollView,
459
+ {
460
+ contentContainerStyle: [styles.scrollContent, { padding: contentPadding }],
461
+ style: styles.scroll,
462
+ testID: `${testID}${APP_SHELL_SUFFIX.content}`,
463
+ children: /* @__PURE__ */ jsx(View, { style: columnStyle, children: body })
464
+ }
465
+ )
466
+ ] });
467
+ };
283
468
  function roleRoutesToNavItems(routes, translate) {
284
469
  return routes.map((entry) => ({
285
470
  key: entry.role,
@@ -291,6 +476,6 @@ function accessibleNavItems(user, table, translate) {
291
476
  return roleRoutesToNavItems(resolveAccessibleRoutes(user, table), translate);
292
477
  }
293
478
 
294
- export { ACTIVE_BORDER_RADIUS, BASE_INDENT, CHEVRON_ICON_SIZE, NAV_ICON_SIZE, NavExpandableItem, Sidebar, Topbar, accessibleNavItems, expandableStyles, isRouteActive, navStyles, roleRoutesToNavItems };
479
+ 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
480
  //# sourceMappingURL=index.mjs.map
296
481
  //# sourceMappingURL=index.mjs.map
@@ -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 // Non-flex variant used to cap the nav strip's inner content to the content\n // column so the nav aligns with the page content instead of running full-bleed.\n chromeCapped: { 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 /**\n * Whether the nav strip aligns with the width-capped content column\n * (`'content'`) or runs full-bleed (`'full'`, the default). Only has an effect\n * when `width` caps the content. The header always renders full-bleed.\n */\n chromeAlignment?: 'full' | 'content';\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 chromeAlignment = 'full',\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 // Align the nav strip's inner content with the capped content column when asked\n // (and only when the content is actually capped); otherwise it runs full-bleed.\n const navInnerStyle: ViewStyle | ViewStyle[] | undefined =\n chromeAlignment === 'content' && maxWidth !== 'full'\n ? [styles.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }]\n : undefined;\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 ? (\n <View testID={`${testID}${APP_SHELL_SUFFIX.nav}`}>\n {navInnerStyle !== undefined ? <View style={navInnerStyle}>{nav}</View> : nav}\n </View>\n ) : 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.1",
3
+ "version": "1.1.1",
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",