@burdenoff/fe-libs 2026.1027.6 → 2026.1027.8

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.
@@ -26,7 +26,15 @@ export interface MobileTabBarProps {
26
26
  */
27
27
  hideWhenKeyboardOpen?: boolean;
28
28
  className?: string;
29
+ /**
30
+ * Controlled open state for the More sheet, so a header hamburger button
31
+ * can open the SAME sheet instead of a second, divergent one. Omit to let
32
+ * the tab bar manage it internally (the default, tap-the-More-tab-only
33
+ * behavior).
34
+ */
35
+ open?: boolean;
36
+ onOpenChange?: (open: boolean) => void;
29
37
  }
30
- export declare function MobileTabBar({ appName, logo, groupOrder, groupLabels, maxTabs, moreLabel, mobileActions, hideWhenKeyboardOpen, className, }: MobileTabBarProps): import('react/jsx-runtime').JSX.Element;
38
+ export declare function MobileTabBar({ appName, logo, groupOrder, groupLabels, maxTabs, moreLabel, mobileActions, hideWhenKeyboardOpen, className, open, onOpenChange, }: MobileTabBarProps): import('react/jsx-runtime').JSX.Element;
31
39
  export { useMobileTabBarVisible } from './mobileTabBar/useMobileTabBarVisible';
32
40
  export { selectMobileTabs } from './mobileTabBar/selectTabs';
@@ -5,19 +5,19 @@ import { useShellLabel as o } from "../shared/providers/shell/useShellLabel.js";
5
5
  import { appendContextToPath as s, useCurrentContextSearchParams as c } from "../shared/hooks/useCurrentContextSearchParams.js";
6
6
  import "./sideNavigation/navVisibility.js";
7
7
  import { useVisibleNavItems as l } from "./mobileTabBar/useVisibleNavItems.js";
8
- import { isItemOrChildActive as u } from "./sideNavigation/helpers.js";
9
- import { resolveTabPath as d, selectMobileTabs as f } from "./mobileTabBar/selectTabs.js";
8
+ import { findAncestorIds as u, isItemOrChildActive as d } from "./sideNavigation/helpers.js";
9
+ import { resolveTabPath as f, selectMobileTabs as p } from "./mobileTabBar/selectTabs.js";
10
10
  import "./mobileTabBar/useMobileTabBarVisible.js";
11
- import { useCallback as p, useEffect as m, useLayoutEffect as h, useMemo as g, useRef as _, useState as v } from "react";
12
- import { MoreHorizontal as y } from "lucide-react";
13
- import { Fragment as b, jsx as x, jsxs as S } from "react/jsx-runtime";
14
- import { useLocation as C, useNavigate as w } from "react-router";
11
+ import { useCallback as m, useEffect as h, useLayoutEffect as g, useMemo as _, useRef as v, useState as y } from "react";
12
+ import { ChevronDown as b, ChevronRight as x, MoreHorizontal as S } from "lucide-react";
13
+ import { Fragment as C, jsx as w, jsxs as T } from "react/jsx-runtime";
14
+ import { useLocation as E, useNavigate as ee } from "react-router";
15
15
  //#region src/chrome/MobileTabBar.tsx
16
- var T = "--mobile-tabbar-height";
17
- function E() {
16
+ var D = "--mobile-tabbar-height";
17
+ function O() {
18
18
  a()?.selection?.();
19
19
  }
20
- function D(e, t, n) {
20
+ function k(e, t, n) {
21
21
  let r = /* @__PURE__ */ new Map();
22
22
  for (let t of e) {
23
23
  if (t.id.startsWith("__nav_more__:") && t.children) {
@@ -39,42 +39,57 @@ function D(e, t, n) {
39
39
  items: r.get(e) ?? []
40
40
  }));
41
41
  }
42
- function O({ appName: a, logo: O, groupOrder: A = [], groupLabels: j = {}, maxTabs: M = 4, moreLabel: N, mobileActions: P, hideWhenKeyboardOpen: F = !0, className: I }) {
43
- let L = o(), R = N ?? L("nav.more", "More"), z = w(), B = C(), V = c(), { items: H } = l(), [U, W] = v(!1), G = _(null), { tabs: K, more: q, hasMore: J } = g(() => f(H, M), [H, M]), Y = g(() => D(q, A, j), [
42
+ function A({ appName: a, logo: b, groupOrder: x = [], groupLabels: A = {}, maxTabs: M = 4, moreLabel: N, mobileActions: P, hideWhenKeyboardOpen: te = !0, className: ne, open: re, onOpenChange: ie }) {
43
+ let F = o(), I = N ?? F("nav.more", "More"), L = ee(), R = E(), z = c(), { items: B } = l(), [V, H] = y(!1), U = re ?? V, W = ie ?? H, G = v(null), { tabs: K, more: q, hasMore: J } = _(() => p(B, M), [B, M]), ae = _(() => k(q, x, A), [
44
44
  q,
45
- A,
46
- j
47
- ]), X = !!P && P.length > 0, Z = J || X;
45
+ x,
46
+ A
47
+ ]), Y = !!P && P.length > 0, X = J || Y, [oe, Z] = y(/* @__PURE__ */ new Set());
48
48
  h(() => {
49
+ let e = u(q, R.pathname);
50
+ e.size !== 0 && Z((t) => {
51
+ let n = new Set(t), r = !1;
52
+ return e.forEach((e) => {
53
+ n.has(e) || (n.add(e), r = !0);
54
+ }), r ? n : t;
55
+ });
56
+ }, [q, R.pathname]);
57
+ let se = m((e) => {
58
+ Z((t) => {
59
+ let n = new Set(t);
60
+ return n.has(e) ? n.delete(e) : n.add(e), n;
61
+ });
62
+ }, []);
63
+ g(() => {
49
64
  let e = G.current;
50
65
  if (!e || typeof window > "u") return;
51
- let t = document.documentElement, n = () => t.style.setProperty(T, `${e.offsetHeight}px`);
66
+ let t = document.documentElement, n = () => t.style.setProperty(D, `${e.offsetHeight}px`);
52
67
  n();
53
68
  let r = typeof ResizeObserver < "u" ? new ResizeObserver(n) : null;
54
69
  return r?.observe(e), () => {
55
- r?.disconnect(), t.style.removeProperty(T);
70
+ r?.disconnect(), t.style.removeProperty(D);
56
71
  };
57
- }, []), m(() => {
72
+ }, []), h(() => {
58
73
  W(!1);
59
- }, [B.pathname]);
60
- let Q = p((e) => {
61
- E(), z(s(e, V));
62
- }, [z, V]), $ = g(() => !!J && q.some((e) => u(e, B.pathname)), [
74
+ }, [R.pathname, W]);
75
+ let Q = m((e) => {
76
+ O(), L(s(e, z));
77
+ }, [L, z]), $ = _(() => !!J && q.some((e) => d(e, R.pathname)), [
63
78
  J,
64
79
  q,
65
- B.pathname
80
+ R.pathname
66
81
  ]);
67
- return /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ S("nav", {
82
+ return /* @__PURE__ */ T(C, { children: [/* @__PURE__ */ T("nav", {
68
83
  ref: G,
69
84
  role: "tablist",
70
- "aria-label": L("nav.primaryNavigation", `${a} navigation`),
85
+ "aria-label": F("nav.primaryNavigation", `${a} navigation`),
71
86
  "data-mobile-tabbar": "",
72
87
  "data-shell-chrome": "",
73
- "data-keyboard-keep": F ? void 0 : "",
74
- className: e("fixed inset-x-0 bottom-0 z-[60] flex items-stretch", "bg-bg-surface text-text-primary", I),
88
+ "data-keyboard-keep": te ? void 0 : "",
89
+ className: e("fixed inset-x-0 bottom-0 z-[60] flex items-stretch", "bg-bg-surface text-text-primary", ne),
75
90
  children: [K.map((t) => {
76
- let n = u(t, B.pathname);
77
- return /* @__PURE__ */ S("button", {
91
+ let n = d(t, R.pathname);
92
+ return /* @__PURE__ */ T("button", {
78
93
  type: "button",
79
94
  role: "tab",
80
95
  "aria-selected": n,
@@ -83,91 +98,102 @@ function O({ appName: a, logo: O, groupOrder: A = [], groupLabels: j = {}, maxTa
83
98
  "data-mobile-tab": t.id,
84
99
  className: e("mobile-tab relative flex min-w-0 flex-1 flex-col items-center justify-center gap-0.5", "text-text-muted transition-colors duration-150", n && "text-text-link"),
85
100
  onClick: () => {
86
- n && !t.children?.length || Q(d(t));
101
+ n && !t.children?.length || Q(f(t));
87
102
  },
88
- children: [/* @__PURE__ */ S("span", {
103
+ children: [/* @__PURE__ */ T("span", {
89
104
  className: "mobile-tab__icon relative inline-flex items-center justify-center",
90
105
  "aria-hidden": "true",
91
- children: [t.icon, t.badge ? /* @__PURE__ */ x("span", {
106
+ children: [t.icon, t.badge ? /* @__PURE__ */ w("span", {
92
107
  className: "absolute -top-1 -right-2 min-w-4 rounded-full bg-status-error-bg px-1 text-[10px] font-semibold leading-4 text-status-error-text",
93
108
  children: t.badge > 99 ? "99+" : t.badge
94
109
  }) : null]
95
- }), /* @__PURE__ */ x("span", {
110
+ }), /* @__PURE__ */ w("span", {
96
111
  className: "mobile-tab__label max-w-full truncate",
97
112
  children: t.label
98
113
  })]
99
114
  }, t.id);
100
- }), Z ? /* @__PURE__ */ S("button", {
115
+ }), X ? /* @__PURE__ */ T("button", {
101
116
  type: "button",
102
117
  role: "tab",
103
118
  "aria-selected": $,
104
119
  "aria-haspopup": "dialog",
105
120
  "aria-expanded": U,
106
- "aria-label": R,
121
+ "aria-label": I,
107
122
  "data-active": $ ? "" : void 0,
108
123
  "data-mobile-tab": "__more",
109
124
  className: e("mobile-tab relative flex min-w-0 flex-1 flex-col items-center justify-center gap-0.5", "text-text-muted transition-colors duration-150", $ && "text-text-link"),
110
125
  onClick: () => {
111
- E(), W(!0);
126
+ O(), W(!0);
112
127
  },
113
- children: [/* @__PURE__ */ x("span", {
128
+ children: [/* @__PURE__ */ w("span", {
114
129
  className: "mobile-tab__icon inline-flex items-center justify-center",
115
130
  "aria-hidden": "true",
116
- children: /* @__PURE__ */ x(y, {})
117
- }), /* @__PURE__ */ x("span", {
131
+ children: /* @__PURE__ */ w(S, {})
132
+ }), /* @__PURE__ */ w("span", {
118
133
  className: "mobile-tab__label max-w-full truncate",
119
- children: R
134
+ children: I
120
135
  })]
121
136
  }) : null]
122
- }), Z ? /* @__PURE__ */ x(t, {
137
+ }), X ? /* @__PURE__ */ w(t, {
123
138
  open: U,
124
139
  onOpenChange: W,
125
- children: /* @__PURE__ */ S(n, {
126
- "aria-label": R,
140
+ children: /* @__PURE__ */ T(n, {
141
+ "aria-label": I,
127
142
  "data-swipe-ignore": "",
128
- children: [/* @__PURE__ */ S(r, { children: [O ? /* @__PURE__ */ S("div", {
143
+ children: [/* @__PURE__ */ T(r, { children: [b ? /* @__PURE__ */ T("div", {
129
144
  className: "mb-1 flex items-center gap-2",
130
- children: [/* @__PURE__ */ x("div", {
145
+ children: [/* @__PURE__ */ w("div", {
131
146
  className: "flex size-8 flex-shrink-0 items-center justify-center",
132
- children: O
133
- }), /* @__PURE__ */ x("span", {
147
+ children: b
148
+ }), /* @__PURE__ */ w("span", {
134
149
  className: "text-heading-h4 font-semibold text-text-primary",
135
150
  children: a
136
151
  })]
137
- }) : null, /* @__PURE__ */ x(i, { children: R })] }), /* @__PURE__ */ S("div", {
152
+ }) : null, /* @__PURE__ */ w(i, { children: I })] }), /* @__PURE__ */ T("div", {
138
153
  className: "flex flex-col gap-4 pb-2",
139
154
  "data-mobile-tabbar-more": "",
140
- children: [Y.map((e) => /* @__PURE__ */ S("section", {
155
+ children: [ae.map((e) => /* @__PURE__ */ T("section", {
141
156
  "aria-label": e.label ?? void 0,
142
- children: [e.label ? /* @__PURE__ */ x("p", {
157
+ children: [e.label ? /* @__PURE__ */ w("p", {
143
158
  className: "mb-1 px-1 text-[11px] font-medium uppercase tracking-wider text-text-muted",
144
159
  children: e.label
145
- }) : null, /* @__PURE__ */ x("ul", {
160
+ }) : null, /* @__PURE__ */ w("ul", {
146
161
  className: "flex flex-col",
147
162
  children: e.items.map((e) => {
148
- let t = u(e, B.pathname);
149
- return /* @__PURE__ */ x("li", { children: /* @__PURE__ */ x(k, {
163
+ let t = d(e, R.pathname), n = !!e.children && e.children.length > 0, r = oe.has(e.id);
164
+ return /* @__PURE__ */ T("li", { children: [/* @__PURE__ */ w(j, {
150
165
  icon: e.icon,
151
166
  label: e.label,
152
167
  badge: e.badge,
153
168
  active: t,
154
- onSelect: () => Q(d(e))
155
- }) }, e.id);
169
+ expandable: n,
170
+ expanded: r,
171
+ onSelect: () => n ? se(e.id) : Q(f(e))
172
+ }), n && r ? /* @__PURE__ */ w("ul", {
173
+ className: "flex flex-col pl-7",
174
+ children: e.children.map((e) => /* @__PURE__ */ w("li", { children: /* @__PURE__ */ w(j, {
175
+ icon: e.icon,
176
+ label: e.label,
177
+ badge: e.badge,
178
+ active: d(e, R.pathname),
179
+ onSelect: () => Q(f(e))
180
+ }) }, e.id))
181
+ }) : null] }, e.id);
156
182
  })
157
183
  })]
158
- }, e.key || "__ungrouped")), X ? /* @__PURE__ */ S("section", {
159
- "aria-label": L("appShell.quickActions", "Quick Actions"),
160
- children: [/* @__PURE__ */ x("p", {
184
+ }, e.key || "__ungrouped")), Y ? /* @__PURE__ */ T("section", {
185
+ "aria-label": F("appShell.quickActions", "Quick Actions"),
186
+ children: [/* @__PURE__ */ w("p", {
161
187
  className: "mb-1 px-1 text-[11px] font-medium uppercase tracking-wider text-text-muted",
162
- children: L("appShell.quickActions", "Quick Actions")
163
- }), /* @__PURE__ */ x("ul", {
188
+ children: F("appShell.quickActions", "Quick Actions")
189
+ }), /* @__PURE__ */ w("ul", {
164
190
  className: "flex flex-col",
165
- children: P?.map((e) => /* @__PURE__ */ x("li", { children: /* @__PURE__ */ x(k, {
191
+ children: P?.map((e) => /* @__PURE__ */ w("li", { children: /* @__PURE__ */ w(j, {
166
192
  icon: e.icon,
167
193
  label: e.label,
168
194
  active: !!e.isToggle && !!e.isActive,
169
195
  onSelect: () => {
170
- E(), e.onClick(), W(!1);
196
+ O(), e.onClick(), W(!1);
171
197
  }
172
198
  }) }, e.id))
173
199
  })]
@@ -176,23 +202,29 @@ function O({ appName: a, logo: O, groupOrder: A = [], groupLabels: j = {}, maxTa
176
202
  })
177
203
  }) : null] });
178
204
  }
179
- function k({ icon: t, label: n, badge: r, active: i, onSelect: a }) {
180
- return /* @__PURE__ */ S("button", {
205
+ function j({ icon: t, label: n, badge: r, active: i, onSelect: a, expandable: o, expanded: s }) {
206
+ return /* @__PURE__ */ T("button", {
181
207
  type: "button",
182
208
  onClick: a,
183
- "aria-current": i ? "page" : void 0,
209
+ "aria-current": !o && i ? "page" : void 0,
210
+ "aria-expanded": o ? s : void 0,
184
211
  className: e("flex w-full min-h-11 items-center gap-3 rounded-button px-3 py-2 text-left text-body-sm", "transition-colors duration-150", i ? "bg-[var(--color-accent-soft)] text-text-primary font-medium" : "text-text-secondary hover:bg-bg-sunken hover:text-text-primary"),
185
212
  children: [
186
- t ? /* @__PURE__ */ x("span", {
213
+ t ? /* @__PURE__ */ w("span", {
187
214
  className: "inline-flex size-5 shrink-0 items-center justify-center",
188
215
  "aria-hidden": "true",
189
216
  children: t
190
217
  }) : null,
191
- /* @__PURE__ */ x("span", {
218
+ /* @__PURE__ */ w("span", {
192
219
  className: "min-w-0 flex-1 truncate",
193
220
  children: n
194
221
  }),
195
- r ? /* @__PURE__ */ x("span", {
222
+ o ? /* @__PURE__ */ w("span", {
223
+ className: "inline-flex size-4 shrink-0 items-center justify-center text-text-muted",
224
+ "aria-hidden": "true",
225
+ children: w(s ? b : x, { className: "size-4" })
226
+ }) : null,
227
+ r ? /* @__PURE__ */ w("span", {
196
228
  className: "rounded-full bg-status-error-bg px-1.5 text-[10px] font-semibold leading-4 text-status-error-text",
197
229
  children: r > 99 ? "99+" : r
198
230
  }) : null
@@ -200,4 +232,4 @@ function k({ icon: t, label: n, badge: r, active: i, onSelect: a }) {
200
232
  });
201
233
  }
202
234
  //#endregion
203
- export { O as MobileTabBar };
235
+ export { A as MobileTabBar };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/fe-libs",
3
- "version": "2026.1027.6",
3
+ "version": "2026.1027.8",
4
4
  "description": "Burdenoff frontend primitives and domain libraries",
5
5
  "type": "module",
6
6
  "bin": {
@@ -252,10 +252,27 @@ body.keyboard-open [data-native-shell] main {
252
252
  backdrop-filter: saturate(180%) blur(20px);
253
253
  }
254
254
 
255
+ /* Normal flow, not absolutely centered: with 5+ trailing actions (context,
256
+ search, notifications, profile, plus a leading hamburger) an
257
+ absolutely-positioned title ignores the row's actual layout and visually
258
+ overlaps the icons instead of yielding space to them. Flex "centers" it
259
+ well enough when lead/trailing are similar widths, and truncates instead
260
+ of overlapping when they are not — same mechanism Android already used
261
+ below, just keeping iOS's centered alignment. */
255
262
  .platform-ios [data-native-topbar] .native-topbar__title {
263
+ position: static;
264
+ margin: 0;
265
+ max-width: none;
266
+ min-width: 0;
267
+ flex: 1 1 auto;
256
268
  text-align: center;
257
269
  }
258
270
 
271
+ .platform-ios [data-native-topbar] .native-topbar__lead,
272
+ .platform-ios [data-native-topbar] .native-topbar__actions {
273
+ flex: 0 0 auto;
274
+ }
275
+
259
276
  /* Android: opaque 56px bar, title left-aligned next to the back arrow, and no
260
277
  iOS-style back TEXT label. */
261
278
  .platform-android [data-native-topbar] {