oneslash-design-system 1.6.0 → 1.7.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.
Files changed (43) hide show
  1. package/README.md +18 -0
  2. package/dist/components/alert.jsx +31 -16
  3. package/dist/components/button.jsx +30 -108
  4. package/dist/components/checkBox.d.ts +2 -1
  5. package/dist/components/checkBox.jsx +24 -24
  6. package/dist/components/emptyBox.jsx +6 -4
  7. package/dist/components/iconButton.d.ts +4 -1
  8. package/dist/components/iconButton.jsx +39 -36
  9. package/dist/components/link.jsx +29 -13
  10. package/dist/components/loadingScreen.jsx +4 -4
  11. package/dist/components/menu.jsx +3 -2
  12. package/dist/components/menuItem.d.ts +8 -1
  13. package/dist/components/menuItem.jsx +23 -94
  14. package/dist/components/modal.jsx +51 -12
  15. package/dist/components/navigation.jsx +1 -1
  16. package/dist/components/popover.jsx +22 -19
  17. package/dist/components/radioGroup.jsx +25 -15
  18. package/dist/components/select.jsx +102 -133
  19. package/dist/components/sharedStyles.d.ts +9 -0
  20. package/dist/components/sharedStyles.jsx +18 -0
  21. package/dist/components/tab.jsx +20 -92
  22. package/dist/components/tableCell.jsx +1 -1
  23. package/dist/components/tableContainer.jsx +1 -1
  24. package/dist/components/tableHeader.jsx +1 -1
  25. package/dist/components/tableHeaderCell.jsx +1 -1
  26. package/dist/components/tableRow.jsx +1 -1
  27. package/dist/components/tabsContainer.jsx +20 -2
  28. package/dist/components/tag.jsx +43 -26
  29. package/dist/components/textField.d.ts +3 -1
  30. package/dist/components/textField.jsx +23 -32
  31. package/dist/components/textarea.d.ts +3 -1
  32. package/dist/components/textarea.jsx +38 -41
  33. package/dist/components/timeStamp.jsx +6 -4
  34. package/dist/components/tooltip.d.ts +1 -1
  35. package/dist/components/tooltip.jsx +81 -44
  36. package/dist/components/useOverlayTransition.d.ts +9 -0
  37. package/dist/components/useOverlayTransition.js +31 -0
  38. package/dist/components/userImage.jsx +2 -2
  39. package/dist/designTokens.js +12 -12
  40. package/dist/output.css +382 -151
  41. package/dist/tailwind.config.js +12 -11
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +1 -1
@@ -2,8 +2,9 @@
2
2
  import React, { forwardRef } from 'react';
3
3
  var Menu = forwardRef(function (_a, ref) {
4
4
  var children = _a.children, width = _a.width, _b = _a.className, className = _b === void 0 ? '' : _b;
5
- return (<div ref={ref} className={"\n bg-light-background-accent300 dark:bg-dark-background-accent300\n rounded-[8px]\n shadow-lg\n overflow-hidden\n ".concat(className, "\n ")} style={{ width: width || 'auto' }}>
6
- <div className="p-[10px]">
5
+ return (<div ref={ref} className={"\n bg-light-background-accent300 dark:bg-dark-background-accent300\n rounded-[12px]\n shadow-lg\n dark:ring-1 dark:ring-dark-misc-divider\n overflow-hidden\n ".concat(className, "\n ")} style={{ width: width || 'auto' }}>
6
+ {/* 4px padding keeps the 8px item corners concentric with the 12px menu corners */}
7
+ <div className="p-1">
7
8
  {children}
8
9
  </div>
9
10
  </div>);
@@ -19,6 +19,13 @@ interface MenuItemProps {
19
19
  };
20
20
  iconRight?: string;
21
21
  rightAction?: React.ReactNode;
22
+ /** ARIA role; Select passes 'option'. Defaults to 'menuitem'. */
23
+ role?: string;
24
+ id?: string;
25
+ /** Keyboard highlight used by Select's listbox */
26
+ isActive?: boolean;
27
+ /** Select keeps focus on its trigger and passes -1 */
28
+ tabIndex?: number;
22
29
  }
23
- export default function MenuItem({ href, iconName, icon, userHandle, userImgUrl, label, secondLabel, isSelected, onClick, className, size, tag, iconRight, rightAction, }: MenuItemProps): JSX.Element;
30
+ export default function MenuItem({ href, iconName, icon, userHandle, userImgUrl, label, secondLabel, isSelected, onClick, className, size, tag, iconRight, rightAction, role, id, isActive, tabIndex, }: MenuItemProps): JSX.Element;
24
31
  export {};
@@ -1,106 +1,33 @@
1
1
  'use client';
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- var __generator = (this && this.__generator) || function (thisArg, body) {
12
- var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
- return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
- function verb(n) { return function (v) { return step([n, v]); }; }
15
- function step(op) {
16
- if (f) throw new TypeError("Generator is already executing.");
17
- while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
- if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
- if (y = 0, t) op = [op[0] & 2, t.value];
20
- switch (op[0]) {
21
- case 0: case 1: t = op; break;
22
- case 4: _.label++; return { value: op[1], done: false };
23
- case 5: _.label++; y = op[1]; op = [0]; continue;
24
- case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
- default:
26
- if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
- if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
- if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
- if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
- if (t[2]) _.ops.pop();
31
- _.trys.pop(); continue;
32
- }
33
- op = body.call(thisArg, _);
34
- } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
- if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
- }
37
- };
38
- import React, { useState, useEffect, useCallback } from 'react';
2
+ import React from 'react';
3
+ import * as LucideIcons from 'lucide-react';
39
4
  import NextLink from 'next/link';
40
5
  import UserImage from './userImage';
41
6
  import Tag from './tag';
7
+ import { focusRing, pressFeedbackAny, stateTransition } from './sharedStyles';
42
8
  export default function MenuItem(_a) {
43
- var _this = this;
44
- var href = _a.href, iconName = _a.iconName, icon = _a.icon, userHandle = _a.userHandle, userImgUrl = _a.userImgUrl, label = _a.label, secondLabel = _a.secondLabel, isSelected = _a.isSelected, onClick = _a.onClick, _b = _a.className, className = _b === void 0 ? '' : _b, _c = _a.size, size = _c === void 0 ? 'medium' : _c, tag = _a.tag, iconRight = _a.iconRight, rightAction = _a.rightAction;
45
- var _d = useState(null), IconLeft = _d[0], setIconLeft = _d[1];
46
- var _e = useState(null), IconRight = _e[0], setIconRight = _e[1];
47
- var loadIcon = useCallback(function (iconName) { return __awaiter(_this, void 0, void 0, function () {
48
- var module_1, IconComponent, error_1;
49
- return __generator(this, function (_a) {
50
- switch (_a.label) {
51
- case 0:
52
- if (!iconName)
53
- return [2 /*return*/, null];
54
- _a.label = 1;
55
- case 1:
56
- _a.trys.push([1, 3, , 4]);
57
- return [4 /*yield*/, import('lucide-react')];
58
- case 2:
59
- module_1 = _a.sent();
60
- IconComponent = module_1[iconName];
61
- return [2 /*return*/, IconComponent || null];
62
- case 3:
63
- error_1 = _a.sent();
64
- console.error("Failed to load icon ".concat(iconName, ":"), error_1);
65
- return [2 /*return*/, null];
66
- case 4: return [2 /*return*/];
67
- }
68
- });
69
- }); }, []);
70
- useEffect(function () {
71
- var fetchIcon = function () { return __awaiter(_this, void 0, void 0, function () {
72
- var _a, _b;
73
- return __generator(this, function (_c) {
74
- switch (_c.label) {
75
- case 0:
76
- if (!iconName) return [3 /*break*/, 2];
77
- _a = setIconLeft;
78
- return [4 /*yield*/, loadIcon(iconName)];
79
- case 1:
80
- _a.apply(void 0, [_c.sent()]);
81
- _c.label = 2;
82
- case 2:
83
- if (!iconRight) return [3 /*break*/, 4];
84
- _b = setIconRight;
85
- return [4 /*yield*/, loadIcon(iconRight)];
86
- case 3:
87
- _b.apply(void 0, [_c.sent()]);
88
- _c.label = 4;
89
- case 4: return [2 /*return*/];
90
- }
91
- });
92
- }); };
93
- fetchIcon();
94
- }, [iconName, iconRight, loadIcon]);
9
+ var _b;
10
+ var href = _a.href, iconName = _a.iconName, icon = _a.icon, userHandle = _a.userHandle, userImgUrl = _a.userImgUrl, label = _a.label, secondLabel = _a.secondLabel, isSelected = _a.isSelected, onClick = _a.onClick, _c = _a.className, className = _c === void 0 ? '' : _c, _d = _a.size, size = _d === void 0 ? 'medium' : _d, tag = _a.tag, iconRight = _a.iconRight, rightAction = _a.rightAction, _e = _a.role, role = _e === void 0 ? 'menuitem' : _e, id = _a.id, _f = _a.isActive, isActive = _f === void 0 ? false : _f, _g = _a.tabIndex, tabIndex = _g === void 0 ? 0 : _g;
11
+ // Synchronous lookup so icons render on the first paint
12
+ var IconLeft = iconName ? LucideIcons[iconName] : null;
13
+ var IconRight = iconRight ? LucideIcons[iconRight] : null;
95
14
  // Size-based icon and text classes
96
15
  var iconSize = { tiny: 'w-4 h-4', small: 'w-5 h-5', medium: 'w-6 h-6' }[size];
97
16
  var labelClass = { tiny: 'text-caption', small: 'text-body2', medium: 'text-body1' }[size];
98
17
  var secondLabelClass = size === 'tiny' ? 'text-caption' : 'text-body2';
99
- var tagSize = { tiny: 'tiny', small: 'small', medium: 'medium' }[size];
18
+ // One step smaller than the row so a tag never makes its row taller
19
+ var tagSize = { tiny: 'tiny', small: 'tiny', medium: 'small' }[size];
100
20
  var paddingClass = size === 'tiny' ? 'p-1' : 'p-2';
101
- var content = (<div className={"\n flex items-center ".concat(paddingClass, " rounded-[8px] cursor-pointer justify-between transition-colors duration-200 ease-in-out\n ").concat(isSelected
21
+ var content = (<div id={id} role={href ? undefined : role}
22
+ // A link wrapper is already focusable; otherwise the row itself takes focus
23
+ tabIndex={href ? undefined : tabIndex} aria-selected={role === 'option' ? !!isSelected : undefined} className={"\n flex items-center ".concat(paddingClass, " rounded-[8px] cursor-pointer justify-between text-left ").concat(stateTransition, " ").concat(pressFeedbackAny, " ").concat(focusRing, "\n ").concat(isSelected
102
24
  ? 'bg-light-primary-dark dark:bg-dark-primary-dark text-light-text-contrast dark:text-dark-text-contrast'
103
- : 'hover:bg-light-background-accent200 hover:dark:bg-dark-background-accent200', "\n ").concat(className, "\n ")} style={{ width: '100%' }} onClick={onClick}>
25
+ : "hover:bg-light-background-accent200 dark:hover:bg-dark-background-accent200 ".concat(isActive ? 'bg-light-background-accent200 dark:bg-dark-background-accent200' : ''), "\n ").concat(className, "\n ")} style={{ width: '100%' }} onClick={onClick} onKeyDown={function (e) {
26
+ if (!href && e.target === e.currentTarget && (e.key === 'Enter' || e.key === ' ')) {
27
+ e.preventDefault();
28
+ e.currentTarget.click();
29
+ }
30
+ }}>
104
31
  {/* Left group: icon/userImg + label with 8px gap */}
105
32
  <div className="flex items-center gap-2 min-w-0">
106
33
  {userImgUrl ? (<UserImage userHandle={userHandle || ''} userImgUrl={userImgUrl}/>) : icon ? (<span className={"".concat(iconSize, " flex-shrink-0 flex items-center justify-center")}>{icon}</span>) : (IconLeft && (<IconLeft className={"".concat(iconSize, " flex-shrink-0 ").concat(isSelected ? 'text-light-text-contrast dark:text-dark-text-contrast' : 'text-light-text-secondary dark:text-dark-text-secondary')} strokeWidth={2}/>))}
@@ -116,12 +43,14 @@ export default function MenuItem(_a) {
116
43
 
117
44
  {/* Right group: tag + icon aligned to the right */}
118
45
  <div className="flex items-center gap-2 flex-shrink-0">
119
- {tag && (<Tag variant={tag.variant || 'contained'} size={tagSize} label={tag.label} iconName={tag.iconName} color={tag.color}/>)}
46
+ {tag && (<Tag variant={tag.variant || 'contained'} size={tagSize} label={tag.label} iconName={tag.iconName}
47
+ // 'default' has a fill that stays visible on the menu background
48
+ color={(_b = tag.color) !== null && _b !== void 0 ? _b : 'default'}/>)}
120
49
  {IconRight && (<IconRight className={"".concat(iconSize, " ").concat(isSelected ? 'text-light-text-contrast dark:text-dark-text-contrast' : 'text-light-text-secondary dark:text-dark-text-secondary', " flex-shrink-0")} strokeWidth={2}/>)}
121
50
  {rightAction && (<div onClick={function (e) { return e.stopPropagation(); }}>
122
51
  {rightAction}
123
52
  </div>)}
124
53
  </div>
125
54
  </div>);
126
- return href ? <NextLink href={href}>{content}</NextLink> : content;
55
+ return href ? <NextLink href={href} className={"block rounded-[8px] ".concat(focusRing)}>{content}</NextLink> : content;
127
56
  }
@@ -1,38 +1,77 @@
1
1
  'use client';
2
- import React, { useEffect } from 'react';
2
+ import React, { useEffect, useId, useRef } from 'react';
3
+ import { overlayHidden, overlayTransition, overlayVisible } from './sharedStyles';
4
+ import { useOverlayTransition } from './useOverlayTransition';
5
+ var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
3
6
  export default function Modal(_a) {
4
7
  var isOpen = _a.isOpen, title = _a.title, secondTitle = _a.secondTitle, children = _a.children, onClose = _a.onClose, actions = _a.actions, _b = _a.size, size = _b === void 0 ? 'medium' : _b;
5
- // close modal by esc keypress
8
+ var panelRef = useRef(null);
9
+ var titleId = useId();
10
+ var _c = useOverlayTransition(isOpen), mounted = _c.mounted, visible = _c.visible;
11
+ // Keep onClose in a ref so a new function each render doesn't re-run the focus effect
12
+ var onCloseRef = useRef(onClose);
13
+ onCloseRef.current = onClose;
14
+ // Move focus into the panel, trap Tab inside it, close on Escape,
15
+ // lock page scroll, and give focus back to the trigger on close
6
16
  useEffect(function () {
7
- if (!isOpen)
17
+ var _a;
18
+ // Wait for the panel to mount before moving focus into it
19
+ if (!isOpen || !mounted)
8
20
  return;
21
+ var previouslyFocused = document.activeElement;
22
+ var panel = panelRef.current;
23
+ var firstFocusable = panel === null || panel === void 0 ? void 0 : panel.querySelector(FOCUSABLE);
24
+ (_a = (firstFocusable !== null && firstFocusable !== void 0 ? firstFocusable : panel)) === null || _a === void 0 ? void 0 : _a.focus();
25
+ var previousOverflow = document.body.style.overflow;
26
+ document.body.style.overflow = 'hidden';
9
27
  var handleKeyDown = function (e) {
10
28
  if (e.key === 'Escape') {
11
- onClose();
29
+ onCloseRef.current();
30
+ return;
31
+ }
32
+ if (e.key !== 'Tab' || !panel)
33
+ return;
34
+ var focusable = Array.from(panel.querySelectorAll(FOCUSABLE));
35
+ if (focusable.length === 0) {
36
+ e.preventDefault();
37
+ return;
38
+ }
39
+ var first = focusable[0];
40
+ var last = focusable[focusable.length - 1];
41
+ if (e.shiftKey && document.activeElement === first) {
42
+ e.preventDefault();
43
+ last.focus();
44
+ }
45
+ else if (!e.shiftKey && document.activeElement === last) {
46
+ e.preventDefault();
47
+ first.focus();
12
48
  }
13
49
  };
14
50
  window.addEventListener('keydown', handleKeyDown);
15
51
  return function () {
52
+ var _a;
16
53
  window.removeEventListener('keydown', handleKeyDown);
54
+ document.body.style.overflow = previousOverflow;
55
+ (_a = previouslyFocused === null || previouslyFocused === void 0 ? void 0 : previouslyFocused.focus) === null || _a === void 0 ? void 0 : _a.call(previouslyFocused);
17
56
  };
18
- }, [isOpen, onClose]);
57
+ }, [isOpen, mounted]);
19
58
  // Determine width based on size prop
20
59
  var sizeConfig = {
21
60
  small: { width: 'w-[480px]', maxWidth: '480px' },
22
61
  medium: { width: 'w-[600px]', maxWidth: '600px' },
23
62
  large: { width: 'w-[1024px]', maxWidth: '1024px' },
24
63
  };
25
- var _c = sizeConfig[size], modalWidth = _c.width, maxWidth = _c.maxWidth;
64
+ var _d = sizeConfig[size], modalWidth = _d.width, maxWidth = _d.maxWidth;
26
65
  // close modal by clicking elsewhere
27
66
  var handleOverlayClick = function (e) {
28
67
  if (e.target === e.currentTarget) {
29
68
  onClose();
30
69
  }
31
70
  };
32
- if (!isOpen)
71
+ if (!mounted)
33
72
  return null;
34
- return (<div className="fixed bg-light-misc-scrim/40 dark:bg-dark-misc-scrim/40 flex items-center justify-center z-50" style={{ top: 0, left: 0, right: 0, bottom: 0 }} onClick={handleOverlayClick} role="dialog" aria-labelledby="modal-title" aria-modal="true" tabIndex={-1}>
35
- <div className={"bg-light-background-default dark:bg-dark-background-default rounded-[8px] ".concat(modalWidth, " flex flex-col")} style={{
73
+ return (<div className={"fixed bg-light-misc-scrim/40 dark:bg-dark-misc-scrim/40 flex items-center justify-center z-50\n transition-opacity duration-150 ease-out motion-reduce:transition-none ".concat(visible ? 'opacity-100' : 'opacity-0')} style={{ top: 0, left: 0, right: 0, bottom: 0 }} onClick={handleOverlayClick}>
74
+ <div ref={panelRef} role="dialog" aria-modal="true" aria-labelledby={title ? titleId : undefined} tabIndex={-1} className={"bg-light-background-default dark:bg-dark-background-accent100 dark:ring-1 dark:ring-dark-misc-divider rounded-[8px] ".concat(modalWidth, " flex flex-col outline-none\n ").concat(overlayTransition, " ").concat(visible ? overlayVisible : overlayHidden)} style={{
36
75
  maxWidth: maxWidth,
37
76
  width: 'calc(100vw - 64px)',
38
77
  maxHeight: '90vh',
@@ -40,7 +79,7 @@ export default function Modal(_a) {
40
79
  }}>
41
80
  {(title || actions) && (<div className="px-6 py-3 border-b border-light-misc-divider dark:border-dark-misc-divider flex justify-between items-start" style={{ flexShrink: 0 }}>
42
81
  {title && (<div className="flex flex-col">
43
- <h2 id="modal-title" className="text-h5 text-light-text-primary dark:text-dark-text-primary">
82
+ <h2 id={titleId} className="text-h5 text-light-text-primary dark:text-dark-text-primary">
44
83
  {title}
45
84
  </h2>
46
85
  {secondTitle && (<span className="text-body2 opacity-70 text-light-text-primary dark:text-dark-text-primary">
@@ -56,8 +95,8 @@ export default function Modal(_a) {
56
95
  overflowY: 'auto',
57
96
  flexGrow: 1,
58
97
  minHeight: 0,
59
- scrollbarWidth: 'none',
60
- msOverflowStyle: 'none',
98
+ // A thin scrollbar is the only cue that long content scrolls
99
+ scrollbarWidth: 'thin',
61
100
  }}>
62
101
  {children}
63
102
  </div>
@@ -2,7 +2,7 @@
2
2
  import React from 'react';
3
3
  export default function Navigation(_a) {
4
4
  var children = _a.children, _b = _a.className, className = _b === void 0 ? '' : _b;
5
- return (<nav className={"\n bg-light-background-default dark:bg-dark-background-default\n border-r border-light-misc-divider dark:border-dark-misc-divider\n p-[10px]\n ".concat(className, "\n ")}>
5
+ return (<nav className={"\n bg-light-background-default dark:bg-dark-background-default\n border-r border-light-misc-divider dark:border-dark-misc-divider\n p-2\n ".concat(className, "\n ")}>
6
6
  {children}
7
7
  </nav>);
8
8
  }
@@ -1,52 +1,55 @@
1
1
  'use client';
2
- var __assign = (this && this.__assign) || function () {
3
- __assign = Object.assign || function(t) {
4
- for (var s, i = 1, n = arguments.length; i < n; i++) {
5
- s = arguments[i];
6
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
- t[p] = s[p];
8
- }
9
- return t;
10
- };
11
- return __assign.apply(this, arguments);
12
- };
13
2
  import React, { useState, useEffect } from 'react';
14
3
  import { createPortal } from 'react-dom';
15
4
  import { usePopper } from 'react-popper';
5
+ import { overlayHidden, overlayTransition, overlayVisible } from './sharedStyles';
6
+ import { useOverlayTransition } from './useOverlayTransition';
16
7
  export default function Popover(_a) {
17
8
  var id = _a.id, anchorEl = _a.anchorEl, open = _a.open, onClose = _a.onClose, children = _a.children;
18
9
  var _b = useState(null), popoverElement = _b[0], setPopoverElement = _b[1];
19
10
  var _c = useState(false), hasMounted = _c[0], setHasMounted = _c[1];
11
+ var _d = useOverlayTransition(open), mounted = _d.mounted, visible = _d.visible;
20
12
  // Initialize Popper.js
21
- var _d = usePopper(anchorEl, popoverElement, {
13
+ var _e = usePopper(anchorEl, popoverElement, {
22
14
  placement: 'bottom-start', // Default placement, can be customized
23
15
  modifiers: [
24
- { name: 'offset', options: { offset: [0, 8] } }, // Offset for spacing between anchor and popover
16
+ { name: 'offset', options: { offset: [0, 4] } }, // Same 4px gap as the Select menu and Tooltip
25
17
  ],
26
- }), styles = _d.styles, attributes = _d.attributes;
27
- // Handle outside clicks to close the popover
18
+ }), styles = _e.styles, attributes = _e.attributes;
19
+ // Close on outside clicks and Escape. Clicks on the anchor are left to the
20
+ // anchor's own toggle, otherwise the popover would close and reopen.
28
21
  useEffect(function () {
29
22
  var handleClickOutside = function (event) {
30
- if (popoverElement && !popoverElement.contains(event.target) && anchorEl) {
23
+ var target = event.target;
24
+ if (popoverElement && !popoverElement.contains(target) && anchorEl && !anchorEl.contains(target)) {
31
25
  onClose();
32
26
  }
33
27
  };
28
+ var handleKeyDown = function (event) {
29
+ if (event.key === 'Escape')
30
+ onClose();
31
+ };
34
32
  if (open) {
35
33
  document.addEventListener('mousedown', handleClickOutside);
34
+ document.addEventListener('keydown', handleKeyDown);
36
35
  }
37
36
  return function () {
38
37
  document.removeEventListener('mousedown', handleClickOutside);
38
+ document.removeEventListener('keydown', handleKeyDown);
39
39
  };
40
40
  }, [open, anchorEl, popoverElement, onClose]);
41
41
  // Ensure popover is only rendered after the component mounts
42
42
  useEffect(function () {
43
43
  setHasMounted(true);
44
44
  }, []);
45
- if (!open || !hasMounted || !anchorEl)
45
+ if (!mounted || !hasMounted || !anchorEl)
46
46
  return null;
47
47
  // Render popover in a portal to prevent layout shifts and positioning issues
48
- return createPortal(<div id={id} ref={setPopoverElement} style={__assign(__assign({}, styles.popper), { display: open ? 'block' : 'none' })} {...attributes.popper} className="bg-light-background-accent300 dark:bg-dark-background-accent300 rounded-[8px] shadow-lg p-2" role="dialog">
49
- {children}
48
+ return createPortal(<div id={id} ref={setPopoverElement} style={styles.popper} {...attributes.popper} className="z-50" role="dialog">
49
+ {/* Popper owns the outer transform, so the fade and scale go on this inner panel */}
50
+ <div className={"bg-light-background-accent300 dark:bg-dark-background-accent300 dark:ring-1 dark:ring-dark-misc-divider rounded-[8px] shadow-lg p-2 origin-top-left\n ".concat(overlayTransition, " ").concat(visible ? overlayVisible : overlayHidden)}>
51
+ {children}
52
+ </div>
50
53
  </div>, document.body // Mounting the popover in the document body for isolation
51
54
  );
52
55
  }
@@ -1,20 +1,30 @@
1
1
  'use client';
2
- import React from 'react';
2
+ import React, { useId } from 'react';
3
+ import { popTransition, stateTransition } from './sharedStyles';
3
4
  export default function RadioGroup(_a) {
4
5
  var options = _a.options, selectedValue = _a.selectedValue, onChange = _a.onChange, _b = _a.direction, direction = _b === void 0 ? 'vertical' : _b;
5
- return (<div className={"flex ".concat(direction === 'horizontal' ? 'space-x-4' : 'flex-col space-y-2')}>
6
- {options.map(function (option) { return (<label key={option.value} className="flex items-center cursor-pointer" onClick={function () { return onChange(option.value); }}>
7
- {/* outer circle */}
8
- <div className={"relative flex justify-center items-center w-4 h-4 rounded-full border-2\n ".concat(selectedValue === option.value
9
- ? 'border-light-text-primary dark:border-dark-text-primary'
10
- : 'border-light-text-secondary dark:border-dark-text-secondary', "\n ")}>
11
- {/* Inner circle */}
12
- {selectedValue === option.value && (<div className='absolute w-2 h-2 rounded-full bg-light-text-primary dark:bg-dark-text-primary'/>)}
13
- </div>
14
- <div className="flex flex-col ml-2">
15
- <span className="text-body1 text-light-text-primary dark:text-dark-text-primary">{option.label}</span>
16
- {option.secondLabel && (<span className="text-body2 opacity-70 text-light-text-primary dark:text-dark-text-primary">{option.secondLabel}</span>)}
17
- </div>
18
- </label>); })}
6
+ // One shared name gives native arrow-key movement between the options
7
+ var groupName = useId();
8
+ return (<div role="radiogroup" className={"flex ".concat(direction === 'horizontal' ? 'gap-4' : 'flex-col gap-2')}>
9
+ {options.map(function (option) {
10
+ var isSelected = selectedValue === option.value;
11
+ return (<label key={option.value} className="group flex items-start cursor-pointer">
12
+ <input type="radio" name={groupName} value={option.value} checked={isSelected} onChange={function () { return onChange(option.value); }} className="peer sr-only"/>
13
+ {/* Hover halo and hit area, same as Checkbox */}
14
+ <span className={"relative flex flex-shrink-0 items-center justify-center w-6 h-6 rounded-full ".concat(stateTransition, "\n group-hover:bg-light-action-selected dark:group-hover:bg-dark-action-selected\n peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-0 peer-focus-visible:outline-light-accent-main dark:peer-focus-visible:outline-dark-accent-main")}>
15
+ {/* outer circle */}
16
+ <span className={"relative flex justify-center items-center w-4 h-4 rounded-full border-2 ".concat(stateTransition, "\n ").concat(isSelected
17
+ ? 'border-light-text-primary dark:border-dark-text-primary'
18
+ : 'border-light-text-secondary dark:border-dark-text-secondary', "\n ")}>
19
+ {/* Inner circle; always rendered so it can scale in and out */}
20
+ <span className={"absolute w-2 h-2 rounded-full bg-light-text-primary dark:bg-dark-text-primary ".concat(popTransition, " ").concat(isSelected ? 'scale-100' : 'scale-0')}/>
21
+ </span>
22
+ </span>
23
+ <span className="flex flex-col ml-2">
24
+ <span className="text-body1 text-light-text-primary dark:text-dark-text-primary">{option.label}</span>
25
+ {option.secondLabel && (<span className="text-body2 opacity-70 text-light-text-primary dark:text-dark-text-primary">{option.secondLabel}</span>)}
26
+ </span>
27
+ </label>);
28
+ })}
19
29
  </div>);
20
30
  }