oneslash-design-system 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +18 -0
  2. package/dist/components/alert.jsx +31 -16
  3. package/dist/components/button.d.ts +1 -1
  4. package/dist/components/button.jsx +32 -108
  5. package/dist/components/checkBox.d.ts +2 -1
  6. package/dist/components/checkBox.jsx +24 -24
  7. package/dist/components/emptyBox.jsx +6 -4
  8. package/dist/components/icon.d.ts +1 -1
  9. package/dist/components/icon.jsx +1 -0
  10. package/dist/components/iconButton.d.ts +4 -1
  11. package/dist/components/iconButton.jsx +40 -40
  12. package/dist/components/link.jsx +29 -13
  13. package/dist/components/loadingScreen.jsx +4 -4
  14. package/dist/components/menu.jsx +3 -2
  15. package/dist/components/menuItem.d.ts +9 -2
  16. package/dist/components/menuItem.jsx +28 -97
  17. package/dist/components/modal.jsx +51 -12
  18. package/dist/components/navigation.jsx +1 -1
  19. package/dist/components/popover.jsx +22 -19
  20. package/dist/components/radioGroup.jsx +25 -15
  21. package/dist/components/select.jsx +102 -133
  22. package/dist/components/sharedStyles.d.ts +9 -0
  23. package/dist/components/sharedStyles.jsx +18 -0
  24. package/dist/components/tab.d.ts +2 -1
  25. package/dist/components/tab.jsx +38 -99
  26. package/dist/components/tableCell.jsx +1 -1
  27. package/dist/components/tableContainer.jsx +1 -1
  28. package/dist/components/tableHeader.jsx +1 -1
  29. package/dist/components/tableHeaderCell.jsx +1 -1
  30. package/dist/components/tableRow.jsx +1 -1
  31. package/dist/components/tabsContainer.d.ts +1 -0
  32. package/dist/components/tabsContainer.jsx +86 -4
  33. package/dist/components/tag.d.ts +1 -1
  34. package/dist/components/tag.jsx +48 -27
  35. package/dist/components/textField.d.ts +4 -2
  36. package/dist/components/textField.jsx +24 -32
  37. package/dist/components/textarea.d.ts +4 -2
  38. package/dist/components/textarea.jsx +39 -41
  39. package/dist/components/timeStamp.jsx +6 -4
  40. package/dist/components/tooltip.d.ts +1 -1
  41. package/dist/components/tooltip.jsx +81 -44
  42. package/dist/components/useOverlayTransition.d.ts +9 -0
  43. package/dist/components/useOverlayTransition.js +31 -0
  44. package/dist/components/userImage.jsx +2 -2
  45. package/dist/designTokens.js +16 -16
  46. package/dist/output.css +439 -136
  47. package/dist/stories/Button.stories.jsx +1 -1
  48. package/dist/stories/Icon.stories.jsx +1 -1
  49. package/dist/stories/MenuItem.stories.jsx +1 -1
  50. package/dist/stories/TabsContainer.stories.d.ts +2 -0
  51. package/dist/stories/TabsContainer.stories.jsx +14 -2
  52. package/dist/stories/Tag.stories.jsx +1 -1
  53. package/dist/stories/TextField.stories.jsx +1 -1
  54. package/dist/stories/Textarea.stories.jsx +1 -1
  55. package/dist/tailwind.config.js +12 -11
  56. package/dist/tsconfig.tsbuildinfo +1 -1
  57. package/package.json +1 -1
@@ -12,29 +12,31 @@ var __assign = (this && this.__assign) || function () {
12
12
  };
13
13
  import React from 'react';
14
14
  import NextLink from 'next/link';
15
+ import { focusRing } from './sharedStyles';
15
16
  export default function Link(_a) {
16
17
  var href = _a.href, children = _a.children, _b = _a.color, color = _b === void 0 ? 'info' : _b, _c = _a.size, size = _c === void 0 ? 'body1' : _c, _d = _a.openInNewTab, openInNewTab = _d === void 0 ? false : _d, _e = _a.className, className = _e === void 0 ? '' : _e;
17
18
  // Color classes based on the color prop
19
+ // Light mode uses the -dark shade so link text keeps 4.5:1 on light backgrounds
18
20
  var colorClasses = {
19
21
  info: {
20
- default: 'text-light-info-main dark:text-dark-info-main',
21
- hover: 'hover:text-light-info-dark hover:dark:text-dark-info-dark',
22
- visited: 'visited:text-light-info-light visited:dark:text-dark-info-light',
22
+ default: 'text-light-info-dark dark:text-dark-info-main',
23
+ hover: 'hover:underline-offset-[4px] dark:hover:text-dark-info-light',
24
+ visited: 'visited:text-light-info-dark dark:visited:text-dark-info-light',
23
25
  },
24
26
  error: {
25
- default: 'text-light-error-main dark:text-dark-error-main',
26
- hover: 'hover:text-light-error-dark hover:dark:text-dark-error-dark',
27
- visited: 'visited:text-light-error-light visited:dark:text-dark-error-light',
27
+ default: 'text-light-error-dark dark:text-dark-error-main',
28
+ hover: 'hover:underline-offset-[4px] dark:hover:text-dark-error-light',
29
+ visited: 'visited:text-light-error-dark dark:visited:text-dark-error-light',
28
30
  },
29
31
  warning: {
30
- default: 'text-light-warning-main dark:text-dark-warning-main',
31
- hover: 'hover:text-light-warning-dark hover:dark:text-dark-warning-dark',
32
- visited: 'visited:text-light-warning-light visited:dark:text-dark-warning-light',
32
+ default: 'text-light-warning-dark dark:text-dark-warning-main',
33
+ hover: 'hover:underline-offset-[4px] dark:hover:text-dark-warning-light',
34
+ visited: 'visited:text-light-warning-dark dark:visited:text-dark-warning-light',
33
35
  },
34
36
  success: {
35
- default: 'text-light-success-main dark:text-dark-success-main',
36
- hover: 'hover:text-light-success-dark hover:dark:text-dark-success-dark',
37
- visited: 'visited:text-light-success-light visited:dark:text-dark-success-light',
37
+ default: 'text-light-success-dark dark:text-dark-success-main',
38
+ hover: 'hover:underline-offset-[4px] dark:hover:text-dark-success-light',
39
+ visited: 'visited:text-light-success-dark dark:visited:text-dark-success-light',
38
40
  },
39
41
  };
40
42
  var selectedColorClasses = colorClasses[color];
@@ -42,7 +44,21 @@ export default function Link(_a) {
42
44
  var isExternal = href.startsWith('http://') || href.startsWith('https://');
43
45
  // Determine if we should open in new tab
44
46
  var shouldOpenInNewTab = openInNewTab || isExternal;
45
- var linkClasses = "\n text-".concat(size, "\n ").concat(selectedColorClasses.default, "\n ").concat(selectedColorClasses.hover, "\n ").concat(selectedColorClasses.visited, "\n underline\n underline-offset-[2px]\n decoration-2\n cursor-pointer\n transition-colors\n duration-200\n ease-in-out\n ").concat(className, "\n ").trim().replace(/\s+/g, ' ');
47
+ // Literal class names so Tailwind can find them when scanning this file
48
+ var sizeClass = {
49
+ h1: 'text-h1',
50
+ h2: 'text-h2',
51
+ h3: 'text-h3',
52
+ h4: 'text-h4',
53
+ h5: 'text-h5',
54
+ h6: 'text-h6',
55
+ subtitle1: 'text-subtitle1',
56
+ subtitle2: 'text-subtitle2',
57
+ body1: 'text-body1',
58
+ body2: 'text-body2',
59
+ caption: 'text-caption',
60
+ }[size];
61
+ var linkClasses = "\n ".concat(sizeClass, "\n ").concat(selectedColorClasses.default, "\n ").concat(selectedColorClasses.hover, "\n ").concat(selectedColorClasses.visited, "\n underline\n underline-offset-[2px]\n decoration-2\n cursor-pointer\n rounded-[2px]\n transition-[color,text-underline-offset]\n duration-150\n ease-out\n motion-reduce:transition-none\n ").concat(focusRing, "\n ").concat(className, "\n ").trim().replace(/\s+/g, ' ');
46
62
  var commonProps = __assign({ className: linkClasses }, (shouldOpenInNewTab && {
47
63
  target: '_blank',
48
64
  rel: 'noopener noreferrer',
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
  import React from 'react';
3
3
  export default function LoadingScreen() {
4
- return (<div className="flex justify-center items-center h-full w-full min-h-[200px]">
5
- <div className="w-12 h-12 rounded-full border-4 border-t-transparent border-r-light-text-primary border-b-light-text-primary border-l-light-text-primary dark:border-t-transparent dark:border-r-dark-text-primary dark:border-b-dark-text-primary dark:border-l-dark-text-primary animate-spin"></div>
4
+ return (<div role="status" aria-label="Loading" className="flex justify-center items-center h-full w-full min-h-[200px]">
5
+ <div className="w-12 h-12 rounded-full border-4 border-current border-t-transparent text-light-text-primary dark:text-dark-text-primary animate-spin"></div>
6
6
  </div>);
7
7
  }
8
8
  ;
@@ -14,7 +14,7 @@ export function LoadingSmall(_a) {
14
14
  small: 'w-5 h-5 border-2', // 20px, slightly thinner border
15
15
  tiny: 'w-4 h-4 border-2', // 16px
16
16
  };
17
- return (<div className="flex justify-center items-center">
18
- <div className={"rounded-full border-t-transparent border-r-light-text-primary border-b-light-text-primary border-l-light-text-primary dark:border-t-transparent dark:border-r-dark-text-primary dark:border-b-dark-text-primary dark:border-l-dark-text-primary animate-spin ".concat(spinnerSizeClasses[size])}/>
17
+ return (<div role="status" aria-label="Loading" className="flex justify-center items-center">
18
+ <div className={"rounded-full border-current border-t-transparent text-light-text-primary dark:text-dark-text-primary animate-spin ".concat(spinnerSizeClasses[size])}/>
19
19
  </div>);
20
20
  }
@@ -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>);
@@ -10,7 +10,7 @@ interface MenuItemProps {
10
10
  isSelected?: boolean;
11
11
  onClick?: any;
12
12
  className?: string;
13
- size?: 'small' | 'medium';
13
+ size?: 'tiny' | 'small' | 'medium';
14
14
  tag?: {
15
15
  label: React.ReactNode;
16
16
  iconName?: string;
@@ -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,104 +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
- var iconSize = size === 'small' ? 'w-5 h-5' : 'w-6 h-6';
97
- var labelClass = size === 'small' ? 'text-body2' : 'text-body1';
98
- var tagSize = size === 'small' ? 'small' : 'medium';
99
- var content = (<div className={"\n flex items-center p-2 rounded-[8px] cursor-pointer justify-between transition-colors duration-200 ease-in-out\n ".concat(isSelected
15
+ var iconSize = { tiny: 'w-4 h-4', small: 'w-5 h-5', medium: 'w-6 h-6' }[size];
16
+ var labelClass = { tiny: 'text-caption', small: 'text-body2', medium: 'text-body1' }[size];
17
+ var secondLabelClass = size === 'tiny' ? 'text-caption' : 'text-body2';
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];
20
+ var paddingClass = size === 'tiny' ? 'p-1' : 'p-2';
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
100
24
  ? 'bg-light-primary-dark dark:bg-dark-primary-dark text-light-text-contrast dark:text-dark-text-contrast'
101
- : '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
+ }}>
102
31
  {/* Left group: icon/userImg + label with 8px gap */}
103
32
  <div className="flex items-center gap-2 min-w-0">
104
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}/>))}
@@ -106,7 +35,7 @@ export default function MenuItem(_a) {
106
35
  <span className={"truncate ".concat(labelClass, " ").concat(isSelected ? 'text-light-text-contrast dark:text-dark-text-contrast' : 'text-light-text-primary dark:text-dark-text-primary')}>
107
36
  {label}
108
37
  </span>
109
- {secondLabel && (<span className={"truncate text-body2 opacity-70 ".concat(isSelected ? 'text-light-text-contrast dark:text-dark-text-contrast' : 'text-light-text-primary dark:text-dark-text-primary')}>
38
+ {secondLabel && (<span className={"truncate ".concat(secondLabelClass, " opacity-70 ").concat(isSelected ? 'text-light-text-contrast dark:text-dark-text-contrast' : 'text-light-text-primary dark:text-dark-text-primary')}>
110
39
  {secondLabel}
111
40
  </span>)}
112
41
  </div>
@@ -114,12 +43,14 @@ export default function MenuItem(_a) {
114
43
 
115
44
  {/* Right group: tag + icon aligned to the right */}
116
45
  <div className="flex items-center gap-2 flex-shrink-0">
117
- {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'}/>)}
118
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}/>)}
119
50
  {rightAction && (<div onClick={function (e) { return e.stopPropagation(); }}>
120
51
  {rightAction}
121
52
  </div>)}
122
53
  </div>
123
54
  </div>);
124
- return href ? <NextLink href={href}>{content}</NextLink> : content;
55
+ return href ? <NextLink href={href} className={"block rounded-[8px] ".concat(focusRing)}>{content}</NextLink> : content;
125
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
  }