@helloimjolopez-pathway/pathway-components 0.8.0 → 0.8.1
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/_shared/focus-ring.js +45 -0
- package/package.json +1 -1
- package/components/bottom-sheet/bottom-sheet-demo.jsx +0 -145
- package/components/nav-shell/nav-shell-demo.jsx +0 -59
- package/components/radio/radio-demo.jsx +0 -175
- package/components/scroll-to-enable/scroll-to-enable-demo.jsx +0 -125
- package/components/toggle-button/toggle-button-demo.jsx +0 -193
- package/components/tooltip/tooltip-demo.jsx +0 -149
- package/components/top-nav/top-nav-demo.jsx +0 -79
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard focus ring shared by controls that style themselves inline.
|
|
3
|
+
*
|
|
4
|
+
* One ring for every Pathway control: FocusRing/Width of Stroke/FocusRing/Base,
|
|
5
|
+
* drawn as an outline that hugs the element's edge (no gap), the way Figma draws
|
|
6
|
+
* it: an outside stroke on the control's own container in each Focused variant.
|
|
7
|
+
* Menu rows take the inset variant, as PopoverMenu.Item does in Figma. Where the
|
|
8
|
+
* Figma ring takes the place of the control's border, FOCUS_RING_REPLACE also
|
|
9
|
+
* hides that border while the ring shows.
|
|
10
|
+
*
|
|
11
|
+
* A stylesheet rule rather than inline style, because :focus-visible (keyboard
|
|
12
|
+
* focus only, never a mouse click) can only be matched by a selector.
|
|
13
|
+
*/
|
|
14
|
+
export const FOCUS_RING = "pw-focus-ring";
|
|
15
|
+
export const FOCUS_RING_INSET = "pw-focus-ring pw-focus-ring--inset";
|
|
16
|
+
// The ring replaces the control's border (drawn as an inset box-shadow or a CSS
|
|
17
|
+
// border). A control that also carries an elevation keeps it by setting
|
|
18
|
+
// --pw-ring-keep-shadow in its own style.
|
|
19
|
+
export const FOCUS_RING_REPLACE = "pw-focus-ring pw-focus-ring--replace";
|
|
20
|
+
// For a button that is a bigger touch target than what it draws (an action
|
|
21
|
+
// icon's 44 target around its 28 layer, the 48 profile button around its 32
|
|
22
|
+
// avatar): the ring goes on the drawn layer, marked data-pw-ring-target, as
|
|
23
|
+
// the Figma Focus Ring layer does.
|
|
24
|
+
export const FOCUS_RING_LAYER = "pw-focus-ring pw-focus-ring--layer";
|
|
25
|
+
|
|
26
|
+
// Also exported so a shadow root can carry it: a rule in document.head does not
|
|
27
|
+
// reach inside one, so the custom elements append this to their own styles.
|
|
28
|
+
export const FOCUS_RING_CSS = `
|
|
29
|
+
.pw-focus-ring:focus{outline:none}
|
|
30
|
+
.pw-focus-ring:focus-visible{outline:var(--contextual-layout-units-focusring-width) solid var(--semantic-color-stroke-focusring-base);outline-offset:0}
|
|
31
|
+
.pw-focus-ring--inset:focus-visible{outline-offset:calc(-1 * var(--contextual-layout-units-focusring-width))}
|
|
32
|
+
.pw-focus-ring--replace:focus-visible{border-color:transparent !important;box-shadow:var(--pw-ring-keep-shadow, none) !important}
|
|
33
|
+
.pw-focus-ring--layer:focus-visible{outline:none}
|
|
34
|
+
.pw-focus-ring--layer:focus-visible [data-pw-ring-target]{outline:var(--contextual-layout-units-focusring-width) solid var(--semantic-color-stroke-focusring-base);outline-offset:0}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
let injected = false;
|
|
38
|
+
export function injectFocusRing() {
|
|
39
|
+
if (injected || typeof document === "undefined") return;
|
|
40
|
+
injected = true;
|
|
41
|
+
const el = document.createElement("style");
|
|
42
|
+
el.setAttribute("data-pathway", "focus-ring");
|
|
43
|
+
el.textContent = FOCUS_RING_CSS;
|
|
44
|
+
document.head.appendChild(el);
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BottomSheet's standalone demo. Imports the real module, and the rows are the
|
|
3
|
-
* real DropdownItem and DropdownDivider, so nothing here can fall out of step
|
|
4
|
-
* with what ships: see components/_demo/harness.jsx for why demos are generated.
|
|
5
|
-
*/
|
|
6
|
-
import React, { useRef, useState } from "react";
|
|
7
|
-
import { DemoChrome, mount } from "../_demo/harness.jsx";
|
|
8
|
-
import { BottomSheet } from "./bottom-sheet.jsx";
|
|
9
|
-
import { DropdownItem, DropdownDivider, DropdownClose } from "../dropdown/dropdown.jsx";
|
|
10
|
-
import { Button } from "../button/button.jsx";
|
|
11
|
-
import { ActionIcon } from "../icon/action-icon.jsx";
|
|
12
|
-
|
|
13
|
-
const U = (n) => `var(--semantic-layout-units-${n})`;
|
|
14
|
-
const C = (n) => `var(--semantic-color-${n})`;
|
|
15
|
-
const Y = (n) => `var(--semantic-type-${n})`;
|
|
16
|
-
|
|
17
|
-
const APPS = ["Financials Home", "Accounts Payable", "Accounts Receivable", "General Ledger", "Fixed Assets", "Purchasing Management"];
|
|
18
|
-
const ROWS = [
|
|
19
|
-
{ id: 1, payee: "Grace Office Supply", amount: "$412.50" },
|
|
20
|
-
{ id: 2, payee: "Northside Utilities", amount: "$1,208.00" },
|
|
21
|
-
{ id: 3, payee: "Covenant Insurance", amount: "$3,940.12" },
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
const caption = { fontSize: Y("font-size-xs"), lineHeight: Y("line-height-xs-single"), color: C("foreground-static-neutral-subtle") };
|
|
25
|
-
|
|
26
|
-
/* The real screen: Accounts Payable on a phone. The app switcher, a row's More
|
|
27
|
-
menu, and Filters each open a sheet. */
|
|
28
|
-
function InContext() {
|
|
29
|
-
const [app, setApp] = useState("Accounts Payable");
|
|
30
|
-
const [sheet, setSheet] = useState(null); // "apps" | { row } | "filters"
|
|
31
|
-
const [moving, setMoving] = useState(false); // the row menu swapped to "Move to fund"
|
|
32
|
-
const [log, setLog] = useState("");
|
|
33
|
-
const close = () => { setSheet(null); setMoving(false); };
|
|
34
|
-
|
|
35
|
-
const row = sheet && typeof sheet === "object" ? sheet.row : null;
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<section style={{ background: C("fill-surface-elevated"), border: `1px solid ${C("stroke-static-neutral-base")}`,
|
|
39
|
-
borderRadius: U("cornerradius-large"), overflow: "hidden", maxWidth: 420, width: "100%", margin: "0 auto" }}>
|
|
40
|
-
<div style={{ padding: U("padding-base"), display: "flex", flexDirection: "column", gap: U("gap-base") }}>
|
|
41
|
-
<button type="button" aria-haspopup="dialog" onClick={() => setSheet("apps")}
|
|
42
|
-
style={{ font: "inherit", display: "flex", alignItems: "center", gap: U("gap-tight"), minHeight: 44, cursor: "pointer",
|
|
43
|
-
padding: `0 ${U("padding-tight")}`, borderRadius: U("cornerradius-base"), textAlign: "left",
|
|
44
|
-
border: `1px solid ${C("stroke-action-primary-strong-pressed")}`, background: C("fill-action-primary-subtle-pressed"),
|
|
45
|
-
color: C("foreground-action-primary-on-subtle-pressed"), fontSize: Y("font-size-s"), fontWeight: Y("weight-medium") }}>
|
|
46
|
-
<span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 24 }}>apps</span>
|
|
47
|
-
<span style={{ flex: 1 }}>{app}</span>
|
|
48
|
-
<span className="material-symbols-rounded" aria-hidden="true" style={{ fontSize: 14 }}>chevron_right</span>
|
|
49
|
-
</button>
|
|
50
|
-
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
51
|
-
<h2 style={{ margin: 0, fontSize: Y("font-size-m"), lineHeight: Y("line-height-m-single"), fontWeight: Y("weight-semibold"), color: C("foreground-static-neutral-strong") }}>Bills to pay</h2>
|
|
52
|
-
<Button buttonStyle="Outlined" type="Secondary" size="S" text="Filters" showLeadingIcon leadingIcon="filter_list"
|
|
53
|
-
aria-haspopup="dialog" onClick={() => setSheet("filters")} />
|
|
54
|
-
</div>
|
|
55
|
-
<div>
|
|
56
|
-
{ROWS.map((r) => (
|
|
57
|
-
<div key={r.id} style={{ display: "flex", alignItems: "center", gap: U("gap-tight"), padding: `${U("padding-xtight")} 0`,
|
|
58
|
-
borderBottom: `1px solid ${C("stroke-static-neutral-faint")}`, fontSize: Y("font-size-s"), color: C("foreground-static-neutral-base") }}>
|
|
59
|
-
<span style={{ flex: 1, minWidth: 0 }}>{r.payee}</span>
|
|
60
|
-
<span>{r.amount}</span>
|
|
61
|
-
<ActionIcon name="more_vert" size="Base" label={`More for ${r.payee}`} aria-haspopup="dialog"
|
|
62
|
-
onClick={() => setSheet({ row: r })} />
|
|
63
|
-
</div>
|
|
64
|
-
))}
|
|
65
|
-
</div>
|
|
66
|
-
<p role="status" style={{ margin: 0, ...caption }}>{log || "Open the app switcher, a row's More menu, or Filters."}</p>
|
|
67
|
-
</div>
|
|
68
|
-
|
|
69
|
-
<BottomSheet open={sheet === "apps"} onOpenChange={(o) => !o && close()} title="Financial Applications" menu>
|
|
70
|
-
{APPS.map((a, i) => (
|
|
71
|
-
<React.Fragment key={a}>
|
|
72
|
-
<DropdownItem label={a} checked={a === app} onSelect={() => { setApp(a); setLog(`Switched to ${a}.`); }} />
|
|
73
|
-
{i === 0 && <DropdownDivider />}
|
|
74
|
-
</React.Fragment>
|
|
75
|
-
))}
|
|
76
|
-
</BottomSheet>
|
|
77
|
-
|
|
78
|
-
<BottomSheet open={!!row} onOpenChange={(o) => !o && close()} menu
|
|
79
|
-
title={moving ? "Move to fund" : row?.payee}
|
|
80
|
-
showBack={moving} onBack={() => setMoving(false)}>
|
|
81
|
-
{moving ? (
|
|
82
|
-
["General Fund", "Building Fund", "Missions Fund"].map((f) => (
|
|
83
|
-
<DropdownItem key={f} label={f} onSelect={() => setLog(`${row?.payee} moved to ${f}.`)} />
|
|
84
|
-
))
|
|
85
|
-
) : (
|
|
86
|
-
<>
|
|
87
|
-
<DropdownItem label="Edit" icon="edit" onSelect={() => setLog(`Edit ${row?.payee}.`)} />
|
|
88
|
-
<DropdownItem label="Duplicate" icon="content_copy" onSelect={() => setLog(`Duplicated ${row?.payee}.`)} />
|
|
89
|
-
{/* A follow-up choice swaps the Body in place; it never opens a second sheet. */}
|
|
90
|
-
<MoveItem onMove={() => setMoving(true)} />
|
|
91
|
-
<DropdownDivider />
|
|
92
|
-
<DropdownItem label="Delete" icon="delete" danger onSelect={() => setLog(`Delete ${row?.payee} would ask for confirmation.`)} />
|
|
93
|
-
</>
|
|
94
|
-
)}
|
|
95
|
-
</BottomSheet>
|
|
96
|
-
|
|
97
|
-
<Filters open={sheet === "filters"} onClose={close} onApply={(n) => setLog(`Showing ${n} results.`)} />
|
|
98
|
-
</section>
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/* "Move to fund…" swaps the Body instead of closing the sheet, so it is the
|
|
103
|
-
real DropdownItem with the close context cleared. */
|
|
104
|
-
function MoveItem({ onMove }) {
|
|
105
|
-
return (
|
|
106
|
-
<DropdownClose.Provider value={null}>
|
|
107
|
-
<DropdownItem label="Move to fund…" icon="drive_file_move" onSelect={onMove} />
|
|
108
|
-
</DropdownClose.Provider>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function Filters({ open, onClose, onApply }) {
|
|
113
|
-
const [unpaid, setUnpaid] = useState(true);
|
|
114
|
-
const [overdue, setOverdue] = useState(false);
|
|
115
|
-
const count = 24 - (unpaid ? 0 : 6) - (overdue ? 10 : 0);
|
|
116
|
-
const reset = () => { setUnpaid(false); setOverdue(false); };
|
|
117
|
-
const Check = ({ label, on, set }) => (
|
|
118
|
-
<label style={{ display: "flex", alignItems: "center", gap: U("gap-medium"), minHeight: 44, padding: `0 ${U("padding-tight")}`,
|
|
119
|
-
fontSize: Y("font-size-s"), color: C("foreground-static-neutral-strong"), cursor: "pointer" }}>
|
|
120
|
-
<input type="checkbox" checked={on} onChange={(e) => set(e.target.checked)} /> {label}
|
|
121
|
-
</label>
|
|
122
|
-
);
|
|
123
|
-
return (
|
|
124
|
-
<BottomSheet open={open} onOpenChange={(o) => !o && onClose()} title="Filters"
|
|
125
|
-
footer={<>
|
|
126
|
-
<Button buttonStyle="Fill" type="Secondary" size="M" text="Reset" onClick={reset} />
|
|
127
|
-
<Button buttonStyle="Fill" type="Primary" size="M" text={`Show ${count} results`} onClick={() => { onApply(count); onClose(); }} />
|
|
128
|
-
</>}>
|
|
129
|
-
<Check label="Unpaid" on={unpaid} set={setUnpaid} />
|
|
130
|
-
<Check label="Overdue only" on={overdue} set={setOverdue} />
|
|
131
|
-
</BottomSheet>
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function App() {
|
|
136
|
-
return (
|
|
137
|
-
<DemoChrome title="BottomSheet"
|
|
138
|
-
blurb={"The mobile way to show a menu, a switcher or a short set of options. Close, the overlay, Esc and a swipe down all close it, " +
|
|
139
|
-
"and focus returns to the control that opened it. Choosing an item closes it. Below 768 only: the host decides, so switch to Mobile."}>
|
|
140
|
-
<InContext />
|
|
141
|
-
</DemoChrome>
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
mount(<App />);
|
|
@@ -1,59 +0,0 @@
|
|
|
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, { useState } from "react";
|
|
12
|
-
import { DemoChrome, mount } from "../_demo/harness.jsx";
|
|
13
|
-
import { NavShell, ACCOUNTING_APP_SWITCHER } 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
|
-
// The app switcher's on/off, the same switch as the web component's
|
|
29
|
-
// `app-switcher` attribute. It lives in the page so it works in every width.
|
|
30
|
-
const [appSwitcher, setAppSwitcher] = useState(false);
|
|
31
|
-
return (
|
|
32
|
-
<DemoChrome title="NavShell" widths={WIDTHS}
|
|
33
|
-
blurb={"TopNav, SideNav and the page area composed into one application frame. " +
|
|
34
|
-
"Desktop pushes the page with a collapsible nav, Tablet locks the 72px rail, " +
|
|
35
|
-
"Mobile opens the nav as an overlay from the hamburger. Tick App switcher to turn on the Accounting app switcher."}>
|
|
36
|
-
<NavShell activeModuleId="accounting" org={ORG} user={USER}
|
|
37
|
-
navSections={NAV_SECTIONS} activeNavId="balance_sheet"
|
|
38
|
-
// NavShell tracks the current page and app itself. With the switcher on,
|
|
39
|
-
// each Accounting app brings its own side nav.
|
|
40
|
-
appSwitcher={appSwitcher ? ACCOUNTING_APP_SWITCHER : null}>
|
|
41
|
-
<PageTemplate>
|
|
42
|
-
<label style={{ display: "flex", alignItems: "center", gap: "var(--semantic-layout-units-gap-tight)",
|
|
43
|
-
fontSize: Y("font-size-s"), lineHeight: Y("line-height-s-single"),
|
|
44
|
-
color: C("foreground-static-neutral-base"), cursor: "pointer" }}>
|
|
45
|
-
<input id="navshell-app-switcher" type="checkbox" checked={appSwitcher}
|
|
46
|
-
onChange={(e) => setAppSwitcher(e.target.checked)} />
|
|
47
|
-
App switcher (Accounting)
|
|
48
|
-
</label>
|
|
49
|
-
<h1 style={{ margin: 0, fontSize: Y("font-size-xl"), lineHeight: Y("line-height-xl-single"),
|
|
50
|
-
fontWeight: Y("weight-semibold"), color: C("foreground-static-neutral-bold") }}>
|
|
51
|
-
Balance Sheet
|
|
52
|
-
</h1>
|
|
53
|
-
</PageTemplate>
|
|
54
|
-
</NavShell>
|
|
55
|
-
</DemoChrome>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
mount(<App />);
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Radio's standalone demo. Imports the real module, so there is nothing here
|
|
3
|
-
* that can fall out of step with the shipped component: see
|
|
4
|
-
* components/_demo/harness.jsx for why the newer demos are generated.
|
|
5
|
-
*/
|
|
6
|
-
import React, { useState } from "react";
|
|
7
|
-
import { DemoChrome, mount } from "../_demo/harness.jsx";
|
|
8
|
-
import { RadioGroup, Radio, RadioMark, SIZES, ORIENTATIONS } from "./radio.jsx";
|
|
9
|
-
import { Button } from "../button/button.jsx";
|
|
10
|
-
|
|
11
|
-
const U = (n) => `var(--semantic-layout-units-${n})`;
|
|
12
|
-
const C = (n) => `var(--semantic-color-${n})`;
|
|
13
|
-
const Y = (n) => `var(--semantic-type-${n})`;
|
|
14
|
-
|
|
15
|
-
const label = { fontSize: Y("font-size-xs"), lineHeight: Y("line-height-xs-single"), color: C("foreground-static-neutral-subtle") };
|
|
16
|
-
const card = {
|
|
17
|
-
background: C("fill-surface-elevated"), border: `1px solid ${C("stroke-static-neutral-base")}`,
|
|
18
|
-
borderRadius: U("cornerradius-large"), padding: U("padding-relaxed"),
|
|
19
|
-
display: "flex", flexDirection: "column", gap: U("gap-base"), minWidth: 0,
|
|
20
|
-
};
|
|
21
|
-
const h2 = { margin: 0, fontSize: Y("font-size-m"), lineHeight: Y("line-height-m-single"), fontWeight: Y("weight-semibold"), color: C("foreground-static-neutral-bold") };
|
|
22
|
-
|
|
23
|
-
const TYPES = ["Unselected", "Selected", "Error Unselected", "Error Selected"];
|
|
24
|
-
const STATES = ["Active", "Hover", "Focused", "Pressed", "Disabled"];
|
|
25
|
-
|
|
26
|
-
function Pick({ list, value, onPick }) {
|
|
27
|
-
return (
|
|
28
|
-
<div style={{ display: "flex", gap: U("gap-xtight"), flexWrap: "wrap" }}>
|
|
29
|
-
{list.map((x) => <Button key={x} size="S" buttonStyle={x === value ? "Fill" : "Outlined"} type="Secondary" text={x} onClick={() => onPick(x)} />)}
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function InContext() {
|
|
35
|
-
const [contact, setContact] = useState(null);
|
|
36
|
-
const [tried, setTried] = useState(false);
|
|
37
|
-
const error = tried && !contact;
|
|
38
|
-
return (
|
|
39
|
-
<section style={card}>
|
|
40
|
-
<h2 style={h2}>Set up a recurring gift</h2>
|
|
41
|
-
<span style={label}>Press Save gift without picking how to reach Ana. Tab into a group and use the arrow keys.</span>
|
|
42
|
-
<RadioGroup label="Frequency" helpText="The first gift is taken on the start date." defaultValue="monthly" name="frequency">
|
|
43
|
-
<Radio value="once" text="One time" />
|
|
44
|
-
<Radio value="weekly" text="Weekly" />
|
|
45
|
-
<Radio value="biweekly" text="Every two weeks" />
|
|
46
|
-
<Radio value="monthly" text="Monthly" />
|
|
47
|
-
</RadioGroup>
|
|
48
|
-
<RadioGroup label="Statement delivery" orientation="Horizontal" defaultValue="email" name="statement">
|
|
49
|
-
<Radio value="email" text="Email" />
|
|
50
|
-
<Radio value="mail" text="Mail" />
|
|
51
|
-
<Radio value="sms" text="Text message" disabled />
|
|
52
|
-
</RadioGroup>
|
|
53
|
-
<RadioGroup label="Best way to reach Ana" required name="contact" value={contact} onChange={setContact}
|
|
54
|
-
error={error} errorMessage="Choose how to reach Ana">
|
|
55
|
-
<Radio value="phone" text="Phone" />
|
|
56
|
-
<Radio value="email" text="Email" />
|
|
57
|
-
<Radio value="none" text="Don’t contact" />
|
|
58
|
-
</RadioGroup>
|
|
59
|
-
<div><Button text="Save gift" type="Primary" buttonStyle="Fill" onClick={() => setTried(true)} /></div>
|
|
60
|
-
</section>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function Playground() {
|
|
65
|
-
const [size, setSize] = useState("Base");
|
|
66
|
-
const [orientation, setOrientation] = useState("Vertical");
|
|
67
|
-
const [state, setState] = useState("Default");
|
|
68
|
-
const [required, setRequired] = useState("off");
|
|
69
|
-
return (
|
|
70
|
-
<section style={card}>
|
|
71
|
-
<h2 style={h2}>Playground</h2>
|
|
72
|
-
<div style={{ display: "flex", gap: U("gap-relaxed"), flexWrap: "wrap" }}>
|
|
73
|
-
<div><div style={label}>Size</div><Pick list={SIZES} value={size} onPick={setSize} /></div>
|
|
74
|
-
<div><div style={label}>Orientation</div><Pick list={ORIENTATIONS} value={orientation} onPick={setOrientation} /></div>
|
|
75
|
-
<div><div style={label}>State</div><Pick list={["Default", "Error", "Disabled"]} value={state} onPick={setState} /></div>
|
|
76
|
-
<div><div style={label}>Required</div><Pick list={["off", "on"]} value={required} onPick={setRequired} /></div>
|
|
77
|
-
</div>
|
|
78
|
-
<RadioGroup label="Gift type" size={size} orientation={orientation} required={required === "on"}
|
|
79
|
-
error={state === "Error"} errorMessage="Choose a gift type" disabled={state === "Disabled"}
|
|
80
|
-
helpText="You can change this later.">
|
|
81
|
-
<Radio value="one" text="One-time gift" />
|
|
82
|
-
<Radio value="recurring" text="Recurring gift" />
|
|
83
|
-
<Radio value="pledge" text="Pledge payment" />
|
|
84
|
-
</RadioGroup>
|
|
85
|
-
</section>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function Matrix() {
|
|
90
|
-
return (
|
|
91
|
-
<section style={card}>
|
|
92
|
-
<h2 style={h2}>Every Type, State and Size</h2>
|
|
93
|
-
<div style={{ overflowX: "auto" }}>
|
|
94
|
-
<table style={{ borderCollapse: "collapse", fontSize: Y("font-size-xs"), color: C("foreground-static-neutral-subtle") }}>
|
|
95
|
-
<thead><tr><th />{STATES.map((s) => <th key={s} style={{ padding: U("padding-xtight"), textAlign: "left", fontWeight: Y("weight-medium") }}>{s}</th>)}</tr></thead>
|
|
96
|
-
<tbody>
|
|
97
|
-
{SIZES.flatMap((size) => TYPES.map((t) => (
|
|
98
|
-
<tr key={size + t}>
|
|
99
|
-
<th style={{ padding: U("padding-xtight"), textAlign: "left", fontWeight: Y("weight-medium") }}>{t}, {size}</th>
|
|
100
|
-
{STATES.map((s) => (
|
|
101
|
-
<td key={s} style={{ padding: U("padding-xtight") }}>
|
|
102
|
-
<RadioGroup label={`${t}, ${s}, ${size}`} showLabel={false} size={size} error={t.startsWith("Error")}
|
|
103
|
-
value={t.endsWith("Selected") && !t.endsWith("Unselected") ? "x" : null} onChange={() => {}}>
|
|
104
|
-
<Radio value="x" text="Label" disabled={s === "Disabled"} forceState={["Hover", "Focused", "Pressed"].includes(s) ? s : undefined} />
|
|
105
|
-
</RadioGroup>
|
|
106
|
-
</td>
|
|
107
|
-
))}
|
|
108
|
-
</tr>
|
|
109
|
-
)))}
|
|
110
|
-
</tbody>
|
|
111
|
-
</table>
|
|
112
|
-
</div>
|
|
113
|
-
</section>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function Content() {
|
|
118
|
-
return (
|
|
119
|
-
<section style={card}>
|
|
120
|
-
<h2 style={h2}>Long and translated labels</h2>
|
|
121
|
-
<div style={{ display: "flex", gap: U("gap-relaxed"), flexWrap: "wrap", alignItems: "flex-start" }}>
|
|
122
|
-
<RadioGroup label="Gift acknowledgement" defaultValue="letter">
|
|
123
|
-
<Radio value="email" text="Send a thank-you email" />
|
|
124
|
-
<Radio value="letter" text="Send a printed thank-you letter with the year-end giving statement, and copy the primary contact" />
|
|
125
|
-
<Radio value="none" text="Don’t acknowledge" />
|
|
126
|
-
</RadioGroup>
|
|
127
|
-
<RadioGroup label="Frecuencia" helpText="La primera donación se cobra en la fecha de inicio." defaultValue="m">
|
|
128
|
-
<Radio value="o" text="Una vez" />
|
|
129
|
-
<Radio value="w" text="Cada semana" />
|
|
130
|
-
<Radio value="b" text="Cada dos semanas" />
|
|
131
|
-
<Radio value="m" text="Cada mes" />
|
|
132
|
-
</RadioGroup>
|
|
133
|
-
</div>
|
|
134
|
-
</section>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function InMenu() {
|
|
139
|
-
const options = ["Date, newest first", "Amount, largest first", "Donor name, A to Z"];
|
|
140
|
-
const [picked, setPicked] = useState(options[0]);
|
|
141
|
-
return (
|
|
142
|
-
<section style={card}>
|
|
143
|
-
<h2 style={h2}>RadioMark inside a menu</h2>
|
|
144
|
-
<span style={label}>The Menu owns the menuitemradio role and draws RadioMark (Base) as its leading visual.</span>
|
|
145
|
-
<div role="menu" aria-label="Sort by" style={{ display: "flex", flexDirection: "column", width: 280,
|
|
146
|
-
borderRadius: U("cornerradius-base"), border: `1px solid ${C("stroke-static-neutral-base")}`, background: C("fill-surface-elevated") }}>
|
|
147
|
-
{options.map((o) => (
|
|
148
|
-
<div key={o} role="menuitemradio" aria-checked={picked === o} tabIndex={0}
|
|
149
|
-
onClick={() => setPicked(o)} onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") { e.preventDefault(); setPicked(o); } }}
|
|
150
|
-
style={{ display: "flex", alignItems: "center", cursor: "pointer", fontSize: Y("font-size-s"), color: C("foreground-static-neutral-base") }}>
|
|
151
|
-
<RadioMark selected={picked === o} />{o}
|
|
152
|
-
</div>
|
|
153
|
-
))}
|
|
154
|
-
</div>
|
|
155
|
-
</section>
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function App() {
|
|
160
|
-
return (
|
|
161
|
-
<DemoChrome title="Radio"
|
|
162
|
-
blurb={"Pick exactly one option from a short list people can see all at once. Base on desktop, Large on touch. " +
|
|
163
|
-
"Tab lands on the picked option, arrows move and pick, Space picks. The focus ring shows for the keyboard only."}>
|
|
164
|
-
<div style={{ display: "flex", flexDirection: "column", gap: U("gap-relaxed") }}>
|
|
165
|
-
<InContext />
|
|
166
|
-
<Playground />
|
|
167
|
-
<Matrix />
|
|
168
|
-
<Content />
|
|
169
|
-
<InMenu />
|
|
170
|
-
</div>
|
|
171
|
-
</DemoChrome>
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
mount(<App />);
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scroll-to-Enable's standalone demo. Imports the real module, so there is
|
|
3
|
-
* nothing here that can fall out of step with the shipped component: see
|
|
4
|
-
* components/_demo/harness.jsx for why the newer demos are generated.
|
|
5
|
-
*/
|
|
6
|
-
import React, { useState } from "react";
|
|
7
|
-
import { DemoChrome, isEmbed, mount } from "../_demo/harness.jsx";
|
|
8
|
-
import { ScrollToEnable } from "./scroll-to-enable.jsx";
|
|
9
|
-
|
|
10
|
-
const U = (n) => `var(--semantic-layout-units-${n})`;
|
|
11
|
-
const C = (n) => `var(--semantic-color-${n})`;
|
|
12
|
-
const Y = (n) => `var(--semantic-type-${n})`;
|
|
13
|
-
|
|
14
|
-
const FCRA = [
|
|
15
|
-
"In connection with your employment application with [Org Name] (the “Organization”), this notice is intended to inform you that a consumer report will be obtained on you from a consumer reporting agency for employment purposes. These purposes may include hiring, promotion, reassignment or retention.",
|
|
16
|
-
"The consumer report may contain information about your character, general reputation, personal characteristics and mode of living. It may include, but is not limited to, criminal history, motor vehicle records, education and employment verification, and credit history, where permitted by law.",
|
|
17
|
-
"You have the right, upon written request made within a reasonable time, to request whether a consumer report has been run about you, and to request a copy of your report. You may also request the nature and scope of any investigative consumer report.",
|
|
18
|
-
"This online application is protected by a secure certificate authority, which supports up to a TLS 1.2 256-bit encryption process. All information provided on this form is secure.",
|
|
19
|
-
"A summary of your rights under the Fair Credit Reporting Act is available on request, and a copy will be provided with any adverse action notice.",
|
|
20
|
-
];
|
|
21
|
-
const SHORT = [FCRA[0]];
|
|
22
|
-
const LABEL = "I acknowledge receipt of the disclosure regarding background investigation and certify that I have read and understand this document.";
|
|
23
|
-
|
|
24
|
-
const body = (paras) => paras.map((p, i) => <p key={i}>{p}</p>);
|
|
25
|
-
const card = {
|
|
26
|
-
background: C("fill-surface-elevated"), border: `1px solid ${C("stroke-static-neutral-base")}`,
|
|
27
|
-
borderRadius: U("cornerradius-large"), padding: U("padding-relaxed"),
|
|
28
|
-
display: "flex", flexDirection: "column", gap: U("gap-base"), minWidth: 0,
|
|
29
|
-
};
|
|
30
|
-
const h2 = { margin: 0, fontSize: Y("font-size-m"), lineHeight: Y("line-height-m-single"), fontWeight: Y("weight-semibold"), color: C("foreground-static-neutral-bold") };
|
|
31
|
-
const caption = { fontSize: Y("font-size-xs"), lineHeight: Y("line-height-xs-single"), color: C("foreground-static-neutral-subtle") };
|
|
32
|
-
|
|
33
|
-
/* The real screen it lives on: the background check form, step 3. */
|
|
34
|
-
function InContext() {
|
|
35
|
-
const [short, setShort] = useState(false);
|
|
36
|
-
const [done, setDone] = useState(false);
|
|
37
|
-
return (
|
|
38
|
-
<section style={{ ...card, padding: 0, overflow: "hidden" }}>
|
|
39
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: `${U("padding-tight")} ${U("padding-relaxed")}`,
|
|
40
|
-
background: C("fill-static-brand-strong"), color: C("foreground-static-brand-on-strong"), fontSize: Y("font-size-s") }}>
|
|
41
|
-
<span>[Org Name] Background Check</span><span>Step 3 of 6</span>
|
|
42
|
-
</div>
|
|
43
|
-
<div style={{ padding: U("padding-relaxed"), display: "flex", justifyContent: "center" }}>
|
|
44
|
-
<div style={{ width: "100%", maxWidth: 456, display: "flex", flexDirection: "column", gap: U("gap-base") }}>
|
|
45
|
-
<div>
|
|
46
|
-
<h2 style={{ ...h2, fontSize: Y("font-size-xl") }}>FCRA Notice</h2>
|
|
47
|
-
<p style={{ margin: 0, fontSize: Y("font-size-s"), color: C("foreground-static-neutral-base") }}>Please read and acknowledge receipt of the disclosure.</p>
|
|
48
|
-
</div>
|
|
49
|
-
<ScrollToEnable key={`${short}-${done}`} title="Terms & Conditions" agreementLabel={LABEL}
|
|
50
|
-
onContinue={() => setDone(true)} onPrint={() => {}}>
|
|
51
|
-
{body(short ? SHORT : FCRA)}
|
|
52
|
-
</ScrollToEnable>
|
|
53
|
-
{done && <p role="status" style={{ margin: 0, ...caption }}>Continue accepted: the form would move to step 4.</p>}
|
|
54
|
-
<div style={{ display: "flex", gap: U("gap-tight"), alignItems: "center", flexWrap: "wrap", ...caption }}>
|
|
55
|
-
Notice:
|
|
56
|
-
{[["Long", false], ["Short, fits", true]].map(([name, v]) => (
|
|
57
|
-
<button key={name} type="button" aria-pressed={short === v} onClick={() => { setShort(v); setDone(false); }}
|
|
58
|
-
style={{ font: "inherit", padding: `${U("padding-xxxtight")} ${U("padding-tight")}`, borderRadius: U("cornerradius-base"),
|
|
59
|
-
border: `1px solid ${short === v ? C("stroke-action-primary-strong-rest") : C("stroke-static-neutral-base")}`,
|
|
60
|
-
background: short === v ? C("fill-action-primary-subtle-rest") : "transparent", color: C("foreground-static-neutral-base"), cursor: "pointer" }}>
|
|
61
|
-
{name}
|
|
62
|
-
</button>
|
|
63
|
-
))}
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
</section>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function Cell({ label, children }) {
|
|
72
|
-
return (
|
|
73
|
-
<div style={{ display: "flex", flexDirection: "column", gap: U("gap-tight"), minWidth: 0 }}>
|
|
74
|
-
<span style={caption}>{label}</span>
|
|
75
|
-
{children}
|
|
76
|
-
</div>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function States() {
|
|
81
|
-
const grid = { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(min(340px, 100%), 1fr))", gap: U("gap-relaxed") };
|
|
82
|
-
const inline = ["Unread", "Reading", "Read", "Agreed", "Blocked", "Agreement Missing"];
|
|
83
|
-
const page = ["Unread", "Blocked", "Read", "Agreed", "Agreement Missing"];
|
|
84
|
-
const sheet = ["Sheet Unread", "Sheet Blocked", "Sheet Read"];
|
|
85
|
-
const make = (s, layout) => (
|
|
86
|
-
<ScrollToEnable layout={layout} demoState={s} title={layout === "sheet" ? "FCRA Notice" : "Terms & Conditions"}
|
|
87
|
-
agreementLabel={LABEL} noticeHeight={layout === "sheet" && s.startsWith("Sheet") ? 180 : undefined} onPrint={() => {}}>
|
|
88
|
-
{body(FCRA)}
|
|
89
|
-
</ScrollToEnable>
|
|
90
|
-
);
|
|
91
|
-
return (
|
|
92
|
-
<>
|
|
93
|
-
<section style={card}>
|
|
94
|
-
<h2 style={h2}>Inline (viewport 768 and up)</h2>
|
|
95
|
-
<div style={grid}>{inline.map((s) => <Cell key={s} label={s}>{make(s, "inline")}</Cell>)}</div>
|
|
96
|
-
</section>
|
|
97
|
-
<section style={card}>
|
|
98
|
-
<h2 style={h2}>Sheet: the page (viewport under 768)</h2>
|
|
99
|
-
<div style={grid}>{page.map((s) => <Cell key={s} label={s}>{make(s, "sheet")}</Cell>)}</div>
|
|
100
|
-
</section>
|
|
101
|
-
<section style={card}>
|
|
102
|
-
<h2 style={h2}>Sheet: open</h2>
|
|
103
|
-
<div style={grid}>{sheet.map((s) => <Cell key={s} label={s}>{make(s, "sheet")}</Cell>)}</div>
|
|
104
|
-
</section>
|
|
105
|
-
</>
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function App() {
|
|
110
|
-
return (
|
|
111
|
-
<DemoChrome title="Scroll-to-Enable"
|
|
112
|
-
blurb={"The person reaches the end of a legal notice before agreeing and continuing. Nothing is disabled: " +
|
|
113
|
-
"press Continue or the checkbox early and it says what to do and moves focus there. " +
|
|
114
|
-
"Inline at 768 and wider; switch to Mobile for the sheet. Tab into the notice and use the arrow keys; Esc closes the sheet."}>
|
|
115
|
-
<div style={{ display: "flex", flexDirection: "column", gap: U("gap-relaxed") }}>
|
|
116
|
-
<InContext />
|
|
117
|
-
{/* The Tablet and Mobile frames show the real screen only: the state
|
|
118
|
-
grids force each layout and are reference, not responsive. */}
|
|
119
|
-
{!isEmbed() && <States />}
|
|
120
|
-
</div>
|
|
121
|
-
</DemoChrome>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
mount(<App />);
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Toggle Button's standalone demo. Imports the real module, so there is nothing
|
|
3
|
-
* here that can fall out of step with the shipped component: see
|
|
4
|
-
* components/_demo/harness.jsx for why the newer demos are generated.
|
|
5
|
-
*/
|
|
6
|
-
import React, { useState } from "react";
|
|
7
|
-
import { DemoChrome, mount } from "../_demo/harness.jsx";
|
|
8
|
-
import { ToggleButton, ToggleButtonOption, SIZES } from "./toggle-button.jsx";
|
|
9
|
-
import { Widget } from "../widget/widget.jsx";
|
|
10
|
-
import { TooltipProvider } from "../tooltip/tooltip.jsx";
|
|
11
|
-
|
|
12
|
-
const U = (n) => `var(--semantic-layout-units-${n})`;
|
|
13
|
-
const C = (n) => `var(--semantic-color-${n})`;
|
|
14
|
-
const Y = (n) => `var(--semantic-type-${n})`;
|
|
15
|
-
|
|
16
|
-
const STYLES = ["Primary", "Secondary", "Tertiary"];
|
|
17
|
-
const SIZE_KEYS = Object.keys(SIZES);
|
|
18
|
-
|
|
19
|
-
const VIEW = [
|
|
20
|
-
{ value: "bar", label: "Bar", icon: "bar_chart" },
|
|
21
|
-
{ value: "line", label: "Line", icon: "show_chart" },
|
|
22
|
-
{ value: "table", label: "Table", icon: "table_rows" },
|
|
23
|
-
];
|
|
24
|
-
const MONTHLY = "Budgets are set by month.";
|
|
25
|
-
const PERIOD = [
|
|
26
|
-
{ value: "d", label: "D", disabled: true, reason: `Daily totals aren't available. ${MONTHLY}` },
|
|
27
|
-
{ value: "w", label: "W", disabled: true, reason: `Weekly totals aren't available. ${MONTHLY}` },
|
|
28
|
-
{ value: "m", label: "M" },
|
|
29
|
-
{ value: "p", label: "P" },
|
|
30
|
-
{ value: "q", label: "Q" },
|
|
31
|
-
{ value: "y", label: "Y" },
|
|
32
|
-
];
|
|
33
|
-
const RANGE = [
|
|
34
|
-
{ value: "this", label: "This fiscal year" },
|
|
35
|
-
{ value: "last", label: "Last fiscal year" },
|
|
36
|
-
{ value: "ytd", label: "Year to date" },
|
|
37
|
-
{ value: "quarter", label: "This quarter" },
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const card = {
|
|
41
|
-
background: C("fill-surface-elevated"), border: `1px solid ${C("stroke-static-neutral-base")}`,
|
|
42
|
-
borderRadius: U("cornerradius-large"), padding: U("padding-relaxed"),
|
|
43
|
-
display: "flex", flexDirection: "column", gap: U("gap-base"), minWidth: 0,
|
|
44
|
-
};
|
|
45
|
-
const h2 = { margin: 0, fontSize: Y("font-size-m"), lineHeight: Y("line-height-m-single"), fontWeight: Y("weight-semibold"), color: C("foreground-static-neutral-bold") };
|
|
46
|
-
const small = { fontSize: Y("font-size-xs"), lineHeight: Y("line-height-xs-single"), color: C("foreground-static-neutral-subtle") };
|
|
47
|
-
|
|
48
|
-
/* A chart placeholder: bars whose heights change with the view, so a choice is
|
|
49
|
-
visible straight away, which is the job the toggle does. */
|
|
50
|
-
function Chart({ view }) {
|
|
51
|
-
const data = [62, 44, 30, 28, 46, 80, 58, 64, 40, 54, 70, 70];
|
|
52
|
-
if (view === "table") {
|
|
53
|
-
return (
|
|
54
|
-
<div style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: U("gap-xtight"), ...small }}>
|
|
55
|
-
{["Jan", "Feb", "Mar", "Apr", "May", "Jun"].map((m, i) => (
|
|
56
|
-
<React.Fragment key={m}><span>{m}</span><span style={{ color: C("foreground-static-neutral-base") }}>${(data[i] * 31).toLocaleString()}</span></React.Fragment>
|
|
57
|
-
))}
|
|
58
|
-
</div>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
return (
|
|
62
|
-
<div style={{ display: "flex", alignItems: "flex-end", gap: U("gap-xtight"), height: 140 }}>
|
|
63
|
-
{data.map((v, i) => (
|
|
64
|
-
<div key={i} style={{ flex: 1, height: `${v}%`, background: C("fill-chart-sequential-07"),
|
|
65
|
-
borderRadius: view === "line" ? U("cornerradius-full") : U("cornerradius-small"),
|
|
66
|
-
maxHeight: view === "line" ? 6 : undefined, alignSelf: view === "line" ? "auto" : undefined,
|
|
67
|
-
marginBottom: view === "line" ? `${v - 6}%` : 0 }} />
|
|
68
|
-
))}
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function InWidget() {
|
|
74
|
-
const [view, setView] = useState("bar");
|
|
75
|
-
const [period, setPeriod] = useState("m");
|
|
76
|
-
const [bank, setBank] = useState("bar");
|
|
77
|
-
return (
|
|
78
|
-
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 360px), 1fr))", gap: U("gap-relaxed") }}>
|
|
79
|
-
<Widget title="Budget Compared to Actual" size="detail" inGrid={false}>
|
|
80
|
-
<div style={{ display: "flex", flexDirection: "column", gap: U("gap-base") }}>
|
|
81
|
-
<div style={{ display: "flex", justifyContent: "flex-end", minWidth: 0 }}>
|
|
82
|
-
<ToggleButton label="Chart view" options={VIEW} value={view} onChange={setView} showIcon />
|
|
83
|
-
</div>
|
|
84
|
-
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: U("gap-base"), minWidth: 0 }}>
|
|
85
|
-
<span style={{ fontSize: Y("font-size-xl"), lineHeight: Y("line-height-xl-single"), fontWeight: Y("weight-semibold"), color: C("foreground-static-neutral-strong") }}>2,000</span>
|
|
86
|
-
<ToggleButton label="Period" options={PERIOD} value={period} onChange={setPeriod} />
|
|
87
|
-
</div>
|
|
88
|
-
<Chart view={view} />
|
|
89
|
-
</div>
|
|
90
|
-
</Widget>
|
|
91
|
-
<Widget title="Bank Balances" size="detail" inGrid={false}>
|
|
92
|
-
<div style={{ display: "flex", flexDirection: "column", gap: U("gap-base") }}>
|
|
93
|
-
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
|
94
|
-
<ToggleButton label="Chart view" options={VIEW} value={bank} onChange={setBank} showIcon showLabel={false} />
|
|
95
|
-
</div>
|
|
96
|
-
<Chart view={bank} />
|
|
97
|
-
</div>
|
|
98
|
-
</Widget>
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function Styles() {
|
|
104
|
-
const [v, setV] = useState({});
|
|
105
|
-
return (
|
|
106
|
-
<section style={card}>
|
|
107
|
-
<h2 style={h2}>Styles and sizes</h2>
|
|
108
|
-
<p style={{ ...small, margin: 0 }}>Primary for one page-level toggle per view. Secondary inside cards, widgets, dialogs and sheets. Tertiary where Secondary fits but the selection should carry brand colour.</p>
|
|
109
|
-
{STYLES.map((st) => (
|
|
110
|
-
<div key={st} style={{ display: "flex", flexDirection: "column", gap: U("gap-xtight") }}>
|
|
111
|
-
<div style={small}>{st}</div>
|
|
112
|
-
<div style={{ display: "flex", alignItems: "center", gap: U("gap-relaxed"), flexWrap: "wrap" }}>
|
|
113
|
-
{SIZE_KEYS.map((sz) => (
|
|
114
|
-
<ToggleButton key={sz} label={`${st} ${sz}`} toggleStyle={st} size={sz} options={VIEW} showIcon
|
|
115
|
-
value={v[st + sz] ?? "bar"} onChange={(x) => setV((o) => ({ ...o, [st + sz]: x }))} />
|
|
116
|
-
))}
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
))}
|
|
120
|
-
<div style={small}>Icon only</div>
|
|
121
|
-
<div style={{ display: "flex", alignItems: "center", gap: U("gap-relaxed"), flexWrap: "wrap" }}>
|
|
122
|
-
{SIZE_KEYS.map((sz) => (
|
|
123
|
-
<ToggleButton key={sz} label={`Icon only ${sz}`} size={sz} options={VIEW} showIcon showLabel={false}
|
|
124
|
-
value={v["i" + sz] ?? "bar"} onChange={(x) => setV((o) => ({ ...o, ["i" + sz]: x }))} />
|
|
125
|
-
))}
|
|
126
|
-
</div>
|
|
127
|
-
</section>
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function States() {
|
|
132
|
-
const [st, setSt] = useState("Secondary");
|
|
133
|
-
const states = [null, "hover", "focused", "pressed"];
|
|
134
|
-
const names = ["Rest", "Hover", "Focused", "Pressed"];
|
|
135
|
-
return (
|
|
136
|
-
<section style={card}>
|
|
137
|
-
<h2 style={h2}>States</h2>
|
|
138
|
-
<ToggleButton label="Style" options={STYLES.map((s) => ({ value: s, label: s }))} value={st} onChange={setSt} size="XS" />
|
|
139
|
-
<div style={{ display: "grid", gridTemplateColumns: "auto repeat(5, max-content)", gap: `${U("gap-base")} ${U("gap-relaxed")}`, alignItems: "center", overflowX: "auto" }}>
|
|
140
|
-
<span />{[...names, "Unavailable"].map((n) => <span key={n} style={small}>{n}</span>)}
|
|
141
|
-
{[false, true].map((sel) => (
|
|
142
|
-
<React.Fragment key={String(sel)}>
|
|
143
|
-
<span style={small}>{sel ? "Selected" : "Unselected"}</span>
|
|
144
|
-
{states.map((s, i) => (
|
|
145
|
-
<ToggleButtonOption key={i} toggleStyle={st} size="M" selected={sel} position="Only" label="Option" icon="bar_chart" forceState={s} tabIndex={-1} />
|
|
146
|
-
))}
|
|
147
|
-
{sel ? <span style={small}>never</span>
|
|
148
|
-
: <ToggleButtonOption toggleStyle={st} size="M" position="Only" label="Option" icon="bar_chart" disabled tabIndex={-1} />}
|
|
149
|
-
</React.Fragment>
|
|
150
|
-
))}
|
|
151
|
-
</div>
|
|
152
|
-
</section>
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function Narrow() {
|
|
157
|
-
const [w, setW] = useState(520);
|
|
158
|
-
const [period, setPeriod] = useState("m");
|
|
159
|
-
const [range, setRange] = useState("this");
|
|
160
|
-
return (
|
|
161
|
-
<section style={card}>
|
|
162
|
-
<h2 style={h2}>Narrow space</h2>
|
|
163
|
-
<p style={{ ...small, margin: 0 }}>Drag the slider. Unavailable options move into More first, then options from the end. The selected option always stays in the row.</p>
|
|
164
|
-
<label style={{ ...small, display: "flex", alignItems: "center", gap: U("gap-tight") }}>
|
|
165
|
-
Container width {w}px
|
|
166
|
-
<input type="range" min={120} max={640} value={w} onChange={(e) => setW(+e.target.value)} style={{ flex: 1, maxWidth: 320 }} />
|
|
167
|
-
</label>
|
|
168
|
-
<div style={{ width: w, maxWidth: "100%", padding: U("padding-xtight"), border: `1px dashed ${C("stroke-static-neutral-bold")}`, borderRadius: U("cornerradius-base"), display: "flex", flexDirection: "column", gap: U("gap-base") }}>
|
|
169
|
-
<ToggleButton label="Period" options={PERIOD} value={period} onChange={setPeriod} />
|
|
170
|
-
<ToggleButton label="Range" options={RANGE} value={range} onChange={setRange} size="M" />
|
|
171
|
-
</div>
|
|
172
|
-
</section>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function App() {
|
|
177
|
-
return (
|
|
178
|
-
<DemoChrome title="Toggle Button"
|
|
179
|
-
blurb={"Pick one of two to five options; one is always selected. Keyboard: Tab to the group, arrows move and select, " +
|
|
180
|
-
"Home and End jump. Hover, focus or tap an unavailable option to see why."}>
|
|
181
|
-
<TooltipProvider>
|
|
182
|
-
<div style={{ display: "flex", flexDirection: "column", gap: U("gap-relaxed") }}>
|
|
183
|
-
<InWidget />
|
|
184
|
-
<Narrow />
|
|
185
|
-
<Styles />
|
|
186
|
-
<States />
|
|
187
|
-
</div>
|
|
188
|
-
</TooltipProvider>
|
|
189
|
-
</DemoChrome>
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
mount(<App />);
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tooltip's standalone demo. Imports the real module, so there is nothing here
|
|
3
|
-
* that can fall out of step with the shipped component: see
|
|
4
|
-
* components/_demo/harness.jsx for why the newer demos are generated.
|
|
5
|
-
*/
|
|
6
|
-
import React, { useState } from "react";
|
|
7
|
-
import { DemoChrome, mount } from "../_demo/harness.jsx";
|
|
8
|
-
import { Tooltip, TooltipProvider } from "./tooltip.jsx";
|
|
9
|
-
import { Button } from "../button/button.jsx";
|
|
10
|
-
|
|
11
|
-
const U = (n) => `var(--semantic-layout-units-${n})`;
|
|
12
|
-
const C = (n) => `var(--semantic-color-${n})`;
|
|
13
|
-
const Y = (n) => `var(--semantic-type-${n})`;
|
|
14
|
-
|
|
15
|
-
const SIDES = ["top", "bottom", "left", "right"];
|
|
16
|
-
const ALIGNS = ["start", "center", "end"];
|
|
17
|
-
const LONG = "Weekly totals aren't available because budgets are set by month. Switch to Month to compare.";
|
|
18
|
-
|
|
19
|
-
const label = { fontSize: Y("font-size-xs"), lineHeight: Y("line-height-xs-single"), color: C("foreground-static-neutral-subtle") };
|
|
20
|
-
const card = {
|
|
21
|
-
background: C("fill-surface-elevated"), border: `1px solid ${C("stroke-static-neutral-base")}`,
|
|
22
|
-
borderRadius: U("cornerradius-large"), padding: U("padding-relaxed"),
|
|
23
|
-
display: "flex", flexDirection: "column", gap: U("gap-base"), minWidth: 0,
|
|
24
|
-
};
|
|
25
|
-
const h2 = { margin: 0, fontSize: Y("font-size-m"), lineHeight: Y("line-height-m-single"), fontWeight: Y("weight-semibold"), color: C("foreground-static-neutral-bold") };
|
|
26
|
-
|
|
27
|
-
/* An unavailable option stays focusable so it can say why, which a native
|
|
28
|
-
disabled button cannot. This stands in for a Toggle Button option. */
|
|
29
|
-
const Unavailable = React.forwardRef(function Unavailable({ children, ...rest }, ref) {
|
|
30
|
-
return (
|
|
31
|
-
<button ref={ref} type="button" aria-disabled="true" {...rest}
|
|
32
|
-
style={{ minWidth: U("accessibility-touch-target-aa-width"), minHeight: U("accessibility-touch-target-aa-height"),
|
|
33
|
-
padding: `0 ${U("padding-tight")}`, border: 0, borderRadius: U("cornerradius-base"), cursor: "not-allowed",
|
|
34
|
-
background: C("fill-action-disabled"), color: C("foreground-action-disabled"),
|
|
35
|
-
fontFamily: Y("family-brand"), fontSize: Y("font-size-s"), fontWeight: Y("weight-medium") }}>
|
|
36
|
-
{children}
|
|
37
|
-
</button>
|
|
38
|
-
);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
function Playground() {
|
|
42
|
-
const [side, setSide] = useState("top");
|
|
43
|
-
const [align, setAlign] = useState("center");
|
|
44
|
-
const [arrow, setArrow] = useState(true);
|
|
45
|
-
const [text, setText] = useState("Bar chart");
|
|
46
|
-
const pick = (list, v, set) => (
|
|
47
|
-
<div style={{ display: "flex", gap: U("gap-xtight"), flexWrap: "wrap" }}>
|
|
48
|
-
{list.map((x) => <Button key={x} size="S" buttonStyle={x === v ? "Fill" : "Outlined"} type="Secondary" text={x} onClick={() => set(x)} />)}
|
|
49
|
-
</div>
|
|
50
|
-
);
|
|
51
|
-
return (
|
|
52
|
-
<section style={card}>
|
|
53
|
-
<h2 style={h2}>Playground</h2>
|
|
54
|
-
<div style={{ display: "flex", gap: U("gap-relaxed"), flexWrap: "wrap" }}>
|
|
55
|
-
<div><div style={label}>Side</div>{pick(SIDES, side, setSide)}</div>
|
|
56
|
-
<div><div style={label}>Align</div>{pick(ALIGNS, align, setAlign)}</div>
|
|
57
|
-
<div><div style={label}>Arrow</div>{pick(["on", "off"], arrow ? "on" : "off", (x) => setArrow(x === "on"))}</div>
|
|
58
|
-
</div>
|
|
59
|
-
<label style={{ ...label, display: "flex", flexDirection: "column", gap: U("gap-xxtight"), maxWidth: 420 }}>
|
|
60
|
-
Content
|
|
61
|
-
<input id="tooltip-content" value={text} onChange={(e) => setText(e.target.value)}
|
|
62
|
-
style={{ font: "inherit", fontSize: Y("font-size-s"), padding: U("padding-xtight"), borderRadius: U("cornerradius-small"),
|
|
63
|
-
border: `1px solid ${C("stroke-static-neutral-bold")}`, background: C("fill-surface-elevated"), color: C("foreground-static-neutral-base") }} />
|
|
64
|
-
</label>
|
|
65
|
-
<div style={{ minHeight: 160, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
|
66
|
-
<Tooltip content={text} side={side} align={align} arrow={arrow} open purpose="description">
|
|
67
|
-
<button type="button" style={{ width: 120, height: 44, border: `1px dashed ${C("stroke-static-neutral-bold")}`,
|
|
68
|
-
borderRadius: U("cornerradius-base"), background: "transparent", color: C("foreground-static-neutral-subtle"), font: "inherit" }}>
|
|
69
|
-
Trigger
|
|
70
|
-
</button>
|
|
71
|
-
</Tooltip>
|
|
72
|
-
</div>
|
|
73
|
-
</section>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function Uses() {
|
|
78
|
-
return (
|
|
79
|
-
<section style={card}>
|
|
80
|
-
<h2 style={h2}>Try it: hover, Tab, tap, Esc</h2>
|
|
81
|
-
<div style={{ display: "flex", gap: U("gap-wide"), flexWrap: "wrap", alignItems: "flex-end" }}>
|
|
82
|
-
<div style={{ display: "flex", flexDirection: "column", gap: U("gap-tight") }}>
|
|
83
|
-
<span style={label}>Names icon-only controls</span>
|
|
84
|
-
<div style={{ display: "flex", gap: U("gap-xxtight") }}>
|
|
85
|
-
{[["bar_chart", "Bar chart"], ["show_chart", "Line chart"], ["table_rows", "Table"]].map(([icon, name]) => (
|
|
86
|
-
<Tooltip key={icon} content={name} purpose="label">
|
|
87
|
-
<Button buttonStyle="Naked" type="Secondary" size="M" showText={false} text={name}
|
|
88
|
-
leadingIcon={icon} showLeadingIcon />
|
|
89
|
-
</Tooltip>
|
|
90
|
-
))}
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
<div style={{ display: "flex", flexDirection: "column", gap: U("gap-tight") }}>
|
|
94
|
-
<span style={label}>Says why an option is unavailable</span>
|
|
95
|
-
<Tooltip content="Weekly totals aren't available. Budgets are set by month." persistOnPress>
|
|
96
|
-
<Unavailable>Week</Unavailable>
|
|
97
|
-
</Tooltip>
|
|
98
|
-
</div>
|
|
99
|
-
<div style={{ display: "flex", flexDirection: "column", gap: U("gap-tight") }}>
|
|
100
|
-
<span style={label}>Adds a short hint</span>
|
|
101
|
-
<Tooltip content="Includes payments that haven't cleared yet.">
|
|
102
|
-
<Button buttonStyle="Naked" type="Secondary" size="M" showText={false} text="About this balance"
|
|
103
|
-
leadingIcon="info" showLeadingIcon />
|
|
104
|
-
</Tooltip>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
</section>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function Grid() {
|
|
112
|
-
return (
|
|
113
|
-
<section style={card}>
|
|
114
|
-
<h2 style={h2}>Sides and alignment</h2>
|
|
115
|
-
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: U("gap-base") }}>
|
|
116
|
-
{SIDES.flatMap((s) => ALIGNS.map((a) => (
|
|
117
|
-
<div key={s + a} style={{ minHeight: 150, display: "flex", flexDirection: "column", gap: U("gap-tight"),
|
|
118
|
-
border: `1px dashed ${C("stroke-static-neutral-base")}`, borderRadius: U("cornerradius-base"), padding: U("padding-tight") }}>
|
|
119
|
-
<span style={label}>{s}, {a}</span>
|
|
120
|
-
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
|
121
|
-
<Tooltip content={a === "center" ? "Bar chart" : LONG.slice(0, 44)} side={s} align={a} open>
|
|
122
|
-
<span tabIndex={-1} style={{ width: 28, height: 28, borderRadius: U("cornerradius-small"), background: C("fill-static-neutral-base") }} />
|
|
123
|
-
</Tooltip>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
)))}
|
|
127
|
-
</div>
|
|
128
|
-
</section>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function App() {
|
|
133
|
-
return (
|
|
134
|
-
<DemoChrome title="Tooltip"
|
|
135
|
-
blurb={"A short, plain-text label beside a control. It is always the opposite mode to the page: " +
|
|
136
|
-
"switch to Midnight to see it turn light. Hover waits 300ms, keyboard focus opens it at once, " +
|
|
137
|
-
"a tap shows it for 2.6 seconds, Esc closes it."}>
|
|
138
|
-
<TooltipProvider>
|
|
139
|
-
<div style={{ display: "flex", flexDirection: "column", gap: U("gap-relaxed") }}>
|
|
140
|
-
<Uses />
|
|
141
|
-
<Playground />
|
|
142
|
-
<Grid />
|
|
143
|
-
</div>
|
|
144
|
-
</TooltipProvider>
|
|
145
|
-
</DemoChrome>
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
mount(<App />);
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TopNav's standalone demo. Imports the real module, so there is nothing here
|
|
3
|
-
* that can fall out of step with the shipped component: see
|
|
4
|
-
* components/_demo/harness.jsx for why the newer demos are generated.
|
|
5
|
-
*
|
|
6
|
-
* Replaced the hand-written top-nav.html on 2026-10-30. That page carried its
|
|
7
|
-
* own copy of TopNav, and the copy had drifted from top-nav.jsx: it still
|
|
8
|
-
* abbreviated the mobile org label, still drew the campus line in Mono, and
|
|
9
|
-
* still sized the module mark at 24.
|
|
10
|
-
*/
|
|
11
|
-
import React, { useState } from "react";
|
|
12
|
-
import { DemoChrome, mount } from "../_demo/harness.jsx";
|
|
13
|
-
import { TopNav } from "./top-nav.jsx";
|
|
14
|
-
|
|
15
|
-
const U = (n) => `var(--semantic-layout-units-${n})`;
|
|
16
|
-
const C = (n) => `var(--semantic-color-${n})`;
|
|
17
|
-
const Y = (n) => `var(--semantic-type-${n})`;
|
|
18
|
-
|
|
19
|
-
const BARS = [
|
|
20
|
-
{ id: "desktop", label: "Desktop", width: 1200 },
|
|
21
|
-
{ id: "tablet", label: "Tablet", width: 768 },
|
|
22
|
-
{ id: "mobile", label: "Mobile", width: 393 },
|
|
23
|
-
];
|
|
24
|
-
const ORG = { id: "gcc", name: "Grace Community Church", shortName: "Grace Comm", campus: "Knoxville", initials: "GC" };
|
|
25
|
-
const USER = { name: "Jo Lopez", initials: "JL", email: "jo@gracecommunity.org" };
|
|
26
|
-
|
|
27
|
-
const label = { fontSize: Y("font-size-xs"), lineHeight: Y("line-height-xs-single"), color: C("foreground-static-neutral-subtle") };
|
|
28
|
-
const card = {
|
|
29
|
-
background: C("fill-surface-elevated"), border: `1px solid ${C("stroke-static-neutral-base")}`,
|
|
30
|
-
borderRadius: U("cornerradius-large"), padding: U("padding-relaxed"),
|
|
31
|
-
display: "flex", flexDirection: "column", gap: U("gap-base"), minWidth: 0,
|
|
32
|
-
};
|
|
33
|
-
const h2 = { margin: 0, fontSize: Y("font-size-m"), lineHeight: Y("line-height-m-single"), fontWeight: Y("weight-semibold"), color: C("foreground-static-neutral-bold") };
|
|
34
|
-
|
|
35
|
-
function App() {
|
|
36
|
-
const [showCampus, setShowCampus] = useState(false);
|
|
37
|
-
const [module, setModule] = useState("people");
|
|
38
|
-
const [brand, setBrand] = useState("amplify");
|
|
39
|
-
return (
|
|
40
|
-
<DemoChrome title="TopNav"
|
|
41
|
-
blurb={"The global bar across the top of every Pathway screen. Hover and open the module and org switchers. " +
|
|
42
|
-
"The campus line is off by default, as in Figma; turn it on below."}>
|
|
43
|
-
<div style={{ display: "flex", flexDirection: "column", gap: U("gap-relaxed") }}>
|
|
44
|
-
<section style={card}>
|
|
45
|
-
<h2 style={h2}>Options</h2>
|
|
46
|
-
<label style={{ ...label, display: "flex", alignItems: "center", gap: U("gap-xtight") }}>
|
|
47
|
-
<input id="topnav-show-campus" type="checkbox" checked={showCampus} onChange={(e) => setShowCampus(e.target.checked)} />
|
|
48
|
-
Show campus line (Figma "Show CityName")
|
|
49
|
-
</label>
|
|
50
|
-
<label style={{ ...label, display: "flex", alignItems: "center", gap: U("gap-xtight") }}>
|
|
51
|
-
Brand layout
|
|
52
|
-
<select id="topnav-brand" value={brand} onChange={(e) => setBrand(e.target.value)}>
|
|
53
|
-
<option value="amplify">Amplify</option>
|
|
54
|
-
<option value="newco">NewCo</option>
|
|
55
|
-
</select>
|
|
56
|
-
</label>
|
|
57
|
-
<label style={{ ...label, display: "flex", alignItems: "center", gap: U("gap-xtight") }}>
|
|
58
|
-
Module
|
|
59
|
-
<select id="topnav-module" value={module} onChange={(e) => setModule(e.target.value)}>
|
|
60
|
-
{["home", "people", "giving", "accounting", "communications"].map((m) => <option key={m} value={m}>{m}</option>)}
|
|
61
|
-
</select>
|
|
62
|
-
</label>
|
|
63
|
-
</section>
|
|
64
|
-
{BARS.map((b) => (
|
|
65
|
-
<section key={b.id} style={card}>
|
|
66
|
-
<h2 style={h2}>{b.label}</h2>
|
|
67
|
-
<div style={{ overflowX: "auto" }}>
|
|
68
|
-
<div style={{ width: b.width }}>
|
|
69
|
-
<TopNav key={module + brand} brand={brand} breakpoint={b.id} activeModuleId={module} org={ORG} user={USER} showCampus={showCampus} />
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
</section>
|
|
73
|
-
))}
|
|
74
|
-
</div>
|
|
75
|
-
</DemoChrome>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
mount(<App />);
|