@boost33/ui 0.1.0 → 0.3.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/README.md +5 -2
- package/dist/anchored-overlay.d.ts +86 -0
- package/dist/anchored-overlay.js +125 -0
- package/dist/anchored-overlay.js.map +1 -0
- package/dist/components/AppShell/AppShell.d.ts +80 -0
- package/dist/components/AppShell/AppShell.js +59 -0
- package/dist/components/AppShell/AppShell.js.map +1 -0
- package/dist/components/AppShell/AppShell.module.css.js +20 -0
- package/dist/components/AppShell/AppShell.module.css.js.map +1 -0
- package/dist/components/AppSwitcher/AppSwitcher.d.ts +96 -0
- package/dist/components/AppSwitcher/AppSwitcher.js +81 -0
- package/dist/components/AppSwitcher/AppSwitcher.js.map +1 -0
- package/dist/components/AppSwitcher/AppSwitcher.module.css.js +44 -0
- package/dist/components/AppSwitcher/AppSwitcher.module.css.js.map +1 -0
- package/dist/components/AppSwitcher/icons.d.ts +20 -0
- package/dist/components/AppSwitcher/icons.js +25 -0
- package/dist/components/AppSwitcher/icons.js.map +1 -0
- package/dist/components/AppSwitcher/labels.d.ts +60 -0
- package/dist/components/AppSwitcher/labels.js +16 -0
- package/dist/components/AppSwitcher/labels.js.map +1 -0
- package/dist/components/Boost33Mark/Boost33Mark.d.ts +65 -0
- package/dist/components/Boost33Mark/Boost33Mark.js +62 -0
- package/dist/components/Boost33Mark/Boost33Mark.js.map +1 -0
- package/dist/components/Boost33Mark/Boost33Mark.module.css.js +12 -0
- package/dist/components/Boost33Mark/Boost33Mark.module.css.js.map +1 -0
- package/dist/components/NavItem/NavItem.d.ts +18 -2
- package/dist/components/NavItem/NavItem.js +18 -0
- package/dist/components/NavItem/NavItem.js.map +1 -0
- package/dist/components/NavItem/NavItem.module.css.js +18 -0
- package/dist/components/NavItem/NavItem.module.css.js.map +1 -0
- package/dist/components/NavSection/NavSection.d.ts +12 -0
- package/dist/components/NavSection/NavSection.js +14 -0
- package/dist/components/NavSection/NavSection.js.map +1 -0
- package/dist/components/NavSection/NavSection.module.css.js +10 -0
- package/dist/components/NavSection/NavSection.module.css.js.map +1 -0
- package/dist/components/PageHeader/PageHeader.d.ts +10 -0
- package/dist/components/PageHeader/PageHeader.js +15 -0
- package/dist/components/PageHeader/PageHeader.js.map +1 -0
- package/dist/components/PageHeader/PageHeader.module.css.js +16 -0
- package/dist/components/PageHeader/PageHeader.module.css.js.map +1 -0
- package/dist/components/ProductMark/ProductMark.d.ts +63 -0
- package/dist/components/ProductMark/ProductMark.js +73 -0
- package/dist/components/ProductMark/ProductMark.js.map +1 -0
- package/dist/components/ProductMark/ProductMark.module.css.js +10 -0
- package/dist/components/ProductMark/ProductMark.module.css.js.map +1 -0
- package/dist/components/Sidebar/NavHeightBudgetWarning.d.ts +3 -0
- package/dist/components/Sidebar/NavHeightBudgetWarning.js +28 -0
- package/dist/components/Sidebar/NavHeightBudgetWarning.js.map +1 -0
- package/dist/components/Sidebar/Sidebar.d.ts +19 -0
- package/dist/components/Sidebar/Sidebar.js +14 -0
- package/dist/components/Sidebar/Sidebar.js.map +1 -0
- package/dist/components/Sidebar/Sidebar.module.css.js +8 -0
- package/dist/components/Sidebar/Sidebar.module.css.js.map +1 -0
- package/dist/components/TopBar/TopBar.d.ts +8 -0
- package/dist/components/TopBar/TopBar.js +12 -0
- package/dist/components/TopBar/TopBar.js.map +1 -0
- package/dist/components/TopBar/TopBar.module.css.js +12 -0
- package/dist/components/TopBar/TopBar.module.css.js.map +1 -0
- package/dist/components/WalletChip/WalletChip.d.ts +60 -0
- package/dist/components/WalletChip/WalletChip.js +68 -0
- package/dist/components/WalletChip/WalletChip.js.map +1 -0
- package/dist/components/WalletChip/WalletChip.module.css.js +56 -0
- package/dist/components/WalletChip/WalletChip.module.css.js.map +1 -0
- package/dist/components/WalletChip/format.d.ts +47 -0
- package/dist/components/WalletChip/format.js +29 -0
- package/dist/components/WalletChip/format.js.map +1 -0
- package/dist/components/WalletChip/icons.d.ts +8 -0
- package/dist/components/WalletChip/icons.js +18 -0
- package/dist/components/WalletChip/icons.js.map +1 -0
- package/dist/components/WalletChip/labels.d.ts +143 -0
- package/dist/components/WalletChip/labels.js +38 -0
- package/dist/components/WalletChip/labels.js.map +1 -0
- package/dist/components/Wordmark/Wordmark.d.ts +12 -0
- package/dist/components/Wordmark/Wordmark.js +12 -0
- package/dist/components/Wordmark/Wordmark.js.map +1 -0
- package/dist/components/Wordmark/Wordmark.module.css.js +12 -0
- package/dist/components/Wordmark/Wordmark.module.css.js.map +1 -0
- package/dist/index.d.ts +26 -2
- package/dist/index.js +41 -35
- package/dist/index.js.map +1 -1
- package/dist/nav-height-budget.d.ts +58 -1
- package/dist/nav-height-budget.js +38 -0
- package/dist/nav-height-budget.js.map +1 -0
- package/dist/style.css +1 -1
- package/dist/theme.css +189 -0
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# @boost33/ui
|
|
2
2
|
|
|
3
|
-
The app shell every Boost33 product renders inside.
|
|
4
|
-
design tokens they are drawn with, published to public npm.
|
|
3
|
+
The app shell every Boost33 product renders inside. Fourteen components and the
|
|
4
|
+
design tokens they are drawn with, published to public npm. Eleven ship today;
|
|
5
|
+
AccountMenu, NotificationCenter and CommandPalette are in v1's scope and not
|
|
6
|
+
built yet.
|
|
5
7
|
|
|
6
8
|
Built because the product switcher and the wallet chip worked in the Wallet and
|
|
7
9
|
nowhere else, and because a sidebar fixed in one app fixed nothing in the other
|
|
@@ -15,6 +17,7 @@ three.
|
|
|
15
17
|
| What we are building | Linear document **The shared app shell** |
|
|
16
18
|
| Vocabulary | `CONTEXT.md`, and the initiative's **Vocabulary** document |
|
|
17
19
|
| Settled decisions | `docs/adr/` |
|
|
20
|
+
| What changed between versions | `CHANGELOG.md` |
|
|
18
21
|
| Tickets | Linear, project **Boost33 Shared UI** |
|
|
19
22
|
|
|
20
23
|
## Commands
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { KeyboardEvent } from 'react';
|
|
2
|
+
/** What `aria-haspopup` announces, when a consumer has a popup role to announce. */
|
|
3
|
+
export type AnchoredOverlayPopup = 'menu' | 'dialog' | 'listbox' | 'tree' | 'grid';
|
|
4
|
+
export interface UseAnchoredOverlayOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The value for the trigger's `aria-haspopup`, for a consumer that has a popup
|
|
7
|
+
* role to announce. Per consumer, not fixed: the value differs by what the
|
|
8
|
+
* panel actually is, so one hardcoded here would misannounce another.
|
|
9
|
+
*
|
|
10
|
+
* Optional, and its absence means only "no popup role", nothing more. ADR-0010
|
|
11
|
+
* puts AppSwitcher, AccountMenu and NotificationCenter on the Disclosure
|
|
12
|
+
* pattern: their trigger wants `aria-expanded` while a container of ordinary
|
|
13
|
+
* links and buttons must not claim a popup role. Supply it and the hook emits
|
|
14
|
+
* `aria-haspopup`; omit it and the trigger still carries `aria-expanded` and
|
|
15
|
+
* `aria-controls`. Whether there is a trigger at all is `hasTrigger`, not this.
|
|
16
|
+
*/
|
|
17
|
+
haspopup?: AnchoredOverlayPopup;
|
|
18
|
+
/**
|
|
19
|
+
* Whether this overlay has a trigger element. Defaults to `true`, because
|
|
20
|
+
* three of the four consumers open from one. Set it `false` for the ⌘K-opened
|
|
21
|
+
* command palette (B33-495), which may have no trigger at all: then no
|
|
22
|
+
* `triggerProps` is produced -- there is nothing to spread it onto, and
|
|
23
|
+
* `aria-expanded`/`aria-controls` would announce a trigger that does not
|
|
24
|
+
* exist. Focus still restores, to the captured opener.
|
|
25
|
+
*
|
|
26
|
+
* This is the gate `haspopup` used to stand in for. Conflating "has no
|
|
27
|
+
* trigger" with "has no popup role" is what B33-720 corrects: a disclosure
|
|
28
|
+
* has a trigger and no popup role, and could not say so before.
|
|
29
|
+
*/
|
|
30
|
+
hasTrigger?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Drives the overlay from outside. Supplying it makes the overlay
|
|
33
|
+
* controlled: it opens and closes only when this changes, and every attempt
|
|
34
|
+
* to move it arrives at `onOpenChange` instead. A specimen route passes it
|
|
35
|
+
* to draw the open panel with no interaction, which is Stage A.
|
|
36
|
+
*/
|
|
37
|
+
open?: boolean;
|
|
38
|
+
/** Starting state when uncontrolled. Ignored once `open` is supplied. */
|
|
39
|
+
defaultOpen?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Every open and close, including the ones this hook decides on its own. A
|
|
42
|
+
* consumer whose ancestor clips overflow needs this to un-clip while the
|
|
43
|
+
* panel overhangs -- the sidebar does exactly that in boostledger.
|
|
44
|
+
*/
|
|
45
|
+
onOpenChange?: (open: boolean) => void;
|
|
46
|
+
/**
|
|
47
|
+
* The modal variant, for the command palette (B33-495) alone. It additionally
|
|
48
|
+
* traps focus inside the panel, marks the rest of the document `inert`, and
|
|
49
|
+
* skips the focusout dismissal listener -- under a trap focus never leaves the
|
|
50
|
+
* panel, so that listener could never fire and is not attached. Everything
|
|
51
|
+
* else in the contract is unchanged: Escape restores to the opener and an
|
|
52
|
+
* outside (backdrop) pointer-down closes.
|
|
53
|
+
*
|
|
54
|
+
* A per-consumer flag, and only the palette sets it. AppSwitcher, AccountMenu
|
|
55
|
+
* and NotificationCenter never become modal -- B33-479 carries that as a hard
|
|
56
|
+
* acceptance criterion. See docs/adr/0009.
|
|
57
|
+
*/
|
|
58
|
+
modal?: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface AnchoredOverlay {
|
|
61
|
+
open: boolean;
|
|
62
|
+
/** Move the overlay. On a controlled overlay this only reports. */
|
|
63
|
+
setOpen: (next: boolean) => void;
|
|
64
|
+
toggle: () => void;
|
|
65
|
+
/** Close and return focus, for a row that acts and dismisses. */
|
|
66
|
+
close: () => void;
|
|
67
|
+
/**
|
|
68
|
+
* Undefined when `hasTrigger` is `false`: a trigger-less overlay has nothing
|
|
69
|
+
* to carry these attributes, so the hook produces none. `aria-haspopup` is
|
|
70
|
+
* present only when a `haspopup` value was supplied -- a disclosure trigger
|
|
71
|
+
* carries `aria-expanded` and `aria-controls` without it.
|
|
72
|
+
*/
|
|
73
|
+
triggerProps?: {
|
|
74
|
+
ref: (node: HTMLElement | null) => void;
|
|
75
|
+
'aria-haspopup'?: AnchoredOverlayPopup;
|
|
76
|
+
'aria-expanded': boolean;
|
|
77
|
+
'aria-controls': string | undefined;
|
|
78
|
+
onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
|
|
79
|
+
};
|
|
80
|
+
panelProps: {
|
|
81
|
+
ref: (node: HTMLElement | null) => void;
|
|
82
|
+
id: string;
|
|
83
|
+
onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export declare function useAnchoredOverlay(options: UseAnchoredOverlayOptions): AnchoredOverlay;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useId as B, useRef as i, useState as N, useCallback as s, useEffect as m } from "react";
|
|
2
|
+
const R = [
|
|
3
|
+
"a[href]",
|
|
4
|
+
"button:not([disabled])",
|
|
5
|
+
"input:not([disabled])",
|
|
6
|
+
"select:not([disabled])",
|
|
7
|
+
"textarea:not([disabled])",
|
|
8
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
9
|
+
].join(",");
|
|
10
|
+
function x(b) {
|
|
11
|
+
return Array.from(b.querySelectorAll(R));
|
|
12
|
+
}
|
|
13
|
+
function j(b) {
|
|
14
|
+
const {
|
|
15
|
+
haspopup: O,
|
|
16
|
+
hasTrigger: K = !0,
|
|
17
|
+
open: P,
|
|
18
|
+
defaultOpen: T = !1,
|
|
19
|
+
onOpenChange: w,
|
|
20
|
+
modal: c = !1
|
|
21
|
+
} = b, A = B(), g = i(null), f = i(null), y = P !== void 0, [C, k] = N(T), n = y ? P : C, d = i(w);
|
|
22
|
+
d.current = w;
|
|
23
|
+
const p = i(!0), h = i(null), v = i(n), a = s(
|
|
24
|
+
(e) => {
|
|
25
|
+
var t;
|
|
26
|
+
y || k(e), (t = d.current) == null || t.call(d, e);
|
|
27
|
+
},
|
|
28
|
+
[y]
|
|
29
|
+
), l = s((e) => {
|
|
30
|
+
var t, r;
|
|
31
|
+
return e ? !!((t = g.current) != null && t.contains(e) || (r = f.current) != null && r.contains(e)) : !1;
|
|
32
|
+
}, []), u = s(
|
|
33
|
+
(e) => {
|
|
34
|
+
p.current = e, a(!1);
|
|
35
|
+
},
|
|
36
|
+
[a]
|
|
37
|
+
);
|
|
38
|
+
m(() => {
|
|
39
|
+
var e;
|
|
40
|
+
if (n && !v.current) {
|
|
41
|
+
const t = document.activeElement;
|
|
42
|
+
h.current = t && t !== document.body ? t : null, p.current = !0;
|
|
43
|
+
} else if (!n && v.current) {
|
|
44
|
+
const t = h.current;
|
|
45
|
+
p.current && (t != null && t.isConnected ? t.focus() : (e = g.current) == null || e.focus()), h.current = null, p.current = !0;
|
|
46
|
+
}
|
|
47
|
+
v.current = n;
|
|
48
|
+
}, [n]), m(() => {
|
|
49
|
+
if (!n || c) return;
|
|
50
|
+
const e = (t) => {
|
|
51
|
+
if (!l(t.target)) return;
|
|
52
|
+
const r = t.relatedTarget;
|
|
53
|
+
r && (l(r) || u(!1));
|
|
54
|
+
};
|
|
55
|
+
return document.addEventListener("focusout", e, !0), () => document.removeEventListener("focusout", e, !0);
|
|
56
|
+
}, [n, c, l, u]), m(() => {
|
|
57
|
+
if (!n) return;
|
|
58
|
+
const e = (t) => {
|
|
59
|
+
l(t.target) || u(!1);
|
|
60
|
+
};
|
|
61
|
+
return document.addEventListener("pointerdown", e, !0), () => document.removeEventListener("pointerdown", e, !0);
|
|
62
|
+
}, [n, l, u]), m(() => {
|
|
63
|
+
if (!n || !c) return;
|
|
64
|
+
const e = f.current;
|
|
65
|
+
if (!e) return;
|
|
66
|
+
(x(e)[0] ?? e).focus();
|
|
67
|
+
const r = Array.from(document.body.children).filter(
|
|
68
|
+
(o) => o instanceof HTMLElement && !o.contains(e)
|
|
69
|
+
);
|
|
70
|
+
for (const o of r) o.setAttribute("inert", "");
|
|
71
|
+
return () => {
|
|
72
|
+
for (const o of r) o.removeAttribute("inert");
|
|
73
|
+
};
|
|
74
|
+
}, [n, c]);
|
|
75
|
+
const E = s(
|
|
76
|
+
(e) => {
|
|
77
|
+
e.key !== "Escape" || !n || (e.stopPropagation(), u(!0));
|
|
78
|
+
},
|
|
79
|
+
[n, u]
|
|
80
|
+
), F = s(
|
|
81
|
+
(e) => {
|
|
82
|
+
if (E(e), !c || e.key !== "Tab" || e.defaultPrevented) return;
|
|
83
|
+
const t = f.current;
|
|
84
|
+
if (!t) return;
|
|
85
|
+
const r = x(t);
|
|
86
|
+
if (r.length === 0) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const o = r[0], D = r[r.length - 1], L = document.activeElement;
|
|
91
|
+
e.shiftKey && L === o ? (e.preventDefault(), D.focus()) : !e.shiftKey && L === D && (e.preventDefault(), o.focus());
|
|
92
|
+
},
|
|
93
|
+
[E, c]
|
|
94
|
+
), I = s((e) => {
|
|
95
|
+
g.current = e;
|
|
96
|
+
}, []), S = s((e) => {
|
|
97
|
+
f.current = e;
|
|
98
|
+
}, []);
|
|
99
|
+
return {
|
|
100
|
+
open: n,
|
|
101
|
+
setOpen: a,
|
|
102
|
+
toggle: s(() => a(!n), [a, n]),
|
|
103
|
+
close: s(() => u(!0), [u]),
|
|
104
|
+
triggerProps: K ? {
|
|
105
|
+
ref: I,
|
|
106
|
+
// Only when a value was supplied. A disclosure has a trigger and no
|
|
107
|
+
// popup role, so it registers a trigger without announcing one.
|
|
108
|
+
...O ? { "aria-haspopup": O } : {},
|
|
109
|
+
"aria-expanded": n,
|
|
110
|
+
// Only while the panel exists. Pointing at an id that is not in the
|
|
111
|
+
// document is a dangling reference an AT has to resolve and cannot.
|
|
112
|
+
"aria-controls": n ? A : void 0,
|
|
113
|
+
onKeyDown: E
|
|
114
|
+
} : void 0,
|
|
115
|
+
panelProps: {
|
|
116
|
+
ref: S,
|
|
117
|
+
id: A,
|
|
118
|
+
onKeyDown: F
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
j as useAnchoredOverlay
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=anchored-overlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anchored-overlay.js","sources":["../src/anchored-overlay.ts"],"sourcesContent":["import { useCallback, useEffect, useId, useRef, useState } from 'react'\nimport type { KeyboardEvent } from 'react'\n\n/*\n * The dismissal contract every anchored overlay in this package shares.\n *\n * Four components open a panel from a trigger -- AppSwitcher (B33-479),\n * AccountMenu (B33-493), NotificationCenter (B33-494) and CommandPalette\n * (B33-495) -- and B33-533 asked for one hook covering all four, extracted\n * from the switcher onBlur in boostledger. All four use this. It owns\n * dismissal, focus restoration and trigger ARIA; it does NOT own what keys do\n * inside the panel, and it does not position anything. The command palette is\n * the modal variant: pass `modal` and it also traps focus, marks the\n * background `inert`, and drops the focusout listener a trap makes dead. See\n * docs/adr/0009.\n *\n * Internal on purpose: absent from src/index.ts, so consumers of the package\n * get components rather than behaviour they could wire up three ways.\n *\n * Three things here are corrections to the reference implementation rather\n * than transcriptions of it, and each one is a bug it has:\n *\n * - Containment is a ref test, not `currentTarget.parentElement.contains`.\n * DOM parentage is false the moment a panel is portalled, and a portalled\n * panel would then close on its first Tab.\n * - A focusout with no relatedTarget does not close. macOS Safari and\n * Firefox do not focus a clicked button, so the trigger blurs with a null\n * relatedTarget while the pointer is still travelling to a row -- closing\n * there shuts the panel underneath the click that opened a row.\n * - Outside pointer-down closes. The reference has no outside handler at\n * all, so once focus entered its panel nothing could shut it: the trigger\n * had already blurred and never blurs again.\n *\n * Reduced motion is deliberately absent. It stays in CSS, in each component's\n * own module, the way NavItem.module.css and WalletChip.module.css already do\n * it -- there is no matchMedia anywhere in src/, and jsdom ships none.\n */\n\n/** What `aria-haspopup` announces, when a consumer has a popup role to announce. */\nexport type AnchoredOverlayPopup = 'menu' | 'dialog' | 'listbox' | 'tree' | 'grid'\n\nexport interface UseAnchoredOverlayOptions {\n /**\n * The value for the trigger's `aria-haspopup`, for a consumer that has a popup\n * role to announce. Per consumer, not fixed: the value differs by what the\n * panel actually is, so one hardcoded here would misannounce another.\n *\n * Optional, and its absence means only \"no popup role\", nothing more. ADR-0010\n * puts AppSwitcher, AccountMenu and NotificationCenter on the Disclosure\n * pattern: their trigger wants `aria-expanded` while a container of ordinary\n * links and buttons must not claim a popup role. Supply it and the hook emits\n * `aria-haspopup`; omit it and the trigger still carries `aria-expanded` and\n * `aria-controls`. Whether there is a trigger at all is `hasTrigger`, not this.\n */\n haspopup?: AnchoredOverlayPopup\n /**\n * Whether this overlay has a trigger element. Defaults to `true`, because\n * three of the four consumers open from one. Set it `false` for the ⌘K-opened\n * command palette (B33-495), which may have no trigger at all: then no\n * `triggerProps` is produced -- there is nothing to spread it onto, and\n * `aria-expanded`/`aria-controls` would announce a trigger that does not\n * exist. Focus still restores, to the captured opener.\n *\n * This is the gate `haspopup` used to stand in for. Conflating \"has no\n * trigger\" with \"has no popup role\" is what B33-720 corrects: a disclosure\n * has a trigger and no popup role, and could not say so before.\n */\n hasTrigger?: boolean\n /**\n * Drives the overlay from outside. Supplying it makes the overlay\n * controlled: it opens and closes only when this changes, and every attempt\n * to move it arrives at `onOpenChange` instead. A specimen route passes it\n * to draw the open panel with no interaction, which is Stage A.\n */\n open?: boolean\n /** Starting state when uncontrolled. Ignored once `open` is supplied. */\n defaultOpen?: boolean\n /**\n * Every open and close, including the ones this hook decides on its own. A\n * consumer whose ancestor clips overflow needs this to un-clip while the\n * panel overhangs -- the sidebar does exactly that in boostledger.\n */\n onOpenChange?: (open: boolean) => void\n /**\n * The modal variant, for the command palette (B33-495) alone. It additionally\n * traps focus inside the panel, marks the rest of the document `inert`, and\n * skips the focusout dismissal listener -- under a trap focus never leaves the\n * panel, so that listener could never fire and is not attached. Everything\n * else in the contract is unchanged: Escape restores to the opener and an\n * outside (backdrop) pointer-down closes.\n *\n * A per-consumer flag, and only the palette sets it. AppSwitcher, AccountMenu\n * and NotificationCenter never become modal -- B33-479 carries that as a hard\n * acceptance criterion. See docs/adr/0009.\n */\n modal?: boolean\n}\n\nexport interface AnchoredOverlay {\n open: boolean\n /** Move the overlay. On a controlled overlay this only reports. */\n setOpen: (next: boolean) => void\n toggle: () => void\n /** Close and return focus, for a row that acts and dismisses. */\n close: () => void\n /**\n * Undefined when `hasTrigger` is `false`: a trigger-less overlay has nothing\n * to carry these attributes, so the hook produces none. `aria-haspopup` is\n * present only when a `haspopup` value was supplied -- a disclosure trigger\n * carries `aria-expanded` and `aria-controls` without it.\n */\n triggerProps?: {\n ref: (node: HTMLElement | null) => void\n 'aria-haspopup'?: AnchoredOverlayPopup\n 'aria-expanded': boolean\n 'aria-controls': string | undefined\n onKeyDown: (event: KeyboardEvent<HTMLElement>) => void\n }\n panelProps: {\n ref: (node: HTMLElement | null) => void\n id: string\n onKeyDown: (event: KeyboardEvent<HTMLElement>) => void\n }\n}\n\n// The tabbable elements inside a modal panel, in document order, for the focus\n// trap. Deliberately narrow: the palette's own search input and results are all\n// it holds, and jsdom cannot tell a visible element from a hidden one anyway.\n// `[tabindex=\"-1\"]` is excluded because a programmatically-focusable container\n// is not a Tab stop. Query order is document order, which is what Tab follows.\nconst FOCUSABLE = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',')\n\nfunction focusableWithin(root: HTMLElement): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE))\n}\n\nexport function useAnchoredOverlay(options: UseAnchoredOverlayOptions): AnchoredOverlay {\n const {\n haspopup,\n hasTrigger = true,\n open: openProp,\n defaultOpen = false,\n onOpenChange,\n modal = false,\n } = options\n\n const panelId = useId()\n const triggerRef = useRef<HTMLElement | null>(null)\n const panelRef = useRef<HTMLElement | null>(null)\n\n const controlled = openProp !== undefined\n const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen)\n const open = controlled ? openProp : uncontrolledOpen\n\n // Read through a ref inside the document listeners. An inline arrow for\n // onOpenChange is a new function every render, and without this the\n // listeners detach and reattach on each one.\n const onOpenChangeRef = useRef(onOpenChange)\n onOpenChangeRef.current = onOpenChange\n\n // Whether the close that is about to happen should move focus. Escape and a\n // component's own close() restore; a dismissal caused by focus or a pointer\n // already moving somewhere else does not, because focus has arrived where\n // the person sent it and dragging it back would fight them.\n const restoreOnClose = useRef(true)\n const openedFrom = useRef<HTMLElement | null>(null)\n const wasOpen = useRef(open)\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (!controlled) setUncontrolledOpen(next)\n onOpenChangeRef.current?.(next)\n },\n [controlled],\n )\n\n const isInside = useCallback((node: Node | null) => {\n if (!node) return false\n return Boolean(triggerRef.current?.contains(node) || panelRef.current?.contains(node))\n }, [])\n\n const dismiss = useCallback(\n (restore: boolean) => {\n restoreOnClose.current = restore\n setOpen(false)\n },\n [setOpen],\n )\n\n // Capture the opener as the overlay opens, restore as it closes. Reading\n // document.activeElement at open time rather than assuming the trigger is\n // what makes a palette opened from a table return focus to the table -- and\n // for the three non-modal consumers the opener IS the trigger, so \"restore\n // the opener\" and \"restore the trigger\" name the same element.\n useEffect(() => {\n if (open && !wasOpen.current) {\n // document.activeElement is <body> when nothing is focused, and restoring\n // focus to the body is indistinguishable from dropping it. Treat that as\n // no opener so the close falls through to the trigger.\n const active = document.activeElement\n openedFrom.current = active && active !== document.body ? (active as HTMLElement) : null\n restoreOnClose.current = true\n } else if (!open && wasOpen.current) {\n const opener = openedFrom.current\n if (restoreOnClose.current) {\n // isConnected, not a truthiness check: the opener can have unmounted\n // while the panel was up, and focusing a detached node silently sends\n // focus to the body instead.\n if (opener?.isConnected) opener.focus()\n else triggerRef.current?.focus()\n }\n openedFrom.current = null\n restoreOnClose.current = true\n }\n wasOpen.current = open\n }, [open])\n\n // focusout rather than React's onBlur, on the document rather than on either\n // element. A portalled panel is not a DOM descendant of anything the trigger\n // can see, so this is the only listener that observes both halves.\n useEffect(() => {\n // A modal traps focus in the panel, so focus never leaves the trigger and\n // panel together and this listener could never fire. Skip attaching it\n // rather than leave a handler that only ever runs its early return.\n if (!open || modal) return\n const onFocusOut = (event: FocusEvent) => {\n if (!isInside(event.target as Node | null)) return\n const next = event.relatedTarget as Node | null\n if (!next) return\n if (isInside(next)) return\n dismiss(false)\n }\n document.addEventListener('focusout', onFocusOut, true)\n return () => document.removeEventListener('focusout', onFocusOut, true)\n }, [open, modal, isInside, dismiss])\n\n // pointerdown, and never preventDefault: the click that dismissed the panel\n // still has to reach whatever it landed on. The trigger counts as inside, so\n // its own click falls through to its onClick and toggles -- were it outside,\n // pointerdown would close and the following click would reopen, and the\n // trigger could never be clicked shut.\n useEffect(() => {\n if (!open) return\n const onPointerDown = (event: Event) => {\n if (isInside(event.target as Node | null)) return\n dismiss(false)\n }\n document.addEventListener('pointerdown', onPointerDown, true)\n return () => document.removeEventListener('pointerdown', onPointerDown, true)\n }, [open, isInside, dismiss])\n\n // Modal only: seal the panel off from the rest of the document while it is\n // open. Pull focus in (the opener was already captured above, so restoration\n // still lands where the palette was summoned from), and mark every top-level\n // sibling that does not contain the panel `inert` so neither pointer nor AT\n // reaches behind it. The panel is portalled to the body, so its own top-level\n // node is skipped and stays live. Both undone on close.\n useEffect(() => {\n if (!open || !modal) return\n const panel = panelRef.current\n if (!panel) return\n const focusables = focusableWithin(panel)\n ;(focusables[0] ?? panel).focus()\n\n const sealed = Array.from(document.body.children).filter(\n (el): el is HTMLElement => el instanceof HTMLElement && !el.contains(panel),\n )\n for (const el of sealed) el.setAttribute('inert', '')\n return () => {\n for (const el of sealed) el.removeAttribute('inert')\n }\n }, [open, modal])\n\n const onKeyDown = useCallback(\n (event: KeyboardEvent<HTMLElement>) => {\n if (event.key !== 'Escape' || !open) return\n // Stop here rather than letting it climb. An overlay that consumed the\n // key and an ancestor that also closes on it would both act on one press.\n event.stopPropagation()\n dismiss(true)\n },\n [open, dismiss],\n )\n\n // The panel shares the trigger's Escape and, under a modal, also keeps Tab\n // inside itself. Wrapping at the ends is the whole trap the keyboard needs;\n // the `inert` background stops the pointer reaching anything behind it.\n const onPanelKeyDown = useCallback(\n (event: KeyboardEvent<HTMLElement>) => {\n onKeyDown(event)\n if (!modal || event.key !== 'Tab' || event.defaultPrevented) return\n const panel = panelRef.current\n if (!panel) return\n const focusables = focusableWithin(panel)\n if (focusables.length === 0) {\n event.preventDefault()\n return\n }\n const first = focusables[0] as HTMLElement\n const last = focusables[focusables.length - 1] as HTMLElement\n const active = document.activeElement\n if (event.shiftKey && active === first) {\n event.preventDefault()\n last.focus()\n } else if (!event.shiftKey && active === last) {\n event.preventDefault()\n first.focus()\n }\n },\n [onKeyDown, modal],\n )\n\n const setTriggerNode = useCallback((node: HTMLElement | null) => {\n triggerRef.current = node\n }, [])\n\n const setPanelNode = useCallback((node: HTMLElement | null) => {\n panelRef.current = node\n }, [])\n\n return {\n open,\n setOpen,\n toggle: useCallback(() => setOpen(!open), [setOpen, open]),\n close: useCallback(() => dismiss(true), [dismiss]),\n triggerProps: hasTrigger\n ? {\n ref: setTriggerNode,\n // Only when a value was supplied. A disclosure has a trigger and no\n // popup role, so it registers a trigger without announcing one.\n ...(haspopup ? { 'aria-haspopup': haspopup } : {}),\n 'aria-expanded': open,\n // Only while the panel exists. Pointing at an id that is not in the\n // document is a dangling reference an AT has to resolve and cannot.\n 'aria-controls': open ? panelId : undefined,\n onKeyDown,\n }\n : undefined,\n panelProps: {\n ref: setPanelNode,\n id: panelId,\n onKeyDown: onPanelKeyDown,\n },\n }\n}\n"],"names":["FOCUSABLE","focusableWithin","root","useAnchoredOverlay","options","haspopup","hasTrigger","openProp","defaultOpen","onOpenChange","modal","panelId","useId","triggerRef","useRef","panelRef","controlled","uncontrolledOpen","setUncontrolledOpen","useState","open","onOpenChangeRef","restoreOnClose","openedFrom","wasOpen","setOpen","useCallback","next","_a","isInside","node","_b","dismiss","restore","useEffect","active","opener","onFocusOut","event","onPointerDown","panel","sealed","el","onKeyDown","onPanelKeyDown","focusables","first","last","setTriggerNode","setPanelNode"],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ElementType, ReactNode } from 'react';
|
|
2
|
+
import { TopBarProps } from '../TopBar/TopBar';
|
|
3
|
+
import { PageHeaderProps } from '../PageHeader/PageHeader';
|
|
4
|
+
/** One nav row, described as data. The consumer never hands the shell markup. */
|
|
5
|
+
export interface AppShellNavItem {
|
|
6
|
+
/** The row's name. Truncates rather than wrapping; on the rail it collapses to the accessibility tree. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** The leading icon. Decorative -- the label carries the name. On the rail it is all that shows. */
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
/** Marks this row as the current page. Sets `aria-current="page"`. */
|
|
11
|
+
active?: boolean;
|
|
12
|
+
/** Trailing-slot count. Omit to leave the slot empty; it still reserves its width so counts line up. */
|
|
13
|
+
count?: number;
|
|
14
|
+
/** Renders `count` in the primary color instead of muted. */
|
|
15
|
+
countUrgent?: boolean;
|
|
16
|
+
/** Where the row goes. A nav row's destination is data; the shell renders it as a link. */
|
|
17
|
+
href?: string;
|
|
18
|
+
}
|
|
19
|
+
/** One nav section: an optional header over a run of rows. */
|
|
20
|
+
export interface AppShellNavSection {
|
|
21
|
+
/** The section header. Omit for a headerless section (the nav's first section has none). On the rail every header drops. */
|
|
22
|
+
label?: string;
|
|
23
|
+
/** The section's rows, as data. */
|
|
24
|
+
items: AppShellNavItem[];
|
|
25
|
+
}
|
|
26
|
+
export interface AppShellProps {
|
|
27
|
+
/**
|
|
28
|
+
* The product this shell renders for. The consumer names it once, here, and
|
|
29
|
+
* the shell carries it down -- it labels the sidebar and its navigation, and
|
|
30
|
+
* nothing above the nav (the Boost33 wordmark and brand) changes between
|
|
31
|
+
* products. This one prop is the whole of the shell's product-awareness.
|
|
32
|
+
*/
|
|
33
|
+
product: string;
|
|
34
|
+
/** The navigation, as data. No product hardcodes rows; the shell composes them. */
|
|
35
|
+
nav: AppShellNavSection[];
|
|
36
|
+
/** The top bar's content -- the breadcrumb and the shell actions on its right. */
|
|
37
|
+
topBar: TopBarProps;
|
|
38
|
+
/**
|
|
39
|
+
* The page header's content -- the page title, its optional subtitle and
|
|
40
|
+
* trailing controls. Omit it for a screen that has no title: the Wallet's own
|
|
41
|
+
* overview is one, which is why its 9 Sep baseline shows the top bar's
|
|
42
|
+
* hairline sitting directly on the body (B33-750). The header draws no border
|
|
43
|
+
* of its own, so dropping it leaves that hairline in place rather than a gap.
|
|
44
|
+
*/
|
|
45
|
+
pageHeader?: PageHeaderProps;
|
|
46
|
+
/** The Boost33 mark for the wordmark lockup. Passed in, the way Wordmark takes it; on the rail it is all that shows. */
|
|
47
|
+
mark?: ReactNode;
|
|
48
|
+
/**
|
|
49
|
+
* The product switcher, sitting under the wordmark. A slot: the registry it
|
|
50
|
+
* draws is served by Identity (ADR-0003), not owned here. Render its own
|
|
51
|
+
* collapsed form for the rail.
|
|
52
|
+
*/
|
|
53
|
+
switcher?: ReactNode;
|
|
54
|
+
/**
|
|
55
|
+
* The wallet chip, pinned above the footer. A slot: the balance comes from
|
|
56
|
+
* `@boost33/ledger-client`, never read here. Render its own collapsed form
|
|
57
|
+
* for the rail.
|
|
58
|
+
*/
|
|
59
|
+
wallet?: ReactNode;
|
|
60
|
+
/** The sidebar footer, below the wallet chip. A slot -- the product's own. */
|
|
61
|
+
footer?: ReactNode;
|
|
62
|
+
/**
|
|
63
|
+
* The sidebar collapsed to the 56px rail. A state of the shell: the sidebar
|
|
64
|
+
* narrows and every piece it owns collapses off this one flag. The slots
|
|
65
|
+
* (switcher, wallet, footer) render their own collapsed form.
|
|
66
|
+
*/
|
|
67
|
+
collapsed?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The element every linked nav row renders through. Defaults to a plain `a`;
|
|
70
|
+
* a consumer whose router owns navigation passes its own once here and the
|
|
71
|
+
* shell threads it to every row, so the whole nav goes client-side with the
|
|
72
|
+
* locale prefix intact rather than full-page-reloading (B33-847). This is the
|
|
73
|
+
* shell's one nod to how a consumer routes -- it renders the element, never
|
|
74
|
+
* knowing what it is. See docs/adr/0011.
|
|
75
|
+
*/
|
|
76
|
+
linkComponent?: ElementType;
|
|
77
|
+
/** The page body. Sits on white, below the header. */
|
|
78
|
+
children: ReactNode;
|
|
79
|
+
}
|
|
80
|
+
export declare function AppShell({ product, nav, topBar, pageHeader, mark, switcher, wallet, footer, collapsed, linkComponent, children, }: AppShellProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsxs as i, jsx as r } from "react/jsx-runtime";
|
|
2
|
+
import { Sidebar as b } from "../Sidebar/Sidebar.js";
|
|
3
|
+
import { NavSection as x } from "../NavSection/NavSection.js";
|
|
4
|
+
import { NavItem as S } from "../NavItem/NavItem.js";
|
|
5
|
+
import { TopBar as g } from "../TopBar/TopBar.js";
|
|
6
|
+
import { PageHeader as j } from "../PageHeader/PageHeader.js";
|
|
7
|
+
import { Wordmark as k } from "../Wordmark/Wordmark.js";
|
|
8
|
+
import l from "./AppShell.module.css.js";
|
|
9
|
+
function q({
|
|
10
|
+
product: e,
|
|
11
|
+
nav: d,
|
|
12
|
+
topBar: h,
|
|
13
|
+
pageHeader: m,
|
|
14
|
+
mark: v,
|
|
15
|
+
switcher: o,
|
|
16
|
+
wallet: c,
|
|
17
|
+
footer: s,
|
|
18
|
+
collapsed: n = !1,
|
|
19
|
+
linkComponent: f,
|
|
20
|
+
children: p
|
|
21
|
+
}) {
|
|
22
|
+
return /* @__PURE__ */ i("div", { className: l.shell, children: [
|
|
23
|
+
/* @__PURE__ */ i(b, { label: e, collapsed: n, children: [
|
|
24
|
+
/* @__PURE__ */ r("div", { className: l.wordmark, children: /* @__PURE__ */ r(k, { mark: v, collapsed: n }) }),
|
|
25
|
+
o != null && /* @__PURE__ */ r("div", { className: l.switcher, children: o }),
|
|
26
|
+
/* @__PURE__ */ r("nav", { "aria-label": e, className: l.nav, children: d.map((t, u) => (
|
|
27
|
+
// No room for a header at 56px, so on the rail every section renders
|
|
28
|
+
// headerless -- its rows collapse to their icons and line up.
|
|
29
|
+
/* @__PURE__ */ r(x, { label: n ? void 0 : t.label, children: t.items.map((a, N) => /* @__PURE__ */ r(
|
|
30
|
+
S,
|
|
31
|
+
{
|
|
32
|
+
icon: a.icon,
|
|
33
|
+
active: a.active,
|
|
34
|
+
collapsed: n,
|
|
35
|
+
count: a.count,
|
|
36
|
+
countUrgent: a.countUrgent,
|
|
37
|
+
href: a.href,
|
|
38
|
+
linkComponent: f,
|
|
39
|
+
children: a.label
|
|
40
|
+
},
|
|
41
|
+
N
|
|
42
|
+
)) }, u)
|
|
43
|
+
)) }),
|
|
44
|
+
(c != null || s != null) && /* @__PURE__ */ i("div", { className: l.foot, children: [
|
|
45
|
+
c,
|
|
46
|
+
s
|
|
47
|
+
] })
|
|
48
|
+
] }),
|
|
49
|
+
/* @__PURE__ */ i("div", { className: l.main, children: [
|
|
50
|
+
/* @__PURE__ */ r(g, { ...h }),
|
|
51
|
+
m != null && /* @__PURE__ */ r(j, { ...m }),
|
|
52
|
+
/* @__PURE__ */ r("div", { className: l.content, children: p })
|
|
53
|
+
] })
|
|
54
|
+
] });
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
q as AppShell
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=AppShell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AppShell.js","sources":["../../../src/components/AppShell/AppShell.tsx"],"sourcesContent":["import type { ElementType, ReactNode } from 'react'\nimport { Sidebar } from '../Sidebar/Sidebar'\nimport { NavSection } from '../NavSection/NavSection'\nimport { NavItem } from '../NavItem/NavItem'\nimport { TopBar, type TopBarProps } from '../TopBar/TopBar'\nimport { PageHeader, type PageHeaderProps } from '../PageHeader/PageHeader'\nimport { Wordmark } from '../Wordmark/Wordmark'\nimport styles from './AppShell.module.css'\n\n/** One nav row, described as data. The consumer never hands the shell markup. */\nexport interface AppShellNavItem {\n /** The row's name. Truncates rather than wrapping; on the rail it collapses to the accessibility tree. */\n label: string\n /** The leading icon. Decorative -- the label carries the name. On the rail it is all that shows. */\n icon?: ReactNode\n /** Marks this row as the current page. Sets `aria-current=\"page\"`. */\n active?: boolean\n /** Trailing-slot count. Omit to leave the slot empty; it still reserves its width so counts line up. */\n count?: number\n /** Renders `count` in the primary color instead of muted. */\n countUrgent?: boolean\n /** Where the row goes. A nav row's destination is data; the shell renders it as a link. */\n href?: string\n}\n\n/** One nav section: an optional header over a run of rows. */\nexport interface AppShellNavSection {\n /** The section header. Omit for a headerless section (the nav's first section has none). On the rail every header drops. */\n label?: string\n /** The section's rows, as data. */\n items: AppShellNavItem[]\n}\n\nexport interface AppShellProps {\n /**\n * The product this shell renders for. The consumer names it once, here, and\n * the shell carries it down -- it labels the sidebar and its navigation, and\n * nothing above the nav (the Boost33 wordmark and brand) changes between\n * products. This one prop is the whole of the shell's product-awareness.\n */\n product: string\n /** The navigation, as data. No product hardcodes rows; the shell composes them. */\n nav: AppShellNavSection[]\n /** The top bar's content -- the breadcrumb and the shell actions on its right. */\n topBar: TopBarProps\n /**\n * The page header's content -- the page title, its optional subtitle and\n * trailing controls. Omit it for a screen that has no title: the Wallet's own\n * overview is one, which is why its 9 Sep baseline shows the top bar's\n * hairline sitting directly on the body (B33-750). The header draws no border\n * of its own, so dropping it leaves that hairline in place rather than a gap.\n */\n pageHeader?: PageHeaderProps\n /** The Boost33 mark for the wordmark lockup. Passed in, the way Wordmark takes it; on the rail it is all that shows. */\n mark?: ReactNode\n /**\n * The product switcher, sitting under the wordmark. A slot: the registry it\n * draws is served by Identity (ADR-0003), not owned here. Render its own\n * collapsed form for the rail.\n */\n switcher?: ReactNode\n /**\n * The wallet chip, pinned above the footer. A slot: the balance comes from\n * `@boost33/ledger-client`, never read here. Render its own collapsed form\n * for the rail.\n */\n wallet?: ReactNode\n /** The sidebar footer, below the wallet chip. A slot -- the product's own. */\n footer?: ReactNode\n /**\n * The sidebar collapsed to the 56px rail. A state of the shell: the sidebar\n * narrows and every piece it owns collapses off this one flag. The slots\n * (switcher, wallet, footer) render their own collapsed form.\n */\n collapsed?: boolean\n /**\n * The element every linked nav row renders through. Defaults to a plain `a`;\n * a consumer whose router owns navigation passes its own once here and the\n * shell threads it to every row, so the whole nav goes client-side with the\n * locale prefix intact rather than full-page-reloading (B33-847). This is the\n * shell's one nod to how a consumer routes -- it renders the element, never\n * knowing what it is. See docs/adr/0011.\n */\n linkComponent?: ElementType\n /** The page body. Sits on white, below the header. */\n children: ReactNode\n}\n\n// The shell every authenticated Boost33 screen renders inside. It composes the\n// pieces built in the earlier tickets -- sidebar, wordmark, switcher, nav,\n// wallet chip, top bar, page header -- into one thing a product imports instead\n// of building a sidebar. Product-aware only through `product`; everything below\n// that is the product's own data, and nothing above it changes.\nexport function AppShell({\n product,\n nav,\n topBar,\n pageHeader,\n mark,\n switcher,\n wallet,\n footer,\n collapsed = false,\n linkComponent,\n children,\n}: AppShellProps) {\n return (\n <div className={styles.shell}>\n <Sidebar label={product} collapsed={collapsed}>\n <div className={styles.wordmark}>\n <Wordmark mark={mark} collapsed={collapsed} />\n </div>\n {switcher != null && <div className={styles.switcher}>{switcher}</div>}\n <nav aria-label={product} className={styles.nav}>\n {nav.map((section, sectionIndex) => (\n // No room for a header at 56px, so on the rail every section renders\n // headerless -- its rows collapse to their icons and line up.\n <NavSection key={sectionIndex} label={collapsed ? undefined : section.label}>\n {section.items.map((item, itemIndex) => (\n <NavItem\n key={itemIndex}\n icon={item.icon}\n active={item.active}\n collapsed={collapsed}\n count={item.count}\n countUrgent={item.countUrgent}\n href={item.href}\n linkComponent={linkComponent}\n >\n {item.label}\n </NavItem>\n ))}\n </NavSection>\n ))}\n </nav>\n {(wallet != null || footer != null) && (\n <div className={styles.foot}>\n {wallet}\n {footer}\n </div>\n )}\n </Sidebar>\n <div className={styles.main}>\n <TopBar {...topBar} />\n {pageHeader != null && <PageHeader {...pageHeader} />}\n <div className={styles.content}>{children}</div>\n </div>\n </div>\n )\n}\n"],"names":["AppShell","product","nav","topBar","pageHeader","mark","switcher","wallet","footer","collapsed","linkComponent","children","jsxs","styles","Sidebar","jsx","Wordmark","section","sectionIndex","NavSection","item","itemIndex","NavItem","TopBar","PageHeader"],"mappings":";;;;;;;;AA6FO,SAASA,EAAS;AAAA,EACvB,SAAAC;AAAA,EACA,KAAAC;AAAA,EACA,QAAAC;AAAA,EACA,YAAAC;AAAA,EACA,MAAAC;AAAA,EACA,UAAAC;AAAA,EACA,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA,WAAAC,IAAY;AAAA,EACZ,eAAAC;AAAA,EACA,UAAAC;AACF,GAAkB;AAChB,SACE,gBAAAC,EAAC,OAAA,EAAI,WAAWC,EAAO,OACrB,UAAA;AAAA,IAAA,gBAAAD,EAACE,GAAA,EAAQ,OAAOb,GAAS,WAAAQ,GACvB,UAAA;AAAA,MAAA,gBAAAM,EAAC,OAAA,EAAI,WAAWF,EAAO,UACrB,4BAACG,GAAA,EAAS,MAAAX,GAAY,WAAAI,GAAsB,EAAA,CAC9C;AAAA,MACCH,KAAY,QAAQ,gBAAAS,EAAC,SAAI,WAAWF,EAAO,UAAW,UAAAP,GAAS;AAAA,MAChE,gBAAAS,EAAC,OAAA,EAAI,cAAYd,GAAS,WAAWY,EAAO,KACzC,UAAAX,EAAI,IAAI,CAACe,GAASC;AAAA;AAAA;AAAA,QAGjB,gBAAAH,EAACI,GAAA,EAA8B,OAAOV,IAAY,SAAYQ,EAAQ,OACnE,UAAAA,EAAQ,MAAM,IAAI,CAACG,GAAMC,MACxB,gBAAAN;AAAA,UAACO;AAAA,UAAA;AAAA,YAEC,MAAMF,EAAK;AAAA,YACX,QAAQA,EAAK;AAAA,YACb,WAAAX;AAAA,YACA,OAAOW,EAAK;AAAA,YACZ,aAAaA,EAAK;AAAA,YAClB,MAAMA,EAAK;AAAA,YACX,eAAAV;AAAA,YAEC,UAAAU,EAAK;AAAA,UAAA;AAAA,UATDC;AAAA,QAAA,CAWR,KAdcH,CAejB;AAAA,OACD,GACH;AAAA,OACEX,KAAU,QAAQC,KAAU,2BAC3B,OAAA,EAAI,WAAWK,EAAO,MACpB,UAAA;AAAA,QAAAN;AAAA,QACAC;AAAA,MAAA,EAAA,CACH;AAAA,IAAA,GAEJ;AAAA,IACA,gBAAAI,EAAC,OAAA,EAAI,WAAWC,EAAO,MACrB,UAAA;AAAA,MAAA,gBAAAE,EAACQ,GAAA,EAAQ,GAAGpB,GAAQ;AAAA,MACnBC,KAAc,QAAQ,gBAAAW,EAACS,GAAA,EAAY,GAAGpB,EAAA,CAAY;AAAA,MACnD,gBAAAW,EAAC,OAAA,EAAI,WAAWF,EAAO,SAAU,UAAAF,EAAA,CAAS;AAAA,IAAA,EAAA,CAC5C;AAAA,EAAA,GACF;AAEJ;"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const o = "_shell_19o9q_10", t = "_wordmark_19o9q_20", _ = "_switcher_19o9q_26", n = "_foot_19o9q_33", s = "_nav_19o9q_45", c = "_main_19o9q_54", e = "_content_19o9q_65", a = {
|
|
2
|
+
shell: o,
|
|
3
|
+
wordmark: t,
|
|
4
|
+
switcher: _,
|
|
5
|
+
foot: n,
|
|
6
|
+
nav: s,
|
|
7
|
+
main: c,
|
|
8
|
+
content: e
|
|
9
|
+
};
|
|
10
|
+
export {
|
|
11
|
+
e as content,
|
|
12
|
+
a as default,
|
|
13
|
+
n as foot,
|
|
14
|
+
c as main,
|
|
15
|
+
s as nav,
|
|
16
|
+
o as shell,
|
|
17
|
+
_ as switcher,
|
|
18
|
+
t as wordmark
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=AppShell.module.css.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AppShell.module.css.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ElementType } from 'react';
|
|
2
|
+
import { AppSwitcherLabels } from './labels';
|
|
3
|
+
/**
|
|
4
|
+
* One product the switcher can jump to. It is Identity's registry row, supplied
|
|
5
|
+
* by the consuming app (ADR-0003) -- the switcher fetches nothing and hardcodes
|
|
6
|
+
* no list. Every product is rendered, entitled or not: hiding an unowned one
|
|
7
|
+
* means nobody learns the family exists (B33-479).
|
|
8
|
+
*/
|
|
9
|
+
export interface AppSwitcherProduct {
|
|
10
|
+
/** Identity's product slug, which selects the ProductMark artwork. */
|
|
11
|
+
slug: string;
|
|
12
|
+
/** The product's display name, e.g. "Boost Score". Identity's, not the slug. */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Where selecting this product goes -- the product's own URL, never the hub. */
|
|
15
|
+
href: string;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the account holds this product. Defaults to `true`. `false` dims the
|
|
18
|
+
* mark and shows the "Not on plan" tag, but the row stays an ordinary focusable
|
|
19
|
+
* link: entitlement is a rendering hint, and the switcher is not the thing
|
|
20
|
+
* deciding what someone may open (ADR-0010, "Not entitled is not disabled").
|
|
21
|
+
*/
|
|
22
|
+
entitled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** The "all products" row above the divider. Its blurb and URL are the consumer's. */
|
|
25
|
+
export interface AppSwitcherHub {
|
|
26
|
+
/** The row's name, e.g. "Hub". */
|
|
27
|
+
name: string;
|
|
28
|
+
/** The line beneath it, e.g. "All Boost33 products". */
|
|
29
|
+
description: string;
|
|
30
|
+
/** The hub URL. */
|
|
31
|
+
href: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The account/workspace row at the foot of the panel. A row that opens a second
|
|
35
|
+
* panel (a later ticket owns that panel), so it is a button rather than a link;
|
|
36
|
+
* this ticket renders the row and calls `onSelect` when it is chosen.
|
|
37
|
+
*/
|
|
38
|
+
export interface AppSwitcherAccount {
|
|
39
|
+
/** The organisation's name, e.g. "Acme Coaching". */
|
|
40
|
+
name: string;
|
|
41
|
+
/** The avatar's initials, e.g. "AC". */
|
|
42
|
+
initials: string;
|
|
43
|
+
/** The line beneath the name, preformatted, e.g. "Admin · 2 workspaces". */
|
|
44
|
+
description: string;
|
|
45
|
+
/** Chosen -- opens the workspace panel. */
|
|
46
|
+
onSelect?: () => void;
|
|
47
|
+
}
|
|
48
|
+
export interface AppSwitcherProps {
|
|
49
|
+
/** The products, in the order Identity serves them. Rendered whole, entitled or not. */
|
|
50
|
+
products: AppSwitcherProduct[];
|
|
51
|
+
/**
|
|
52
|
+
* The slug of the product this shell is rendering for. It names the trigger
|
|
53
|
+
* and is the row that carries the check -- the product you are in.
|
|
54
|
+
*
|
|
55
|
+
* A slug matching nothing in `products` is a supported state, not a caller
|
|
56
|
+
* error: the registry is Identity's and can run ahead of this package
|
|
57
|
+
* (ADR-0003). The mark degrades to a bare tile and the trigger falls back to
|
|
58
|
+
* `labels.switchProduct` for its name (B33-919).
|
|
59
|
+
*/
|
|
60
|
+
currentSlug: string;
|
|
61
|
+
/** The "all products" row above the products. */
|
|
62
|
+
hub: AppSwitcherHub;
|
|
63
|
+
/** The account/workspace row below the products. */
|
|
64
|
+
account: AppSwitcherAccount;
|
|
65
|
+
/**
|
|
66
|
+
* Every user-facing string that is not row data. Each field defaults to
|
|
67
|
+
* English, and a field left out -- or supplied as `undefined`, which is what
|
|
68
|
+
* a missed next-intl key looks like -- keeps its default (B33-873).
|
|
69
|
+
*/
|
|
70
|
+
labels?: Partial<AppSwitcherLabels>;
|
|
71
|
+
/**
|
|
72
|
+
* The element the product and hub rows render through. Defaults to a plain
|
|
73
|
+
* `a`; a router-owning consumer passes its own so the rows route client-side
|
|
74
|
+
* with the locale prefix intact (ADR-0011, B33-847). The account row stays a
|
|
75
|
+
* button -- it opens a panel, it does not navigate.
|
|
76
|
+
*/
|
|
77
|
+
linkComponent?: ElementType;
|
|
78
|
+
/** The 56px rail. The trigger collapses to the current product's mark alone. */
|
|
79
|
+
collapsed?: boolean;
|
|
80
|
+
/** Render the panel open on first paint -- the specimen's Stage-A open state. */
|
|
81
|
+
defaultOpen?: boolean;
|
|
82
|
+
/** Every open and close, so a consumer can un-clip an ancestor's overflow while the panel overhangs. */
|
|
83
|
+
onOpenChange?: (open: boolean) => void;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The product switcher under the wordmark. It opens, lists every Boost33 product
|
|
87
|
+
* with the current one checked, and takes you straight into the one you pick --
|
|
88
|
+
* the real switcher the Wallet's chevron-that-opens-nothing never was (B33-479).
|
|
89
|
+
*
|
|
90
|
+
* A disclosure, not a menu (ADR-0010): the panel carries no widget role, the
|
|
91
|
+
* rows are the real links and buttons they already are, and Tab is the
|
|
92
|
+
* document's own tab order -- so tabbing through the products does not close the
|
|
93
|
+
* panel on the first row. Dismissal, focus restoration and trigger ARIA are the
|
|
94
|
+
* shared anchored-overlay contract's (ADR-0009); it never becomes a modal.
|
|
95
|
+
*/
|
|
96
|
+
export declare function AppSwitcher({ products, currentSlug, hub, account, labels, linkComponent, collapsed, defaultOpen, onOpenChange, }: AppSwitcherProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as s, jsx as a, Fragment as b } from "react/jsx-runtime";
|
|
3
|
+
import { useAnchoredOverlay as C } from "../../anchored-overlay.js";
|
|
4
|
+
import { ProductMark as u } from "../ProductMark/ProductMark.js";
|
|
5
|
+
import { Chevron as x, HubMark as y, Check as P, ChevronRight as R } from "./icons.js";
|
|
6
|
+
import { resolveAppSwitcherLabels as A } from "./labels.js";
|
|
7
|
+
import e from "./AppSwitcher.module.css.js";
|
|
8
|
+
function E({
|
|
9
|
+
products: N,
|
|
10
|
+
currentSlug: i,
|
|
11
|
+
hub: t,
|
|
12
|
+
account: l,
|
|
13
|
+
labels: v,
|
|
14
|
+
linkComponent: f,
|
|
15
|
+
collapsed: c = !1,
|
|
16
|
+
defaultOpen: w,
|
|
17
|
+
onOpenChange: k
|
|
18
|
+
}) {
|
|
19
|
+
const n = C({ defaultOpen: w, onOpenChange: k }), o = A(v), p = f ?? "a", m = N.find((r) => r.slug === i), g = (m == null ? void 0 : m.name) ?? o.switchProduct;
|
|
20
|
+
return /* @__PURE__ */ s("div", { className: e.root, children: [
|
|
21
|
+
/* @__PURE__ */ s(
|
|
22
|
+
"button",
|
|
23
|
+
{
|
|
24
|
+
type: "button",
|
|
25
|
+
className: c ? e.railTrigger : e.trigger,
|
|
26
|
+
onClick: n.toggle,
|
|
27
|
+
...n.triggerProps,
|
|
28
|
+
children: [
|
|
29
|
+
/* @__PURE__ */ a(u, { slug: i, title: c ? g : void 0 }),
|
|
30
|
+
!c && /* @__PURE__ */ s(b, { children: [
|
|
31
|
+
/* @__PURE__ */ a("span", { className: e.triggerName, children: g }),
|
|
32
|
+
/* @__PURE__ */ a(x, { className: e.chevron })
|
|
33
|
+
] })
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
),
|
|
37
|
+
n.open && /* @__PURE__ */ s("div", { ...n.panelProps, className: e.panel, "aria-label": o.panel, children: [
|
|
38
|
+
/* @__PURE__ */ s(p, { className: e.hubRow, href: t.href, onClick: n.close, children: [
|
|
39
|
+
/* @__PURE__ */ a(y, { className: e.hubMark }),
|
|
40
|
+
/* @__PURE__ */ s("span", { className: e.rowText, children: [
|
|
41
|
+
/* @__PURE__ */ a("span", { className: e.rowName, children: t.name }),
|
|
42
|
+
/* @__PURE__ */ a("span", { className: e.rowSub, children: t.description })
|
|
43
|
+
] })
|
|
44
|
+
] }),
|
|
45
|
+
/* @__PURE__ */ a("hr", { className: e.divider }),
|
|
46
|
+
/* @__PURE__ */ a("ul", { className: e.products, children: N.map((r) => {
|
|
47
|
+
const h = r.slug === i, d = r.entitled === !1;
|
|
48
|
+
return /* @__PURE__ */ a("li", { children: /* @__PURE__ */ s(
|
|
49
|
+
p,
|
|
50
|
+
{
|
|
51
|
+
className: e.productRow,
|
|
52
|
+
href: r.href,
|
|
53
|
+
"aria-current": h ? "true" : void 0,
|
|
54
|
+
"data-current": h ? "true" : void 0,
|
|
55
|
+
"data-entitled": d ? "false" : void 0,
|
|
56
|
+
onClick: n.close,
|
|
57
|
+
children: [
|
|
58
|
+
/* @__PURE__ */ a(u, { slug: r.slug, muted: d }),
|
|
59
|
+
/* @__PURE__ */ a("span", { className: e.rowName, children: r.name }),
|
|
60
|
+
d && /* @__PURE__ */ a("span", { className: e.tag, children: o.notOnPlan }),
|
|
61
|
+
h && /* @__PURE__ */ a(P, { className: e.check })
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
) }, r.slug);
|
|
65
|
+
}) }),
|
|
66
|
+
/* @__PURE__ */ a("hr", { className: e.divider }),
|
|
67
|
+
/* @__PURE__ */ s("button", { type: "button", className: e.accountRow, onClick: l.onSelect, children: [
|
|
68
|
+
/* @__PURE__ */ a("span", { className: e.avatar, "aria-hidden": "true", children: l.initials }),
|
|
69
|
+
/* @__PURE__ */ s("span", { className: e.rowText, children: [
|
|
70
|
+
/* @__PURE__ */ a("span", { className: e.rowName, children: l.name }),
|
|
71
|
+
/* @__PURE__ */ a("span", { className: e.rowSub, children: l.description })
|
|
72
|
+
] }),
|
|
73
|
+
/* @__PURE__ */ a(R, { className: e.chevronRight })
|
|
74
|
+
] })
|
|
75
|
+
] })
|
|
76
|
+
] });
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
E as AppSwitcher
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=AppSwitcher.js.map
|