@helloimjolopez-pathway/pathway-components 0.7.0 → 0.7.2
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 +18 -12
- 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 +25 -0
- package/components/nav-shell/nav-shell.jsx +8 -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 +113 -0
- package/components/sidenav/sidenav.jsx +102 -41
- package/components/toggle-button/toggle-button.jsx +3 -2
- package/components/top-nav/top-nav.jsx +17 -14
- package/dist/pathway-accounting-sidenav.js +139 -0
- package/dist/pathway-components.js +45 -38
- package/dist/pathway-sidenav.js +16 -9
- package/package.json +4 -2
|
@@ -15,17 +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
|
-
*
|
|
28
|
-
*
|
|
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
|
|
29
31
|
* bottom, NO gap.
|
|
30
32
|
* Container.Label Padding/XXXTight left and right; Field Label to value
|
|
31
33
|
* Gap/XXTight.
|
|
@@ -98,7 +100,7 @@ const ACTION = (s) => {
|
|
|
98
100
|
};
|
|
99
101
|
|
|
100
102
|
export const BADGE_SIZE_SPEC = {
|
|
101
|
-
Small: { fontSize: "font-size-
|
|
103
|
+
Small: { fontSize: "font-size-xs", lineHeight: "line-height-xs-single", icon: 11, ico: 12, contentMax: 200, height: 20 },
|
|
102
104
|
Medium: { fontSize: "font-size-xs", lineHeight: "line-height-xs-single", icon: 12, ico: 14, contentMax: 196 },
|
|
103
105
|
Large: { fontSize: "font-size-s", lineHeight: "line-height-xs-single", icon: 14, ico: 16, contentMax: 192 },
|
|
104
106
|
};
|
|
@@ -138,12 +140,15 @@ const cut = { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSp
|
|
|
138
140
|
const outer = { display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
|
|
139
141
|
minWidth: T.touchW, minHeight: T.touchH, boxSizing: "border-box", padding: 0, margin: 0, border: 0,
|
|
140
142
|
background: "transparent", font: "inherit", color: "inherit", textDecoration: "none", outline: "none" };
|
|
141
|
-
const strokeShadow = (stroke) => (stroke ? `0 0 0 ${L.border} ${stroke}` : "none");
|
|
142
|
-
/* Focus ring: FocusRing/Width of Stroke/FocusRing/Base, FocusRing/Offset outside the pill
|
|
143
|
-
* as an outline so the gap shows whatever surface the badge sits
|
|
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. */
|
|
144
147
|
const focusRing = (focused) => (focused
|
|
145
148
|
? { outline: `${T.ringWidth} solid ${T.ring}`, outlineOffset: T.ringOffset }
|
|
146
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 } : {});
|
|
147
152
|
const transition = `background ${T.duration} ${T.easing}, color ${T.duration} ${T.easing}, box-shadow ${T.duration} ${T.easing}`;
|
|
148
153
|
|
|
149
154
|
/**
|
|
@@ -212,9 +217,10 @@ export function Badge({
|
|
|
212
217
|
style={{
|
|
213
218
|
display: "inline-flex", alignItems: "center", boxSizing: "border-box", gap: L.gap,
|
|
214
219
|
paddingTop: L.padTop, paddingRight: L.padRight, paddingBottom: L.padBottom, paddingLeft: L.padLeft,
|
|
220
|
+
...fixedHeight(spec),
|
|
215
221
|
borderRadius: L.radius,
|
|
216
222
|
background: painted.fill ? C(fam.fill) : "transparent",
|
|
217
|
-
boxShadow: stroke ? `0 0 0 ${L.border} ${stroke}` : "none",
|
|
223
|
+
boxShadow: stroke ? `inset 0 0 0 ${L.border} ${stroke}` : "none",
|
|
218
224
|
color: C(badgeStyle === "Naked" && fam.textNaked ? fam.textNaked : fam.text),
|
|
219
225
|
...font(spec), ...style,
|
|
220
226
|
}}
|
|
@@ -230,7 +236,7 @@ export function Badge({
|
|
|
230
236
|
display: "inline-flex", alignItems: "center", boxSizing: "border-box", gap: 0,
|
|
231
237
|
paddingTop: L.padTop, paddingBottom: L.padBottom, paddingLeft: T.pill, paddingRight: T.pill,
|
|
232
238
|
borderRadius: L.radius, maxWidth: showFieldLabel ? "none" : T.badgeMax, minWidth: 0,
|
|
233
|
-
transition, ...font(spec),
|
|
239
|
+
transition, ...font(spec), ...fixedHeight(spec),
|
|
234
240
|
};
|
|
235
241
|
const textSlot = { display: "inline-flex", minWidth: 0, paddingLeft: T.text, paddingRight: T.text };
|
|
236
242
|
const ico = (name, fill = 0) => <Icon name={name} size={spec.ico} fill={fill} style={{ flexShrink: 0 }} />;
|
|
@@ -255,7 +261,7 @@ export function Badge({
|
|
|
255
261
|
{leadingIcon && ico(leadingIcon, 1)}
|
|
256
262
|
<span className="pw-badge__label" style={{ ...textSlot, gap: T.fieldGap }}>
|
|
257
263
|
{showFieldLabel && fieldLabel && <span style={{ ...cut, maxWidth: T.fieldMax }}>{fieldLabel}</span>}
|
|
258
|
-
<span style={{ ...cut, maxWidth: spec.contentMax
|
|
264
|
+
<span style={{ ...cut, maxWidth: spec.contentMax }}>{label}</span>
|
|
259
265
|
</span>
|
|
260
266
|
<button type="button" className="pw-badge__remove" aria-label={`Remove ${name}`}
|
|
261
267
|
aria-disabled={xOff || undefined}
|
|
@@ -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,29 @@ export function ModuleIcon({
|
|
|
384
384
|
);
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Module.Mark, Size=Large, Style=Emphasis (Figma): the module's mark in a 28
|
|
389
|
+
* tinted box beside its name. Used in the SideNav header of a module shell.
|
|
390
|
+
* box 28, CornerRadius/Small, Fill/Static/Brand/Faint
|
|
391
|
+
* glyph ModuleIcon at 20, mono, Fill/Static/Brand/Strongest
|
|
392
|
+
* label 16 / 600, Fill/Static/Brand/Strongest, gap Gap/XXTight
|
|
393
|
+
*/
|
|
394
|
+
export function ModuleMark({ module, label }) {
|
|
395
|
+
const ink = "var(--semantic-color-fill-static-brand-strongest)";
|
|
396
|
+
return (
|
|
397
|
+
<span style={{ display: "inline-flex", alignItems: "center", minWidth: 0,
|
|
398
|
+
gap: "var(--semantic-layout-units-gap-xxtight)", paddingRight: "var(--semantic-layout-units-padding-xxxtight)" }}>
|
|
399
|
+
<span aria-hidden="true" style={{ width: 28, height: 28, flexShrink: 0, display: "inline-flex",
|
|
400
|
+
alignItems: "center", justifyContent: "center",
|
|
401
|
+
borderRadius: "var(--semantic-layout-units-cornerradius-small)",
|
|
402
|
+
background: "var(--semantic-color-fill-static-brand-faint)", color: ink }}>
|
|
403
|
+
<ModuleIcon module={module} size={20} color="mono" />
|
|
404
|
+
</span>
|
|
405
|
+
<span style={{ fontFamily: "var(--semantic-type-family-brand)", fontWeight: "var(--semantic-type-weight-semibold)",
|
|
406
|
+
fontSize: "var(--semantic-type-font-size-r)", lineHeight: "var(--semantic-type-line-height-r-single)",
|
|
407
|
+
color: ink, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{label}</span>
|
|
408
|
+
</span>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
387
412
|
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
|
|
@@ -309,7 +286,10 @@ export function NavShell({
|
|
|
309
286
|
hideCollapseButton={isTablet || isMobile}
|
|
310
287
|
headerEnd={navHeaderEnd}
|
|
311
288
|
footer={navFooter}
|
|
312
|
-
|
|
289
|
+
headerStart={appSwitcher && appSwitcher.moduleMark ? <ModuleMark {...appSwitcher.moduleMark} /> : null}
|
|
290
|
+
appSwitcher={appSwitcher ? { ...appSwitcher, activeId: appActive, onSelect: handleAppSelect,
|
|
291
|
+
// Mobile opens the menu as a BottomSheet (Figma 40016737:141824).
|
|
292
|
+
presentation: isMobile ? "sheet" : "popover" } : null}
|
|
313
293
|
>
|
|
314
294
|
{navChildren}
|
|
315
295
|
</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,113 @@
|
|
|
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
|
+
// Title of the mobile BottomSheet (Figma "Financial Applications").
|
|
55
|
+
sheetTitle: "Financial Applications",
|
|
56
|
+
home: { id: "financials-home", label: "Financials Home",
|
|
57
|
+
navItems: accountingNav("fh", ["Dashboard", "Enter", "Manage", "View", "Reports", "Modify", "Help", "Utilities", "Security Setup"]) },
|
|
58
|
+
apps: [
|
|
59
|
+
{ id: "accounts-payable", label: "Accounts Payable", navItems: accountingNav("ap", APP_NAV_STANDARD) },
|
|
60
|
+
{ id: "accounts-receivable", label: "Accounts Receivable", navItems: accountingNav("ar", APP_NAV_STANDARD) },
|
|
61
|
+
{ id: "general-ledger", label: "General Ledger", navItems: accountingNav("gl", APP_NAV_STANDARD) },
|
|
62
|
+
{ id: "fixed-assets", label: "Fixed Assets", navItems: accountingNav("fa", ["Dashboard", "Manage", "Reports", "Modify", "Help"]) },
|
|
63
|
+
{ id: "purchasing-management", label: "Purchasing Management", navItems: accountingNav("pm", ["Dashboard", "Enter", "Manage", "Reports", "Modify", "Help"]) },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
const MOBILE_MAX = 767; // below 768, as NavShell's breakpoint
|
|
67
|
+
|
|
68
|
+
function useIsMobile() {
|
|
69
|
+
const get = () => typeof window !== "undefined" && window.innerWidth <= MOBILE_MAX;
|
|
70
|
+
const [m, setM] = useState(get);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const on = () => setM(get());
|
|
73
|
+
window.addEventListener("resize", on);
|
|
74
|
+
return () => window.removeEventListener("resize", on);
|
|
75
|
+
}, []);
|
|
76
|
+
return m;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function AccountingSideNav({
|
|
80
|
+
appSwitcher = ACCOUNTING_APP_SWITCHER,
|
|
81
|
+
activeAppId, defaultAppId, onAppSelect,
|
|
82
|
+
activeId, onNavigate,
|
|
83
|
+
presentation = "auto",
|
|
84
|
+
...rest
|
|
85
|
+
}) {
|
|
86
|
+
const apps = [appSwitcher.home, ...(appSwitcher.apps || [])].filter(Boolean);
|
|
87
|
+
const [ownApp, setOwnApp] = useState(defaultAppId ?? (appSwitcher.home && appSwitcher.home.id));
|
|
88
|
+
const appId = activeAppId ?? ownApp;
|
|
89
|
+
const current = apps.find((a) => a.id === appId) || apps[0];
|
|
90
|
+
const items = (current && current.navItems) || [];
|
|
91
|
+
const [ownActive, setOwnActive] = useState(items[0] && items[0].id);
|
|
92
|
+
const active = activeId ?? ownActive;
|
|
93
|
+
const isMobile = useIsMobile();
|
|
94
|
+
const mode = presentation === "auto" ? (isMobile ? "sheet" : "popover") : presentation;
|
|
95
|
+
|
|
96
|
+
const navigate = (id) => { setOwnActive(id); onNavigate && onNavigate(id); };
|
|
97
|
+
const selectApp = (id) => {
|
|
98
|
+
setOwnApp(id);
|
|
99
|
+
onAppSelect && onAppSelect(id);
|
|
100
|
+
const next = apps.find((a) => a.id === id);
|
|
101
|
+
const first = next && next.navItems && next.navItems[0];
|
|
102
|
+
if (first) navigate(first.id);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<SideNav key={current && current.id} items={items} activeId={active} onNavigate={navigate}
|
|
107
|
+
headerStart={appSwitcher.moduleMark ? <ModuleMark {...appSwitcher.moduleMark} /> : null}
|
|
108
|
+
appSwitcher={{ ...appSwitcher, activeId: current && current.id, onSelect: selectApp, presentation: mode }}
|
|
109
|
+
{...rest} />
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default AccountingSideNav;
|