@dloizides/ui-nav 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -45
- package/dist/index.d.mts +60 -1
- package/dist/index.d.ts +60 -1
- package/dist/index.js +145 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +146 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,45 +1,90 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 1.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
Make the `AppShell` back-office `sidebar` layout **responsive** — a mobile drawer.
|
|
6
|
+
|
|
7
|
+
The `sidebar` slot (1.4.0) always rendered a persistent fixed-width left rail in a
|
|
8
|
+
`flexDirection:'row'` body with no width breakpoint, so below ~768px the content column
|
|
9
|
+
starved (headings wrapped a character per line, inputs truncated) — the merchant admin was
|
|
10
|
+
unusable on a phone. Now `AppShell` reads `useWindowDimensions`:
|
|
11
|
+
|
|
12
|
+
- **Desktop (≥768px): IDENTICAL to 1.4.0** — the persistent rail + row layout, untouched.
|
|
13
|
+
Purely additive; no consuming product's desktop layout changes.
|
|
14
|
+
- **Below 768px:** the rail is not rendered beside the content; instead a **hamburger**
|
|
15
|
+
(in the header) opens the same `Sidebar` as an overlay **drawer** (a tap-to-close scrim +
|
|
16
|
+
a left panel) and the content takes the full width. The drawer closes on a scrim tap and
|
|
17
|
+
on a nav-item press (web: a bubbled click on a `role="button"` / link inside the panel).
|
|
18
|
+
|
|
19
|
+
Accessibility: the hamburger has a testID + `accessibilityLabel` + `accessibilityHint` and a
|
|
20
|
+
themed focus ring; the drawer panel is `role="dialog"` `aria-modal`; the scrim is a labelled
|
|
21
|
+
button. New optional prop `mobileMenu?: Partial<DrawerLabels>` supplies pre-localized
|
|
22
|
+
open/close labels (English defaults otherwise — this package has no i18n). New testID
|
|
23
|
+
suffixes: `-menu-toggle`, `-drawer`, `-drawer-scrim`.
|
|
24
|
+
|
|
25
|
+
**Backwards compatible.** No breaking prop changes — consumers pass the same `sidebar` slot.
|
|
26
|
+
Omit `sidebar` and the tree is byte-identical to 1.4.0 (erevna / katalogos / kefi untouched).
|
|
27
|
+
Only a `sidebar` consumer on a narrow viewport sees the drawer. Pinned by tests asserting the
|
|
28
|
+
rail is present on desktop and absent (hamburger present) on a narrow viewport.
|
|
29
|
+
|
|
30
|
+
## 1.4.0
|
|
31
|
+
|
|
32
|
+
Add an optional **`sidebar`** slot to `AppShell` — the back-office layout.
|
|
33
|
+
|
|
34
|
+
`AppShell` could only stack vertically (header → nav strip → banner → scrolling content),
|
|
35
|
+
so a product with a persistent LEFT RAIL (Agora's merchant admin: products / categories /
|
|
36
|
+
coupons / settings) had to hand-compose `Topbar` + `Sidebar` + a `ScrollView` row and give
|
|
37
|
+
up the shell's auth gate and loading/error/forbidden state cards. Supplying `sidebar` now
|
|
38
|
+
lays the body out as a ROW — the rail on the left, the width-disciplined scrolling content
|
|
39
|
+
column on the right — while every existing feature (gate, state cards, `width`,
|
|
40
|
+
`chromeAlignment`) keeps working.
|
|
41
|
+
|
|
42
|
+
The new region is exposed as `` `${testID}${APP_SHELL_SUFFIX.sidebar}` `` (`-sidebar`).
|
|
43
|
+
|
|
44
|
+
**Backwards compatible.** Omit `sidebar` and the render tree is byte-identical to 1.3.0 —
|
|
45
|
+
erevna / katalogos / kefi / aml-v2 are untouched. Pinned by a test asserting no `-sidebar`
|
|
46
|
+
region exists when the slot is omitted.
|
|
47
|
+
|
|
48
|
+
## 1.3.0
|
|
49
|
+
|
|
50
|
+
Give the `NavBar` the v1 `.gn-links` look + a full accessibility pass (and extend the
|
|
51
|
+
same a11y bar to `Sidebar`/`Topbar`).
|
|
52
|
+
|
|
53
|
+
**Look & hover (NavBar).** Each link is a rounded-pill target with the v1 hover
|
|
54
|
+
affordance — muted at rest, and on HOVER the text brightens to full contrast while a
|
|
55
|
+
subtle rounded-pill background appears; the ACTIVE link is a solid accent pill. All
|
|
56
|
+
theme-driven (rest = `textSecondary`, hover text = `text`, hover bg = `surfaceElevated`,
|
|
57
|
+
active/ring = `palette.primary`). Hover/focus are web-only (`onHoverIn/Out`, `onFocus/Blur`
|
|
58
|
+
via `Pressable`); native stays at the rest style. The hover cross-fade is cheap CSS and is
|
|
59
|
+
disabled under `prefers-reduced-motion: reduce` (new `useReducedMotion`). Backwards
|
|
60
|
+
compatible — the hover pill is the default (aml-v2 is the only `NavBar` consumer) and the
|
|
61
|
+
brand/right slots, responsive hamburger collapse, and `containerStyle` width behaviour are
|
|
62
|
+
unchanged.
|
|
63
|
+
|
|
64
|
+
**Accessibility.** NavBar toggle now carries `aria-controls` pointing at the collapsible
|
|
65
|
+
links region (id via `nativeID`), keeps `aria-expanded`, and gains a themed keyboard focus
|
|
66
|
+
ring; links keep `aria-current="page"` + `role="link"` and gain a focus ring; link/toggle
|
|
67
|
+
touch targets are now ≥44×44. `Sidebar` leaves gain `aria-current="page"` + a focus ring;
|
|
68
|
+
`Topbar` action buttons gain a focus ring (new internal `FocusableTouchable`). Focus rings
|
|
69
|
+
never remove the outline without a themed replacement.
|
|
70
|
+
|
|
71
|
+
## 1.2.0
|
|
72
|
+
|
|
73
|
+
Add `NavBar` — a config-driven **horizontal top navigation bar**, the top-bar
|
|
74
|
+
counterpart to `Sidebar`. It renders the same caller-supplied `NavItem[]` as inline
|
|
75
|
+
links across the top (brand left slot + inline links + free-form right slot), with
|
|
76
|
+
an active-link highlight (`aria-current="page"`) and a responsive collapse to a
|
|
77
|
+
hamburger toggle below a configurable breakpoint (`collapseBelow`, default `760`) —
|
|
78
|
+
mirroring the v1 AML console `.global-nav`. Theme-driven (UiProvider colours),
|
|
79
|
+
`role="navigation"` landmark, keyboard-operable links. Purely additive: `Sidebar`,
|
|
80
|
+
`Topbar`, `AppShell`, and the role-gating helpers are unchanged. New `NAV_TEST_IDS`
|
|
81
|
+
entries `navBarToggle` / `navBarLinks`.
|
|
82
|
+
|
|
83
|
+
## 1.0.0
|
|
84
|
+
|
|
85
|
+
Initial release. `Sidebar` (leaf + expandable items, active-route highlight, header/footer
|
|
86
|
+
slots), `Topbar` (logo/language/notification/user/account/logout slots), and the
|
|
87
|
+
`accessibleNavItems` / `roleRoutesToNavItems` role-gating helpers (reusing
|
|
88
|
+
`resolveAccessibleRoutes` from `@dloizides/auth-web`). Chrome + metrics ported verbatim from
|
|
89
|
+
the byte-identical erevna-web / katalogos-web nav twins; every colour reads from the
|
|
90
|
+
`@dloizides/ui-feedback` UiProvider theme.
|
package/dist/index.d.mts
CHANGED
|
@@ -195,6 +195,33 @@ interface NavBarProps {
|
|
|
195
195
|
}
|
|
196
196
|
declare const NavBar: ({ items, pathname, onNavigate, regionLabel, navigateHint, brand, right, menuLabel, menuHint, renderMenuIcon, collapseBelow, containerStyle, }: NavBarProps) => React.ReactElement;
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* MobileDrawer — the below-the-breakpoint face of `AppShell`'s back-office
|
|
200
|
+
* `sidebar` layout. On a narrow viewport (<768px) the persistent left rail would
|
|
201
|
+
* starve the content column, so instead of the rail we render a hamburger toggle
|
|
202
|
+
* (`MenuToggle`) in the header and slide the same `Sidebar` in as an overlay
|
|
203
|
+
* drawer (a scrim + a left panel). The drawer closes on a scrim tap and on any
|
|
204
|
+
* nav-item press inside the panel (web: a bubbled click on a `role="button"` /
|
|
205
|
+
* link element). Desktop is untouched — AppShell never mounts this above 768px.
|
|
206
|
+
*
|
|
207
|
+
* Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM / router /
|
|
208
|
+
* store imports. Labels are pre-localized strings (English defaults, overridable
|
|
209
|
+
* by the consumer via `AppShell`'s `mobileMenu` prop); every colour is read from
|
|
210
|
+
* the `@dloizides/ui-feedback` UiProvider theme.
|
|
211
|
+
*/
|
|
212
|
+
|
|
213
|
+
/** Pre-localized labels for the drawer's hamburger + scrim. */
|
|
214
|
+
interface DrawerLabels {
|
|
215
|
+
/** Accessible name of the hamburger that opens the drawer. */
|
|
216
|
+
openLabel: string;
|
|
217
|
+
/** Accessible hint of the hamburger that opens the drawer. */
|
|
218
|
+
openHint: string;
|
|
219
|
+
/** Accessible name of the scrim (tap to close). */
|
|
220
|
+
closeLabel: string;
|
|
221
|
+
/** Accessible hint of the scrim (tap to close). */
|
|
222
|
+
closeHint: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
198
225
|
/** Content-width policy: a capped column, optionally wider past a viewport breakpoint, or full-bleed. */
|
|
199
226
|
type AppShellWidth = {
|
|
200
227
|
max: number;
|
|
@@ -218,6 +245,13 @@ declare function useContentMaxWidth(width: AppShellWidth): number | 'full';
|
|
|
218
245
|
* (pending spinner / redirect-when-unauthenticated) and page state cards
|
|
219
246
|
* (loading / error / forbidden).
|
|
220
247
|
*
|
|
248
|
+
* A BACK-OFFICE variant is supported via the optional `sidebar` slot (added for
|
|
249
|
+
* Agora's merchant admin): supply a `<Sidebar>` and the body below the header /
|
|
250
|
+
* nav / banner becomes a ROW — a persistent left rail beside the scrolling
|
|
251
|
+
* content column — instead of the default single vertical stack. Omit `sidebar`
|
|
252
|
+
* and the render tree is byte-identical to before, so every existing consumer
|
|
253
|
+
* (erevna / katalogos / kefi) is untouched.
|
|
254
|
+
*
|
|
221
255
|
* Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM, router,
|
|
222
256
|
* or store imports. The app passes wired elements, pre-localized strings, and
|
|
223
257
|
* callbacks; every colour is routed through the `@dloizides/ui-feedback` theme.
|
|
@@ -233,6 +267,23 @@ interface AppShellProps {
|
|
|
233
267
|
header: React.ReactNode;
|
|
234
268
|
/** Per-app nav region (kefi coral pills / erevna sidebar+drawer). */
|
|
235
269
|
nav?: React.ReactNode;
|
|
270
|
+
/**
|
|
271
|
+
* Optional persistent LEFT RAIL (a `<Sidebar>`), for back-office surfaces. When
|
|
272
|
+
* supplied, the region below header/nav/banner lays out as a row: the rail on the
|
|
273
|
+
* left, the scrolling content column on the right. Omit for the default vertical
|
|
274
|
+
* stack — the tree is then byte-identical to the pre-`sidebar` AppShell.
|
|
275
|
+
*
|
|
276
|
+
* Responsive: at/under ~768px the persistent rail is REPLACED by a hamburger (in
|
|
277
|
+
* the header) that opens the same `sidebar` as an overlay drawer, so the content
|
|
278
|
+
* takes the full width. Desktop (≥768px) is unchanged. No effect without `sidebar`.
|
|
279
|
+
*/
|
|
280
|
+
sidebar?: React.ReactNode;
|
|
281
|
+
/**
|
|
282
|
+
* Pre-localized labels for the mobile nav drawer (only used in the `sidebar`
|
|
283
|
+
* layout, below the breakpoint): the hamburger's open label/hint and the scrim's
|
|
284
|
+
* close label/hint. Optional — English defaults apply. NO i18n in this package.
|
|
285
|
+
*/
|
|
286
|
+
mobileMenu?: Partial<DrawerLabels>;
|
|
236
287
|
/** Banner slot (e.g. verification-pending). */
|
|
237
288
|
banner?: React.ReactNode;
|
|
238
289
|
/** Content-width policy. Defaults to full-bleed. */
|
|
@@ -261,7 +312,7 @@ interface AppShellProps {
|
|
|
261
312
|
testID: string;
|
|
262
313
|
children: React.ReactNode;
|
|
263
314
|
}
|
|
264
|
-
declare const AppShell: ({ header, nav, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
|
|
315
|
+
declare const AppShell: ({ header, nav, sidebar, mobileMenu, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
|
|
265
316
|
|
|
266
317
|
/**
|
|
267
318
|
* Default testIDs for `@dloizides/ui-nav`. Kept as a small central map (mirrors
|
|
@@ -289,6 +340,14 @@ declare const APP_SHELL_SUFFIX: {
|
|
|
289
340
|
readonly content: "-content";
|
|
290
341
|
readonly header: "-header";
|
|
291
342
|
readonly nav: "-nav";
|
|
343
|
+
/** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
|
|
344
|
+
readonly sidebar: "-sidebar";
|
|
345
|
+
/** Mobile hamburger that opens the nav drawer (back-office layout, below the breakpoint). */
|
|
346
|
+
readonly menuToggle: "-menu-toggle";
|
|
347
|
+
/** The mobile nav drawer panel (holds the `sidebar` slot when opened on a narrow viewport). */
|
|
348
|
+
readonly drawer: "-drawer";
|
|
349
|
+
/** The mobile nav drawer scrim (tap to close). */
|
|
350
|
+
readonly scrim: "-drawer-scrim";
|
|
292
351
|
readonly banner: "-banner";
|
|
293
352
|
readonly pending: "-pending";
|
|
294
353
|
readonly loading: "-loading";
|
package/dist/index.d.ts
CHANGED
|
@@ -195,6 +195,33 @@ interface NavBarProps {
|
|
|
195
195
|
}
|
|
196
196
|
declare const NavBar: ({ items, pathname, onNavigate, regionLabel, navigateHint, brand, right, menuLabel, menuHint, renderMenuIcon, collapseBelow, containerStyle, }: NavBarProps) => React.ReactElement;
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* MobileDrawer — the below-the-breakpoint face of `AppShell`'s back-office
|
|
200
|
+
* `sidebar` layout. On a narrow viewport (<768px) the persistent left rail would
|
|
201
|
+
* starve the content column, so instead of the rail we render a hamburger toggle
|
|
202
|
+
* (`MenuToggle`) in the header and slide the same `Sidebar` in as an overlay
|
|
203
|
+
* drawer (a scrim + a left panel). The drawer closes on a scrim tap and on any
|
|
204
|
+
* nav-item press inside the panel (web: a bubbled click on a `role="button"` /
|
|
205
|
+
* link element). Desktop is untouched — AppShell never mounts this above 768px.
|
|
206
|
+
*
|
|
207
|
+
* Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM / router /
|
|
208
|
+
* store imports. Labels are pre-localized strings (English defaults, overridable
|
|
209
|
+
* by the consumer via `AppShell`'s `mobileMenu` prop); every colour is read from
|
|
210
|
+
* the `@dloizides/ui-feedback` UiProvider theme.
|
|
211
|
+
*/
|
|
212
|
+
|
|
213
|
+
/** Pre-localized labels for the drawer's hamburger + scrim. */
|
|
214
|
+
interface DrawerLabels {
|
|
215
|
+
/** Accessible name of the hamburger that opens the drawer. */
|
|
216
|
+
openLabel: string;
|
|
217
|
+
/** Accessible hint of the hamburger that opens the drawer. */
|
|
218
|
+
openHint: string;
|
|
219
|
+
/** Accessible name of the scrim (tap to close). */
|
|
220
|
+
closeLabel: string;
|
|
221
|
+
/** Accessible hint of the scrim (tap to close). */
|
|
222
|
+
closeHint: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
198
225
|
/** Content-width policy: a capped column, optionally wider past a viewport breakpoint, or full-bleed. */
|
|
199
226
|
type AppShellWidth = {
|
|
200
227
|
max: number;
|
|
@@ -218,6 +245,13 @@ declare function useContentMaxWidth(width: AppShellWidth): number | 'full';
|
|
|
218
245
|
* (pending spinner / redirect-when-unauthenticated) and page state cards
|
|
219
246
|
* (loading / error / forbidden).
|
|
220
247
|
*
|
|
248
|
+
* A BACK-OFFICE variant is supported via the optional `sidebar` slot (added for
|
|
249
|
+
* Agora's merchant admin): supply a `<Sidebar>` and the body below the header /
|
|
250
|
+
* nav / banner becomes a ROW — a persistent left rail beside the scrolling
|
|
251
|
+
* content column — instead of the default single vertical stack. Omit `sidebar`
|
|
252
|
+
* and the render tree is byte-identical to before, so every existing consumer
|
|
253
|
+
* (erevna / katalogos / kefi) is untouched.
|
|
254
|
+
*
|
|
221
255
|
* Contract discipline (same as the rest of `@dloizides/ui-nav`): NO FM, router,
|
|
222
256
|
* or store imports. The app passes wired elements, pre-localized strings, and
|
|
223
257
|
* callbacks; every colour is routed through the `@dloizides/ui-feedback` theme.
|
|
@@ -233,6 +267,23 @@ interface AppShellProps {
|
|
|
233
267
|
header: React.ReactNode;
|
|
234
268
|
/** Per-app nav region (kefi coral pills / erevna sidebar+drawer). */
|
|
235
269
|
nav?: React.ReactNode;
|
|
270
|
+
/**
|
|
271
|
+
* Optional persistent LEFT RAIL (a `<Sidebar>`), for back-office surfaces. When
|
|
272
|
+
* supplied, the region below header/nav/banner lays out as a row: the rail on the
|
|
273
|
+
* left, the scrolling content column on the right. Omit for the default vertical
|
|
274
|
+
* stack — the tree is then byte-identical to the pre-`sidebar` AppShell.
|
|
275
|
+
*
|
|
276
|
+
* Responsive: at/under ~768px the persistent rail is REPLACED by a hamburger (in
|
|
277
|
+
* the header) that opens the same `sidebar` as an overlay drawer, so the content
|
|
278
|
+
* takes the full width. Desktop (≥768px) is unchanged. No effect without `sidebar`.
|
|
279
|
+
*/
|
|
280
|
+
sidebar?: React.ReactNode;
|
|
281
|
+
/**
|
|
282
|
+
* Pre-localized labels for the mobile nav drawer (only used in the `sidebar`
|
|
283
|
+
* layout, below the breakpoint): the hamburger's open label/hint and the scrim's
|
|
284
|
+
* close label/hint. Optional — English defaults apply. NO i18n in this package.
|
|
285
|
+
*/
|
|
286
|
+
mobileMenu?: Partial<DrawerLabels>;
|
|
236
287
|
/** Banner slot (e.g. verification-pending). */
|
|
237
288
|
banner?: React.ReactNode;
|
|
238
289
|
/** Content-width policy. Defaults to full-bleed. */
|
|
@@ -261,7 +312,7 @@ interface AppShellProps {
|
|
|
261
312
|
testID: string;
|
|
262
313
|
children: React.ReactNode;
|
|
263
314
|
}
|
|
264
|
-
declare const AppShell: ({ header, nav, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
|
|
315
|
+
declare const AppShell: ({ header, nav, sidebar, mobileMenu, banner, width, contentPadding, chromeAlignment, gate, state, testID, children, }: AppShellProps) => React.ReactElement | null;
|
|
265
316
|
|
|
266
317
|
/**
|
|
267
318
|
* Default testIDs for `@dloizides/ui-nav`. Kept as a small central map (mirrors
|
|
@@ -289,6 +340,14 @@ declare const APP_SHELL_SUFFIX: {
|
|
|
289
340
|
readonly content: "-content";
|
|
290
341
|
readonly header: "-header";
|
|
291
342
|
readonly nav: "-nav";
|
|
343
|
+
/** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
|
|
344
|
+
readonly sidebar: "-sidebar";
|
|
345
|
+
/** Mobile hamburger that opens the nav drawer (back-office layout, below the breakpoint). */
|
|
346
|
+
readonly menuToggle: "-menu-toggle";
|
|
347
|
+
/** The mobile nav drawer panel (holds the `sidebar` slot when opened on a narrow viewport). */
|
|
348
|
+
readonly drawer: "-drawer";
|
|
349
|
+
/** The mobile nav drawer scrim (tap to close). */
|
|
350
|
+
readonly scrim: "-drawer-scrim";
|
|
292
351
|
readonly banner: "-banner";
|
|
293
352
|
readonly pending: "-pending";
|
|
294
353
|
readonly loading: "-loading";
|
package/dist/index.js
CHANGED
|
@@ -346,6 +346,14 @@ var APP_SHELL_SUFFIX = {
|
|
|
346
346
|
content: "-content",
|
|
347
347
|
header: "-header",
|
|
348
348
|
nav: "-nav",
|
|
349
|
+
/** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
|
|
350
|
+
sidebar: "-sidebar",
|
|
351
|
+
/** Mobile hamburger that opens the nav drawer (back-office layout, below the breakpoint). */
|
|
352
|
+
menuToggle: "-menu-toggle",
|
|
353
|
+
/** The mobile nav drawer panel (holds the `sidebar` slot when opened on a narrow viewport). */
|
|
354
|
+
drawer: "-drawer",
|
|
355
|
+
/** The mobile nav drawer scrim (tap to close). */
|
|
356
|
+
scrim: "-drawer-scrim",
|
|
349
357
|
banner: "-banner",
|
|
350
358
|
pending: "-pending",
|
|
351
359
|
loading: "-loading",
|
|
@@ -669,6 +677,82 @@ var NavBar = ({
|
|
|
669
677
|
}
|
|
670
678
|
);
|
|
671
679
|
};
|
|
680
|
+
var MOBILE_BREAKPOINT = 768;
|
|
681
|
+
var SCRIM_COLOR = "rgba(0, 0, 0, 0.5)";
|
|
682
|
+
var MENU_GLYPH2 = "\u2630";
|
|
683
|
+
var MENU_MIN_TARGET = 44;
|
|
684
|
+
var MENU_GLYPH_FONT_SIZE = 20;
|
|
685
|
+
var MENU_TOGGLE_PADDING = 12;
|
|
686
|
+
var DRAWER_Z_INDEX = 50;
|
|
687
|
+
var NAV_ACTIVATABLE_SELECTOR = 'a, [role="button"], [role="link"]';
|
|
688
|
+
var DEFAULT_DRAWER_LABELS = {
|
|
689
|
+
openLabel: "Menu",
|
|
690
|
+
openHint: "Open the navigation menu",
|
|
691
|
+
closeLabel: "Close menu",
|
|
692
|
+
closeHint: "Close the navigation menu"
|
|
693
|
+
};
|
|
694
|
+
var styles = reactNative.StyleSheet.create({
|
|
695
|
+
overlay: { ...reactNative.StyleSheet.absoluteFillObject, zIndex: DRAWER_Z_INDEX },
|
|
696
|
+
scrim: { ...reactNative.StyleSheet.absoluteFillObject, backgroundColor: SCRIM_COLOR },
|
|
697
|
+
panel: { position: "absolute", top: 0, bottom: 0, left: 0 },
|
|
698
|
+
menuToggle: {
|
|
699
|
+
minWidth: MENU_MIN_TARGET,
|
|
700
|
+
minHeight: MENU_MIN_TARGET,
|
|
701
|
+
alignItems: "center",
|
|
702
|
+
justifyContent: "center",
|
|
703
|
+
paddingHorizontal: MENU_TOGGLE_PADDING
|
|
704
|
+
},
|
|
705
|
+
menuToggleGlyph: { fontSize: MENU_GLYPH_FONT_SIZE, fontWeight: "700" }
|
|
706
|
+
});
|
|
707
|
+
var MenuToggle = ({ label, hint, onOpen, testID }) => {
|
|
708
|
+
const { theme } = uiFeedback.useUi();
|
|
709
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
710
|
+
FocusableTouchable,
|
|
711
|
+
{
|
|
712
|
+
accessibilityHint: hint,
|
|
713
|
+
accessibilityLabel: label,
|
|
714
|
+
ringColor: theme.palette.primary["500"],
|
|
715
|
+
style: styles.menuToggle,
|
|
716
|
+
testID,
|
|
717
|
+
onPress: onOpen,
|
|
718
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
|
|
719
|
+
}
|
|
720
|
+
);
|
|
721
|
+
};
|
|
722
|
+
var MobileDrawer = ({
|
|
723
|
+
sidebar,
|
|
724
|
+
labels,
|
|
725
|
+
onClose,
|
|
726
|
+
drawerTestID,
|
|
727
|
+
scrimTestID
|
|
728
|
+
}) => {
|
|
729
|
+
const panelRef = react.useRef(null);
|
|
730
|
+
react.useEffect(() => {
|
|
731
|
+
const node = panelRef.current;
|
|
732
|
+
if (node === null || typeof node.addEventListener !== "function") return void 0;
|
|
733
|
+
const handleClick = (event) => {
|
|
734
|
+
const target = event.target;
|
|
735
|
+
const onNavItem = target !== null && target.closest(NAV_ACTIVATABLE_SELECTOR) !== null;
|
|
736
|
+
if (onNavItem) onClose();
|
|
737
|
+
};
|
|
738
|
+
node.addEventListener("click", handleClick);
|
|
739
|
+
return () => node.removeEventListener("click", handleClick);
|
|
740
|
+
}, [onClose]);
|
|
741
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles.overlay, children: [
|
|
742
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
743
|
+
reactNative.Pressable,
|
|
744
|
+
{
|
|
745
|
+
accessibilityHint: labels.closeHint,
|
|
746
|
+
accessibilityLabel: labels.closeLabel,
|
|
747
|
+
accessibilityRole: "button",
|
|
748
|
+
style: styles.scrim,
|
|
749
|
+
testID: scrimTestID,
|
|
750
|
+
onPress: onClose
|
|
751
|
+
}
|
|
752
|
+
),
|
|
753
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ref: panelRef, "aria-modal": true, role: "dialog", style: styles.panel, testID: drawerTestID, children: sidebar })
|
|
754
|
+
] });
|
|
755
|
+
};
|
|
672
756
|
function resolveContentMaxWidth(width, viewport) {
|
|
673
757
|
if (width === "full") return "full";
|
|
674
758
|
const wideMinViewport = width.wideMinViewport ?? Number.POSITIVE_INFINITY;
|
|
@@ -686,11 +770,16 @@ var CARD_BORDER_WIDTH = 1;
|
|
|
686
770
|
var CARD_TITLE_FONT_SIZE = 16;
|
|
687
771
|
var CARD_MESSAGE_FONT_SIZE = 14;
|
|
688
772
|
var CARD_TITLE_MARGIN_BOTTOM = 8;
|
|
689
|
-
var
|
|
773
|
+
var styles2 = reactNative.StyleSheet.create({
|
|
690
774
|
root: { flex: 1 },
|
|
691
775
|
centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
|
|
692
776
|
scroll: { flex: 1 },
|
|
693
777
|
scrollContent: { flexGrow: 1 },
|
|
778
|
+
// Mobile back-office header: the hamburger sits inline before the header slot.
|
|
779
|
+
headerRow: { flexDirection: "row", alignItems: "center" },
|
|
780
|
+
headerFill: { flex: 1 },
|
|
781
|
+
// Back-office layout: a persistent left rail beside the scrolling content column.
|
|
782
|
+
bodyRow: { flex: 1, flexDirection: "row" },
|
|
694
783
|
columnFull: { flex: 1 },
|
|
695
784
|
columnCapped: { flex: 1, width: "100%", alignSelf: "center" },
|
|
696
785
|
// Non-flex variant used to cap the nav strip's inner content to the content
|
|
@@ -714,11 +803,11 @@ function MessageCard({
|
|
|
714
803
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
715
804
|
reactNative.View,
|
|
716
805
|
{
|
|
717
|
-
style: [
|
|
806
|
+
style: [styles2.card, { backgroundColor: colors.surface, borderColor: accentColor }],
|
|
718
807
|
testID,
|
|
719
808
|
children: [
|
|
720
|
-
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [
|
|
721
|
-
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [
|
|
809
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.cardTitle, { color: accentColor }], children: message.titleText }),
|
|
810
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.cardMessage, { color: colors.textSecondary }], children: message.messageText })
|
|
722
811
|
]
|
|
723
812
|
}
|
|
724
813
|
);
|
|
@@ -732,12 +821,14 @@ function useContentBody(state, children, testID) {
|
|
|
732
821
|
if (state?.error)
|
|
733
822
|
return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
|
|
734
823
|
if (state?.loading === true)
|
|
735
|
-
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style:
|
|
824
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.centerFill, testID: `${testID}${APP_SHELL_SUFFIX.loading}`, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" }) });
|
|
736
825
|
return children;
|
|
737
826
|
}
|
|
738
827
|
var AppShell = ({
|
|
739
828
|
header,
|
|
740
829
|
nav,
|
|
830
|
+
sidebar,
|
|
831
|
+
mobileMenu,
|
|
741
832
|
banner,
|
|
742
833
|
width = "full",
|
|
743
834
|
contentPadding = DEFAULT_CONTENT_PADDING,
|
|
@@ -751,28 +842,65 @@ var AppShell = ({
|
|
|
751
842
|
const primary = theme.palette.primary["500"];
|
|
752
843
|
const maxWidth = useContentMaxWidth(width);
|
|
753
844
|
const body = useContentBody(state, children, testID);
|
|
845
|
+
const { width: viewportWidth } = reactNative.useWindowDimensions();
|
|
846
|
+
const isNarrow = viewportWidth < MOBILE_BREAKPOINT;
|
|
847
|
+
const useDrawer = sidebar !== void 0 && isNarrow;
|
|
848
|
+
const [drawerOpen, setDrawerOpen] = react.useState(false);
|
|
849
|
+
const openDrawer = react.useCallback(() => setDrawerOpen(true), []);
|
|
850
|
+
const closeDrawer = react.useCallback(() => setDrawerOpen(false), []);
|
|
851
|
+
react.useEffect(() => {
|
|
852
|
+
if (!isNarrow && drawerOpen) setDrawerOpen(false);
|
|
853
|
+
}, [isNarrow, drawerOpen]);
|
|
854
|
+
const drawerLabels = { ...DEFAULT_DRAWER_LABELS, ...mobileMenu };
|
|
754
855
|
const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
|
|
755
856
|
react.useEffect(() => {
|
|
756
857
|
if (isUnauthenticated && gate !== void 0) gate.onRedirect();
|
|
757
858
|
}, [isUnauthenticated, gate]);
|
|
758
859
|
if (gate?.pending === true)
|
|
759
|
-
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [
|
|
860
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles2.root, styles2.centerFill, { backgroundColor: theme.colors.background }], testID: `${testID}${APP_SHELL_SUFFIX.pending}`, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" }) });
|
|
760
861
|
if (isUnauthenticated) return null;
|
|
761
|
-
const columnStyle = maxWidth === "full" ?
|
|
762
|
-
const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [
|
|
763
|
-
|
|
764
|
-
|
|
862
|
+
const columnStyle = maxWidth === "full" ? styles2.columnFull : [styles2.columnCapped, { maxWidth }];
|
|
863
|
+
const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles2.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
|
|
864
|
+
const scroller = /* @__PURE__ */ jsxRuntime.jsx(
|
|
865
|
+
reactNative.ScrollView,
|
|
866
|
+
{
|
|
867
|
+
contentContainerStyle: [styles2.scrollContent, { padding: contentPadding }],
|
|
868
|
+
style: styles2.scroll,
|
|
869
|
+
testID: `${testID}${APP_SHELL_SUFFIX.content}`,
|
|
870
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: columnStyle, children: body })
|
|
871
|
+
}
|
|
872
|
+
);
|
|
873
|
+
const headerRegion = useDrawer ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.headerRow, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
|
|
874
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
875
|
+
MenuToggle,
|
|
876
|
+
{
|
|
877
|
+
hint: drawerLabels.openHint,
|
|
878
|
+
label: drawerLabels.openLabel,
|
|
879
|
+
testID: `${testID}${APP_SHELL_SUFFIX.menuToggle}`,
|
|
880
|
+
onOpen: openDrawer
|
|
881
|
+
}
|
|
882
|
+
),
|
|
883
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.headerFill, children: header })
|
|
884
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header });
|
|
885
|
+
const showRail = sidebar !== void 0 && !isNarrow;
|
|
886
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles2.root, { backgroundColor: theme.colors.background }], testID, children: [
|
|
887
|
+
headerRegion,
|
|
765
888
|
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,
|
|
766
889
|
banner !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: banner }) : null,
|
|
767
|
-
/* @__PURE__ */ jsxRuntime.
|
|
768
|
-
reactNative.
|
|
890
|
+
showRail ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles2.bodyRow, children: [
|
|
891
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { testID: `${testID}${APP_SHELL_SUFFIX.sidebar}`, children: sidebar }),
|
|
892
|
+
scroller
|
|
893
|
+
] }) : scroller,
|
|
894
|
+
useDrawer && drawerOpen ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
895
|
+
MobileDrawer,
|
|
769
896
|
{
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
897
|
+
drawerTestID: `${testID}${APP_SHELL_SUFFIX.drawer}`,
|
|
898
|
+
labels: drawerLabels,
|
|
899
|
+
scrimTestID: `${testID}${APP_SHELL_SUFFIX.scrim}`,
|
|
900
|
+
sidebar,
|
|
901
|
+
onClose: closeDrawer
|
|
774
902
|
}
|
|
775
|
-
)
|
|
903
|
+
) : null
|
|
776
904
|
] });
|
|
777
905
|
};
|
|
778
906
|
function roleRoutesToNavItems(routes, translate) {
|