@poly-x/react 0.1.0-alpha.10 → 0.1.0-alpha.11
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/dist/index.cjs +140 -21
- package/dist/index.d.cts +70 -6
- package/dist/index.d.mts +70 -6
- package/dist/index.mjs +140 -23
- package/dist/styles.css +52 -2
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1409,6 +1409,59 @@ function AuthCallback({ children = null }) {
|
|
|
1409
1409
|
return children;
|
|
1410
1410
|
}
|
|
1411
1411
|
//#endregion
|
|
1412
|
+
//#region src/components/placement.ts
|
|
1413
|
+
const OPPOSITE = {
|
|
1414
|
+
top: "bottom",
|
|
1415
|
+
bottom: "top",
|
|
1416
|
+
left: "right",
|
|
1417
|
+
right: "left"
|
|
1418
|
+
};
|
|
1419
|
+
/** Preference order when nothing is requested: vertical reads better for a menu than sideways. */
|
|
1420
|
+
const AUTO_ORDER = [
|
|
1421
|
+
"bottom",
|
|
1422
|
+
"top",
|
|
1423
|
+
"right",
|
|
1424
|
+
"left"
|
|
1425
|
+
];
|
|
1426
|
+
/** Room between the trigger and each viewport edge. */
|
|
1427
|
+
function spaceAround(trigger, viewport) {
|
|
1428
|
+
return {
|
|
1429
|
+
top: trigger.top,
|
|
1430
|
+
bottom: viewport.height - trigger.bottom,
|
|
1431
|
+
left: trigger.left,
|
|
1432
|
+
right: viewport.width - trigger.right
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
function required(menu, gap) {
|
|
1436
|
+
return {
|
|
1437
|
+
top: menu.height + gap,
|
|
1438
|
+
bottom: menu.height + gap,
|
|
1439
|
+
left: menu.width + gap,
|
|
1440
|
+
right: menu.width + gap
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Which side the menu should open on.
|
|
1445
|
+
*
|
|
1446
|
+
* An explicit side is honoured when it fits and flipped to its opposite when it doesn't — a
|
|
1447
|
+
* requested side that runs off-screen is a worse answer than the one that doesn't. `auto` takes
|
|
1448
|
+
* the first side that fits, and if none do, the roomiest: something has to be chosen, and the
|
|
1449
|
+
* least-bad option beats an arbitrary default.
|
|
1450
|
+
*/
|
|
1451
|
+
function resolveSide(preferred, trigger, menu, viewport, gap = 8) {
|
|
1452
|
+
const space = spaceAround(trigger, viewport);
|
|
1453
|
+
const need = required(menu, gap);
|
|
1454
|
+
const fits = (side) => space[side] >= need[side];
|
|
1455
|
+
if (preferred !== "auto") {
|
|
1456
|
+
if (fits(preferred)) return preferred;
|
|
1457
|
+
const opposite = OPPOSITE[preferred];
|
|
1458
|
+
return fits(opposite) ? opposite : preferred;
|
|
1459
|
+
}
|
|
1460
|
+
const firstFitting = AUTO_ORDER.find(fits);
|
|
1461
|
+
if (firstFitting) return firstFitting;
|
|
1462
|
+
return AUTO_ORDER.reduce((best, side) => space[side] > space[best] ? side : best, AUTO_ORDER[0]);
|
|
1463
|
+
}
|
|
1464
|
+
//#endregion
|
|
1412
1465
|
//#region src/components/user-avatar.tsx
|
|
1413
1466
|
/**
|
|
1414
1467
|
* Up to two initials from a display name. Handles the shapes the platform actually
|
|
@@ -1462,9 +1515,13 @@ function isBuiltIn(label) {
|
|
|
1462
1515
|
}
|
|
1463
1516
|
/**
|
|
1464
1517
|
* Declarative markers. `<UserButton/>` reads their props off the element tree and renders the
|
|
1465
|
-
* menu itself, so these never render anything
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
1518
|
+
* menu itself, so these never render anything. Their public prop types come from the signatures
|
|
1519
|
+
* declared on the exported object below.
|
|
1520
|
+
*
|
|
1521
|
+
* `Custom` and `Header` take arbitrary nodes on purpose: an app adopting this component in place
|
|
1522
|
+
* of its own user menu has to be able to bring its own UI, or it won't adopt it. That is
|
|
1523
|
+
* composition, not the style injection FR-BRAND-1 guards against — a consumer renders their own
|
|
1524
|
+
* subtree in a slot; they still cannot restyle the SDK's own chrome.
|
|
1468
1525
|
*/
|
|
1469
1526
|
function MenuItems() {
|
|
1470
1527
|
return null;
|
|
@@ -1475,6 +1532,12 @@ function Action() {
|
|
|
1475
1532
|
function Link() {
|
|
1476
1533
|
return null;
|
|
1477
1534
|
}
|
|
1535
|
+
function Custom() {
|
|
1536
|
+
return null;
|
|
1537
|
+
}
|
|
1538
|
+
function Header() {
|
|
1539
|
+
return null;
|
|
1540
|
+
}
|
|
1478
1541
|
/**
|
|
1479
1542
|
* Read the declarative children into a flat item list. A custom entry naming a built-in
|
|
1480
1543
|
* *moves* it; unnamed built-ins keep their default order at the end. This is the whole reason
|
|
@@ -1482,7 +1545,7 @@ function Link() {
|
|
|
1482
1545
|
* inject arbitrary markup or styles into the menu (FR-BRAND-1).
|
|
1483
1546
|
*/
|
|
1484
1547
|
function resolveItems(children, builtIns) {
|
|
1485
|
-
const
|
|
1548
|
+
const supplied = [];
|
|
1486
1549
|
const placed = /* @__PURE__ */ new Set();
|
|
1487
1550
|
react.Children.forEach(children, (child) => {
|
|
1488
1551
|
if (!(0, react.isValidElement)(child) || child.type !== MenuItems) return;
|
|
@@ -1493,12 +1556,12 @@ function resolveItems(children, builtIns) {
|
|
|
1493
1556
|
if (isBuiltIn(props.label)) {
|
|
1494
1557
|
const builtIn = builtIns.get(props.label);
|
|
1495
1558
|
if (builtIn) {
|
|
1496
|
-
|
|
1559
|
+
supplied.push(builtIn);
|
|
1497
1560
|
placed.add(props.label);
|
|
1498
1561
|
}
|
|
1499
1562
|
return;
|
|
1500
1563
|
}
|
|
1501
|
-
|
|
1564
|
+
supplied.push({
|
|
1502
1565
|
key: `action-${index}`,
|
|
1503
1566
|
label: props.label,
|
|
1504
1567
|
labelIcon: props.labelIcon,
|
|
@@ -1508,17 +1571,30 @@ function resolveItems(children, builtIns) {
|
|
|
1508
1571
|
}
|
|
1509
1572
|
if (item.type === Link) {
|
|
1510
1573
|
const props = item.props;
|
|
1511
|
-
|
|
1574
|
+
supplied.push({
|
|
1512
1575
|
key: `link-${index}`,
|
|
1513
1576
|
label: props.label,
|
|
1514
1577
|
labelIcon: props.labelIcon,
|
|
1515
1578
|
href: props.href
|
|
1516
1579
|
});
|
|
1580
|
+
return;
|
|
1517
1581
|
}
|
|
1582
|
+
if (item.type === Custom) supplied.push({
|
|
1583
|
+
key: `custom-${index}`,
|
|
1584
|
+
custom: item.props.children
|
|
1585
|
+
});
|
|
1518
1586
|
});
|
|
1519
1587
|
});
|
|
1520
1588
|
const remaining = [...builtIns.entries()].filter(([name]) => !placed.has(name)).map(([, item]) => item);
|
|
1521
|
-
return [...
|
|
1589
|
+
return [...supplied, ...remaining];
|
|
1590
|
+
}
|
|
1591
|
+
/** The consumer's replacement for the identity block, if they supplied one. */
|
|
1592
|
+
function findHeader(children) {
|
|
1593
|
+
let header;
|
|
1594
|
+
react.Children.forEach(children, (child) => {
|
|
1595
|
+
if ((0, react.isValidElement)(child) && child.type === Header) header = child.props.children;
|
|
1596
|
+
});
|
|
1597
|
+
return header;
|
|
1522
1598
|
}
|
|
1523
1599
|
function UserButtonRoot(props) {
|
|
1524
1600
|
const labels = {
|
|
@@ -1529,9 +1605,12 @@ function UserButtonRoot(props) {
|
|
|
1529
1605
|
const user = useUser();
|
|
1530
1606
|
const [open, setOpen] = (0, react.useState)(props.defaultOpen ?? false);
|
|
1531
1607
|
const [focused, setFocused] = (0, react.useState)(0);
|
|
1608
|
+
const [side, setSide] = (0, react.useState)(props.side && props.side !== "auto" ? props.side : "bottom");
|
|
1532
1609
|
const triggerRef = (0, react.useRef)(null);
|
|
1610
|
+
const menuRef = (0, react.useRef)(null);
|
|
1533
1611
|
const itemRefs = (0, react.useRef)([]);
|
|
1534
1612
|
const menuId = (0, react.useId)();
|
|
1613
|
+
const align = props.align ?? "end";
|
|
1535
1614
|
const close = (0, react.useCallback)((returnFocus) => {
|
|
1536
1615
|
setOpen(false);
|
|
1537
1616
|
if (returnFocus) triggerRef.current?.focus();
|
|
@@ -1555,6 +1634,33 @@ function UserButtonRoot(props) {
|
|
|
1555
1634
|
(0, react.useEffect)(() => {
|
|
1556
1635
|
if (open) itemRefs.current[focused]?.focus();
|
|
1557
1636
|
}, [open, focused]);
|
|
1637
|
+
/**
|
|
1638
|
+
* Choose the side once the menu exists and can be measured. `useLayoutEffect` runs before
|
|
1639
|
+
* paint, so the correction never shows as a jump — the menu is only ever painted where it
|
|
1640
|
+
* belongs. Re-measured on resize and on scroll, since either can invalidate the choice.
|
|
1641
|
+
*/
|
|
1642
|
+
(0, react.useLayoutEffect)(() => {
|
|
1643
|
+
if (!open) return;
|
|
1644
|
+
const place = () => {
|
|
1645
|
+
const trigger = triggerRef.current?.getBoundingClientRect();
|
|
1646
|
+
const menu = menuRef.current?.getBoundingClientRect();
|
|
1647
|
+
if (!trigger || !menu) return;
|
|
1648
|
+
setSide(resolveSide(props.side ?? "auto", trigger, {
|
|
1649
|
+
width: menu.width,
|
|
1650
|
+
height: menu.height
|
|
1651
|
+
}, {
|
|
1652
|
+
width: window.innerWidth,
|
|
1653
|
+
height: window.innerHeight
|
|
1654
|
+
}));
|
|
1655
|
+
};
|
|
1656
|
+
place();
|
|
1657
|
+
window.addEventListener("resize", place);
|
|
1658
|
+
window.addEventListener("scroll", place, true);
|
|
1659
|
+
return () => {
|
|
1660
|
+
window.removeEventListener("resize", place);
|
|
1661
|
+
window.removeEventListener("scroll", place, true);
|
|
1662
|
+
};
|
|
1663
|
+
}, [open, props.side]);
|
|
1558
1664
|
if (!isLoaded) return props.fallback ?? null;
|
|
1559
1665
|
if (!isSignedIn || !user) return null;
|
|
1560
1666
|
const name = user.displayName ?? user.email ?? "";
|
|
@@ -1581,14 +1687,17 @@ function UserButtonRoot(props) {
|
|
|
1581
1687
|
})
|
|
1582
1688
|
});
|
|
1583
1689
|
const items = resolveItems(props.children, builtIns);
|
|
1690
|
+
const customHeader = findHeader(props.children);
|
|
1691
|
+
const navigable = items.map((item, index) => ({
|
|
1692
|
+
item,
|
|
1693
|
+
index
|
|
1694
|
+
})).filter(({ item }) => !item.custom);
|
|
1584
1695
|
const onMenuKeyDown = (event) => {
|
|
1585
|
-
if (
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
setFocused((current) => (current - 1 + items.length) % items.length);
|
|
1591
|
-
}
|
|
1696
|
+
if (navigable.length === 0) return;
|
|
1697
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
1698
|
+
event.preventDefault();
|
|
1699
|
+
const next = (navigable.findIndex(({ index }) => index === focused) + (event.key === "ArrowDown" ? 1 : -1) + navigable.length) % navigable.length;
|
|
1700
|
+
setFocused(navigable[next].index);
|
|
1592
1701
|
};
|
|
1593
1702
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1594
1703
|
className: [
|
|
@@ -1597,7 +1706,7 @@ function UserButtonRoot(props) {
|
|
|
1597
1706
|
props.className
|
|
1598
1707
|
].filter(Boolean).join(" "),
|
|
1599
1708
|
"data-polyx-userbutton": open ? "open" : "closed",
|
|
1600
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1709
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1601
1710
|
ref: triggerRef,
|
|
1602
1711
|
type: "button",
|
|
1603
1712
|
className: "polyx-userbutton__trigger",
|
|
@@ -1609,20 +1718,23 @@ function UserButtonRoot(props) {
|
|
|
1609
1718
|
setFocused(0);
|
|
1610
1719
|
setOpen((current) => !current);
|
|
1611
1720
|
},
|
|
1612
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserAvatar, {
|
|
1721
|
+
children: props.trigger ?? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserAvatar, {
|
|
1613
1722
|
src: user.avatarUrl,
|
|
1614
1723
|
name,
|
|
1615
1724
|
size: props.avatarSize
|
|
1616
1725
|
}), props.showName ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1617
1726
|
className: "polyx-userbutton__trigger-name",
|
|
1618
1727
|
children: name
|
|
1619
|
-
}) : null]
|
|
1728
|
+
}) : null] })
|
|
1620
1729
|
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1730
|
+
ref: menuRef,
|
|
1621
1731
|
id: menuId,
|
|
1622
1732
|
role: "menu",
|
|
1623
1733
|
className: "polyx-userbutton__menu",
|
|
1734
|
+
"data-polyx-side": side,
|
|
1735
|
+
"data-polyx-align": align,
|
|
1624
1736
|
onKeyDown: onMenuKeyDown,
|
|
1625
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1737
|
+
children: [customHeader ?? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1626
1738
|
className: "polyx-userbutton__identity",
|
|
1627
1739
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserAvatar, {
|
|
1628
1740
|
src: user.avatarUrl,
|
|
@@ -1640,7 +1752,10 @@ function UserButtonRoot(props) {
|
|
|
1640
1752
|
})]
|
|
1641
1753
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1642
1754
|
className: "polyx-userbutton__items",
|
|
1643
|
-
children: items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: item.
|
|
1755
|
+
children: items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: item.custom ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1756
|
+
className: "polyx-userbutton__custom",
|
|
1757
|
+
children: item.custom
|
|
1758
|
+
}) : item.href ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
1644
1759
|
ref: (node) => {
|
|
1645
1760
|
itemRefs.current[index] = node;
|
|
1646
1761
|
},
|
|
@@ -1725,7 +1840,9 @@ function ExitIcon() {
|
|
|
1725
1840
|
const UserButton = Object.assign(UserButtonRoot, {
|
|
1726
1841
|
MenuItems,
|
|
1727
1842
|
Action,
|
|
1728
|
-
Link
|
|
1843
|
+
Link,
|
|
1844
|
+
Custom,
|
|
1845
|
+
Header
|
|
1729
1846
|
});
|
|
1730
1847
|
//#endregion
|
|
1731
1848
|
//#region src/index.ts
|
|
@@ -1753,6 +1870,8 @@ exports.UserButton = UserButton;
|
|
|
1753
1870
|
exports.WebLocksLock = WebLocksLock;
|
|
1754
1871
|
exports.WindowBrowserBridge = WindowBrowserBridge;
|
|
1755
1872
|
exports.createLoginController = createLoginController;
|
|
1873
|
+
exports.initialsFrom = initialsFrom;
|
|
1874
|
+
exports.resolveSide = resolveSide;
|
|
1756
1875
|
exports.useAuth = useAuth;
|
|
1757
1876
|
exports.useAuthCallback = useAuthCallback;
|
|
1758
1877
|
exports.useSession = useSession;
|
package/dist/index.d.cts
CHANGED
|
@@ -386,6 +386,46 @@ declare function AuthCallback({
|
|
|
386
386
|
children
|
|
387
387
|
}: AuthCallbackProps): ReactNode;
|
|
388
388
|
//#endregion
|
|
389
|
+
//#region src/components/placement.d.ts
|
|
390
|
+
/**
|
|
391
|
+
* Collision-aware placement for the SDK's popovers.
|
|
392
|
+
*
|
|
393
|
+
* A menu pinned to one side is wrong somewhere: an avatar in a page header wants to open
|
|
394
|
+
* down-and-left, the same component in a sidebar footer has to open up-and-right. Rather than
|
|
395
|
+
* make every consumer work that out, measure the trigger against the viewport and pick.
|
|
396
|
+
*
|
|
397
|
+
* Deliberately hand-rolled: `@poly-x/react` has no runtime dependency but `@poly-x/core`, and a
|
|
398
|
+
* full positioning engine is a lot of surface to inherit for one menu. The component only needs
|
|
399
|
+
* to know *which side* — CSS does the actual placing — so this stays a pure function over
|
|
400
|
+
* rectangles, and is unit-testable without a layout engine.
|
|
401
|
+
*/
|
|
402
|
+
type Side = "top" | "bottom" | "left" | "right";
|
|
403
|
+
type SideOption = Side | "auto";
|
|
404
|
+
type Align = "start" | "center" | "end";
|
|
405
|
+
interface Rect {
|
|
406
|
+
top: number;
|
|
407
|
+
bottom: number;
|
|
408
|
+
left: number;
|
|
409
|
+
right: number;
|
|
410
|
+
}
|
|
411
|
+
interface Size {
|
|
412
|
+
width: number;
|
|
413
|
+
height: number;
|
|
414
|
+
}
|
|
415
|
+
interface Viewport {
|
|
416
|
+
width: number;
|
|
417
|
+
height: number;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Which side the menu should open on.
|
|
421
|
+
*
|
|
422
|
+
* An explicit side is honoured when it fits and flipped to its opposite when it doesn't — a
|
|
423
|
+
* requested side that runs off-screen is a worse answer than the one that doesn't. `auto` takes
|
|
424
|
+
* the first side that fits, and if none do, the roomiest: something has to be chosen, and the
|
|
425
|
+
* least-bad option beats an arbitrary default.
|
|
426
|
+
*/
|
|
427
|
+
declare function resolveSide(preferred: SideOption, trigger: Rect, menu: Size, viewport: Viewport, gap?: number): Side;
|
|
428
|
+
//#endregion
|
|
389
429
|
//#region src/components/user-button.d.ts
|
|
390
430
|
/** Every user-facing string, overridable for i18n / white-labelling. */
|
|
391
431
|
interface UserButtonLabels {
|
|
@@ -409,6 +449,9 @@ interface UserButtonLinkProps {
|
|
|
409
449
|
labelIcon?: ReactNode;
|
|
410
450
|
href: string;
|
|
411
451
|
}
|
|
452
|
+
interface UserButtonSlotProps {
|
|
453
|
+
children?: ReactNode;
|
|
454
|
+
}
|
|
412
455
|
interface UserButtonProps {
|
|
413
456
|
/** Show the user's name next to the avatar. Default `false`. */
|
|
414
457
|
showName?: boolean;
|
|
@@ -428,10 +471,26 @@ interface UserButtonProps {
|
|
|
428
471
|
/** Appended to the root element's class list, for escape-hatch styling. */
|
|
429
472
|
className?: string;
|
|
430
473
|
labels?: Partial<UserButtonLabels>;
|
|
431
|
-
/**
|
|
474
|
+
/**
|
|
475
|
+
* `<UserButton.MenuItems>` with `<UserButton.Action>` / `<UserButton.Link>` / `<UserButton.Custom>`
|
|
476
|
+
* entries, and/or a `<UserButton.Header>` replacing the identity block.
|
|
477
|
+
*/
|
|
432
478
|
children?: ReactNode;
|
|
433
479
|
/** Avatar size in pixels. Default 32. */
|
|
434
480
|
avatarSize?: number;
|
|
481
|
+
/**
|
|
482
|
+
* What the trigger shows. Replaces the avatar (and `showName`); the SDK still owns the button
|
|
483
|
+
* and its menu semantics, so a custom trigger costs no accessibility.
|
|
484
|
+
*/
|
|
485
|
+
trigger?: ReactNode;
|
|
486
|
+
/**
|
|
487
|
+
* Which side the menu opens on. Default `auto` — measured against the viewport, because the
|
|
488
|
+
* right answer differs by where the trigger sits (a header avatar opens down, a sidebar-footer
|
|
489
|
+
* one has to open up). An explicit side is flipped only if it would run off-screen.
|
|
490
|
+
*/
|
|
491
|
+
side?: SideOption;
|
|
492
|
+
/** Alignment along the chosen side. Default `end`. */
|
|
493
|
+
align?: Align;
|
|
435
494
|
}
|
|
436
495
|
/**
|
|
437
496
|
* The signed-in user surface: avatar trigger + a menu with their identity and sign-out
|
|
@@ -451,11 +510,11 @@ interface UserButtonProps {
|
|
|
451
510
|
* ```
|
|
452
511
|
*/
|
|
453
512
|
declare const UserButton: ((props: UserButtonProps) => ReactNode) & {
|
|
454
|
-
MenuItems: (props:
|
|
455
|
-
children?: ReactNode;
|
|
456
|
-
}) => ReactNode;
|
|
513
|
+
MenuItems: (props: UserButtonSlotProps) => ReactNode;
|
|
457
514
|
Action: (props: UserButtonActionProps) => ReactNode;
|
|
458
|
-
Link: (props: UserButtonLinkProps) => ReactNode;
|
|
515
|
+
Link: (props: UserButtonLinkProps) => ReactNode; /** Arbitrary JSX as a menu row — bring your own controls. */
|
|
516
|
+
Custom: (props: UserButtonSlotProps) => ReactNode; /** Replaces the built-in identity block. */
|
|
517
|
+
Header: (props: UserButtonSlotProps) => ReactNode;
|
|
459
518
|
};
|
|
460
519
|
//#endregion
|
|
461
520
|
//#region src/components/user-avatar.d.ts
|
|
@@ -469,6 +528,11 @@ interface UserAvatarProps {
|
|
|
469
528
|
/** Appended to the root element's class list, for escape-hatch styling. */
|
|
470
529
|
className?: string;
|
|
471
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Up to two initials from a display name. Handles the shapes the platform actually
|
|
533
|
+
* produces — "Ali Ikram", a bare username, an email address.
|
|
534
|
+
*/
|
|
535
|
+
declare function initialsFrom(name: string | undefined): string;
|
|
472
536
|
/**
|
|
473
537
|
* The user's picture, or their initials. A missing avatar is the ordinary case rather than
|
|
474
538
|
* an error, so initials are a first-class state, not a broken image.
|
|
@@ -488,4 +552,4 @@ declare function UserAvatar({
|
|
|
488
552
|
declare const PACKAGE_NAME = "@poly-x/react";
|
|
489
553
|
declare const version = "0.0.0";
|
|
490
554
|
//#endregion
|
|
491
|
-
export { AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, UserAvatar, type UserAvatarProps, UserButton, type UserButtonActionProps, type UserButtonLabels, type UserButtonLinkProps, type UserButtonProps, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
|
|
555
|
+
export { type Align, AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, type Side, type SideOption, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, UserAvatar, type UserAvatarProps, UserButton, type UserButtonActionProps, type UserButtonLabels, type UserButtonLinkProps, type UserButtonProps, type UserButtonSlotProps, WebLocksLock, WindowBrowserBridge, createLoginController, initialsFrom, resolveSide, useAuth, useAuthCallback, useSession, useUser, version };
|
package/dist/index.d.mts
CHANGED
|
@@ -386,6 +386,46 @@ declare function AuthCallback({
|
|
|
386
386
|
children
|
|
387
387
|
}: AuthCallbackProps): ReactNode;
|
|
388
388
|
//#endregion
|
|
389
|
+
//#region src/components/placement.d.ts
|
|
390
|
+
/**
|
|
391
|
+
* Collision-aware placement for the SDK's popovers.
|
|
392
|
+
*
|
|
393
|
+
* A menu pinned to one side is wrong somewhere: an avatar in a page header wants to open
|
|
394
|
+
* down-and-left, the same component in a sidebar footer has to open up-and-right. Rather than
|
|
395
|
+
* make every consumer work that out, measure the trigger against the viewport and pick.
|
|
396
|
+
*
|
|
397
|
+
* Deliberately hand-rolled: `@poly-x/react` has no runtime dependency but `@poly-x/core`, and a
|
|
398
|
+
* full positioning engine is a lot of surface to inherit for one menu. The component only needs
|
|
399
|
+
* to know *which side* — CSS does the actual placing — so this stays a pure function over
|
|
400
|
+
* rectangles, and is unit-testable without a layout engine.
|
|
401
|
+
*/
|
|
402
|
+
type Side = "top" | "bottom" | "left" | "right";
|
|
403
|
+
type SideOption = Side | "auto";
|
|
404
|
+
type Align = "start" | "center" | "end";
|
|
405
|
+
interface Rect {
|
|
406
|
+
top: number;
|
|
407
|
+
bottom: number;
|
|
408
|
+
left: number;
|
|
409
|
+
right: number;
|
|
410
|
+
}
|
|
411
|
+
interface Size {
|
|
412
|
+
width: number;
|
|
413
|
+
height: number;
|
|
414
|
+
}
|
|
415
|
+
interface Viewport {
|
|
416
|
+
width: number;
|
|
417
|
+
height: number;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Which side the menu should open on.
|
|
421
|
+
*
|
|
422
|
+
* An explicit side is honoured when it fits and flipped to its opposite when it doesn't — a
|
|
423
|
+
* requested side that runs off-screen is a worse answer than the one that doesn't. `auto` takes
|
|
424
|
+
* the first side that fits, and if none do, the roomiest: something has to be chosen, and the
|
|
425
|
+
* least-bad option beats an arbitrary default.
|
|
426
|
+
*/
|
|
427
|
+
declare function resolveSide(preferred: SideOption, trigger: Rect, menu: Size, viewport: Viewport, gap?: number): Side;
|
|
428
|
+
//#endregion
|
|
389
429
|
//#region src/components/user-button.d.ts
|
|
390
430
|
/** Every user-facing string, overridable for i18n / white-labelling. */
|
|
391
431
|
interface UserButtonLabels {
|
|
@@ -409,6 +449,9 @@ interface UserButtonLinkProps {
|
|
|
409
449
|
labelIcon?: ReactNode;
|
|
410
450
|
href: string;
|
|
411
451
|
}
|
|
452
|
+
interface UserButtonSlotProps {
|
|
453
|
+
children?: ReactNode;
|
|
454
|
+
}
|
|
412
455
|
interface UserButtonProps {
|
|
413
456
|
/** Show the user's name next to the avatar. Default `false`. */
|
|
414
457
|
showName?: boolean;
|
|
@@ -428,10 +471,26 @@ interface UserButtonProps {
|
|
|
428
471
|
/** Appended to the root element's class list, for escape-hatch styling. */
|
|
429
472
|
className?: string;
|
|
430
473
|
labels?: Partial<UserButtonLabels>;
|
|
431
|
-
/**
|
|
474
|
+
/**
|
|
475
|
+
* `<UserButton.MenuItems>` with `<UserButton.Action>` / `<UserButton.Link>` / `<UserButton.Custom>`
|
|
476
|
+
* entries, and/or a `<UserButton.Header>` replacing the identity block.
|
|
477
|
+
*/
|
|
432
478
|
children?: ReactNode;
|
|
433
479
|
/** Avatar size in pixels. Default 32. */
|
|
434
480
|
avatarSize?: number;
|
|
481
|
+
/**
|
|
482
|
+
* What the trigger shows. Replaces the avatar (and `showName`); the SDK still owns the button
|
|
483
|
+
* and its menu semantics, so a custom trigger costs no accessibility.
|
|
484
|
+
*/
|
|
485
|
+
trigger?: ReactNode;
|
|
486
|
+
/**
|
|
487
|
+
* Which side the menu opens on. Default `auto` — measured against the viewport, because the
|
|
488
|
+
* right answer differs by where the trigger sits (a header avatar opens down, a sidebar-footer
|
|
489
|
+
* one has to open up). An explicit side is flipped only if it would run off-screen.
|
|
490
|
+
*/
|
|
491
|
+
side?: SideOption;
|
|
492
|
+
/** Alignment along the chosen side. Default `end`. */
|
|
493
|
+
align?: Align;
|
|
435
494
|
}
|
|
436
495
|
/**
|
|
437
496
|
* The signed-in user surface: avatar trigger + a menu with their identity and sign-out
|
|
@@ -451,11 +510,11 @@ interface UserButtonProps {
|
|
|
451
510
|
* ```
|
|
452
511
|
*/
|
|
453
512
|
declare const UserButton: ((props: UserButtonProps) => ReactNode) & {
|
|
454
|
-
MenuItems: (props:
|
|
455
|
-
children?: ReactNode;
|
|
456
|
-
}) => ReactNode;
|
|
513
|
+
MenuItems: (props: UserButtonSlotProps) => ReactNode;
|
|
457
514
|
Action: (props: UserButtonActionProps) => ReactNode;
|
|
458
|
-
Link: (props: UserButtonLinkProps) => ReactNode;
|
|
515
|
+
Link: (props: UserButtonLinkProps) => ReactNode; /** Arbitrary JSX as a menu row — bring your own controls. */
|
|
516
|
+
Custom: (props: UserButtonSlotProps) => ReactNode; /** Replaces the built-in identity block. */
|
|
517
|
+
Header: (props: UserButtonSlotProps) => ReactNode;
|
|
459
518
|
};
|
|
460
519
|
//#endregion
|
|
461
520
|
//#region src/components/user-avatar.d.ts
|
|
@@ -469,6 +528,11 @@ interface UserAvatarProps {
|
|
|
469
528
|
/** Appended to the root element's class list, for escape-hatch styling. */
|
|
470
529
|
className?: string;
|
|
471
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Up to two initials from a display name. Handles the shapes the platform actually
|
|
533
|
+
* produces — "Ali Ikram", a bare username, an email address.
|
|
534
|
+
*/
|
|
535
|
+
declare function initialsFrom(name: string | undefined): string;
|
|
472
536
|
/**
|
|
473
537
|
* The user's picture, or their initials. A missing avatar is the ordinary case rather than
|
|
474
538
|
* an error, so initials are a first-class state, not a broken image.
|
|
@@ -488,4 +552,4 @@ declare function UserAvatar({
|
|
|
488
552
|
declare const PACKAGE_NAME = "@poly-x/react";
|
|
489
553
|
declare const version = "0.0.0";
|
|
490
554
|
//#endregion
|
|
491
|
-
export { AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, UserAvatar, type UserAvatarProps, UserButton, type UserButtonActionProps, type UserButtonLabels, type UserButtonLinkProps, type UserButtonProps, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
|
|
555
|
+
export { type Align, AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, type Side, type SideOption, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, UserAvatar, type UserAvatarProps, UserButton, type UserButtonActionProps, type UserButtonLabels, type UserButtonLinkProps, type UserButtonProps, type UserButtonSlotProps, WebLocksLock, WindowBrowserBridge, createLoginController, initialsFrom, resolveSide, useAuth, useAuthCallback, useSession, useUser, version };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { AuthClient, FetchTransport, InMemoryLock, InMemoryPkceStore, InMemorySessionChannel, InMemorySessionStore, createSessionEngine, generatePkce, randomState, resolveConfig } from "@poly-x/core";
|
|
3
|
-
import { Children, Fragment, createContext, isValidElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { Children, Fragment, createContext, isValidElement, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/auth-channel.ts
|
|
6
6
|
var BroadcastAuthChannel = class {
|
|
@@ -1408,6 +1408,59 @@ function AuthCallback({ children = null }) {
|
|
|
1408
1408
|
return children;
|
|
1409
1409
|
}
|
|
1410
1410
|
//#endregion
|
|
1411
|
+
//#region src/components/placement.ts
|
|
1412
|
+
const OPPOSITE = {
|
|
1413
|
+
top: "bottom",
|
|
1414
|
+
bottom: "top",
|
|
1415
|
+
left: "right",
|
|
1416
|
+
right: "left"
|
|
1417
|
+
};
|
|
1418
|
+
/** Preference order when nothing is requested: vertical reads better for a menu than sideways. */
|
|
1419
|
+
const AUTO_ORDER = [
|
|
1420
|
+
"bottom",
|
|
1421
|
+
"top",
|
|
1422
|
+
"right",
|
|
1423
|
+
"left"
|
|
1424
|
+
];
|
|
1425
|
+
/** Room between the trigger and each viewport edge. */
|
|
1426
|
+
function spaceAround(trigger, viewport) {
|
|
1427
|
+
return {
|
|
1428
|
+
top: trigger.top,
|
|
1429
|
+
bottom: viewport.height - trigger.bottom,
|
|
1430
|
+
left: trigger.left,
|
|
1431
|
+
right: viewport.width - trigger.right
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
function required(menu, gap) {
|
|
1435
|
+
return {
|
|
1436
|
+
top: menu.height + gap,
|
|
1437
|
+
bottom: menu.height + gap,
|
|
1438
|
+
left: menu.width + gap,
|
|
1439
|
+
right: menu.width + gap
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Which side the menu should open on.
|
|
1444
|
+
*
|
|
1445
|
+
* An explicit side is honoured when it fits and flipped to its opposite when it doesn't — a
|
|
1446
|
+
* requested side that runs off-screen is a worse answer than the one that doesn't. `auto` takes
|
|
1447
|
+
* the first side that fits, and if none do, the roomiest: something has to be chosen, and the
|
|
1448
|
+
* least-bad option beats an arbitrary default.
|
|
1449
|
+
*/
|
|
1450
|
+
function resolveSide(preferred, trigger, menu, viewport, gap = 8) {
|
|
1451
|
+
const space = spaceAround(trigger, viewport);
|
|
1452
|
+
const need = required(menu, gap);
|
|
1453
|
+
const fits = (side) => space[side] >= need[side];
|
|
1454
|
+
if (preferred !== "auto") {
|
|
1455
|
+
if (fits(preferred)) return preferred;
|
|
1456
|
+
const opposite = OPPOSITE[preferred];
|
|
1457
|
+
return fits(opposite) ? opposite : preferred;
|
|
1458
|
+
}
|
|
1459
|
+
const firstFitting = AUTO_ORDER.find(fits);
|
|
1460
|
+
if (firstFitting) return firstFitting;
|
|
1461
|
+
return AUTO_ORDER.reduce((best, side) => space[side] > space[best] ? side : best, AUTO_ORDER[0]);
|
|
1462
|
+
}
|
|
1463
|
+
//#endregion
|
|
1411
1464
|
//#region src/components/user-avatar.tsx
|
|
1412
1465
|
/**
|
|
1413
1466
|
* Up to two initials from a display name. Handles the shapes the platform actually
|
|
@@ -1461,9 +1514,13 @@ function isBuiltIn(label) {
|
|
|
1461
1514
|
}
|
|
1462
1515
|
/**
|
|
1463
1516
|
* Declarative markers. `<UserButton/>` reads their props off the element tree and renders the
|
|
1464
|
-
* menu itself, so these never render anything
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
1517
|
+
* menu itself, so these never render anything. Their public prop types come from the signatures
|
|
1518
|
+
* declared on the exported object below.
|
|
1519
|
+
*
|
|
1520
|
+
* `Custom` and `Header` take arbitrary nodes on purpose: an app adopting this component in place
|
|
1521
|
+
* of its own user menu has to be able to bring its own UI, or it won't adopt it. That is
|
|
1522
|
+
* composition, not the style injection FR-BRAND-1 guards against — a consumer renders their own
|
|
1523
|
+
* subtree in a slot; they still cannot restyle the SDK's own chrome.
|
|
1467
1524
|
*/
|
|
1468
1525
|
function MenuItems() {
|
|
1469
1526
|
return null;
|
|
@@ -1474,6 +1531,12 @@ function Action() {
|
|
|
1474
1531
|
function Link() {
|
|
1475
1532
|
return null;
|
|
1476
1533
|
}
|
|
1534
|
+
function Custom() {
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
function Header() {
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1477
1540
|
/**
|
|
1478
1541
|
* Read the declarative children into a flat item list. A custom entry naming a built-in
|
|
1479
1542
|
* *moves* it; unnamed built-ins keep their default order at the end. This is the whole reason
|
|
@@ -1481,7 +1544,7 @@ function Link() {
|
|
|
1481
1544
|
* inject arbitrary markup or styles into the menu (FR-BRAND-1).
|
|
1482
1545
|
*/
|
|
1483
1546
|
function resolveItems(children, builtIns) {
|
|
1484
|
-
const
|
|
1547
|
+
const supplied = [];
|
|
1485
1548
|
const placed = /* @__PURE__ */ new Set();
|
|
1486
1549
|
Children.forEach(children, (child) => {
|
|
1487
1550
|
if (!isValidElement(child) || child.type !== MenuItems) return;
|
|
@@ -1492,12 +1555,12 @@ function resolveItems(children, builtIns) {
|
|
|
1492
1555
|
if (isBuiltIn(props.label)) {
|
|
1493
1556
|
const builtIn = builtIns.get(props.label);
|
|
1494
1557
|
if (builtIn) {
|
|
1495
|
-
|
|
1558
|
+
supplied.push(builtIn);
|
|
1496
1559
|
placed.add(props.label);
|
|
1497
1560
|
}
|
|
1498
1561
|
return;
|
|
1499
1562
|
}
|
|
1500
|
-
|
|
1563
|
+
supplied.push({
|
|
1501
1564
|
key: `action-${index}`,
|
|
1502
1565
|
label: props.label,
|
|
1503
1566
|
labelIcon: props.labelIcon,
|
|
@@ -1507,17 +1570,30 @@ function resolveItems(children, builtIns) {
|
|
|
1507
1570
|
}
|
|
1508
1571
|
if (item.type === Link) {
|
|
1509
1572
|
const props = item.props;
|
|
1510
|
-
|
|
1573
|
+
supplied.push({
|
|
1511
1574
|
key: `link-${index}`,
|
|
1512
1575
|
label: props.label,
|
|
1513
1576
|
labelIcon: props.labelIcon,
|
|
1514
1577
|
href: props.href
|
|
1515
1578
|
});
|
|
1579
|
+
return;
|
|
1516
1580
|
}
|
|
1581
|
+
if (item.type === Custom) supplied.push({
|
|
1582
|
+
key: `custom-${index}`,
|
|
1583
|
+
custom: item.props.children
|
|
1584
|
+
});
|
|
1517
1585
|
});
|
|
1518
1586
|
});
|
|
1519
1587
|
const remaining = [...builtIns.entries()].filter(([name]) => !placed.has(name)).map(([, item]) => item);
|
|
1520
|
-
return [...
|
|
1588
|
+
return [...supplied, ...remaining];
|
|
1589
|
+
}
|
|
1590
|
+
/** The consumer's replacement for the identity block, if they supplied one. */
|
|
1591
|
+
function findHeader(children) {
|
|
1592
|
+
let header;
|
|
1593
|
+
Children.forEach(children, (child) => {
|
|
1594
|
+
if (isValidElement(child) && child.type === Header) header = child.props.children;
|
|
1595
|
+
});
|
|
1596
|
+
return header;
|
|
1521
1597
|
}
|
|
1522
1598
|
function UserButtonRoot(props) {
|
|
1523
1599
|
const labels = {
|
|
@@ -1528,9 +1604,12 @@ function UserButtonRoot(props) {
|
|
|
1528
1604
|
const user = useUser();
|
|
1529
1605
|
const [open, setOpen] = useState(props.defaultOpen ?? false);
|
|
1530
1606
|
const [focused, setFocused] = useState(0);
|
|
1607
|
+
const [side, setSide] = useState(props.side && props.side !== "auto" ? props.side : "bottom");
|
|
1531
1608
|
const triggerRef = useRef(null);
|
|
1609
|
+
const menuRef = useRef(null);
|
|
1532
1610
|
const itemRefs = useRef([]);
|
|
1533
1611
|
const menuId = useId();
|
|
1612
|
+
const align = props.align ?? "end";
|
|
1534
1613
|
const close = useCallback((returnFocus) => {
|
|
1535
1614
|
setOpen(false);
|
|
1536
1615
|
if (returnFocus) triggerRef.current?.focus();
|
|
@@ -1554,6 +1633,33 @@ function UserButtonRoot(props) {
|
|
|
1554
1633
|
useEffect(() => {
|
|
1555
1634
|
if (open) itemRefs.current[focused]?.focus();
|
|
1556
1635
|
}, [open, focused]);
|
|
1636
|
+
/**
|
|
1637
|
+
* Choose the side once the menu exists and can be measured. `useLayoutEffect` runs before
|
|
1638
|
+
* paint, so the correction never shows as a jump — the menu is only ever painted where it
|
|
1639
|
+
* belongs. Re-measured on resize and on scroll, since either can invalidate the choice.
|
|
1640
|
+
*/
|
|
1641
|
+
useLayoutEffect(() => {
|
|
1642
|
+
if (!open) return;
|
|
1643
|
+
const place = () => {
|
|
1644
|
+
const trigger = triggerRef.current?.getBoundingClientRect();
|
|
1645
|
+
const menu = menuRef.current?.getBoundingClientRect();
|
|
1646
|
+
if (!trigger || !menu) return;
|
|
1647
|
+
setSide(resolveSide(props.side ?? "auto", trigger, {
|
|
1648
|
+
width: menu.width,
|
|
1649
|
+
height: menu.height
|
|
1650
|
+
}, {
|
|
1651
|
+
width: window.innerWidth,
|
|
1652
|
+
height: window.innerHeight
|
|
1653
|
+
}));
|
|
1654
|
+
};
|
|
1655
|
+
place();
|
|
1656
|
+
window.addEventListener("resize", place);
|
|
1657
|
+
window.addEventListener("scroll", place, true);
|
|
1658
|
+
return () => {
|
|
1659
|
+
window.removeEventListener("resize", place);
|
|
1660
|
+
window.removeEventListener("scroll", place, true);
|
|
1661
|
+
};
|
|
1662
|
+
}, [open, props.side]);
|
|
1557
1663
|
if (!isLoaded) return props.fallback ?? null;
|
|
1558
1664
|
if (!isSignedIn || !user) return null;
|
|
1559
1665
|
const name = user.displayName ?? user.email ?? "";
|
|
@@ -1580,14 +1686,17 @@ function UserButtonRoot(props) {
|
|
|
1580
1686
|
})
|
|
1581
1687
|
});
|
|
1582
1688
|
const items = resolveItems(props.children, builtIns);
|
|
1689
|
+
const customHeader = findHeader(props.children);
|
|
1690
|
+
const navigable = items.map((item, index) => ({
|
|
1691
|
+
item,
|
|
1692
|
+
index
|
|
1693
|
+
})).filter(({ item }) => !item.custom);
|
|
1583
1694
|
const onMenuKeyDown = (event) => {
|
|
1584
|
-
if (
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
setFocused((current) => (current - 1 + items.length) % items.length);
|
|
1590
|
-
}
|
|
1695
|
+
if (navigable.length === 0) return;
|
|
1696
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
1697
|
+
event.preventDefault();
|
|
1698
|
+
const next = (navigable.findIndex(({ index }) => index === focused) + (event.key === "ArrowDown" ? 1 : -1) + navigable.length) % navigable.length;
|
|
1699
|
+
setFocused(navigable[next].index);
|
|
1591
1700
|
};
|
|
1592
1701
|
return /* @__PURE__ */ jsxs("div", {
|
|
1593
1702
|
className: [
|
|
@@ -1596,7 +1705,7 @@ function UserButtonRoot(props) {
|
|
|
1596
1705
|
props.className
|
|
1597
1706
|
].filter(Boolean).join(" "),
|
|
1598
1707
|
"data-polyx-userbutton": open ? "open" : "closed",
|
|
1599
|
-
children: [/* @__PURE__ */
|
|
1708
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1600
1709
|
ref: triggerRef,
|
|
1601
1710
|
type: "button",
|
|
1602
1711
|
className: "polyx-userbutton__trigger",
|
|
@@ -1608,20 +1717,23 @@ function UserButtonRoot(props) {
|
|
|
1608
1717
|
setFocused(0);
|
|
1609
1718
|
setOpen((current) => !current);
|
|
1610
1719
|
},
|
|
1611
|
-
children: [/* @__PURE__ */ jsx(UserAvatar, {
|
|
1720
|
+
children: props.trigger ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(UserAvatar, {
|
|
1612
1721
|
src: user.avatarUrl,
|
|
1613
1722
|
name,
|
|
1614
1723
|
size: props.avatarSize
|
|
1615
1724
|
}), props.showName ? /* @__PURE__ */ jsx("span", {
|
|
1616
1725
|
className: "polyx-userbutton__trigger-name",
|
|
1617
1726
|
children: name
|
|
1618
|
-
}) : null]
|
|
1727
|
+
}) : null] })
|
|
1619
1728
|
}), open ? /* @__PURE__ */ jsxs("div", {
|
|
1729
|
+
ref: menuRef,
|
|
1620
1730
|
id: menuId,
|
|
1621
1731
|
role: "menu",
|
|
1622
1732
|
className: "polyx-userbutton__menu",
|
|
1733
|
+
"data-polyx-side": side,
|
|
1734
|
+
"data-polyx-align": align,
|
|
1623
1735
|
onKeyDown: onMenuKeyDown,
|
|
1624
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1736
|
+
children: [customHeader ?? /* @__PURE__ */ jsxs("div", {
|
|
1625
1737
|
className: "polyx-userbutton__identity",
|
|
1626
1738
|
children: [/* @__PURE__ */ jsx(UserAvatar, {
|
|
1627
1739
|
src: user.avatarUrl,
|
|
@@ -1639,7 +1751,10 @@ function UserButtonRoot(props) {
|
|
|
1639
1751
|
})]
|
|
1640
1752
|
}), /* @__PURE__ */ jsx("ul", {
|
|
1641
1753
|
className: "polyx-userbutton__items",
|
|
1642
|
-
children: items.map((item, index) => /* @__PURE__ */ jsx("li", { children: item.
|
|
1754
|
+
children: items.map((item, index) => /* @__PURE__ */ jsx("li", { children: item.custom ? /* @__PURE__ */ jsx("div", {
|
|
1755
|
+
className: "polyx-userbutton__custom",
|
|
1756
|
+
children: item.custom
|
|
1757
|
+
}) : item.href ? /* @__PURE__ */ jsxs("a", {
|
|
1643
1758
|
ref: (node) => {
|
|
1644
1759
|
itemRefs.current[index] = node;
|
|
1645
1760
|
},
|
|
@@ -1724,7 +1839,9 @@ function ExitIcon() {
|
|
|
1724
1839
|
const UserButton = Object.assign(UserButtonRoot, {
|
|
1725
1840
|
MenuItems,
|
|
1726
1841
|
Action,
|
|
1727
|
-
Link
|
|
1842
|
+
Link,
|
|
1843
|
+
Custom,
|
|
1844
|
+
Header
|
|
1728
1845
|
});
|
|
1729
1846
|
//#endregion
|
|
1730
1847
|
//#region src/index.ts
|
|
@@ -1735,4 +1852,4 @@ const UserButton = Object.assign(UserButtonRoot, {
|
|
|
1735
1852
|
const PACKAGE_NAME = "@poly-x/react";
|
|
1736
1853
|
const version = "0.0.0";
|
|
1737
1854
|
//#endregion
|
|
1738
|
-
export { AuthCallback, BroadcastAuthChannel, BroadcastChannelSessionChannel, ForgotPassword, PACKAGE_NAME, PolyXContext, PolyXProvider, PopupCancelledError, Protected, ResetPassword, SessionStoragePkceStore, SignIn, UserAvatar, UserButton, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
|
|
1855
|
+
export { AuthCallback, BroadcastAuthChannel, BroadcastChannelSessionChannel, ForgotPassword, PACKAGE_NAME, PolyXContext, PolyXProvider, PopupCancelledError, Protected, ResetPassword, SessionStoragePkceStore, SignIn, UserAvatar, UserButton, WebLocksLock, WindowBrowserBridge, createLoginController, initialsFrom, resolveSide, useAuth, useAuthCallback, useSession, useUser, version };
|
package/dist/styles.css
CHANGED
|
@@ -548,10 +548,12 @@
|
|
|
548
548
|
user-select: none;
|
|
549
549
|
}
|
|
550
550
|
|
|
551
|
+
/* Placement is expressed as data attributes and resolved in CSS: the component measures which
|
|
552
|
+
side has room and says so; where that lands is a styling concern. `--polyx-gap` is the space
|
|
553
|
+
between trigger and menu, and the JS assumes the same 8px when it measures. */
|
|
551
554
|
.polyx-userbutton__menu {
|
|
555
|
+
--polyx-gap: 0.5rem;
|
|
552
556
|
position: absolute;
|
|
553
|
-
top: calc(100% + 0.5rem);
|
|
554
|
-
right: 0;
|
|
555
557
|
z-index: 50;
|
|
556
558
|
min-width: 15rem;
|
|
557
559
|
padding: 0.375rem;
|
|
@@ -561,6 +563,54 @@
|
|
|
561
563
|
box-shadow: var(--polyx-shadow);
|
|
562
564
|
}
|
|
563
565
|
|
|
566
|
+
/* Vertical sides: offset above/below, then align along the horizontal axis. */
|
|
567
|
+
.polyx-userbutton__menu[data-polyx-side="bottom"] {
|
|
568
|
+
top: calc(100% + var(--polyx-gap));
|
|
569
|
+
}
|
|
570
|
+
.polyx-userbutton__menu[data-polyx-side="top"] {
|
|
571
|
+
bottom: calc(100% + var(--polyx-gap));
|
|
572
|
+
}
|
|
573
|
+
.polyx-userbutton__menu[data-polyx-side="bottom"][data-polyx-align="end"],
|
|
574
|
+
.polyx-userbutton__menu[data-polyx-side="top"][data-polyx-align="end"] {
|
|
575
|
+
right: 0;
|
|
576
|
+
}
|
|
577
|
+
.polyx-userbutton__menu[data-polyx-side="bottom"][data-polyx-align="start"],
|
|
578
|
+
.polyx-userbutton__menu[data-polyx-side="top"][data-polyx-align="start"] {
|
|
579
|
+
left: 0;
|
|
580
|
+
}
|
|
581
|
+
.polyx-userbutton__menu[data-polyx-side="bottom"][data-polyx-align="center"],
|
|
582
|
+
.polyx-userbutton__menu[data-polyx-side="top"][data-polyx-align="center"] {
|
|
583
|
+
left: 50%;
|
|
584
|
+
transform: translateX(-50%);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/* Horizontal sides: offset left/right, then align along the vertical axis. */
|
|
588
|
+
.polyx-userbutton__menu[data-polyx-side="right"] {
|
|
589
|
+
left: calc(100% + var(--polyx-gap));
|
|
590
|
+
}
|
|
591
|
+
.polyx-userbutton__menu[data-polyx-side="left"] {
|
|
592
|
+
right: calc(100% + var(--polyx-gap));
|
|
593
|
+
}
|
|
594
|
+
.polyx-userbutton__menu[data-polyx-side="right"][data-polyx-align="end"],
|
|
595
|
+
.polyx-userbutton__menu[data-polyx-side="left"][data-polyx-align="end"] {
|
|
596
|
+
bottom: 0;
|
|
597
|
+
}
|
|
598
|
+
.polyx-userbutton__menu[data-polyx-side="right"][data-polyx-align="start"],
|
|
599
|
+
.polyx-userbutton__menu[data-polyx-side="left"][data-polyx-align="start"] {
|
|
600
|
+
top: 0;
|
|
601
|
+
}
|
|
602
|
+
.polyx-userbutton__menu[data-polyx-side="right"][data-polyx-align="center"],
|
|
603
|
+
.polyx-userbutton__menu[data-polyx-side="left"][data-polyx-align="center"] {
|
|
604
|
+
top: 50%;
|
|
605
|
+
transform: translateY(-50%);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/* Consumer-supplied rows: give them room and get out of the way — no colours, no type, no
|
|
609
|
+
cursor. Whatever they render is theirs. */
|
|
610
|
+
.polyx-userbutton__custom {
|
|
611
|
+
padding: 0.375rem 0.625rem;
|
|
612
|
+
}
|
|
613
|
+
|
|
564
614
|
.polyx-userbutton__identity {
|
|
565
615
|
display: flex;
|
|
566
616
|
align-items: center;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poly-x/react",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.11",
|
|
4
4
|
"description": "PolyX SDK for React SPAs - provider, hooks, drop-in auth UI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"module": "./dist/index.mjs",
|
|
28
28
|
"types": "./dist/index.d.cts",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@poly-x/core": "0.1.0-alpha.
|
|
30
|
+
"@poly-x/core": "0.1.0-alpha.11"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"react": ">=18",
|