@helloimjolopez-pathway/pathway-components 0.10.0 → 0.10.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 +5 -5
- package/components/bottom-sheet/bottom-sheet.jsx +14 -18
- package/components/popover-menu/popover-menu.jsx +747 -0
- package/components/spinner/spinner.jsx +205 -0
- package/components/toggle-button/toggle-button.jsx +16 -6
- package/dist/pathway-accounting-sidenav.js +195 -8
- package/dist/pathway-components.js +226 -99
- package/dist/pathway-sidenav.js +195 -8
- package/package.json +4 -3
- package/components/dropdown/dropdown.jsx +0 -482
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* spinner.jsx: the React module Spinner never had, with NewCo's ring loader.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS
|
|
7
|
+
* Spinner shipped as HTML/SVG/CSS only; the manifest said a .jsx wrapper was on
|
|
8
|
+
* the backlog. Jo asked for NewCo's spinner and loading states to be brought
|
|
9
|
+
* into the repo, and NewCo's loader is a RING, not the sunburst Amplify has. So
|
|
10
|
+
* both live here as variants rather than as two components: a caller picks the
|
|
11
|
+
* shape, and everything else about the contract is shared.
|
|
12
|
+
*
|
|
13
|
+
* WHERE THE NEWCO DESIGN CAME FROM
|
|
14
|
+
* The NewCo and Spired design concept. It is NOT in the newco-ds repo (checked
|
|
15
|
+
* local and origin, no spinner, skeleton or shimmer at any depth and no commit
|
|
16
|
+
* mentioning them), so the geometry and timings are transcribed from the
|
|
17
|
+
* concept: three arcs at 0.32, 0.19 and 0.09 of the circumference, offset 0,
|
|
18
|
+
* 0.4 and 0.68, turning at 1.4s linear. It reads as the mark's DNA, one long,
|
|
19
|
+
* one medium and one short pill, arranged in a ring.
|
|
20
|
+
*
|
|
21
|
+
* WHAT CHANGED COMING ACROSS, AND WHY IT IS BETTER
|
|
22
|
+
* The concept painted its three arcs with hardcoded NewCo purples: three rungs of a `purple` primitive ramp that
|
|
23
|
+
* Pathway does not have at all (and #6E64BE is in fact NewCo's own Brand/400).
|
|
24
|
+
* Here the arcs are currentColor at
|
|
25
|
+
* three stroke opacities, so the ring inherits whatever the tone and emphasis
|
|
26
|
+
* resolve to. That means it renders Amplify blue under Amplify and NewCo purple
|
|
27
|
+
* under NewCo with no branching, and it also answers every tone the sunburst
|
|
28
|
+
* does, which the hardcoded version could not.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** The tones Spinner answers to. Mirrors spinner.html. */
|
|
32
|
+
export const TONES = [
|
|
33
|
+
"neutral", "brand", "info", "warning", "danger", "negative", "positive",
|
|
34
|
+
"accent-amethyst", "accent-jade", "accent-seabreeze",
|
|
35
|
+
];
|
|
36
|
+
export const EMPHASES = ["light", "subtle", "base", "contrast", "bold"];
|
|
37
|
+
|
|
38
|
+
const c = (n) => `var(--semantic-color-${n})`;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* tone + emphasis -> the ONE colour token the spinner takes.
|
|
42
|
+
*
|
|
43
|
+
* Only `neutral` has a real emphasis ramp. Every other tone resolves to a
|
|
44
|
+
* single token at all five emphases, and that is deliberate rather than
|
|
45
|
+
* unfinished: those families have no foreground ladder to ramp through. The
|
|
46
|
+
* prop is still accepted so a caller can pass emphasis uniformly without
|
|
47
|
+
* special-casing neutral.
|
|
48
|
+
*/
|
|
49
|
+
const TONE = {
|
|
50
|
+
neutral: {
|
|
51
|
+
light: c("foreground-static-neutral-faint"),
|
|
52
|
+
subtle: c("foreground-static-neutral-base"),
|
|
53
|
+
base: c("foreground-static-neutral-base"),
|
|
54
|
+
contrast: c("foreground-static-neutral-strong"),
|
|
55
|
+
bold: c("foreground-static-neutral-bold"),
|
|
56
|
+
},
|
|
57
|
+
brand: c("foreground-action-primary-on-subtle-rest"),
|
|
58
|
+
info: c("foreground-static-info-on-subtle"),
|
|
59
|
+
warning: c("foreground-static-attention-on-subtle"),
|
|
60
|
+
danger: c("foreground-static-negative-on-subtle"),
|
|
61
|
+
negative: c("foreground-static-negative-on-subtle"),
|
|
62
|
+
positive: c("foreground-static-positive-on-subtle"),
|
|
63
|
+
"accent-amethyst": c("foreground-static-info-on-subtle"),
|
|
64
|
+
"accent-jade": c("foreground-static-accent-jade-on-subtle"),
|
|
65
|
+
"accent-seabreeze": c("foreground-static-accent-seabreeze-on-subtle"),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function colourFor(tone, emphasis) {
|
|
69
|
+
const entry = TONE[tone] ?? TONE.neutral;
|
|
70
|
+
if (typeof entry === "string") return entry;
|
|
71
|
+
return entry[emphasis] ?? entry.base;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Amplify's sunburst. Eight spokes on a 12x12 viewBox with an opacity ladder
|
|
76
|
+
* from 1.00 down to 0.12, which breaks the 8-fold rotational symmetry so that
|
|
77
|
+
* rotating the whole SVG reads as a head with a fading trail.
|
|
78
|
+
*/
|
|
79
|
+
const SUNBURST = [
|
|
80
|
+
{ d: "M6 1V3", opacity: 1.0 },
|
|
81
|
+
{ d: "M8.1 3.9L9.55 2.45", opacity: 0.87 },
|
|
82
|
+
{ d: "M9 6H11", opacity: 0.75 },
|
|
83
|
+
{ d: "M8.1 8.1L9.55 9.55", opacity: 0.62 },
|
|
84
|
+
{ d: "M6 9V11", opacity: 0.5 },
|
|
85
|
+
{ d: "M2.45 9.55L3.9 8.1", opacity: 0.37 },
|
|
86
|
+
{ d: "M1 6H3", opacity: 0.25 },
|
|
87
|
+
{ d: "M2.45 2.45L3.9 3.9", opacity: 0.12 },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/** NewCo's ring: one long, one medium, one short arc. */
|
|
91
|
+
const RING = [
|
|
92
|
+
{ frac: 0.32, off: 0.0, opacity: 1 },
|
|
93
|
+
{ frac: 0.19, off: 0.4, opacity: 0.7 },
|
|
94
|
+
{ frac: 0.09, off: 0.68, opacity: 0.45 },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {"ring"|"sunburst"} variant ring is NewCo's, sunburst is Amplify's
|
|
99
|
+
* @param {number|string} size px number or any CSS length; default 1em
|
|
100
|
+
* @param {string} tone one of TONES
|
|
101
|
+
* @param {string} emphasis one of EMPHASES; only neutral ramps
|
|
102
|
+
* @param {string} label announced by screen readers
|
|
103
|
+
*/
|
|
104
|
+
export function Spinner({
|
|
105
|
+
variant = "ring",
|
|
106
|
+
size,
|
|
107
|
+
tone = "neutral",
|
|
108
|
+
emphasis = "base",
|
|
109
|
+
label = "Loading",
|
|
110
|
+
className,
|
|
111
|
+
style,
|
|
112
|
+
}) {
|
|
113
|
+
// Invalid values are surfaced loudly rather than rendering as `color: unset`,
|
|
114
|
+
// which is invisible and would look like the spinner simply failed to paint.
|
|
115
|
+
if (!TONES.includes(tone)) {
|
|
116
|
+
console.error(`[Spinner] Invalid tone "${tone}". Allowed: ${TONES.join(", ")}`);
|
|
117
|
+
}
|
|
118
|
+
if (!EMPHASES.includes(emphasis)) {
|
|
119
|
+
console.error(`[Spinner] Invalid emphasis "${emphasis}". Allowed: ${EMPHASES.join(", ")}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const box = size == null ? "1em" : typeof size === "number" ? `${size}px` : size;
|
|
123
|
+
const common = {
|
|
124
|
+
display: "inline-flex", alignItems: "center", justifyContent: "center",
|
|
125
|
+
width: box, height: box, flexShrink: 0,
|
|
126
|
+
color: colourFor(tone, emphasis),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// 24 units of viewBox for the ring so the stroke maths stay whole numbers.
|
|
130
|
+
const V = 24, STROKE = 2.5;
|
|
131
|
+
const r = (V - STROKE) / 2;
|
|
132
|
+
const circ = 2 * Math.PI * r;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<span
|
|
136
|
+
role="status"
|
|
137
|
+
aria-live="polite"
|
|
138
|
+
aria-label={label}
|
|
139
|
+
className={className}
|
|
140
|
+
data-tone={tone}
|
|
141
|
+
data-emphasis={emphasis}
|
|
142
|
+
data-variant={variant}
|
|
143
|
+
style={{ ...common, ...style }}
|
|
144
|
+
>
|
|
145
|
+
{variant === "sunburst" ? (
|
|
146
|
+
<svg
|
|
147
|
+
viewBox="0 0 12 12" width="100%" height="100%" aria-hidden="true" focusable="false"
|
|
148
|
+
style={{ animation: "pw-spin var(--motion-duration-loop) var(--motion-easing-linear) infinite" }}
|
|
149
|
+
>
|
|
150
|
+
{SUNBURST.map((s, i) => (
|
|
151
|
+
<path key={i} d={s.d} opacity={s.opacity} stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
152
|
+
))}
|
|
153
|
+
</svg>
|
|
154
|
+
) : (
|
|
155
|
+
<svg
|
|
156
|
+
viewBox={`0 0 ${V} ${V}`} width="100%" height="100%" fill="none" aria-hidden="true" focusable="false"
|
|
157
|
+
// 1.4s linear, from the concept. Linear is correct for a continuous
|
|
158
|
+
// loop: any eased curve makes a constant rotation appear to stutter
|
|
159
|
+
// once per revolution. motion.css reserves easing-linear for exactly
|
|
160
|
+
// this and says so.
|
|
161
|
+
style={{ animation: "pw-spin 1400ms var(--motion-easing-linear) infinite" }}
|
|
162
|
+
>
|
|
163
|
+
{RING.map((s, i) => (
|
|
164
|
+
<circle
|
|
165
|
+
key={i}
|
|
166
|
+
cx={V / 2} cy={V / 2} r={r}
|
|
167
|
+
stroke="currentColor" strokeOpacity={s.opacity}
|
|
168
|
+
strokeWidth={STROKE} strokeLinecap="round"
|
|
169
|
+
strokeDasharray={`${s.frac * circ} ${circ}`}
|
|
170
|
+
strokeDashoffset={-s.off * circ}
|
|
171
|
+
/>
|
|
172
|
+
))}
|
|
173
|
+
</svg>
|
|
174
|
+
)}
|
|
175
|
+
<span
|
|
176
|
+
style={{
|
|
177
|
+
position: "absolute", width: 1, height: 1, padding: 0, margin: -1,
|
|
178
|
+
overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap", border: 0,
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
{label}
|
|
182
|
+
</span>
|
|
183
|
+
</span>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The keyframes both variants need, plus the reduced-motion opt-out.
|
|
189
|
+
*
|
|
190
|
+
* Returned as a component rather than injected on import, because a module with
|
|
191
|
+
* an import side effect cannot be tree-shaken and would push CSS into any
|
|
192
|
+
* bundle that merely referenced the file. Mount it once per document.
|
|
193
|
+
*/
|
|
194
|
+
export function SpinnerKeyframes() {
|
|
195
|
+
return (
|
|
196
|
+
<style>{`
|
|
197
|
+
@keyframes pw-spin { to { transform: rotate(360deg); } }
|
|
198
|
+
@media (prefers-reduced-motion: reduce) {
|
|
199
|
+
[role="status"][data-variant] svg { animation: none !important; }
|
|
200
|
+
}
|
|
201
|
+
`}</style>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default Spinner;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
17
17
|
import { Tooltip } from "../tooltip/tooltip.jsx";
|
|
18
|
-
import {
|
|
18
|
+
import { PopoverMenu, PopoverMenuItem } from "../popover-menu/popover-menu.jsx";
|
|
19
19
|
import { Icon } from "../icon/icon.jsx";
|
|
20
20
|
import { useReducedMotion } from "../_shared/use-reduced-motion.jsx";
|
|
21
21
|
|
|
@@ -290,6 +290,10 @@ export function ToggleButton({
|
|
|
290
290
|
const btnRefs = useRef({});
|
|
291
291
|
const [fit, setFit] = useState({ visible: options.map((_, i) => i), hidden: [], natural: null });
|
|
292
292
|
const [focusAfter, setFocusAfter] = useState(null);
|
|
293
|
+
/* An option picked from More takes focus once the menu has closed. The menu
|
|
294
|
+
returns focus to its trigger as it goes, so the hand-over waits for that
|
|
295
|
+
return instead of racing it. */
|
|
296
|
+
const focusOnReturn = useRef(null);
|
|
293
297
|
|
|
294
298
|
const refit = useCallback(() => {
|
|
295
299
|
const wrap = wrapRef.current, meas = measureRef.current;
|
|
@@ -394,11 +398,17 @@ export function ToggleButton({
|
|
|
394
398
|
</div>
|
|
395
399
|
|
|
396
400
|
{showMore && (
|
|
397
|
-
<
|
|
401
|
+
<PopoverMenu
|
|
398
402
|
align="end"
|
|
399
403
|
label={iconOnly ? "More options" : moreLabel}
|
|
400
404
|
renderTrigger={({ title, ...trigger }) => (
|
|
401
405
|
<ToggleButtonOption {...shared} {...trigger} as="button" fill={false}
|
|
406
|
+
onFocus={() => {
|
|
407
|
+
if (focusOnReturn.current == null) return;
|
|
408
|
+
const v = focusOnReturn.current;
|
|
409
|
+
focusOnReturn.current = null;
|
|
410
|
+
setFocusAfter(v);
|
|
411
|
+
}}
|
|
402
412
|
label={iconOnly ? "More options" : moreLabel} icon={iconOnly ? "more_horiz" : undefined}
|
|
403
413
|
position={positionOf(count - 1)} />
|
|
404
414
|
)}
|
|
@@ -406,12 +416,12 @@ export function ToggleButton({
|
|
|
406
416
|
{hidden.map((i) => {
|
|
407
417
|
const o = options[i];
|
|
408
418
|
return (
|
|
409
|
-
<
|
|
410
|
-
disabled={!!o.disabled}
|
|
411
|
-
|
|
419
|
+
<PopoverMenuItem key={o.value} id={o.value} label={o.label} leading={showIcon ? o.icon : undefined}
|
|
420
|
+
disabled={!!o.disabled} reason={o.disabled ? o.reason : undefined}
|
|
421
|
+
onAction={() => { select(o.value); focusOnReturn.current = o.value; }} />
|
|
412
422
|
);
|
|
413
423
|
})}
|
|
414
|
-
</
|
|
424
|
+
</PopoverMenu>
|
|
415
425
|
)}
|
|
416
426
|
|
|
417
427
|
{/* Measure layer: every option and the More trigger at natural size, out
|