@helloimjolopez-pathway/pathway-components 0.2.2 → 0.2.3
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/icon/action-icon.jsx +11 -2
- package/components/nav-shell/nav-shell-demo.jsx +46 -0
- package/components/nav-shell/nav-shell.jsx +19 -8
- package/components/search/search.jsx +7 -5
- package/components/sidenav/sidenav.jsx +143 -84
- package/components/top-nav/top-nav.jsx +54 -20
- package/package.json +1 -1
|
@@ -90,6 +90,13 @@ export const T = {
|
|
|
90
90
|
* @param {string} color Override the resting colour. Omit on a light surface;
|
|
91
91
|
* pass one on a dark or on-brand surface, where Ghost
|
|
92
92
|
* is the right family.
|
|
93
|
+
* @param {object} states Override the hover and pressed tokens as
|
|
94
|
+
* { hover, pressed, layerHover, layerPress }. A dark or
|
|
95
|
+
* on-brand surface needs this as well as `color`: the
|
|
96
|
+
* default ladder paints a light Primary/Subtle layer and a
|
|
97
|
+
* dark Secondary glyph, which is right on a light page and
|
|
98
|
+
* wrong on the navy TopNav. SideNav.Control (40007082:7703)
|
|
99
|
+
* binds the Ghost ladder instead.
|
|
93
100
|
*/
|
|
94
101
|
export function ActionIcon({
|
|
95
102
|
name,
|
|
@@ -97,6 +104,7 @@ export function ActionIcon({
|
|
|
97
104
|
label,
|
|
98
105
|
fill = 0,
|
|
99
106
|
color,
|
|
107
|
+
states,
|
|
100
108
|
onClick,
|
|
101
109
|
disabled,
|
|
102
110
|
className = "",
|
|
@@ -107,7 +115,8 @@ export function ActionIcon({
|
|
|
107
115
|
const [hov, setHov] = useState(false);
|
|
108
116
|
const [press, setPress] = useState(false);
|
|
109
117
|
const s = ACTION_ICON_SIZES[size] || ACTION_ICON_SIZES.Small;
|
|
110
|
-
const
|
|
118
|
+
const st = { ...T, ...states };
|
|
119
|
+
const fg = disabled ? T.rest : press ? st.pressed : hov ? st.hover : (color || T.rest);
|
|
111
120
|
|
|
112
121
|
return (
|
|
113
122
|
<button
|
|
@@ -141,7 +150,7 @@ export function ActionIcon({
|
|
|
141
150
|
<span style={{
|
|
142
151
|
display: "inline-flex", alignItems: "center", justifyContent: "center",
|
|
143
152
|
width: s.layer, height: s.layer, borderRadius: U("cornerradius-base"),
|
|
144
|
-
background: press ?
|
|
153
|
+
background: press ? st.layerPress : hov ? st.layerHover : "transparent",
|
|
145
154
|
transition: noMotion ? "none" : `background ${M("duration-2")} ${M("easing-standard")}`,
|
|
146
155
|
}}>
|
|
147
156
|
<Icon name={name} size={s.glyph} fill={fill} style={{
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NavShell's standalone demo. Imports the real NavShell, which composes the
|
|
3
|
+
* real TopNav and SideNav, so nothing here can fall out of step with them: see
|
|
4
|
+
* components/_demo/harness.jsx for why the newer demos are generated.
|
|
5
|
+
*
|
|
6
|
+
* Replaced the hand-written nav-shell.html on 2026-11-03. That page carried its
|
|
7
|
+
* own copies of TopNav and SideNav, both from before the Figma parity passes:
|
|
8
|
+
* a 240px nav, no item radius, a 4px stripe rounded at 4, a 20px glyph, a 22px
|
|
9
|
+
* module mark, raw rgba hover fills, and a 56px NavHeader Figma does not draw.
|
|
10
|
+
*/
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { DemoChrome, mount } from "../_demo/harness.jsx";
|
|
13
|
+
import { NavShell } from "./nav-shell.jsx";
|
|
14
|
+
import { PageTemplate } from "../page-template/page-template.jsx";
|
|
15
|
+
import { NAV_SECTIONS } from "../../src/stories/Library/SideNav/sidenavDemoData.jsx";
|
|
16
|
+
|
|
17
|
+
const C = (n) => `var(--semantic-color-${n})`;
|
|
18
|
+
const Y = (n) => `var(--semantic-type-${n})`;
|
|
19
|
+
|
|
20
|
+
// NavShell is fixed to the viewport, so every width, Desktop included, renders
|
|
21
|
+
// in an inner frame: in this document it would sit on top of the demo chrome.
|
|
22
|
+
const WIDTHS = { Desktop: 1440, Tablet: 900, Mobile: 390 };
|
|
23
|
+
|
|
24
|
+
const ORG = { id: "gcc", name: "Grace Community Church", campus: "Knoxville", initials: "GC" };
|
|
25
|
+
const USER = { name: "Jo Lopez", initials: "JL", email: "jo@gracecommunity.org" };
|
|
26
|
+
|
|
27
|
+
function App() {
|
|
28
|
+
return (
|
|
29
|
+
<DemoChrome title="NavShell" widths={WIDTHS}
|
|
30
|
+
blurb={"TopNav, SideNav and the page area composed into one application frame. " +
|
|
31
|
+
"Desktop pushes the page with a collapsible nav, Tablet locks the 72px rail, " +
|
|
32
|
+
"Mobile opens the nav as an overlay from the hamburger."}>
|
|
33
|
+
<NavShell activeModuleId="accounting" org={ORG} user={USER}
|
|
34
|
+
navSections={NAV_SECTIONS} activeNavId="balance_sheet" onNavigate={() => {}}>
|
|
35
|
+
<PageTemplate>
|
|
36
|
+
<h1 style={{ margin: 0, fontSize: Y("font-size-xl"), lineHeight: Y("line-height-xl-single"),
|
|
37
|
+
fontWeight: Y("weight-semibold"), color: C("foreground-static-neutral-bold") }}>
|
|
38
|
+
Balance Sheet
|
|
39
|
+
</h1>
|
|
40
|
+
</PageTemplate>
|
|
41
|
+
</NavShell>
|
|
42
|
+
</DemoChrome>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
mount(<App />);
|
|
@@ -49,9 +49,9 @@ function useBreakpoint() {
|
|
|
49
49
|
* SideNav fixed, left edge, top 60px → bottom, push layout
|
|
50
50
|
* Shell.Main scrollable content area, margin-left = SideNav width
|
|
51
51
|
*
|
|
52
|
-
* Desktop: SideNav
|
|
53
|
-
* Tablet: SideNav always
|
|
54
|
-
* Mobile: SideNav hidden, slides in as
|
|
52
|
+
* Desktop: SideNav 250px expanded | 72px collapsed, user toggles
|
|
53
|
+
* Tablet: SideNav always 72px rail, toggle locked
|
|
54
|
+
* Mobile: SideNav hidden, slides in as 250px overlay on hamburger tap
|
|
55
55
|
*
|
|
56
56
|
* Props (TopNav pass-through):
|
|
57
57
|
* modules Array<{id, label, icon}> — all modules (default: DEFAULT_MODULES)
|
|
@@ -107,10 +107,13 @@ export function NavShell({
|
|
|
107
107
|
// Mobile overlay open state
|
|
108
108
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
109
109
|
|
|
110
|
-
// Tablet: rail (
|
|
111
|
-
// Mobile:
|
|
110
|
+
// Tablet: rail (72px) is fixed, no user toggle
|
|
111
|
+
// Mobile: expanded inside the overlay, opened and closed via mobileOpen
|
|
112
112
|
// Desktop: user-controlled
|
|
113
|
-
|
|
113
|
+
// Mobile is EXPANDED: the overlay is the full 250 nav, as spec §8 and the
|
|
114
|
+
// NavShell story both have it. This forced it collapsed, so the 250 overlay
|
|
115
|
+
// opened with a 72 rail inside it and 178px of blank.
|
|
116
|
+
const effectiveCollapsed = isTablet ? true : isMobile ? false : collapsed;
|
|
114
117
|
|
|
115
118
|
// Shell.Main margin-left: matches current SideNav width for push layout.
|
|
116
119
|
// Mobile: no margin (SideNav is an overlay).
|
|
@@ -163,6 +166,12 @@ export function NavShell({
|
|
|
163
166
|
</a>
|
|
164
167
|
|
|
165
168
|
{/* ── TopNav: position fixed, full width, 60px ─────────────────────── */}
|
|
169
|
+
{/* The comment above was all there was: TopNav renders a plain in-flow
|
|
170
|
+
<nav>, so it sat in the document flow while Shell.Main ALSO took a
|
|
171
|
+
60px top margin, putting a blank 60px band under the bar, and the
|
|
172
|
+
fixed SideNav sat under any wrapper padding. The wrapper makes it
|
|
173
|
+
fixed, as spec §9.3 says. */}
|
|
174
|
+
<div style={{ position: "fixed", top: 0, left: 0, right: 0, zIndex: 100 }}>
|
|
166
175
|
<TopNav
|
|
167
176
|
modules={modules}
|
|
168
177
|
activeModuleId={activeModuleId}
|
|
@@ -174,6 +183,7 @@ export function NavShell({
|
|
|
174
183
|
onOrgSelect={onOrgSelect}
|
|
175
184
|
onSideNavToggle={handleSideNavToggle}
|
|
176
185
|
/>
|
|
186
|
+
</div>
|
|
177
187
|
|
|
178
188
|
{/* ── Mobile scrim — darkens content while SideNav overlay is open ─── */}
|
|
179
189
|
{isMobile && mobileOpen && (
|
|
@@ -190,7 +200,7 @@ export function NavShell({
|
|
|
190
200
|
|
|
191
201
|
{/* ── SideNav container ────────────────────────────────────────────── */}
|
|
192
202
|
{/* Position: fixed so it stays below TopNav while the content scrolls.
|
|
193
|
-
On mobile: width controls the overlay reveal (0 →
|
|
203
|
+
On mobile: width controls the overlay reveal (0 → 250px); the SideNav
|
|
194
204
|
itself renders inside a clipping container and slides in from the left.
|
|
195
205
|
On desktop/tablet: overflow hidden so the SideNav's own width transition
|
|
196
206
|
stays within the container; no width needed here. */}
|
|
@@ -219,7 +229,8 @@ export function NavShell({
|
|
|
219
229
|
onNavigate={onNavigate}
|
|
220
230
|
collapsed={effectiveCollapsed}
|
|
221
231
|
onCollapseChange={handleCollapseChange}
|
|
222
|
-
|
|
232
|
+
// Mobile: the TopNav hamburger is the only toggle (NavHeader spec).
|
|
233
|
+
hideCollapseButton={isTablet || isMobile}
|
|
223
234
|
headerEnd={navHeaderEnd}
|
|
224
235
|
footer={navFooter}
|
|
225
236
|
>
|
|
@@ -598,17 +598,19 @@ export function TopNavSearch({
|
|
|
598
598
|
flexShrink: 0,
|
|
599
599
|
}}
|
|
600
600
|
>
|
|
601
|
-
{/*
|
|
601
|
+
{/* Container.Icon: 36x36 with CornerRadius/Full and a Button/BorderWidth/Rest
|
|
602
|
+
(0.75) stroke, read from TopNav.Global 2026-11-03. Was 32 with 0.5. */}
|
|
602
603
|
<div
|
|
603
604
|
style={{
|
|
604
605
|
display: "flex",
|
|
605
606
|
alignItems: "center",
|
|
606
607
|
justifyContent: "center",
|
|
607
|
-
width:
|
|
608
|
-
height:
|
|
609
|
-
|
|
608
|
+
width: 36,
|
|
609
|
+
height: 36,
|
|
610
|
+
boxSizing: "border-box",
|
|
611
|
+
borderRadius: "var(--semantic-layout-units-cornerradius-full)",
|
|
610
612
|
background: hov ? T.collapsedBtnHover : T.collapsedBtnFill,
|
|
611
|
-
border: `
|
|
613
|
+
border: `var(--contextual-layout-units-button-borderwidth-rest) solid ${hov ? T.collapsedBtnBorderHov : T.collapsedBtnBorder}`,
|
|
612
614
|
transition: "background var(--motion-duration-2) var(--motion-easing-standard), border-color var(--motion-duration-2) var(--motion-easing-standard)",
|
|
613
615
|
}}
|
|
614
616
|
>
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* accurate; the correct action is still to leave this file alone, name the
|
|
13
13
|
* drift, and wait. Do not resolve it by fetching Figma. See CLAUDE.md 1.1.
|
|
14
14
|
*
|
|
15
|
+
* 2026-11-03: Jo explicitly asked for SideNav to be brought in line with Figma
|
|
16
|
+
* (stripe height and the rest). Pulled from nodes 40003954:284, 40005607:25240,
|
|
17
|
+
* 40004035:27057, 40006794:5975, 40007332:6995 and 40007332:8034. The collapse
|
|
18
|
+
* and expand glyphs stay pinned regardless; see the note at the bottom.
|
|
19
|
+
*
|
|
15
20
|
* Spec: components/sidenav/sidenav-spec.md
|
|
16
21
|
* Figma: https://www.figma.com/design/3sw45aVcngFAmpbP6cfrXP/?node-id=40004059-1375
|
|
17
22
|
* SideNav.Container, a COMPONENT_SET. The id cited here until
|
|
@@ -74,7 +79,10 @@ export const T = {
|
|
|
74
79
|
navHover: c("fill-action-selection-hover"),
|
|
75
80
|
navActive: c("fill-action-selection-selected"),
|
|
76
81
|
navTrail: c("fill-action-selection-trail"),
|
|
77
|
-
|
|
82
|
+
// Container right border (Mode=Stroked) and every divider: Figma binds
|
|
83
|
+
// Stroke/Static/Neutral/Faint on both (40004183:6254, 40006793:3783).
|
|
84
|
+
// Was stroke-static-neutral-base, a rung darker.
|
|
85
|
+
infoSubtle: c("stroke-static-neutral-faint"),
|
|
78
86
|
},
|
|
79
87
|
surface: {
|
|
80
88
|
// Figma binds Fill/Surface/Elevated on SideNav.Container (node 40004059:1375).
|
|
@@ -96,8 +104,9 @@ export const T = {
|
|
|
96
104
|
navHover: c("foreground-action-secondary-hover"),
|
|
97
105
|
navActive: c("foreground-action-secondary-pressed"),
|
|
98
106
|
navDisabled: c("foreground-action-disabled"),
|
|
99
|
-
secondary: c("foreground-static-neutral-
|
|
100
|
-
secondarySubtle: c("foreground-static-neutral-
|
|
107
|
+
secondary: c("foreground-static-neutral-subtle"), // PopoverMenu.SectionHeader
|
|
108
|
+
secondarySubtle: c("foreground-static-neutral-faint"), // NavSectionLabel
|
|
109
|
+
strong: c("foreground-static-neutral-strong"), // PopoverMenu.Item, tooltip
|
|
101
110
|
},
|
|
102
111
|
icon: {
|
|
103
112
|
navBase: c("foreground-action-secondary-rest"),
|
|
@@ -106,10 +115,12 @@ export const T = {
|
|
|
106
115
|
navDisabled: c("foreground-action-disabled"),
|
|
107
116
|
actionSecondary: c("foreground-action-secondary-rest"), // CollapseButton
|
|
108
117
|
},
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
// Figma binds Fill/Action/Selection/Indicator on indicator.stripe (node
|
|
119
|
+
// 40004035:27051). It was fill-action-primary-strong-pressed, which happens
|
|
120
|
+
// to resolve to the same brand-550 today but is not the bound token.
|
|
121
|
+
indicator: c("fill-action-selection-indicator"),
|
|
122
|
+
// Figma binds NavItem/CornerRadius (Contextual) on every SideNavItem variant.
|
|
123
|
+
radius: x("navitem-cornerradius"),
|
|
113
124
|
};
|
|
114
125
|
|
|
115
126
|
/*
|
|
@@ -148,19 +159,43 @@ export const L = {
|
|
|
148
159
|
textPad: u("padding-xtight"),
|
|
149
160
|
childIndent: 24, // no token in Figma, level-1 left indent
|
|
150
161
|
stripeW: 4, // no token in Figma, indicator stripe width
|
|
162
|
+
// Indicator stripe insets, read from Figma 2026-11-03. On a 44px item the
|
|
163
|
+
// stripe sits inside container.indicator (padding 4 top/bottom) and then
|
|
164
|
+
// indicator.stripe (padding 6 top/bottom), so it paints 44 - 8 - 12 = 24px
|
|
165
|
+
// tall. The repo had only the outer 4px, which made it 36px.
|
|
166
|
+
stripeOuterPadV: u("padding-xxxtight"),
|
|
167
|
+
stripeInnerPadV: u("padding-xxtight"),
|
|
168
|
+
stripeRadius: u("cornerradius-base"), // Figma Border Radius/S, 8
|
|
169
|
+
// Expanded rows: container.rowStart padding. Level 0 is 6 (Padding/XXTight),
|
|
170
|
+
// so the leading icon starts at x=10. It was 12, which put it at x=16.
|
|
171
|
+
itemPadH: u("padding-xxtight"),
|
|
172
|
+
// Container.HasChildren: 2px padding around a 16px frame holding a 12px
|
|
173
|
+
// expand_more glyph, 20px wide in total. Was a 40px box with a 10px glyph.
|
|
174
|
+
chevPad: u("padding-xxxxtight"),
|
|
175
|
+
chevFrame: 16, // no token in Figma, icon frame
|
|
176
|
+
chevGlyph: 12, // no token in Figma, expand_more glyph
|
|
177
|
+
// Level 1 (nesting) rows: rowStart 12 + container.main 24 + label 8, so the
|
|
178
|
+
// label starts at x=48 after the 4px indicator column. Was 32 (x=36).
|
|
179
|
+
childRowPadL: u("padding-tight"),
|
|
180
|
+
childMainPadL: u("padding-relaxed"),
|
|
151
181
|
colPadL: u("padding-medium"),
|
|
152
182
|
colPadR: u("padding-tight"),
|
|
153
183
|
collapseGap: u("padding-xxtight"),
|
|
154
184
|
};
|
|
155
185
|
|
|
156
186
|
// ─── IndicatorStripe ──────────────────────────────────────────────────────────
|
|
157
|
-
|
|
187
|
+
// Figma: container.indicator (padding 4 vertical) > indicator.stripe (padding 6
|
|
188
|
+
// vertical) > the 4px bar. `inset` overrides the total vertical inset for the
|
|
189
|
+
// rows where Figma places the stripe differently (SideNavItem.List: 6 only).
|
|
190
|
+
export function IndicatorStripe({ visible, inset }) {
|
|
158
191
|
return (
|
|
159
192
|
<div style={{ width: L.stripeW, alignSelf: "stretch", display: "flex",
|
|
160
193
|
flexDirection: "column", alignItems: "flex-start",
|
|
161
|
-
justifyContent: "center",
|
|
194
|
+
justifyContent: "center",
|
|
195
|
+
padding: inset ? `${inset} 0` : `calc(${L.stripeOuterPadV} + ${L.stripeInnerPadV}) 0`,
|
|
196
|
+
boxSizing: "border-box", flexShrink: 0 }}>
|
|
162
197
|
<div style={{ flex: "1 0 0", width: L.stripeW, minHeight: 1,
|
|
163
|
-
borderRadius:
|
|
198
|
+
borderRadius: `0 ${L.stripeRadius} ${L.stripeRadius} 0`,
|
|
164
199
|
backgroundColor: visible ? T.indicator : "transparent",
|
|
165
200
|
transition: "background-color var(--motion-duration-3) var(--motion-easing-standard)" }} />
|
|
166
201
|
</div>
|
|
@@ -263,7 +298,7 @@ export function SideNavItem({
|
|
|
263
298
|
// Reported 2026-10-02. Auto margins hand the free space back to
|
|
264
299
|
// both sides, which with the 6px right padding above reproduces
|
|
265
300
|
// Figma's x=9 exactly.
|
|
266
|
-
marginLeft: isSidebarCollapsed ? "auto" : L.
|
|
301
|
+
marginLeft: isSidebarCollapsed ? "auto" : L.itemPadH,
|
|
267
302
|
marginRight: isSidebarCollapsed ? "auto" : undefined,
|
|
268
303
|
color: iconColor }}>
|
|
269
304
|
{typeof item.icon === "function"
|
|
@@ -297,8 +332,10 @@ export function SideNavItem({
|
|
|
297
332
|
// box, so the label still occupied 8px of padding on the rail and ate
|
|
298
333
|
// most of the free space the icon's auto margins needed: the icon
|
|
299
334
|
// landed at x=5 instead of Figma's x=9.
|
|
300
|
-
paddingLeft: isSidebarCollapsed ? 0
|
|
301
|
-
|
|
335
|
+
paddingLeft: isSidebarCollapsed ? 0
|
|
336
|
+
: (isChild ? `calc(${L.childRowPadL} + ${L.childMainPadL} + ${L.textPad})` : L.textPad),
|
|
337
|
+
// container.rowStart's right padding: 6 on Level 0, 12 on Level 1.
|
|
338
|
+
paddingRight: isSidebarCollapsed ? 0 : (isChild ? L.childRowPadL : L.itemPadH),
|
|
302
339
|
maxWidth: isSidebarCollapsed ? 0 : 200,
|
|
303
340
|
opacity: isSidebarCollapsed ? 0 : 1,
|
|
304
341
|
overflow: "hidden",
|
|
@@ -321,16 +358,15 @@ export function SideNavItem({
|
|
|
321
358
|
2026-10-02: two distinct glyph centres, 28.8 and 45.8, where there
|
|
322
359
|
should be one. It is now not rendered at all when collapsed. */}
|
|
323
360
|
{!isChild && hasChildren && !isSidebarCollapsed && (
|
|
324
|
-
<div style={{ width:
|
|
361
|
+
<div style={{ width: `calc(${L.chevFrame}px + 2 * ${L.chevPad})`, height: L.iconWrap, display: "flex",
|
|
325
362
|
alignItems: "center", justifyContent: "center", flexShrink: 0,
|
|
326
|
-
maxWidth: isSidebarCollapsed ? 0 : 40,
|
|
327
363
|
opacity: isSidebarCollapsed ? 0 : 1,
|
|
328
364
|
overflow: "hidden",
|
|
329
365
|
transition: "max-width var(--motion-duration-6) var(--motion-easing-emphasized), opacity var(--motion-duration-3) var(--motion-easing-standard)" }}>
|
|
330
366
|
{/* Chevron rotation matches accordion timing, --motion-duration-5 + --motion-easing-accordion */}
|
|
331
367
|
<div style={{ transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
|
|
332
368
|
transition: "transform var(--motion-duration-5) var(--motion-easing-accordion)" }}>
|
|
333
|
-
{chevronDown ? chevronDown({ size:
|
|
369
|
+
{chevronDown ? chevronDown({ size: L.chevGlyph, color: iconColor }) : <ChevDown size={L.chevGlyph} color={iconColor} />}
|
|
334
370
|
</div>
|
|
335
371
|
</div>
|
|
336
372
|
)}
|
|
@@ -380,11 +416,18 @@ export function SideNavTooltip({ label, anchorRect, onMouseEnter, onMouseLeave }
|
|
|
380
416
|
top: anchorRect.top + anchorRect.height / 2,
|
|
381
417
|
zIndex: 1000, backgroundColor: T.surface.navLight,
|
|
382
418
|
border: `0.5px solid ${c("stroke-static-neutral-faint")}`, borderRadius: "var(--semantic-layout-units-cornerradius-base)",
|
|
419
|
+
// Figma binds Elevation/Card/Rest here, which has no CSS token yet.
|
|
420
|
+
// Widget is the nearest existing elevation.
|
|
383
421
|
boxShadow: c2("--elevation-widget"),
|
|
384
|
-
padding: "
|
|
422
|
+
padding: "var(--semantic-layout-units-padding-xxtight) var(--semantic-layout-units-padding-xtight)",
|
|
423
|
+
whiteSpace: "nowrap", pointerEvents: "auto",
|
|
385
424
|
animation: "popoverInCentered var(--motion-duration-3) var(--motion-easing-spring) forwards" }}>
|
|
425
|
+
{/* Text/Body/Small/Regular, Foreground/Static/Neutral/Strong (Figma
|
|
426
|
+
"Text Container", node 40005607:25190). Letter spacing is Figma's
|
|
427
|
+
Wide, 0.3px, which is the repo's `spacious` token: the repo's `wide`
|
|
428
|
+
is 0.1px. */}
|
|
386
429
|
<span style={{ fontFamily: "'Red Hat Text',sans-serif", fontWeight: 400,
|
|
387
|
-
fontSize: "var(--semantic-type-font-size-s)", lineHeight: "var(--semantic-type-line-height-s-single)", letterSpacing: "var(--semantic-type-letter-spacing-
|
|
430
|
+
fontSize: "var(--semantic-type-font-size-s)", lineHeight: "var(--semantic-type-line-height-s-single)", letterSpacing: "var(--semantic-type-letter-spacing-spacious)", color: T.text.strong }}>
|
|
388
431
|
{label}
|
|
389
432
|
</span>
|
|
390
433
|
</div>,
|
|
@@ -393,17 +436,19 @@ export function SideNavTooltip({ label, anchorRect, onMouseEnter, onMouseLeave }
|
|
|
393
436
|
}
|
|
394
437
|
|
|
395
438
|
// ─── SectionLabel ─────────────────────────────────────────────────────────────
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
439
|
+
// The flyout's header is PopoverMenu.SectionHeader (node 40018992:3361), read
|
|
440
|
+
// from SideNavItem.Collapsed on 2026-11-03: Text/Supporting/Base/Semibold
|
|
441
|
+
// (12px / 600 / 18px / 0.6px), Foreground/Static/Neutral/Subtle, sentence case,
|
|
442
|
+
// padding 6 vertical, 12 left, 8 right. It was the 11px uppercase nav section
|
|
443
|
+
// label in neutral-base with a divider under it, which Figma does not have.
|
|
400
444
|
export function SectionLabel({ label }) {
|
|
401
445
|
return (
|
|
402
|
-
<div style={{ display: "flex", alignItems: "center", width: "100%",
|
|
403
|
-
paddingLeft: "var(--semantic-layout-units-padding-tight)",
|
|
446
|
+
<div style={{ display: "flex", alignItems: "center", width: "100%", boxSizing: "border-box",
|
|
447
|
+
paddingLeft: "var(--semantic-layout-units-padding-tight)", paddingRight: "var(--semantic-layout-units-padding-xtight)",
|
|
448
|
+
paddingTop: "var(--semantic-layout-units-padding-xxtight)", paddingBottom: "var(--semantic-layout-units-padding-xxtight)" }}>
|
|
404
449
|
<span style={{ fontFamily: "'Red Hat Text',sans-serif", fontWeight: 600,
|
|
405
|
-
fontSize: "var(--semantic-type-font-size-
|
|
406
|
-
color: T.text.secondary,
|
|
450
|
+
fontSize: "var(--semantic-type-font-size-xs)", lineHeight: "var(--semantic-type-line-height-xs-single)", letterSpacing: "var(--semantic-type-letter-spacing-extraspacious)",
|
|
451
|
+
color: T.text.secondary,
|
|
407
452
|
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
408
453
|
{label}
|
|
409
454
|
</span>
|
|
@@ -418,14 +463,15 @@ export function CollapsedPopover({ item, onClick, anchorRect, onMouseEnter, onMo
|
|
|
418
463
|
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}
|
|
419
464
|
style={{ position: "fixed", left: anchorRect.right + 8, top: anchorRect.top,
|
|
420
465
|
zIndex: 1000, backgroundColor: T.surface.navElevated,
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
466
|
+
// Figma "Popover Menu" in SideNavItem.Collapsed: radius Unit/10, Elevation/Lift,
|
|
467
|
+
// 220 wide (220 to 360), Items slot padding 8 vertical, 6 horizontal, gap 2.
|
|
468
|
+
border: `0.5px solid ${c("stroke-static-neutral-faint")}`, borderRadius: "var(--primitive-unit-unit-10)",
|
|
469
|
+
boxShadow: c2("--elevation-lift"),
|
|
470
|
+
padding: "var(--semantic-layout-units-padding-xtight) var(--semantic-layout-units-padding-xxtight)",
|
|
471
|
+
display: "flex", flexDirection: "column", gap: "var(--semantic-layout-units-gap-xxxtight)",
|
|
472
|
+
boxSizing: "border-box", minWidth: 220, maxWidth: 360, pointerEvents: "auto",
|
|
424
473
|
animation: "popoverIn var(--motion-duration-3) var(--motion-easing-spring) forwards" }}>
|
|
425
|
-
|
|
426
|
-
<div style={{ borderBottom: `0.5px solid ${c("stroke-static-neutral-faint")}` }}>
|
|
427
|
-
<SectionLabel label={item.label} />
|
|
428
|
-
</div>
|
|
474
|
+
<SectionLabel label={item.label} />
|
|
429
475
|
{item.children.map(child =>
|
|
430
476
|
<PopoverRow key={child.id} item={child} onClick={onClick} activeId={activeId} />
|
|
431
477
|
)}
|
|
@@ -439,19 +485,28 @@ export function PopoverRow({ item, onClick, activeId }) {
|
|
|
439
485
|
const [h, setH] = useState(false);
|
|
440
486
|
const isActive = item.id === activeId;
|
|
441
487
|
const bg = isActive ? T.fill.navActive : h ? T.fill.navHover : "transparent";
|
|
442
|
-
|
|
488
|
+
// PopoverMenu.Item label is Foreground/Static/Neutral/Strong in every state;
|
|
489
|
+
// the selected row is marked by its fill and the stripe, never by the text.
|
|
490
|
+
const color = T.text.strong;
|
|
443
491
|
return (
|
|
444
492
|
<div onClick={() => onClick(item.id)}
|
|
445
493
|
onMouseEnter={() => setH(true)} onMouseLeave={() => setH(false)}
|
|
446
|
-
|
|
494
|
+
// PopoverMenu.Item: min 44, padding 12 all round, radius CornerRadius/Base.
|
|
495
|
+
style={{ position: "relative", display: "flex", alignItems: "center", minHeight: L.itemH,
|
|
496
|
+
boxSizing: "border-box", padding: "var(--semantic-layout-units-padding-tight)",
|
|
497
|
+
borderRadius: "var(--semantic-layout-units-cornerradius-base)",
|
|
447
498
|
overflow: "hidden", backgroundColor: bg, cursor: "pointer",
|
|
448
499
|
transition: "background-color var(--motion-duration-3) var(--motion-easing-standard)" }}>
|
|
449
|
-
|
|
500
|
+
{/* Stripe: absolutely placed, inset 8 top and bottom, so 28px on a 44px row. */}
|
|
501
|
+
<div style={{ position: "absolute", left: 0, top: "var(--semantic-layout-units-padding-xtight)",
|
|
502
|
+
bottom: "var(--semantic-layout-units-padding-xtight)", width: L.stripeW,
|
|
503
|
+
borderRadius: `0 ${L.stripeRadius} ${L.stripeRadius} 0`,
|
|
450
504
|
backgroundColor: isActive ? T.indicator : "transparent",
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
505
|
+
transition: "background-color var(--motion-duration-3) var(--motion-easing-standard)" }} />
|
|
506
|
+
{/* Label/Menu/Base/Medium: 14 / 500 / 20 / 0.3 (the repo's `spacious`). */}
|
|
507
|
+
<span style={{ fontFamily: "'Red Hat Text',sans-serif",
|
|
508
|
+
fontWeight: 500, fontSize: "var(--semantic-type-font-size-s)", lineHeight: "var(--semantic-type-line-height-s-single)", letterSpacing: "var(--semantic-type-letter-spacing-spacious)",
|
|
509
|
+
color, flex: 1 }}>
|
|
455
510
|
{item.label}
|
|
456
511
|
</span>
|
|
457
512
|
</div>
|
|
@@ -515,8 +570,9 @@ export function CollapseButton({ isSidebarCollapsed, onToggle, collapseIcon, exp
|
|
|
515
570
|
// ─── SideNav (top-level container) ────────────────────────────────────────────
|
|
516
571
|
// ── NavSectionLabel ──────────────────────────────────────────────────────────
|
|
517
572
|
// Optional in-nav section heading. Figma: SideNav.SectionLabel (node 40006794:5975).
|
|
518
|
-
// Tokens: Label/Section/Small/Semibold (11px/600/16px/0.6px), Foreground/Static/Neutral/
|
|
519
|
-
// Container: h
|
|
573
|
+
// Tokens: Label/Section/Small/Semibold (11px/600/16px/0.6px), Foreground/Static/Neutral/Faint
|
|
574
|
+
// Container: h 40, padding 6 left, 6 right, 12 vertical (read 2026-11-03; was
|
|
575
|
+
// 4 / 4 / 8 in neutral-base)
|
|
520
576
|
// In collapsed rail: hidden, replaced by a Divider (rendered by SideNav itself, see §2.3)
|
|
521
577
|
// ── NavHeader ────────────────────────────────────────────────────────────────
|
|
522
578
|
// Sits at the TOP of the SideNav. 48px row with the expand/collapse action icon
|
|
@@ -532,31 +588,30 @@ export function NavHeader({ isSidebarCollapsed, onToggle, collapseIcon, expandIc
|
|
|
532
588
|
aria-label={isSidebarCollapsed ? "Expand navigation" : "Collapse navigation"}
|
|
533
589
|
role="button" tabIndex={0}
|
|
534
590
|
onKeyDown={e => (e.key === "Enter" || e.key === " ") && onToggle()}
|
|
591
|
+
// Figma SideBar Expand/Collapse: Container.Main 44 tall, the Action.Icon
|
|
592
|
+
// is a 44 touch target holding a 28px Container.Icon (radius Unit/10),
|
|
593
|
+
// which is what takes the hover fill. The whole row used to fill, and
|
|
594
|
+
// the icon box was 36. The glyphs themselves stay pinned (see below).
|
|
535
595
|
style={{ display: "flex", alignItems: "center", height: L.itemH, width: "100%",
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
alignItems: "center", justifyContent: "center" }}>
|
|
551
|
-
{collapseIcon
|
|
552
|
-
? collapseIcon({ size: 12, color: actionIconColor })
|
|
553
|
-
: <RightPanelOpenIcon color={actionIconColor} />}
|
|
554
|
-
</div>
|
|
596
|
+
justifyContent: isSidebarCollapsed ? "center" : "flex-end", cursor: "pointer" }}>
|
|
597
|
+
<div style={{ width: L.itemH, height: L.itemH, display: "flex",
|
|
598
|
+
alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
|
|
599
|
+
<div style={{ width: 28, height: 28, display: "flex", alignItems: "center",
|
|
600
|
+
justifyContent: "center", borderRadius: "var(--primitive-unit-unit-10)",
|
|
601
|
+
backgroundColor: h ? T.fill.navHover : "transparent",
|
|
602
|
+
transition: "background-color var(--motion-duration-3) var(--motion-easing-standard)" }}>
|
|
603
|
+
{isSidebarCollapsed
|
|
604
|
+
? (expandIcon
|
|
605
|
+
? expandIcon({ size: 12, color: actionIconColor })
|
|
606
|
+
: <LeftPanelOpenIcon color={actionIconColor} />)
|
|
607
|
+
: (collapseIcon
|
|
608
|
+
? collapseIcon({ size: 12, color: actionIconColor })
|
|
609
|
+
: <RightPanelOpenIcon color={actionIconColor} />)}
|
|
555
610
|
</div>
|
|
556
|
-
|
|
611
|
+
</div>
|
|
557
612
|
</div>
|
|
558
|
-
{/* Divider,
|
|
559
|
-
<div style={{ padding: "
|
|
613
|
+
{/* Divider, padding 4 vertical in Figma (was 2) */}
|
|
614
|
+
<div style={{ padding: "var(--semantic-layout-units-padding-xxxtight) 0" }}>
|
|
560
615
|
<div style={{ height: 1, backgroundColor: T.fill.infoSubtle }} />
|
|
561
616
|
</div>
|
|
562
617
|
</div>
|
|
@@ -565,8 +620,8 @@ export function NavHeader({ isSidebarCollapsed, onToggle, collapseIcon, expandIc
|
|
|
565
620
|
|
|
566
621
|
export function NavSectionLabel({ label }) {
|
|
567
622
|
return (
|
|
568
|
-
<div style={{ display: "flex", alignItems: "center", height: 40, width: "100%",
|
|
569
|
-
paddingLeft: "var(--semantic-layout-units-padding-
|
|
623
|
+
<div style={{ display: "flex", alignItems: "center", height: 40, width: "100%", boxSizing: "border-box",
|
|
624
|
+
paddingLeft: "var(--semantic-layout-units-padding-xxtight)", paddingRight: "var(--semantic-layout-units-padding-xxtight)", paddingTop: "var(--semantic-layout-units-padding-tight)", paddingBottom: "var(--semantic-layout-units-padding-tight)",
|
|
570
625
|
flexShrink: 0 }}>
|
|
571
626
|
<span style={{ fontFamily: "'Red Hat Text',sans-serif", fontWeight: 600,
|
|
572
627
|
fontSize: "var(--semantic-type-font-size-xxs)", lineHeight: "var(--semantic-type-line-height-xxs-single)", letterSpacing: "var(--semantic-type-letter-spacing-extraspacious)",
|
|
@@ -579,12 +634,15 @@ export function NavSectionLabel({ label }) {
|
|
|
579
634
|
}
|
|
580
635
|
|
|
581
636
|
// ── BulletDot ────────────────────────────────────────────────────────────────
|
|
582
|
-
//
|
|
583
|
-
//
|
|
637
|
+
// Leading icon for SideNavListSection's ListItem: Figma's Icon.Leading there is
|
|
638
|
+
// the Material "circle" glyph in a 16px frame (a 13.3px disc), node 40007332:6989.
|
|
639
|
+
// It was a 6px dot. Color cycles Base / Hover / Active just like SideNavItem icons.
|
|
584
640
|
export function BulletDot({ color }) {
|
|
585
641
|
return (
|
|
586
|
-
<
|
|
587
|
-
|
|
642
|
+
<svg width={L.iconInner} height={L.iconInner} viewBox="0 0 16 16" aria-hidden="true"
|
|
643
|
+
style={{ display: "block", flexShrink: 0 }}>
|
|
644
|
+
<circle cx="8" cy="8" r="6.6667" fill={color} />
|
|
645
|
+
</svg>
|
|
588
646
|
);
|
|
589
647
|
}
|
|
590
648
|
|
|
@@ -602,17 +660,21 @@ export function ListItem({ item, isActive, onClick }) {
|
|
|
602
660
|
onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}
|
|
603
661
|
role="button" tabIndex={0}
|
|
604
662
|
onKeyDown={e => (e.key === "Enter" || e.key === " ") && onClick && onClick(item.id)}
|
|
605
|
-
|
|
663
|
+
// SideNavItem.List is a fixed 48 tall (was min 44).
|
|
664
|
+
style={{ display: "flex", alignItems: "center", height: 48, minHeight: L.itemH, width: "100%",
|
|
606
665
|
borderRadius: T.radius, backgroundColor: bg, cursor: "pointer",
|
|
607
666
|
overflow: "hidden", transition: "background-color var(--motion-duration-3) var(--motion-easing-standard)",
|
|
608
667
|
userSelect: "none" }}>
|
|
609
|
-
|
|
668
|
+
{/* Figma places this stripe with indicator.stripe's 6px inset only, so it
|
|
669
|
+
is 36px on the 48px row. */}
|
|
670
|
+
<IndicatorStripe visible={isActive} inset={L.stripeInnerPadV} />
|
|
610
671
|
<div style={{ width: L.iconWrap, height: L.iconWrap, display: "flex",
|
|
611
672
|
alignItems: "center", justifyContent: "center", flexShrink: 0,
|
|
612
673
|
marginLeft: L.rowPadH }}>
|
|
613
674
|
<BulletDot color={dotColor} />
|
|
614
675
|
</div>
|
|
615
|
-
|
|
676
|
+
{/* text.label padding 8 both sides, then container.rowStart's 12. */}
|
|
677
|
+
<div style={{ flex: 1, minWidth: 0, paddingLeft: L.textPad, paddingRight: `calc(${L.textPad} + ${L.rowPadH})` }}>
|
|
616
678
|
<p style={{ fontFamily: "'Red Hat Text',sans-serif", fontWeight: 400,
|
|
617
679
|
fontSize: "var(--semantic-type-font-size-xs)", lineHeight: "var(--semantic-type-line-height-xs-single)", letterSpacing: "var(--semantic-type-letter-spacing-extraspacious)", color,
|
|
618
680
|
margin: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
|
|
@@ -777,7 +839,7 @@ export function SideNav({
|
|
|
777
839
|
boxSizing: "border-box", flexShrink: 0,
|
|
778
840
|
// Smooth-spring curve, soft personality, no overshoot.
|
|
779
841
|
transition: "width var(--motion-duration-6) var(--motion-easing-emphasized)",
|
|
780
|
-
borderRight: `
|
|
842
|
+
borderRight: `1px solid ${T.fill.infoSubtle}`,
|
|
781
843
|
overflow: "hidden", // the inner menu scrolls; the nav itself does not
|
|
782
844
|
}}>
|
|
783
845
|
{/* NavHeader, TOP of nav, PINNED (flexShrink:0) so it stays visible while the
|
|
@@ -897,17 +959,14 @@ export function SideNav({
|
|
|
897
959
|
}
|
|
898
960
|
|
|
899
961
|
// ─── Built-in chevron icons ───────────────────────────────────────────────────
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
function ChevUp({ size = 10, color }) {
|
|
962
|
+
// Material Symbols Rounded expand_more, the exact glyph Figma places in
|
|
963
|
+
// Container.HasChildren (node 40014002:8313), 12px box. The hand-drawn stroke
|
|
964
|
+
// chevron it replaces was heavier and 10px. Up is the same glyph rotated, which
|
|
965
|
+
// is what the item already does when it opens.
|
|
966
|
+
function ChevDown({ size = 12, color }) {
|
|
908
967
|
return (
|
|
909
|
-
<svg width={size} height={size} viewBox="0 0
|
|
910
|
-
<path d="
|
|
968
|
+
<svg width={size} height={size} viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
969
|
+
<path d="M6 7.475C5.93333 7.475 5.87083 7.46458 5.8125 7.44375C5.75417 7.42292 5.7 7.3875 5.65 7.3375L3.35 5.0375C3.25833 4.94583 3.2125 4.82917 3.2125 4.6875C3.2125 4.54583 3.25833 4.42917 3.35 4.3375C3.44167 4.24583 3.55833 4.2 3.7 4.2C3.84167 4.2 3.95833 4.24583 4.05 4.3375L6 6.2875L7.95 4.3375C8.04167 4.24583 8.15833 4.2 8.3 4.2C8.44167 4.2 8.55833 4.24583 8.65 4.3375C8.74167 4.42917 8.7875 4.54583 8.7875 4.6875C8.7875 4.82917 8.74167 4.94583 8.65 5.0375L6.35 7.3375C6.3 7.3875 6.24583 7.42292 6.1875 7.44375C6.12917 7.46458 6.06667 7.475 6 7.475Z" fill={color} />
|
|
911
970
|
</svg>
|
|
912
971
|
);
|
|
913
972
|
}
|
|
@@ -150,7 +150,11 @@ export const T = {
|
|
|
150
150
|
// the dark bar; Jo had Figma rebound to the OrgName token that day.
|
|
151
151
|
orgSubText: SCD("foreground-action-ghost-rest"),
|
|
152
152
|
avatarBg: SCD("fill-action-primary-subtle-rest"),
|
|
153
|
+
avatarBgHover: SCD("fill-action-primary-subtle-hover"),
|
|
154
|
+
avatarBgPressed: SCD("fill-action-primary-subtle-pressed"),
|
|
153
155
|
avatarText: SCD("foreground-action-primary-on-subtle-rest"),
|
|
156
|
+
avatarTextHover: SCD("foreground-action-primary-on-subtle-hover"),
|
|
157
|
+
avatarTextPressed: SCD("foreground-action-primary-on-subtle-pressed"),
|
|
154
158
|
// White dropdown-menu surface, tracks fill-neutral-light (now warm-neutral-0).
|
|
155
159
|
panelBg: SCL("fill-static-neutral-faint"),
|
|
156
160
|
activeItem: SCL("fill-action-primary-subtle-rest"),
|
|
@@ -180,7 +184,7 @@ export const L = {
|
|
|
180
184
|
navH: c("topnav-height"),
|
|
181
185
|
navGap: r("topnav-gap-horizontal"),
|
|
182
186
|
touchTarget: 48, modInnerH: 36,
|
|
183
|
-
orgAvatarNav:
|
|
187
|
+
orgAvatarNav: 16, orgAvatarSm: 24, orgAvatarPanel: 32,
|
|
184
188
|
searchPill: 32, avatarSize: 32,
|
|
185
189
|
// ICON GEOMETRY, read off the leaf nodes of Figma TopNav.Global 40007067:6508
|
|
186
190
|
// on 2026-10-02 rather than inferred from container padding:
|
|
@@ -393,6 +397,14 @@ export function TopNavActions({ breakpoint = "desktop", actions, onNotifications
|
|
|
393
397
|
);
|
|
394
398
|
}
|
|
395
399
|
|
|
400
|
+
// SideNav.Control's hover and pressed (Figma 40007082:7703): Fill/Action/Ghost on
|
|
401
|
+
// the 32 layer and a Foreground/Action/Ghost glyph. ActionIcon's own ladder is
|
|
402
|
+
// for light pages and painted a pale layer under a dark glyph on the navy bar.
|
|
403
|
+
const GHOST_ACTION_STATES = {
|
|
404
|
+
hover: T.monoHover, pressed: T.monoPressed,
|
|
405
|
+
layerHover: T.controlHover, layerPress: T.controlPressed,
|
|
406
|
+
};
|
|
407
|
+
|
|
396
408
|
// ─── TopNavProfile ─────────────────────────────────────────────────────────────
|
|
397
409
|
export function TopNavProfile({ user, open, onToggle, mobile = false }) {
|
|
398
410
|
const [hov, setHov] = useState(false);
|
|
@@ -410,21 +422,27 @@ export function TopNavProfile({ user, open, onToggle, mobile = false }) {
|
|
|
410
422
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
411
423
|
// Figma TopNav.Profile is 48x48 with pad 4,4,4,4. This was 44 with 6.
|
|
412
424
|
width: L.touchTarget, height: L.touchTarget, boxSizing: "border-box",
|
|
413
|
-
|
|
425
|
+
// The 48 box takes no fill in any state (Figma TopNav.Profile,
|
|
426
|
+
// 40016590:37966): hover and pressed land on the avatar itself, on
|
|
427
|
+
// the Primary/Subtle ladder. This used to paint a Ghost circle
|
|
428
|
+
// behind an avatar that never changed.
|
|
429
|
+
background: "transparent",
|
|
414
430
|
border: "none", borderRadius: "50%", cursor: "pointer", padding: L.profilePad,
|
|
415
431
|
transition: "background var(--motion-duration-2) var(--motion-easing-standard)",
|
|
416
432
|
}}
|
|
417
433
|
>
|
|
418
434
|
<div style={{
|
|
419
435
|
width: L.avatarSize, height: L.avatarSize, borderRadius: "50%",
|
|
420
|
-
background: T.
|
|
436
|
+
background: open ? T.avatarBgPressed : hov ? T.avatarBgHover : T.avatarBg,
|
|
437
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
438
|
+
transition: "background var(--motion-duration-2) var(--motion-easing-standard), color var(--motion-duration-2) var(--motion-easing-standard)",
|
|
421
439
|
// Text/Body/Small/Semibold at EVERY breakpoint, read 2026-10-30. The
|
|
422
440
|
// mobile 11px this used to drop to is not in Figma. Figma names the
|
|
423
441
|
// 0.3 tracking Letter Spacing/Wide; the CSS emits 0.3 as -spacious.
|
|
424
442
|
fontFamily: "var(--semantic-type-family-brand)",
|
|
425
443
|
fontSize: "var(--semantic-type-font-size-s)", fontWeight: "var(--semantic-type-weight-semibold)",
|
|
426
444
|
lineHeight: "var(--semantic-type-line-height-s-single)", letterSpacing: "var(--semantic-type-letter-spacing-spacious)",
|
|
427
|
-
color: T.avatarText, flexShrink: 0, overflow: "hidden",
|
|
445
|
+
color: open ? T.avatarTextPressed : hov ? T.avatarTextHover : T.avatarText, flexShrink: 0, overflow: "hidden",
|
|
428
446
|
}}>
|
|
429
447
|
{user.avatarUrl
|
|
430
448
|
? <img src={user.avatarUrl} alt="" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
|
@@ -482,7 +500,10 @@ export function OrgSwitcher({ org, open, onToggle, mobile = false, showCampus =
|
|
|
482
500
|
style={{
|
|
483
501
|
// Container.Main: gap Gap/XXXTight between RowStart and RowEnd,
|
|
484
502
|
// padding 6,6,6,12 desktop and 6,4,6,8 mobile, 36 tall.
|
|
485
|
-
|
|
503
|
+
// Desktop places the chevron INSIDE Container.RowStart, whose gap is
|
|
504
|
+
// Gap/XXTight (4); mobile keeps it at Main level on Gap/XXXTight (2).
|
|
505
|
+
display: "flex", alignItems: "center",
|
|
506
|
+
gap: mobile ? "var(--semantic-layout-units-gap-xxxtight)" : "var(--semantic-layout-units-gap-xxtight)",
|
|
486
507
|
minHeight: L.modInnerH, boxSizing: "border-box", minWidth: 0,
|
|
487
508
|
maxWidth: mobile ? L.orgMainMaxMobile : L.orgMainMax,
|
|
488
509
|
padding: mobile
|
|
@@ -495,7 +516,9 @@ export function OrgSwitcher({ org, open, onToggle, mobile = false, showCampus =
|
|
|
495
516
|
// Drawn as an inset shadow because Figma strokes INSIDE the frame:
|
|
496
517
|
// a CSS border adds to the box and made Container.Main 37.5, not 36.
|
|
497
518
|
border: "none",
|
|
498
|
-
|
|
519
|
+
// Hover and pressed thicken to Button/BorderWidth/Selected (1px), as
|
|
520
|
+
// Figma binds them on 40006819:14582 and 40006933:15754.
|
|
521
|
+
boxShadow: `inset 0 0 0 ${open || hov ? "var(--contextual-layout-units-button-borderwidth-selected)" : "var(--contextual-layout-units-button-borderwidth-rest)"} ${open ? T.orgStrokePressed : hov ? T.orgStrokeHover : T.orgStroke}`,
|
|
499
522
|
cursor: "pointer", color: fg, fontFamily: "inherit",
|
|
500
523
|
transition: "background var(--motion-duration-3) var(--motion-easing-standard), box-shadow var(--motion-duration-3) var(--motion-easing-standard)",
|
|
501
524
|
}}
|
|
@@ -505,11 +528,16 @@ export function OrgSwitcher({ org, open, onToggle, mobile = false, showCampus =
|
|
|
505
528
|
height: mobile ? 20 : 24, minWidth: 0 }}>
|
|
506
529
|
{/* Logo avatar, only when a URL is provided (Figma "Show Org Avatar", default false) */}
|
|
507
530
|
{hasLogo && (
|
|
508
|
-
|
|
531
|
+
// Figma "Show Org Avatar": a 24 box with Padding/XXXTight, so a 16
|
|
532
|
+
// avatar with a 1px Stroke/Action/Primary/Strong/Rest ring. It was
|
|
533
|
+
// padded 2, a 20 avatar, ringed in Ghost.
|
|
534
|
+
<div style={{ width: L.orgAvatarSm, height: L.orgAvatarSm, padding: "var(--semantic-layout-units-padding-xxxtight)",
|
|
535
|
+
boxSizing: "border-box",
|
|
509
536
|
display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
|
|
510
537
|
<div style={{
|
|
511
538
|
width: L.orgAvatarNav, height: L.orgAvatarNav, borderRadius: L.radiusSm,
|
|
512
|
-
border: `1px solid ${
|
|
539
|
+
border: `1px solid ${SCD("stroke-action-primary-strong-rest")}`,
|
|
540
|
+
boxSizing: "border-box",
|
|
513
541
|
background: "transparent",
|
|
514
542
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
515
543
|
overflow: "hidden", position: "relative", flexShrink: 0,
|
|
@@ -566,12 +594,13 @@ export function OrgSwitcher({ org, open, onToggle, mobile = false, showCampus =
|
|
|
566
594
|
*/
|
|
567
595
|
export function ModuleSwitcher({ modules, activeId, open, onToggle, breakpoint = "desktop", type = "interactive" }) {
|
|
568
596
|
const [hov, setHov] = useState(false);
|
|
597
|
+
const [press, setPress] = useState(false);
|
|
569
598
|
const active = modules.find(m => m.id === activeId) || modules[0];
|
|
570
599
|
const showLabel = breakpoint === "desktop";
|
|
571
600
|
const isStatic = type === "static";
|
|
572
601
|
// The label follows the state (Ghost Rest, Hover, Pressed for Pressed and
|
|
573
602
|
// Open); the mark and the chevron stay on Rest, as in Figma.
|
|
574
|
-
const labelFg = isStatic ? T.monoBase : open ? T.monoPressed : hov ? T.monoHover : T.monoBase;
|
|
603
|
+
const labelFg = isStatic ? T.monoBase : open || press ? T.monoPressed : hov ? T.monoHover : T.monoBase;
|
|
575
604
|
|
|
576
605
|
// Icon + label: identical in both variants. Container.RowStart, gap Gap/XXTight.
|
|
577
606
|
const inner = (
|
|
@@ -608,7 +637,7 @@ export function ModuleSwitcher({ modules, activeId, open, onToggle, breakpoint =
|
|
|
608
637
|
// Figma ModuleSwitcher Container.Main is pad 0,0,0,6: left only.
|
|
609
638
|
// This was a uniform 4 on all four sides.
|
|
610
639
|
padding: `0 0 0 ${L.modPadL}`, borderRadius: L.radius,
|
|
611
|
-
background: "transparent", border: "
|
|
640
|
+
background: "transparent", border: "none", boxSizing: "border-box",
|
|
612
641
|
color: T.monoBase, fontFamily: "inherit", cursor: "default",
|
|
613
642
|
}}
|
|
614
643
|
>
|
|
@@ -623,24 +652,28 @@ export function ModuleSwitcher({ modules, activeId, open, onToggle, breakpoint =
|
|
|
623
652
|
// The 48 slot, Padding/XXTight by Padding/XXXTight. This was a raw 4px 2px.
|
|
624
653
|
<div style={{ position: "relative", padding: `${L.slotPadV} ${L.slotPadNarrow}` }}>
|
|
625
654
|
<button
|
|
626
|
-
onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)}
|
|
655
|
+
onMouseEnter={() => setHov(true)} onMouseLeave={() => { setHov(false); setPress(false); }}
|
|
656
|
+
onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)} onBlur={() => setPress(false)}
|
|
627
657
|
onClick={onToggle}
|
|
628
658
|
aria-haspopup="listbox" aria-expanded={open}
|
|
629
659
|
aria-label={`Switch module: ${active.label}`}
|
|
630
660
|
style={{
|
|
631
|
-
// Container.Main: gap Gap/XXXTight between RowStart and the chevron
|
|
632
|
-
|
|
661
|
+
// Container.Main: gap Gap/XXXTight between RowStart and the chevron
|
|
662
|
+
// on desktop. With the label hidden (tablet, mobile) Figma has no gap:
|
|
663
|
+
// the mark and the chevron touch.
|
|
664
|
+
display: "flex", alignItems: "center", gap: showLabel ? "var(--semantic-layout-units-gap-xxxtight)" : 0,
|
|
633
665
|
maxHeight: L.modInnerH, minHeight: L.modInnerH,
|
|
634
666
|
// Figma ModuleSwitcher Container.Main is pad 0,0,0,6: left only.
|
|
635
667
|
// This was a uniform 4 on all four sides.
|
|
636
668
|
padding: `0 0 0 ${L.modPadL}`, borderRadius: L.radius,
|
|
637
|
-
background: open ? T.controlPressed : hov ? T.controlHover : "transparent",
|
|
638
|
-
// No stroke in any state
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
|
|
669
|
+
background: open || press ? T.controlPressed : hov ? T.controlHover : "transparent",
|
|
670
|
+
// No stroke in any state: Figma's ModuleSwitcher has none. This was a
|
|
671
|
+
// 1px transparent border, which inset the content by 1px (the mark sat
|
|
672
|
+
// at x=7, not 6). The org switcher draws its stroke as an inset
|
|
673
|
+
// shadow, so the two still share the 36 box height.
|
|
674
|
+
border: "none", boxSizing: "border-box",
|
|
642
675
|
cursor: "pointer", color: T.monoBase, fontFamily: "inherit",
|
|
643
|
-
transition: "background var(--motion-duration-3) var(--motion-easing-standard)
|
|
676
|
+
transition: "background var(--motion-duration-3) var(--motion-easing-standard)",
|
|
644
677
|
}}
|
|
645
678
|
>
|
|
646
679
|
{inner}
|
|
@@ -906,7 +939,7 @@ function TopNavNewCo({ navRef, className, breakpoint, org, user, openPanel, togg
|
|
|
906
939
|
gap: isMobile ? "var(--semantic-layout-units-gap-xxtight)" : "var(--semantic-layout-units-gap-tight)" }}>
|
|
907
940
|
{isMobile ? (
|
|
908
941
|
<>
|
|
909
|
-
<ActionIcon name="menu" size="Large" label="Open navigation menu" color={T.monoBase} onClick={onSideNavToggle} />
|
|
942
|
+
<ActionIcon name="menu" size="Large" label="Open navigation menu" color={T.monoBase} states={GHOST_ACTION_STATES} onClick={onSideNavToggle} />
|
|
910
943
|
<PillarTrigger label={brandItem.label} open={openPanel === "pillar"} onToggle={() => toggle("pillar")} />
|
|
911
944
|
{org_}
|
|
912
945
|
</>
|
|
@@ -1128,6 +1161,7 @@ export function TopNav({
|
|
|
1128
1161
|
size="Large"
|
|
1129
1162
|
label="Open navigation menu"
|
|
1130
1163
|
color={T.monoBase}
|
|
1164
|
+
states={GHOST_ACTION_STATES}
|
|
1131
1165
|
onClick={onSideNavToggle}
|
|
1132
1166
|
/>
|
|
1133
1167
|
)}
|