@helloimjolopez-pathway/pathway-components 0.7.1 → 0.8.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/components/badge/badge.jsx +22 -18
- package/components/bottom-sheet/bottom-sheet.jsx +2 -4
- package/components/button/button.jsx +1 -1
- package/components/checkbox/checkbox.jsx +13 -3
- package/components/icon/action-icon.jsx +4 -2
- package/components/module-icon/module-icon.jsx +27 -0
- package/components/nav-shell/nav-shell.jsx +14 -28
- package/components/org-switcher/org-switcher.jsx +2 -2
- package/components/radio/radio.jsx +2 -2
- package/components/scroll-to-enable/scroll-to-enable.jsx +1 -1
- package/components/search/search.jsx +4 -0
- package/components/sidenav/accounting-side-nav.jsx +116 -0
- package/components/sidenav/sidenav.jsx +109 -43
- package/components/toggle-button/toggle-button.jsx +3 -2
- package/components/top-nav/top-nav.jsx +33 -22
- package/dist/pathway-accounting-sidenav.js +139 -0
- package/dist/pathway-components.js +78 -71
- package/dist/pathway-sidenav.js +16 -9
- package/package.json +4 -2
|
@@ -15,18 +15,19 @@
|
|
|
15
15
|
* STATIC, v1 to v2. Three v1 values did not match Figma and now do:
|
|
16
16
|
* Outline & Fill stroke v1 Subtle, Figma Strong (Neutral Bold, accents plain).
|
|
17
17
|
* Neutral Naked text v1 Neutral/Bold, Figma Neutral/Base.
|
|
18
|
-
* Stroke placement
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Stroke placement Inside the badge, which keeps its size in every style
|
|
19
|
+
* (Small 20, Medium and Large 22). An inset box-shadow
|
|
20
|
+
* draws it without changing the box.
|
|
21
21
|
* Figma's default leading icon is the placeholder `error`; in code the icon is
|
|
22
22
|
* passed explicitly, so existing call sites render exactly as before.
|
|
23
23
|
*
|
|
24
24
|
* DISMISSIBLE and INTERACTIVE, every size, with or without icons:
|
|
25
25
|
* outer frame invisible, min width and height Accessibility/Touch Target/AA
|
|
26
26
|
* (44). The pill keeps its size (20 / 22 / 22) inside it.
|
|
27
|
-
* Focus Ring wraps
|
|
28
|
-
*
|
|
29
|
-
* Container.Main
|
|
27
|
+
* Focus Ring wraps Container.Main, padding FocusRing/Offset, ring outside
|
|
28
|
+
* when Focused.
|
|
29
|
+
* Container.Main the badge: fill, stroke inside, CornerRadius/Large;
|
|
30
|
+
* Padding/XXXTight left and right, Padding/XXXXTight top and
|
|
30
31
|
* bottom, NO gap.
|
|
31
32
|
* Container.Label Padding/XXXTight left and right; Field Label to value
|
|
32
33
|
* Gap/XXTight.
|
|
@@ -99,7 +100,7 @@ const ACTION = (s) => {
|
|
|
99
100
|
};
|
|
100
101
|
|
|
101
102
|
export const BADGE_SIZE_SPEC = {
|
|
102
|
-
Small: { fontSize: "font-size-
|
|
103
|
+
Small: { fontSize: "font-size-xs", lineHeight: "line-height-xs-single", icon: 11, ico: 12, contentMax: 200, height: 20 },
|
|
103
104
|
Medium: { fontSize: "font-size-xs", lineHeight: "line-height-xs-single", icon: 12, ico: 14, contentMax: 196 },
|
|
104
105
|
Large: { fontSize: "font-size-s", lineHeight: "line-height-xs-single", icon: 14, ico: 16, contentMax: 192 },
|
|
105
106
|
};
|
|
@@ -139,13 +140,15 @@ const cut = { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSp
|
|
|
139
140
|
const outer = { display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
|
|
140
141
|
minWidth: T.touchW, minHeight: T.touchH, boxSizing: "border-box", padding: 0, margin: 0, border: 0,
|
|
141
142
|
background: "transparent", font: "inherit", color: "inherit", textDecoration: "none", outline: "none" };
|
|
142
|
-
const strokeShadow = (stroke) => (stroke ? `0 0 0 ${L.border} ${stroke}` : "none");
|
|
143
|
-
/* Focus ring: FocusRing/Width of Stroke/FocusRing/Base, FocusRing/Offset
|
|
144
|
-
* border, as an outline so the gap shows whatever surface the badge sits
|
|
145
|
-
*
|
|
146
|
-
const focusRing = (focused
|
|
147
|
-
? { outline: `${T.ringWidth} solid ${T.ring}`, outlineOffset:
|
|
143
|
+
const strokeShadow = (stroke) => (stroke ? `inset 0 0 0 ${L.border} ${stroke}` : "none");
|
|
144
|
+
/* Focus ring: FocusRing/Width of Stroke/FocusRing/Base, FocusRing/Offset outside the pill
|
|
145
|
+
* (its border is inside it), as an outline so the gap shows whatever surface the badge sits
|
|
146
|
+
* on. No white halo. */
|
|
147
|
+
const focusRing = (focused) => (focused
|
|
148
|
+
? { outline: `${T.ringWidth} solid ${T.ring}`, outlineOffset: T.ringOffset }
|
|
148
149
|
: { outline: "none" });
|
|
150
|
+
/* Small has a fixed height (20) with its 12 text centred, so it needs no top and bottom padding. */
|
|
151
|
+
const fixedHeight = (spec) => (spec.height ? { height: spec.height, paddingTop: 0, paddingBottom: 0 } : {});
|
|
149
152
|
const transition = `background ${T.duration} ${T.easing}, color ${T.duration} ${T.easing}, box-shadow ${T.duration} ${T.easing}`;
|
|
150
153
|
|
|
151
154
|
/**
|
|
@@ -214,9 +217,10 @@ export function Badge({
|
|
|
214
217
|
style={{
|
|
215
218
|
display: "inline-flex", alignItems: "center", boxSizing: "border-box", gap: L.gap,
|
|
216
219
|
paddingTop: L.padTop, paddingRight: L.padRight, paddingBottom: L.padBottom, paddingLeft: L.padLeft,
|
|
220
|
+
...fixedHeight(spec),
|
|
217
221
|
borderRadius: L.radius,
|
|
218
222
|
background: painted.fill ? C(fam.fill) : "transparent",
|
|
219
|
-
boxShadow: stroke ? `0 0 0 ${L.border} ${stroke}` : "none",
|
|
223
|
+
boxShadow: stroke ? `inset 0 0 0 ${L.border} ${stroke}` : "none",
|
|
220
224
|
color: C(badgeStyle === "Naked" && fam.textNaked ? fam.textNaked : fam.text),
|
|
221
225
|
...font(spec), ...style,
|
|
222
226
|
}}
|
|
@@ -232,7 +236,7 @@ export function Badge({
|
|
|
232
236
|
display: "inline-flex", alignItems: "center", boxSizing: "border-box", gap: 0,
|
|
233
237
|
paddingTop: L.padTop, paddingBottom: L.padBottom, paddingLeft: T.pill, paddingRight: T.pill,
|
|
234
238
|
borderRadius: L.radius, maxWidth: showFieldLabel ? "none" : T.badgeMax, minWidth: 0,
|
|
235
|
-
transition, ...font(spec),
|
|
239
|
+
transition, ...font(spec), ...fixedHeight(spec),
|
|
236
240
|
};
|
|
237
241
|
const textSlot = { display: "inline-flex", minWidth: 0, paddingLeft: T.text, paddingRight: T.text };
|
|
238
242
|
const ico = (name, fill = 0) => <Icon name={name} size={spec.ico} fill={fill} style={{ flexShrink: 0 }} />;
|
|
@@ -252,12 +256,12 @@ export function Badge({
|
|
|
252
256
|
...pillBase,
|
|
253
257
|
background: painted.fill ? C(fam.fill) : "transparent",
|
|
254
258
|
color: C(badgeStyle === "Naked" && fam.textNaked ? fam.textNaked : fam.text),
|
|
255
|
-
boxShadow: strokeShadow(stroke), ...focusRing(focused
|
|
259
|
+
boxShadow: strokeShadow(stroke), ...focusRing(focused),
|
|
256
260
|
}}>
|
|
257
261
|
{leadingIcon && ico(leadingIcon, 1)}
|
|
258
262
|
<span className="pw-badge__label" style={{ ...textSlot, gap: T.fieldGap }}>
|
|
259
263
|
{showFieldLabel && fieldLabel && <span style={{ ...cut, maxWidth: T.fieldMax }}>{fieldLabel}</span>}
|
|
260
|
-
<span style={{ ...cut, maxWidth: spec.contentMax
|
|
264
|
+
<span style={{ ...cut, maxWidth: spec.contentMax }}>{label}</span>
|
|
261
265
|
</span>
|
|
262
266
|
<button type="button" className="pw-badge__remove" aria-label={`Remove ${name}`}
|
|
263
267
|
aria-disabled={xOff || undefined}
|
|
@@ -320,7 +324,7 @@ export function Badge({
|
|
|
320
324
|
style={{ ...outer, cursor: disabled ? "not-allowed" : "pointer", ...style }}>
|
|
321
325
|
<span className="pw-badge__pill" style={{
|
|
322
326
|
...pillBase, background: fill, color: fg,
|
|
323
|
-
boxShadow: strokeShadow(stroke), ...focusRing(focused
|
|
327
|
+
boxShadow: strokeShadow(stroke), ...focusRing(focused),
|
|
324
328
|
}}>
|
|
325
329
|
{lead && ico(lead, on ? 0 : 1)}
|
|
326
330
|
<span className="pw-badge__label" style={textSlot}><span style={{ ...cut, display: "block", maxWidth: spec.contentMax }}>{label}</span></span>
|
|
@@ -64,10 +64,8 @@ export const T = {
|
|
|
64
64
|
handle: C("fill-static-neutral-strong"),
|
|
65
65
|
title: C("foreground-static-neutral-strong"),
|
|
66
66
|
footerBorder: C("stroke-static-neutral-faint"),
|
|
67
|
-
/* Figma's
|
|
68
|
-
|
|
69
|
-
token. --elevation-sheet is the nearest token that exists, the same
|
|
70
|
-
choice Dropdown made for PopoverMenu's shadow. Kept by Jo, 2026-11-10. */
|
|
67
|
+
/* Elevation/Sheet, the shadow Figma's BottomSheet component now uses. The
|
|
68
|
+
old one-off BottomSheet effect style was retired on 2026-11-11. */
|
|
71
69
|
shadow: "var(--elevation-sheet)",
|
|
72
70
|
};
|
|
73
71
|
|
|
@@ -147,7 +147,7 @@ export const T = {
|
|
|
147
147
|
// the canvas but only 1.69:1 against the Primary fill, so a ring drawn on the
|
|
148
148
|
// fill would fail WCAG 2.4.11. The offset puts it on the surface instead.
|
|
149
149
|
focusOutline: `${CL("focusring-width")} solid ${SC("stroke-focusring-base")}`,
|
|
150
|
-
focusOffset:
|
|
150
|
+
focusOffset: "0",
|
|
151
151
|
};
|
|
152
152
|
|
|
153
153
|
// Type is a scale now, not 111 composites, so each size composes its own five
|
|
@@ -19,6 +19,10 @@ import { useReducedMotion } from "../_shared/use-reduced-motion.jsx";
|
|
|
19
19
|
// Note: some tokens in §5.3 of the spec are missing from the token file.
|
|
20
20
|
// Fallbacks are used where noted and will be updated once Figma is re-synced.
|
|
21
21
|
const V = {
|
|
22
|
+
// Keyboard focus ring
|
|
23
|
+
focusOutline: "var(--contextual-layout-units-focusring-width) solid var(--semantic-color-stroke-focusring-base)",
|
|
24
|
+
focusOffset: "0",
|
|
25
|
+
|
|
22
26
|
// Box fill, standard checked / indeterminate
|
|
23
27
|
fillCheckedBase: "var(--semantic-color-fill-action-primary-strong-rest)",
|
|
24
28
|
fillCheckedHover: "var(--semantic-color-fill-action-primary-strong-hover)",
|
|
@@ -198,6 +202,7 @@ export function Checkbox({
|
|
|
198
202
|
const inputRef = useRef(null);
|
|
199
203
|
const [hovered, setHovered] = useState(false);
|
|
200
204
|
const [focused, setFocused] = useState(false);
|
|
205
|
+
const [focusVisible, setFocusVisible] = useState(false);
|
|
201
206
|
const [pressed, setPressed] = useState(false);
|
|
202
207
|
|
|
203
208
|
// The indeterminate state can only be set on the DOM element: not via HTML.
|
|
@@ -331,8 +336,8 @@ export function Checkbox({
|
|
|
331
336
|
disabled={disabled}
|
|
332
337
|
aria-describedby={describedBy}
|
|
333
338
|
onChange={e => onChange && onChange(e.target.checked)}
|
|
334
|
-
onFocus={
|
|
335
|
-
onBlur={() => setFocused(false)}
|
|
339
|
+
onFocus={e => { setFocused(true); setFocusVisible(e.target.matches(":focus-visible")); }}
|
|
340
|
+
onBlur={() => { setFocused(false); setFocusVisible(false); }}
|
|
336
341
|
style={{
|
|
337
342
|
position: "absolute",
|
|
338
343
|
width: boxSize,
|
|
@@ -355,9 +360,14 @@ export function Checkbox({
|
|
|
355
360
|
width: boxSize,
|
|
356
361
|
height: boxSize,
|
|
357
362
|
borderRadius: V.radius,
|
|
358
|
-
border
|
|
363
|
+
// In Figma the Focused ring takes the border's place.
|
|
364
|
+
border: `${BORDER_WIDTH}px solid ${focusVisible && !disabled ? "transparent" : borderColor}`,
|
|
359
365
|
backgroundColor: boxFill,
|
|
360
366
|
boxSizing: "border-box",
|
|
367
|
+
// Keyboard focus ring, the same stroke ring every Pathway control uses:
|
|
368
|
+
// FocusRing/Width of Stroke/FocusRing/Base, FocusRing/Offset outside the box.
|
|
369
|
+
outline: focusVisible && !disabled ? V.focusOutline : "none",
|
|
370
|
+
outlineOffset: V.focusOffset,
|
|
361
371
|
transition: noMotion ? "none" : "background-color var(--motion-duration-2) var(--motion-easing-decelerate), border-color var(--motion-duration-2) var(--motion-easing-decelerate)",
|
|
362
372
|
flexShrink: 0,
|
|
363
373
|
}}
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
import React, { useState } from "react";
|
|
47
47
|
import { Icon } from "./icon.jsx";
|
|
48
48
|
import { useReducedMotion } from "../_shared/use-reduced-motion.jsx";
|
|
49
|
+
import { FOCUS_RING_LAYER, injectFocusRing } from "../_shared/focus-ring.js";
|
|
50
|
+
injectFocusRing();
|
|
49
51
|
|
|
50
52
|
const C = (n) => `var(--semantic-color-${n})`;
|
|
51
53
|
const U = (n) => `var(--semantic-layout-units-${n})`;
|
|
@@ -130,7 +132,7 @@ export function ActionIcon({
|
|
|
130
132
|
onMouseDown={() => setPress(true)}
|
|
131
133
|
onMouseUp={() => setPress(false)}
|
|
132
134
|
onBlur={() => setPress(false)}
|
|
133
|
-
className={`pw-action-icon ${className}`}
|
|
135
|
+
className={`pw-action-icon ${FOCUS_RING_LAYER} ${className}`}
|
|
134
136
|
data-size={size}
|
|
135
137
|
style={{
|
|
136
138
|
display: "inline-flex", alignItems: "center", justifyContent: "center",
|
|
@@ -147,7 +149,7 @@ export function ActionIcon({
|
|
|
147
149
|
The fill belongs on this box, not on the button: painting the target
|
|
148
150
|
gives a hover area visibly larger than the one in the design, which
|
|
149
151
|
is the bug the Widget header shipped with once. */}
|
|
150
|
-
<span style={{
|
|
152
|
+
<span data-pw-ring-target="" style={{
|
|
151
153
|
display: "inline-flex", alignItems: "center", justifyContent: "center",
|
|
152
154
|
width: s.layer, height: s.layer, borderRadius: U("cornerradius-base"),
|
|
153
155
|
background: press ? st.layerPress : hov ? st.layerHover : "transparent",
|
|
@@ -384,4 +384,31 @@ export function ModuleIcon({
|
|
|
384
384
|
);
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Module.Mark (Figma): the module's mark in a 22 tinted box beside its name.
|
|
389
|
+
* Used in the SideNav header of a module shell (SideNav.Local.Accounting,
|
|
390
|
+
* read 2026-11-12).
|
|
391
|
+
* box 22, CornerRadius/Small, Fill/Static/Brand/Faint
|
|
392
|
+
* glyph ModuleIcon at 16, mono, Fill/Static/Brand/Strongest
|
|
393
|
+
* label Text/Body/Small/Semibold: 14 / 600 / 20 / 0.3 (the repo's `spacious`),
|
|
394
|
+
* Fill/Static/Brand/Strongest, gap Gap/XXTight
|
|
395
|
+
*/
|
|
396
|
+
export function ModuleMark({ module, label }) {
|
|
397
|
+
const ink = "var(--semantic-color-fill-static-brand-strongest)";
|
|
398
|
+
return (
|
|
399
|
+
<span style={{ display: "inline-flex", alignItems: "center", minWidth: 0,
|
|
400
|
+
gap: "var(--semantic-layout-units-gap-xxtight)", paddingRight: "var(--semantic-layout-units-padding-xxxtight)" }}>
|
|
401
|
+
<span aria-hidden="true" style={{ width: 22, height: 22, flexShrink: 0, display: "inline-flex",
|
|
402
|
+
alignItems: "center", justifyContent: "center",
|
|
403
|
+
borderRadius: "var(--semantic-layout-units-cornerradius-small)",
|
|
404
|
+
background: "var(--semantic-color-fill-static-brand-faint)", color: ink }}>
|
|
405
|
+
<ModuleIcon module={module} size={16} color="mono" />
|
|
406
|
+
</span>
|
|
407
|
+
<span style={{ fontFamily: "var(--semantic-type-family-brand)", fontWeight: "var(--semantic-type-weight-semibold)",
|
|
408
|
+
fontSize: "var(--semantic-type-font-size-s)", lineHeight: "var(--semantic-type-line-height-s-single)",
|
|
409
|
+
letterSpacing: "var(--semantic-type-letter-spacing-spacious)", color: ink, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{label}</span>
|
|
410
|
+
</span>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
387
414
|
export default ModuleIcon;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import React, { useState, useEffect, useCallback } from "react";
|
|
14
14
|
import TopNav, { DEFAULT_MODULES } from "../top-nav/top-nav.jsx";
|
|
15
15
|
import { SideNav } from "../sidenav/sidenav.jsx";
|
|
16
|
+
import { ModuleMark } from "../module-icon/module-icon.jsx";
|
|
16
17
|
|
|
17
18
|
// ─── CONSTANTS ────────────────────────────────────────────────────────────────
|
|
18
19
|
const TOPNAV_H = 60; // px — spec §9.3 rule 1
|
|
@@ -75,33 +76,9 @@ function useBreakpoint() {
|
|
|
75
76
|
* defaultCollapsed boolean — initial desktop collapse state (default false)
|
|
76
77
|
* children ReactNode — page content (renders inside Shell.Main)
|
|
77
78
|
*/
|
|
78
|
-
// The Accounting module's apps
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
// Financials Home's nav is Figma's SideNav.Local.Accounting (40016737:141666),
|
|
83
|
-
// icons included. Figma draws no other app's nav, so Accounts Payable,
|
|
84
|
-
// Accounts Receivable and Bank Reconciliation use the item lists the widget
|
|
85
|
-
// demo read from production (Bank Reconciliation takes Bank Account
|
|
86
|
-
// Management's), on the same Figma icons.
|
|
87
|
-
const accountingNav = (app, labels) => labels.map((l) => ({ id: `${app}-${l.toLowerCase().replace(/\s+/g, "-")}`, label: l, icon: ACCOUNTING_ICONS[l] }));
|
|
88
|
-
const ACCOUNTING_ICONS = {
|
|
89
|
-
Dashboard: "space_dashboard", Enter: "input", Manage: "bookmark_manager", View: "visibility",
|
|
90
|
-
Reports: "insert_chart", Modify: "edit_square", Help: "help", Utilities: "tools_wrench",
|
|
91
|
-
"Security Setup": "security",
|
|
92
|
-
};
|
|
93
|
-
const APP_NAV_STANDARD = ["Dashboard", "Enter", "Manage", "View", "Reports", "Modify", "Utilities", "Help"];
|
|
94
|
-
export const ACCOUNTING_APP_SWITCHER = {
|
|
95
|
-
label: "Financials Home",
|
|
96
|
-
icon: "apps",
|
|
97
|
-
home: { id: "financials-home", label: "Financials Home",
|
|
98
|
-
navItems: accountingNav("fh", ["Dashboard", "Enter", "Manage", "View", "Reports", "Modify", "Help", "Utilities", "Security Setup"]) },
|
|
99
|
-
apps: [
|
|
100
|
-
{ id: "accounts-payable", label: "Accounts Payable", navItems: accountingNav("ap", APP_NAV_STANDARD) },
|
|
101
|
-
{ id: "accounts-receivable", label: "Accounts Receivable", navItems: accountingNav("ar", APP_NAV_STANDARD) },
|
|
102
|
-
{ id: "bank-reconciliation", label: "Bank Reconciliation", navItems: accountingNav("br", APP_NAV_STANDARD) },
|
|
103
|
-
],
|
|
104
|
-
};
|
|
79
|
+
// The Accounting module's apps and their navs live with the Accounting side
|
|
80
|
+
// nav, which also ships on its own as <pathway-accounting-sidenav>.
|
|
81
|
+
export { ACCOUNTING_APP_SWITCHER } from "../sidenav/accounting-side-nav.jsx";
|
|
105
82
|
|
|
106
83
|
export function NavShell({
|
|
107
84
|
// TopNav
|
|
@@ -112,6 +89,10 @@ export function NavShell({
|
|
|
112
89
|
user,
|
|
113
90
|
onModuleSelect,
|
|
114
91
|
onOrgSelect,
|
|
92
|
+
// TopNav Slot.RowEnd. Undefined follows the module: an appSwitcher may carry
|
|
93
|
+
// `topNav: { showSearch, showActions }` (Accounting turns both off).
|
|
94
|
+
showSearch,
|
|
95
|
+
showActions,
|
|
115
96
|
|
|
116
97
|
// SideNav
|
|
117
98
|
navItems,
|
|
@@ -256,6 +237,8 @@ export function NavShell({
|
|
|
256
237
|
onModuleSelect={onModuleSelect}
|
|
257
238
|
onOrgSelect={onOrgSelect}
|
|
258
239
|
onSideNavToggle={handleSideNavToggle}
|
|
240
|
+
showSearch={showSearch ?? (appSwitcher && appSwitcher.topNav && appSwitcher.topNav.showSearch) ?? true}
|
|
241
|
+
showActions={showActions ?? (appSwitcher && appSwitcher.topNav && appSwitcher.topNav.showActions) ?? true}
|
|
259
242
|
/>
|
|
260
243
|
</div>
|
|
261
244
|
|
|
@@ -309,7 +292,10 @@ export function NavShell({
|
|
|
309
292
|
hideCollapseButton={isTablet || isMobile}
|
|
310
293
|
headerEnd={navHeaderEnd}
|
|
311
294
|
footer={navFooter}
|
|
312
|
-
|
|
295
|
+
headerStart={appSwitcher && appSwitcher.moduleMark ? <ModuleMark {...appSwitcher.moduleMark} /> : null}
|
|
296
|
+
appSwitcher={appSwitcher ? { ...appSwitcher, activeId: appActive, onSelect: handleAppSelect,
|
|
297
|
+
// Mobile opens the menu as a BottomSheet (Figma 40016737:141824).
|
|
298
|
+
presentation: isMobile ? "sheet" : "popover" } : null}
|
|
313
299
|
>
|
|
314
300
|
{navChildren}
|
|
315
301
|
</SideNav>
|
|
@@ -310,8 +310,8 @@ export function OrgSwitcher({
|
|
|
310
310
|
onFocus={e => {
|
|
311
311
|
// Only show focus ring on keyboard navigation, not on mouse click
|
|
312
312
|
if (e.target.matches?.(':focus-visible')) {
|
|
313
|
-
e.currentTarget.style.outline =
|
|
314
|
-
e.currentTarget.style.outlineOffset = "
|
|
313
|
+
e.currentTarget.style.outline = "var(--contextual-layout-units-focusring-width) solid var(--semantic-color-stroke-focusring-base)";
|
|
314
|
+
e.currentTarget.style.outlineOffset = "0";
|
|
315
315
|
}
|
|
316
316
|
}}
|
|
317
317
|
onBlur={e => (e.currentTarget.style.outline = "none")}
|
|
@@ -171,8 +171,8 @@ const CSS = `
|
|
|
171
171
|
.pw-radio[data-disabled] .pw-radio__dot{background:${T.dotDisabled}}
|
|
172
172
|
.pw-radio[data-disabled] .pw-radio__label{color:${T.labelDisabled}}
|
|
173
173
|
|
|
174
|
-
.pw-radio:is([data-focus-visible],[data-force=Focused]) .pw-radio__mark{background:${T.focusFill};
|
|
175
|
-
outline:${CL("focusring-width")} solid ${T.focusRing};outline-offset
|
|
174
|
+
.pw-radio:is([data-focus-visible],[data-force=Focused]) .pw-radio__mark{background:${T.focusFill};border-color:transparent;
|
|
175
|
+
outline:${CL("focusring-width")} solid ${T.focusRing};outline-offset:0}
|
|
176
176
|
|
|
177
177
|
.pw-radio-mark{display:inline-grid;place-items:center;width:${L.target};height:${L.targetH};flex:none}
|
|
178
178
|
|
|
@@ -80,7 +80,7 @@ export const T = {
|
|
|
80
80
|
messageRadius: SL("cornerradius-small"),
|
|
81
81
|
sheetRadius: RL("sheet-corner-radius-cornerradius"),
|
|
82
82
|
focusWidth: CL("focusring-width"),
|
|
83
|
-
focusOffset:
|
|
83
|
+
focusOffset: "0",
|
|
84
84
|
// Spacing
|
|
85
85
|
sectionGap: SL("gap-medium"),
|
|
86
86
|
progressGap: SL("gap-xtight"),
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
|
|
29
29
|
import React, { useState, useEffect, useRef } from "react";
|
|
30
30
|
import { Icon } from "../icon/icon.jsx";
|
|
31
|
+
import { FOCUS_RING, injectFocusRing } from "../_shared/focus-ring.js";
|
|
32
|
+
|
|
33
|
+
injectFocusRing();
|
|
31
34
|
|
|
32
35
|
// ─── DESIGN TOKENS ─────────────────────────────────────────────────────────────
|
|
33
36
|
// Every value is a semantic CSS variable. Fallbacks are the resolved values from
|
|
@@ -575,6 +578,7 @@ export function TopNavSearch({
|
|
|
575
578
|
(Figma node 40006967:20917, Container.Icon h32 w32 rounded-full). */}
|
|
576
579
|
<button
|
|
577
580
|
ref={collapsedBtnRef}
|
|
581
|
+
className={FOCUS_RING}
|
|
578
582
|
type="button"
|
|
579
583
|
aria-label="Open search"
|
|
580
584
|
aria-expanded={expanded}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AccountingSideNav: the Accounting module's side nav as one component.
|
|
3
|
+
*
|
|
4
|
+
* Figma: SideNav.Local.Accounting (40016737:141666) with Accounting.AppSwitcher
|
|
5
|
+
* (40018575:65691) as the first entry of its menu, and the Financials
|
|
6
|
+
* Module.Mark in the header.
|
|
7
|
+
*
|
|
8
|
+
* It is the same SideNav that NavShell renders for Accounting, packaged so a
|
|
9
|
+
* host that owns its own top bar and page frame can use the Accounting nav on
|
|
10
|
+
* its own (as <pathway-accounting-sidenav>, see pathway-accounting-sidenav.js).
|
|
11
|
+
* NavShell keeps composing TopNav + SideNav itself; this does not replace it.
|
|
12
|
+
*
|
|
13
|
+
* Behaviour, as in NavShell:
|
|
14
|
+
* - the switcher names the current application;
|
|
15
|
+
* - choosing one swaps the nav to that application's items and lands on its
|
|
16
|
+
* first item (its Dashboard);
|
|
17
|
+
* - below 768 the switcher opens the Financial Applications BottomSheet,
|
|
18
|
+
* from 768 up the popover beside the nav.
|
|
19
|
+
*
|
|
20
|
+
* Props:
|
|
21
|
+
* activeAppId / defaultAppId controlled / initial application id
|
|
22
|
+
* onAppSelect(id) an application was chosen
|
|
23
|
+
* activeId / onNavigate(id) the active nav item, as on SideNav
|
|
24
|
+
* presentation "auto" (default), "popover" or "sheet"
|
|
25
|
+
* appSwitcher override the module data (same shape as
|
|
26
|
+
* ACCOUNTING_APP_SWITCHER)
|
|
27
|
+
* ...rest passed to SideNav (collapsed, onCollapseChange,
|
|
28
|
+
* hideCollapseButton, footer, headerEnd, ...)
|
|
29
|
+
*/
|
|
30
|
+
import React, { useEffect, useState } from "react";
|
|
31
|
+
import { SideNav } from "./sidenav.jsx";
|
|
32
|
+
import { ModuleMark } from "../module-icon/module-icon.jsx";
|
|
33
|
+
|
|
34
|
+
// The Accounting module's applications, each with its own side nav. NavShell's
|
|
35
|
+
// `app-switcher` and <pathway-accounting-sidenav> fall back to this when no
|
|
36
|
+
// switcher data has been set.
|
|
37
|
+
//
|
|
38
|
+
// Financials Home's nav is Figma's SideNav.Local.Accounting (40016737:141666),
|
|
39
|
+
// icons included, with Dashboard first. Figma draws no other application's
|
|
40
|
+
// nav, so the others use the item lists read from production, on the same
|
|
41
|
+
// Figma icons.
|
|
42
|
+
const accountingNav = (app, labels) => labels.map((l) => ({ id: `${app}-${l.toLowerCase().replace(/\s+/g, "-")}`, label: l, icon: ACCOUNTING_ICONS[l] }));
|
|
43
|
+
const ACCOUNTING_ICONS = {
|
|
44
|
+
Dashboard: "space_dashboard", Enter: "input", Manage: "bookmark_manager", View: "visibility",
|
|
45
|
+
Reports: "insert_chart", Modify: "edit_square", Help: "help", Utilities: "tools_wrench",
|
|
46
|
+
"Security Setup": "security",
|
|
47
|
+
};
|
|
48
|
+
const APP_NAV_STANDARD = ["Dashboard", "Enter", "Manage", "View", "Reports", "Modify", "Utilities", "Help"];
|
|
49
|
+
export const ACCOUNTING_APP_SWITCHER = {
|
|
50
|
+
label: "Financials Home",
|
|
51
|
+
icon: "apps",
|
|
52
|
+
// The module's mark in the SideNav header (Figma Module.Mark, Accounting).
|
|
53
|
+
moduleMark: { module: "accounting", label: "Financials" },
|
|
54
|
+
// The Accounting TopNav is TopNav.Global with Search and Actions hidden
|
|
55
|
+
// (Figma 40007879:18072): module switcher, org switcher and profile only.
|
|
56
|
+
topNav: { showSearch: false, showActions: false },
|
|
57
|
+
// Title of the mobile BottomSheet (Figma "Financial Applications").
|
|
58
|
+
sheetTitle: "Financial Applications",
|
|
59
|
+
home: { id: "financials-home", label: "Financials Home",
|
|
60
|
+
navItems: accountingNav("fh", ["Dashboard", "Enter", "Manage", "View", "Reports", "Modify", "Help", "Utilities", "Security Setup"]) },
|
|
61
|
+
apps: [
|
|
62
|
+
{ id: "accounts-payable", label: "Accounts Payable", navItems: accountingNav("ap", APP_NAV_STANDARD) },
|
|
63
|
+
{ id: "accounts-receivable", label: "Accounts Receivable", navItems: accountingNav("ar", APP_NAV_STANDARD) },
|
|
64
|
+
{ id: "general-ledger", label: "General Ledger", navItems: accountingNav("gl", APP_NAV_STANDARD) },
|
|
65
|
+
{ id: "fixed-assets", label: "Fixed Assets", navItems: accountingNav("fa", ["Dashboard", "Manage", "Reports", "Modify", "Help"]) },
|
|
66
|
+
{ id: "purchasing-management", label: "Purchasing Management", navItems: accountingNav("pm", ["Dashboard", "Enter", "Manage", "Reports", "Modify", "Help"]) },
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
const MOBILE_MAX = 767; // below 768, as NavShell's breakpoint
|
|
70
|
+
|
|
71
|
+
function useIsMobile() {
|
|
72
|
+
const get = () => typeof window !== "undefined" && window.innerWidth <= MOBILE_MAX;
|
|
73
|
+
const [m, setM] = useState(get);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const on = () => setM(get());
|
|
76
|
+
window.addEventListener("resize", on);
|
|
77
|
+
return () => window.removeEventListener("resize", on);
|
|
78
|
+
}, []);
|
|
79
|
+
return m;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function AccountingSideNav({
|
|
83
|
+
appSwitcher = ACCOUNTING_APP_SWITCHER,
|
|
84
|
+
activeAppId, defaultAppId, onAppSelect,
|
|
85
|
+
activeId, onNavigate,
|
|
86
|
+
presentation = "auto",
|
|
87
|
+
...rest
|
|
88
|
+
}) {
|
|
89
|
+
const apps = [appSwitcher.home, ...(appSwitcher.apps || [])].filter(Boolean);
|
|
90
|
+
const [ownApp, setOwnApp] = useState(defaultAppId ?? (appSwitcher.home && appSwitcher.home.id));
|
|
91
|
+
const appId = activeAppId ?? ownApp;
|
|
92
|
+
const current = apps.find((a) => a.id === appId) || apps[0];
|
|
93
|
+
const items = (current && current.navItems) || [];
|
|
94
|
+
const [ownActive, setOwnActive] = useState(items[0] && items[0].id);
|
|
95
|
+
const active = activeId ?? ownActive;
|
|
96
|
+
const isMobile = useIsMobile();
|
|
97
|
+
const mode = presentation === "auto" ? (isMobile ? "sheet" : "popover") : presentation;
|
|
98
|
+
|
|
99
|
+
const navigate = (id) => { setOwnActive(id); onNavigate && onNavigate(id); };
|
|
100
|
+
const selectApp = (id) => {
|
|
101
|
+
setOwnApp(id);
|
|
102
|
+
onAppSelect && onAppSelect(id);
|
|
103
|
+
const next = apps.find((a) => a.id === id);
|
|
104
|
+
const first = next && next.navItems && next.navItems[0];
|
|
105
|
+
if (first) navigate(first.id);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<SideNav key={current && current.id} items={items} activeId={active} onNavigate={navigate}
|
|
110
|
+
headerStart={appSwitcher.moduleMark ? <ModuleMark {...appSwitcher.moduleMark} /> : null}
|
|
111
|
+
appSwitcher={{ ...appSwitcher, activeId: current && current.id, onSelect: selectApp, presentation: mode }}
|
|
112
|
+
{...rest} />
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default AccountingSideNav;
|