@helloimjolopez-pathway/pathway-components 0.11.1 → 0.12.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 +3 -1
- package/components/bottom-sheet/bottom-sheet.jsx +4 -4
- package/components/icon/action-icon.jsx +57 -45
- package/components/popover-menu/popover-menu.jsx +1 -1
- package/components/top-nav/top-nav.jsx +3 -3
- package/dist/pathway-accounting-sidenav.js +14 -14
- package/dist/pathway-components.js +109 -109
- package/dist/pathway-sidenav.js +52 -52
- package/package.json +1 -1
|
@@ -160,6 +160,7 @@ const transition = `background ${T.duration} ${T.easing}, color ${T.duration} ${
|
|
|
160
160
|
* @param {string} size Small | Medium | Large
|
|
161
161
|
* @param {string} badgeStyle Outline & Fill | Fill | Outline | Naked
|
|
162
162
|
* @param {string} leadingIcon Material Symbols ligature; omitted means no icon
|
|
163
|
+
* @param {string} selectedIcon Interactive On: the icon in place of the leading icon (Figma "Check" swap); default "check"
|
|
163
164
|
* @param {boolean} showTrailingIcon Interactive only: the drill chevron
|
|
164
165
|
* @param {boolean} selected Interactive, controlled
|
|
165
166
|
* @param {boolean} defaultSelected Interactive, uncontrolled
|
|
@@ -179,6 +180,7 @@ export function Badge({
|
|
|
179
180
|
size = "Small",
|
|
180
181
|
badgeStyle = "Outline & Fill",
|
|
181
182
|
leadingIcon,
|
|
183
|
+
selectedIcon = "check",
|
|
182
184
|
trailingIcon,
|
|
183
185
|
showTrailingIcon = false,
|
|
184
186
|
selected,
|
|
@@ -298,7 +300,7 @@ export function Badge({
|
|
|
298
300
|
const fg = disabled ? C("foreground-action-disabled") : on ? a.on.text : a.off.text[st];
|
|
299
301
|
const stroke = disabled || on || !painted.stroke ? null : a.off.stroke[st];
|
|
300
302
|
const drill = !!href || showTrailingIcon;
|
|
301
|
-
const lead = on ?
|
|
303
|
+
const lead = on ? selectedIcon : leadingIcon;
|
|
302
304
|
const Tag = href ? "a" : "button";
|
|
303
305
|
const handlers = {
|
|
304
306
|
onMouseEnter: () => !disabled && setHovered(true),
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
* BottomSheet.Header 40018716:5340: min height Touch Target/AA,
|
|
20
20
|
* pad 8 / 2 / 4 / 2 (XTight, XXXXTight,
|
|
21
21
|
* XXXTight, XXXXTight), gap XXTight
|
|
22
|
-
* Back (Show Back) Action Icon Size=
|
|
22
|
+
* Back (Show Back) Action Icon Size=Medium, `arrow_back`
|
|
23
23
|
* Container.Title pad left Padding/Base
|
|
24
24
|
* Title Heading/Local/Base/Semibold,
|
|
25
25
|
* Foreground/Static/Neutral/Strong
|
|
26
|
-
* Close Action Icon Size=
|
|
26
|
+
* Close Action Icon Size=Medium, `close`
|
|
27
27
|
* Body (slot) pad 0 / XTight / XTight, gap XXTight,
|
|
28
28
|
* scrolls when taller than the space left
|
|
29
29
|
* Footer (Show Footer) BottomSheet.Footer 40018716:5354: top border
|
|
@@ -160,14 +160,14 @@ export function BottomSheetHeader({ title, titleId, titleRef, showBack = false,
|
|
|
160
160
|
display: "flex", alignItems: "center", gap: L.headerGap, minHeight: L.headerMinH, boxSizing: "border-box",
|
|
161
161
|
padding: `${L.headerPadTop} ${L.headerPadX} ${L.headerPadBottom}`,
|
|
162
162
|
}}>
|
|
163
|
-
{showBack && <ActionIcon name="arrow_back" size="
|
|
163
|
+
{showBack && <ActionIcon name="arrow_back" size="Medium" label={backLabel} onClick={onBack} />}
|
|
164
164
|
<div style={{ flex: 1, minWidth: 0, paddingLeft: L.titleInset }}>
|
|
165
165
|
<h2 id={titleId} ref={titleRef} tabIndex={-1} style={{
|
|
166
166
|
margin: 0, outline: "none", color: T.title, ...TYPE.title,
|
|
167
167
|
overflowWrap: "anywhere", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden",
|
|
168
168
|
}}>{title}</h2>
|
|
169
169
|
</div>
|
|
170
|
-
<ActionIcon name="close" size="
|
|
170
|
+
<ActionIcon name="close" size="Medium" label={closeLabel} onClick={onClose} />
|
|
171
171
|
</div>
|
|
172
172
|
);
|
|
173
173
|
}
|
|
@@ -1,43 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ActionIcon: the icon-only control Pathway puts in a header or a toolbar.
|
|
3
3
|
*
|
|
4
|
-
* SOURCE OF TRUTH: Figma set `Action
|
|
5
|
-
* `↳ ❇️ Action Icon` page.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* one whose absence from Code Connect mattered most.
|
|
4
|
+
* SOURCE OF TRUTH: Figma set `Action.Icon` 40006794:19891, on the
|
|
5
|
+
* `↳ ❇️ Action Icon` page. Fifteen variants on two axes, State (Rest / Hover /
|
|
6
|
+
* Pressed / Disabled / Focused) x Size (Small / Medium / Large), 2026-11-11. Used 30 times
|
|
7
|
+
* inside the Widget set and twice inside KPI Tiles.
|
|
9
8
|
*
|
|
10
9
|
* MEASURED, per Size. The box is the target, `Container.Icon` is the state
|
|
11
10
|
* layer, and the glyph is the frame size an `<Icon>` takes:
|
|
12
11
|
*
|
|
13
|
-
* Size
|
|
14
|
-
* Small
|
|
15
|
-
*
|
|
16
|
-
* Large
|
|
12
|
+
* Size box, Touch Target token Container.Icon glyph
|
|
13
|
+
* Small 36x36 Accessibility/Touch Target/Desktop Only 24x24 r=8 14
|
|
14
|
+
* Medium 44x44 Accessibility/Touch Target/AA 28x28 r=8 16
|
|
15
|
+
* Large 48x48 Accessibility/Touch Target/AAA 32x32 r=8 20
|
|
17
16
|
*
|
|
18
|
-
* STATE TOKENS, read off all
|
|
17
|
+
* STATE TOKENS, read off all nine variants.
|
|
19
18
|
*
|
|
20
19
|
* The state layer's fill:
|
|
21
|
-
*
|
|
20
|
+
* Rest none. Figma's Rest variants carry no Container.Icon at all
|
|
22
21
|
* Hover `Fill/Action/Primary/Subtle/Hover`
|
|
23
22
|
* Pressed `Fill/Action/Primary/Subtle/Pressed`
|
|
24
23
|
*
|
|
25
24
|
* The glyph's colour:
|
|
26
|
-
*
|
|
25
|
+
* Rest `Foreground/Action/Secondary/Rest`
|
|
27
26
|
* Hover `Foreground/Action/Secondary/Hover`
|
|
28
27
|
* Pressed `Foreground/Action/Secondary/Pressed`
|
|
29
28
|
*
|
|
30
|
-
*
|
|
29
|
+
* The glyph on Disabled: `Foreground/Action/Disabled`, no layer fill, no hover
|
|
30
|
+
* or press. It keeps `aria-disabled` rather than the native attribute, so Tab
|
|
31
|
+
* still reaches it and its name is still read.
|
|
32
|
+
* Focused (keyboard only): Rest, plus the focus ring around the state layer:
|
|
33
|
+
* `Stroke/FocusRing/Base` at `FocusRing/Width`, hugging the layer (no gap).
|
|
34
|
+
*
|
|
35
|
+
* `State` (Rest / Hover / Pressed / Disabled / Focused) is runtime; only
|
|
36
|
+
* Disabled is a prop, `disabled`, because it is the one a consumer decides.
|
|
31
37
|
* `Icon` is an INSTANCE_SWAP, which in code is the `name` prop: any Material
|
|
32
38
|
* Symbols ligature.
|
|
33
39
|
*
|
|
34
|
-
* THE COMPONENT ITSELF BINDS NO FILL, in any of the nine variants, and the
|
|
35
|
-
* glyph's fill is unbound too so it inherits. That is why this takes its
|
|
36
|
-
* resting colour from `color` on the button and lets the glyph inherit it: the
|
|
37
|
-
* design expresses "whatever the surrounding context is" rather than a fixed
|
|
38
|
-
* icon colour, and a hard-coded one would break the Widget header, the KPI
|
|
39
|
-
* tile and a dark toolbar all differently.
|
|
40
|
-
*
|
|
41
40
|
* WHY IT LIVES HERE rather than in widget.jsx, where it used to. It was a
|
|
42
41
|
* private helper inside the Widget, so the Figma component had nothing
|
|
43
42
|
* importable to point at and the KPI Tile could not reuse it. The Widget now
|
|
@@ -52,21 +51,18 @@ injectFocusRing();
|
|
|
52
51
|
const C = (n) => `var(--semantic-color-${n})`;
|
|
53
52
|
const U = (n) => `var(--semantic-layout-units-${n})`;
|
|
54
53
|
const M = (n) => `var(--motion-${n})`;
|
|
54
|
+
const CL = (n) => `var(--contextual-layout-units-${n})`;
|
|
55
55
|
|
|
56
|
-
/*
|
|
57
|
-
`layer` is the state-layer frame the hover
|
|
58
|
-
the nested Material Symbols instance.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
rendered small inside a correctly sized target: the Widget header's 16px
|
|
63
|
-
ellipsis drew at 14. The three state-layer sizes were right, which is why it
|
|
64
|
-
survived a measurement pass that compared targets and layers and never looked
|
|
65
|
-
at the glyph. Figma's nested instance is 14 at Small, 16 at Base, 18 at Large. */
|
|
56
|
+
/* Per Size, read off every variant on 2026-11-11. `box` is the button, bound in
|
|
57
|
+
Figma to the Touch Target tokens; `layer` is the state-layer frame the hover
|
|
58
|
+
and pressed fills go on; `glyph` is the nested Material Symbols instance. The
|
|
59
|
+
numbers are kept beside the tokens for the stories and for anything that has
|
|
60
|
+
to do arithmetic with the box. Large's glyph is 20, not the 18 it was. */
|
|
61
|
+
const TT = (n) => `var(--semantic-layout-units-accessibility-touch-target-${n})`;
|
|
66
62
|
export const ACTION_ICON_SIZES = {
|
|
67
|
-
Small:
|
|
68
|
-
|
|
69
|
-
Large:
|
|
63
|
+
Small: { box: 36, boxW: TT("desktop-only-width"), boxH: TT("desktop-only-height"), layer: 24, glyph: 14 },
|
|
64
|
+
Medium: { box: 44, boxW: TT("aa-width"), boxH: TT("aa-height"), layer: 28, glyph: 16 },
|
|
65
|
+
Large: { box: 48, boxW: TT("aaa-width"), boxH: TT("aaa-height"), layer: 32, glyph: 20 },
|
|
70
66
|
};
|
|
71
67
|
|
|
72
68
|
export const T = {
|
|
@@ -81,11 +77,15 @@ export const T = {
|
|
|
81
77
|
the hover state is the only thing the layer is for. */
|
|
82
78
|
layerHover: C("fill-action-primary-subtle-hover"),
|
|
83
79
|
layerPress: C("fill-action-primary-subtle-pressed"),
|
|
80
|
+
disabled: C("foreground-action-disabled"),
|
|
81
|
+
focusOutline: `${CL("focusring-width")} solid ${C("stroke-focusring-base")}`,
|
|
84
82
|
};
|
|
85
83
|
|
|
86
84
|
/**
|
|
87
85
|
* @param {string} name Material Symbols ligature, Figma's `Icon` swap.
|
|
88
|
-
* @param {"Small"|"
|
|
86
|
+
* @param {"Small"|"Medium"|"Large"} size Figma's Size axis, same words.
|
|
87
|
+
* Small is Desktop Only (36): dense desktop surfaces
|
|
88
|
+
* only. Medium meets AA (44). Large meets AAA (48).
|
|
89
89
|
* @param {string} label Required: an icon-only control with no accessible
|
|
90
90
|
* name is unusable, so this is not optional.
|
|
91
91
|
* @param {0|1} fill FILL axis. The Widget header is FILL 0.
|
|
@@ -99,6 +99,11 @@ export const T = {
|
|
|
99
99
|
* dark Secondary glyph, which is right on a light page and
|
|
100
100
|
* wrong on the navy TopNav. SideNav.Control (40007082:7703)
|
|
101
101
|
* binds the Ghost ladder instead.
|
|
102
|
+
* @param {boolean} disabled Figma's Disabled state. aria-disabled, never the
|
|
103
|
+
* native attribute: it stays in the tab order.
|
|
104
|
+
* @param {"hover"|"pressed"|"focused"} forceState Stories and demos only: draws
|
|
105
|
+
* a state without the pointer or keyboard, so the state
|
|
106
|
+
* matrix can show every variant Figma has at once.
|
|
102
107
|
*/
|
|
103
108
|
export function ActionIcon({
|
|
104
109
|
name,
|
|
@@ -107,26 +112,29 @@ export function ActionIcon({
|
|
|
107
112
|
fill = 0,
|
|
108
113
|
color,
|
|
109
114
|
states,
|
|
115
|
+
forceState,
|
|
116
|
+
disabled = false,
|
|
110
117
|
onClick,
|
|
111
|
-
disabled,
|
|
112
118
|
className = "",
|
|
113
119
|
style,
|
|
114
120
|
...rest
|
|
115
121
|
}) {
|
|
116
122
|
const noMotion = useReducedMotion();
|
|
117
|
-
const [
|
|
118
|
-
const [
|
|
123
|
+
const [hovRaw, setHov] = useState(false);
|
|
124
|
+
const [pressRaw, setPress] = useState(false);
|
|
125
|
+
const hov = !disabled && (hovRaw || forceState === "hover");
|
|
126
|
+
const press = !disabled && (pressRaw || forceState === "pressed");
|
|
119
127
|
const s = ACTION_ICON_SIZES[size] || ACTION_ICON_SIZES.Small;
|
|
120
128
|
const st = { ...T, ...states };
|
|
121
|
-
const fg = disabled ? T.
|
|
129
|
+
const fg = disabled ? T.disabled : press ? st.pressed : hov ? st.hover : (color || T.rest);
|
|
122
130
|
|
|
123
131
|
return (
|
|
124
132
|
<button
|
|
125
133
|
type="button"
|
|
126
134
|
aria-label={label}
|
|
127
135
|
title={label}
|
|
128
|
-
disabled={disabled}
|
|
129
|
-
onClick={onClick}
|
|
136
|
+
aria-disabled={disabled || undefined}
|
|
137
|
+
onClick={disabled ? undefined : onClick}
|
|
130
138
|
onMouseEnter={() => setHov(true)}
|
|
131
139
|
onMouseLeave={() => { setHov(false); setPress(false); }}
|
|
132
140
|
onMouseDown={() => setPress(true)}
|
|
@@ -136,10 +144,9 @@ export function ActionIcon({
|
|
|
136
144
|
data-size={size}
|
|
137
145
|
style={{
|
|
138
146
|
display: "inline-flex", alignItems: "center", justifyContent: "center",
|
|
139
|
-
width: s.
|
|
140
|
-
background: "transparent", border: "none", padding: 0,
|
|
141
|
-
cursor: disabled ? "
|
|
142
|
-
opacity: disabled ? 0.5 : 1,
|
|
147
|
+
width: s.boxW, height: s.boxH, flexShrink: 0, boxSizing: "border-box",
|
|
148
|
+
background: "transparent", border: "none", padding: 0, outline: "none",
|
|
149
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
143
150
|
color: fg,
|
|
144
151
|
...style,
|
|
145
152
|
}}
|
|
@@ -153,7 +160,12 @@ export function ActionIcon({
|
|
|
153
160
|
display: "inline-flex", alignItems: "center", justifyContent: "center",
|
|
154
161
|
width: s.layer, height: s.layer, borderRadius: U("cornerradius-base"),
|
|
155
162
|
background: press ? st.layerPress : hov ? st.layerHover : "transparent",
|
|
156
|
-
|
|
163
|
+
/* Focused sits AROUND THE STATE LAYER, not the target: Jo, 2026-11-11.
|
|
164
|
+
Keyboard focus draws it through FOCUS_RING_LAYER (hugging the layer,
|
|
165
|
+
no gap); forceState draws the same ring for stories and demos. */
|
|
166
|
+
outline: !disabled && forceState === "focused" ? T.focusOutline : undefined,
|
|
167
|
+
outlineOffset: !disabled && forceState === "focused" ? 0 : undefined,
|
|
168
|
+
transition: noMotion ? "none" : `background ${M("duration-2")} ${M("easing-standard")}, outline ${M("duration-2")} ${M("easing-standard")}`,
|
|
157
169
|
}}>
|
|
158
170
|
<Icon name={name} size={s.glyph} fill={fill} style={{
|
|
159
171
|
transition: noMotion ? "none" : `color ${M("duration-2")} ${M("easing-standard")}`,
|
|
@@ -703,7 +703,7 @@ export function PopoverMenu({
|
|
|
703
703
|
} else if (type === "avatar") {
|
|
704
704
|
trigger = <button type="button" className="pw-menu-trigger__avatar" {...triggerProps} aria-label={label}>{initials}</button>;
|
|
705
705
|
} else if (iconStyle === "action") {
|
|
706
|
-
trigger = <ActionIcon name={icon} size="
|
|
706
|
+
trigger = <ActionIcon name={icon} size="Medium" label={label} {...triggerProps} />;
|
|
707
707
|
} else {
|
|
708
708
|
trigger = (
|
|
709
709
|
<button type="button" className="pw-menu-trigger__icon" {...triggerProps} aria-label={label} title={label}
|
|
@@ -221,7 +221,7 @@ export const L = {
|
|
|
221
221
|
chevronGlyph: 12,
|
|
222
222
|
modChevronBox: 20,
|
|
223
223
|
orgChevronBox: 24,
|
|
224
|
-
// Action Icon Size=
|
|
224
|
+
// Action Icon Size=Medium: 16 glyph in a 28 Container.Icon. The glyph was 14,
|
|
225
225
|
// which also put it below the icon font's smallest optical size.
|
|
226
226
|
actionIconBox: 28, actionIconGlyph: 16,
|
|
227
227
|
// Figma's Action Icon is 44, not the bar's 48. It is the one control on the
|
|
@@ -336,7 +336,7 @@ export const DEFAULT_MODULES = MODULES.map((id) => ({ id, label: MODULE_LABELS[i
|
|
|
336
336
|
* TopNav.Actions, Figma 40007082:7313. A SLOT (Figma property "Slot.Actions"),
|
|
337
337
|
* not a fixed pair of bells: a product puts whatever action icons it needs in
|
|
338
338
|
* it, one bell, or a bell, a cog and a help icon. Each item renders as Figma's
|
|
339
|
-
* nested Action Icon at Size=
|
|
339
|
+
* nested Action Icon at Size=Medium.
|
|
340
340
|
*
|
|
341
341
|
* actions Array<{ icon, label, onClick }>. `icon` is a Material Symbols
|
|
342
342
|
* ligature, `label` the accessible name. Defaults to the two
|
|
@@ -346,7 +346,7 @@ export const DEFAULT_MODULES = MODULES.map((id) => ({ id, label: MODULE_LABELS[i
|
|
|
346
346
|
* ("More actions"), which is what Figma's mobile variant shows. One action on
|
|
347
347
|
* its own stays as itself.
|
|
348
348
|
*
|
|
349
|
-
* Action Icon Size=
|
|
349
|
+
* Action Icon Size=Medium, read 2026-10-30 and 2026-11-02:
|
|
350
350
|
* target accessibility-touch-target-aa (44)
|
|
351
351
|
* Container.Icon 28, cornerradius-base: rest none, hover
|
|
352
352
|
* Fill/Action/Ghost/Hover, pressed Fill/Action/Ghost/Pressed
|