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
package/README.md CHANGED
@@ -1,3 +1,21 @@
1
+ # oneslash-design-system
2
+
3
+ React components and design tokens for oneSlash apps.
4
+
5
+ ## Using the design system in an app
6
+
7
+ The package ships Tailwind class names, not compiled CSS, and its components are untranspiled `.jsx`. Each app compiles both itself:
8
+
9
+ 1. **Next.js:** add `transpilePackages: ["oneslash-design-system"]` to `next.config`.
10
+ 2. **Tailwind v3:** add `./node_modules/oneslash-design-system/dist/**/*.{js,jsx}` to `content`, and build `theme.extend.colors` and `fontSize` from `require("oneslash-design-system/tokens")`.
11
+ 3. **Tailwind v4:** add `@source "../node_modules/oneslash-design-system/dist/**/*.jsx";` to your CSS. For the tokens, point `@config` at a JS config that builds them from `oneslash-design-system/tokens` (see oneslash-homepage's `tailwind.config.ts`).
12
+
13
+ Use class names that mean the same thing in Tailwind v3 and v4 inside components, since apps on both versions compile them.
14
+
15
+ ## Releasing
16
+
17
+ Bump `version` in `package.json`, run `npm run build`, and merge to `main`. The Release workflow publishes to npm, creates the tag and GitHub Release with notes from the merged PR titles, and updates the docs site.
18
+
1
19
  This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
20
 
3
21
  ## Getting Started
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import React, { useEffect, useState } from 'react';
2
+ import React, { useEffect, useRef, useState } from 'react';
3
3
  import IconButton from './iconButton';
4
4
  var bgClassMap = {
5
5
  error: 'bg-light-error-alertBg dark:bg-dark-error-alertBg',
@@ -8,26 +8,27 @@ var bgClassMap = {
8
8
  success: 'bg-light-success-alertBg dark:bg-dark-success-alertBg',
9
9
  default: 'bg-light-secondary-light dark:bg-dark-secondary-light',
10
10
  };
11
+ var AUTO_DISMISS_MS = 5000;
12
+ var EXIT_MS = 150;
13
+ // Toasts pop in with a slight overshoot and leave quickly
14
+ var motionClasses = {
15
+ enter: 'duration-[220ms] ease-[cubic-bezier(0.34,1.56,0.64,1)]',
16
+ exit: 'duration-150 ease-in',
17
+ };
11
18
  export default function Alert(_a) {
12
19
  var _b;
13
20
  var _c = _a.open, open = _c === void 0 ? true : _c, type = _a.type, message = _a.message, secondMessage = _a.secondMessage, onClose = _a.onClose, _d = _a.showCloseButton, showCloseButton = _d === void 0 ? false : _d, _e = _a.variant, variant = _e === void 0 ? 'toast' : _e;
14
21
  var _f = useState(false), isVisible = _f[0], setIsVisible = _f[1];
15
22
  var _g = useState(false), shouldRender = _g[0], setShouldRender = _g[1];
23
+ // Auto-dismiss pauses while the toast is hovered or focused
24
+ var _h = useState(false), isPaused = _h[0], setIsPaused = _h[1];
25
+ var remainingMs = useRef(AUTO_DISMISS_MS);
16
26
  useEffect(function () {
17
27
  if (open) {
18
28
  setShouldRender(true);
29
+ remainingMs.current = AUTO_DISMISS_MS;
19
30
  // Small delay to trigger the animation
20
31
  var showTimer_1 = setTimeout(function () { return setIsVisible(true); }, 10);
21
- // Only auto-dismiss if there's no close button
22
- if (!showCloseButton) {
23
- var dismissTimer_1 = setTimeout(function () {
24
- handleClose();
25
- }, 5000);
26
- return function () {
27
- clearTimeout(showTimer_1);
28
- clearTimeout(dismissTimer_1);
29
- };
30
- }
31
32
  return function () { return clearTimeout(showTimer_1); };
32
33
  }
33
34
  else if (shouldRender) {
@@ -35,30 +36,44 @@ export default function Alert(_a) {
35
36
  setIsVisible(false);
36
37
  var hideTimer_1 = setTimeout(function () {
37
38
  setShouldRender(false);
38
- }, 300);
39
+ }, EXIT_MS);
39
40
  return function () { return clearTimeout(hideTimer_1); };
40
41
  }
41
42
  }, [open, showCloseButton, shouldRender]);
43
+ // Only auto-dismiss if there's no close button
44
+ useEffect(function () {
45
+ if (!open || !shouldRender || showCloseButton || isPaused)
46
+ return;
47
+ var startedAt = Date.now();
48
+ var dismissTimer = setTimeout(function () { return handleClose(); }, remainingMs.current);
49
+ return function () {
50
+ clearTimeout(dismissTimer);
51
+ remainingMs.current -= Date.now() - startedAt;
52
+ };
53
+ }, [open, shouldRender, showCloseButton, isPaused]); // eslint-disable-line react-hooks/exhaustive-deps
42
54
  var handleClose = function () {
43
55
  setIsVisible(false);
44
56
  // Wait for animation to complete before unmounting
45
57
  setTimeout(function () {
46
58
  setShouldRender(false);
47
59
  onClose === null || onClose === void 0 ? void 0 : onClose();
48
- }, 300);
60
+ }, EXIT_MS);
49
61
  };
50
62
  if (!shouldRender)
51
63
  return null;
52
64
  var bgClasses = (_b = bgClassMap[type]) !== null && _b !== void 0 ? _b : bgClassMap.default;
53
- var alertContent = (<div className={"flex items-start justify-between w-full p-2 rounded-[8px] transition-[opacity,transform] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none ".concat(bgClasses, " ").concat(isVisible ? 'opacity-100 translate-y-0 scale-100' : variant === 'toast' ? 'opacity-0 -translate-y-3 scale-95' : 'opacity-0', " ").concat(variant === 'toast' ? 'max-w-md pointer-events-auto' : '')}>
65
+ var alertContent = (<div
66
+ // Errors interrupt screen readers; everything else is announced politely
67
+ role={type === 'error' ? 'alert' : 'status'} onMouseEnter={function () { return setIsPaused(true); }} onMouseLeave={function () { return setIsPaused(false); }} onFocus={function () { return setIsPaused(true); }} onBlur={function () { return setIsPaused(false); }} className={"flex ".concat(secondMessage ? 'items-start' : 'items-center', " justify-between w-full p-2 rounded-[8px] transition-[opacity,transform] ").concat(isVisible ? motionClasses.enter : motionClasses.exit, " motion-reduce:transition-none ").concat(bgClasses, " ").concat(isVisible ? 'opacity-100 translate-y-0 scale-100' : variant === 'toast' ? 'opacity-0 -translate-y-2 scale-90' : 'opacity-0', " ").concat(variant === 'toast' ? 'max-w-md pointer-events-auto' : '')}>
54
68
  <div className="flex items-start gap-2 text-light-text-primary dark:text-dark-text-primary">
55
- <div className="flex flex-col text-left">
69
+ {/* With two lines, nudge the text so its first line centers on the close button */}
70
+ <div className={"flex flex-col text-left ".concat(secondMessage && showCloseButton ? 'pt-[3px]' : '')}>
56
71
  <span className="text-body2">{message}</span>
57
72
  {secondMessage && (<span className="text-body2 opacity-70">{secondMessage}</span>)}
58
73
  </div>
59
74
  </div>
60
75
  {showCloseButton && (<div className="ml-4">
61
- <IconButton color="iconOnly" state="enabled" size="small" iconName="X" onClick={handleClose}/>
76
+ <IconButton color="iconOnly" state="enabled" size="small" iconName="X" ariaLabel="Dismiss" onClick={handleClose}/>
62
77
  </div>)}
63
78
  </div>);
64
79
  if (variant === 'inline') {
@@ -1,100 +1,18 @@
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';
4
+ import { focusRing, focusRingDanger, pressFeedback, stateTransition } from './sharedStyles';
39
5
  export default function Button(_a) {
40
- var _this = this;
41
6
  var size = _a.size, type = _a.type, _b = _a.color, color = _b === void 0 ? 'default' : _b, state = _a.state, label = _a.label, secondLabel = _a.secondLabel, decoIcon = _a.decoIcon, actionIcon = _a.actionIcon, onClickButton = _a.onClickButton, onClickActionIcon = _a.onClickActionIcon, _c = _a.className, className = _c === void 0 ? '' : _c;
42
- var _d = useState(false), isHovered = _d[0], setIsHovered = _d[1];
43
- var _e = useState(null), IconLeft = _e[0], setIconLeft = _e[1];
44
- var _f = useState(null), IconRight = _f[0], setIconRight = _f[1];
45
- var loadIcon = useCallback(function (iconName) { return __awaiter(_this, void 0, void 0, function () {
46
- var module_1, Icon, error_1;
47
- return __generator(this, function (_a) {
48
- switch (_a.label) {
49
- case 0:
50
- if (!iconName)
51
- return [2 /*return*/, null];
52
- _a.label = 1;
53
- case 1:
54
- _a.trys.push([1, 3, , 4]);
55
- return [4 /*yield*/, import('lucide-react')];
56
- case 2:
57
- module_1 = _a.sent();
58
- Icon = module_1[iconName];
59
- return [2 /*return*/, Icon || null];
60
- case 3:
61
- error_1 = _a.sent();
62
- console.error("Failed to load icon ".concat(iconName, ":"), error_1);
63
- return [2 /*return*/, null];
64
- case 4: return [2 /*return*/];
65
- }
66
- });
67
- }); }, []);
68
- useEffect(function () {
69
- var fetchIcons = function () { return __awaiter(_this, void 0, void 0, function () {
70
- var _a, _b;
71
- return __generator(this, function (_c) {
72
- switch (_c.label) {
73
- case 0:
74
- if (!decoIcon) return [3 /*break*/, 2];
75
- _a = setIconLeft;
76
- return [4 /*yield*/, loadIcon(decoIcon)];
77
- case 1:
78
- _a.apply(void 0, [_c.sent()]);
79
- _c.label = 2;
80
- case 2:
81
- if (!actionIcon) return [3 /*break*/, 4];
82
- _b = setIconRight;
83
- return [4 /*yield*/, loadIcon(actionIcon)];
84
- case 3:
85
- _b.apply(void 0, [_c.sent()]);
86
- _c.label = 4;
87
- case 4: return [2 /*return*/];
88
- }
89
- });
90
- }); };
91
- fetchIcons();
92
- }, [decoIcon, actionIcon, loadIcon]);
7
+ // Synchronous lookup so icons render on the first paint
8
+ var IconLeft = decoIcon ? LucideIcons[decoIcon] : null;
9
+ var IconRight = actionIcon ? LucideIcons[actionIcon] : null;
10
+ // Horizontal padding sits on the button so an icon and the label are inset equally
93
11
  var sizeClasses = {
94
- large: 'text-body1 p-2',
95
- medium: 'text-body1 p-1',
96
- small: 'text-body2 p-1',
97
- tiny: 'text-caption px-1 py-px',
12
+ large: 'text-body1 py-2 px-3 gap-1',
13
+ medium: 'text-body1 py-1 px-2 gap-1',
14
+ small: 'text-body2 py-1 px-2 gap-1',
15
+ tiny: 'text-caption py-px px-1 gap-0.5',
98
16
  }[size];
99
17
  var sizeIcon = {
100
18
  large: 'w-6 h-6',
@@ -112,19 +30,20 @@ export default function Button(_a) {
112
30
  danger: {
113
31
  primary: 'bg-light-error-main dark:bg-dark-error-main text-light-error-contrast dark:text-dark-error-contrast',
114
32
  secondary: 'bg-light-error-alertBg dark:bg-dark-error-alertBg text-light-error-dark dark:text-dark-error-light',
115
- tertiary: 'bg-transparent border border-light-error-main dark:border-dark-error-main text-light-error-main dark:text-dark-error-main',
33
+ // Inset ring instead of a border so it adds no height
34
+ tertiary: 'bg-transparent ring-1 ring-inset ring-light-error-main dark:ring-dark-error-main text-light-error-main dark:text-dark-error-main',
116
35
  textOnly: 'text-light-error-main dark:text-dark-error-main',
117
36
  },
118
37
  }[color][type];
119
38
  var hoverTypeClasses = {
120
39
  default: {
121
- primary: 'hover:bg-light-accent-dark hover:dark:bg-dark-accent-dark',
40
+ primary: 'hover:bg-light-accent-dark dark:hover:bg-dark-accent-dark',
122
41
  secondary: 'hover:bg-light-secondary-dark dark:hover:bg-dark-secondary-dark',
123
- tertiary: 'hover:bg-light-background-accent200 hover:dark:bg-dark-background-accent200',
124
- textOnly: 'hover:bg-light-background-accent100 hover:dark:bg-dark-background-accent100',
42
+ tertiary: 'hover:bg-light-background-accent200 dark:hover:bg-dark-background-accent200',
43
+ textOnly: 'hover:bg-light-background-accent100 dark:hover:bg-dark-background-accent100',
125
44
  },
126
45
  danger: {
127
- primary: 'hover:bg-light-error-dark hover:dark:bg-dark-error-dark',
46
+ primary: 'hover:bg-light-error-dark dark:hover:bg-dark-error-dark',
128
47
  secondary: 'hover:bg-light-error-light/60 dark:hover:bg-dark-error-dark/60',
129
48
  tertiary: 'hover:bg-light-error-main/10 dark:hover:bg-dark-error-main/10',
130
49
  textOnly: 'hover:bg-light-error-main/10 dark:hover:bg-dark-error-main/10',
@@ -141,20 +60,23 @@ export default function Button(_a) {
141
60
  : 'cursor-not-allowed text-light-text-disabled dark:text-dark-text-disabled bg-light-actionBackground-disabled dark:bg-dark-actionBackground-disabled',
142
61
  selected: 'cursor-pointer bg-light-primary-dark dark:bg-dark-primary-dark text-light-text-contrast dark:text-dark-text-contrast',
143
62
  };
144
- var buttonClasses = "\n flex flex-row items-center rounded-[8px] transition-colors duration-200 ease-in-out justify-between\n ".concat(sizeClasses, "\n ").concat(state === 'selected' ? stateClasses.selected : '', "\n ").concat(state === 'enabled' ? baseTypeClasses : '', "\n ").concat(state === 'focused' ? "".concat(baseTypeClasses, " ").concat(stateClasses.focused) : '', "\n ").concat(state === 'disabled' ? stateClasses.disabled : '', "\n ").concat(state !== 'disabled' && state !== 'selected' && isHovered ? hoverTypeClasses : '', "\n ").concat(state === 'hovered' ? "".concat(baseTypeClasses, " ").concat(hoverTypeClasses) : '', "\n ").concat(className, "\n ");
145
- return (<button className={buttonClasses} onMouseEnter={function () { if (state !== 'disabled')
146
- setIsHovered(true); }} onMouseLeave={function () { if (state !== 'disabled')
147
- setIsHovered(false); }} onClick={onClickButton}>
63
+ // Hover is pure CSS; `state="hovered"` and `state="focused"` only exist to show those states in docs
64
+ var buttonClasses = "\n flex flex-row items-center rounded-[8px] justify-between ".concat(stateTransition, " ").concat(pressFeedback, "\n ").concat(color === 'danger' ? focusRingDanger : focusRing, "\n ").concat(sizeClasses, "\n ").concat(state === 'selected' ? stateClasses.selected : '', "\n ").concat(state === 'enabled' || state === 'hovered' ? "".concat(stateClasses.enabled, " ").concat(baseTypeClasses, " ").concat(hoverTypeClasses) : '', "\n ").concat(state === 'focused' ? "".concat(baseTypeClasses, " ").concat(hoverTypeClasses, " ").concat(stateClasses.focused) : '', "\n ").concat(state === 'disabled' ? stateClasses.disabled : '', "\n ").concat(className, "\n ");
65
+ return (<button type="button" className={buttonClasses} disabled={state === 'disabled'} onClick={onClickButton}>
148
66
  {/* Group IconLeft and label in a flex container for left alignment */}
149
- <div className="flex items-center">
150
- {IconLeft && <IconLeft className={sizeIcon} strokeWidth={2}/>}
151
- <div className={"flex flex-col text-center ".concat(size === 'tiny' ? 'px-1' : 'px-2')}>
67
+ <div className={"flex items-center ".concat(size === 'tiny' ? 'gap-0.5' : 'gap-1')}>
68
+ {IconLeft && <IconLeft className={"".concat(sizeIcon, " flex-shrink-0")} strokeWidth={2}/>}
69
+ <div className={"flex flex-col text-center px-1"}>
152
70
  <span className="whitespace-nowrap overflow-hidden truncate">{label}</span>
153
71
  {secondLabel && (<span className={"whitespace-nowrap overflow-hidden truncate ".concat(size === 'tiny' ? 'text-caption' : 'text-body2', " opacity-70")}>{secondLabel}</span>)}
154
72
  </div>
155
73
  </div>
156
- {IconRight && (<div onClick={onClickActionIcon} className="cursor-pointer">
157
- <IconRight className={sizeIcon} strokeWidth={2}/>
158
- </div>)}
74
+ {IconRight && (<span onClick={function (e) {
75
+ // Don't also trigger the button's own click
76
+ e.stopPropagation();
77
+ onClickActionIcon === null || onClickActionIcon === void 0 ? void 0 : onClickActionIcon();
78
+ }} className="cursor-pointer">
79
+ <IconRight className={"".concat(sizeIcon, " flex-shrink-0")} strokeWidth={2}/>
80
+ </span>)}
159
81
  </button>);
160
82
  }
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  interface CheckboxProps {
2
3
  label?: string;
3
4
  secondLabel?: string;
@@ -5,5 +6,5 @@ interface CheckboxProps {
5
6
  onChange?: (checked: boolean) => void;
6
7
  disabled?: boolean;
7
8
  }
8
- export default function Checkbox({ label, secondLabel, checked, onChange, disabled }: CheckboxProps): import("react").JSX.Element;
9
+ export default function Checkbox({ label, secondLabel, checked, onChange, disabled }: CheckboxProps): React.JSX.Element;
9
10
  export {};
@@ -1,38 +1,38 @@
1
1
  'use client';
2
- import { useState } from 'react';
2
+ import React, { useEffect, useState } from 'react';
3
+ import { popTransition, stateTransition } from './sharedStyles';
3
4
  export default function Checkbox(_a) {
4
5
  var label = _a.label, secondLabel = _a.secondLabel, _b = _a.checked, checked = _b === void 0 ? false : _b, onChange = _a.onChange, _c = _a.disabled, disabled = _c === void 0 ? false : _c;
5
6
  var _d = useState(checked), isChecked = _d[0], setIsChecked = _d[1];
6
- var handleToggle = function () {
7
+ // Follow the parent when it changes `checked` (e.g. resetting a form)
8
+ useEffect(function () {
9
+ setIsChecked(checked);
10
+ }, [checked]);
11
+ var handleChange = function (event) {
7
12
  if (disabled)
8
13
  return;
9
- var newChecked = !isChecked;
10
- setIsChecked(newChecked);
11
- if (onChange) {
12
- onChange(newChecked);
13
- }
14
+ setIsChecked(event.target.checked);
15
+ onChange === null || onChange === void 0 ? void 0 : onChange(event.target.checked);
14
16
  };
15
- return (<label className={"flex items-center ".concat(disabled ? 'cursor-not-allowed' : 'cursor-pointer')}>
16
- <div onClick={handleToggle} className="relative flex items-center justify-center w-6 h-6 group transition-colors duration-200 ease-in-out">
17
- {/* Circle behind the checkbox */}
18
- <div className={"absolute w-6 h-6 rounded-full ".concat(disabled
19
- ? ''
20
- : 'group-hover:bg-light-action-selected dark:group-hover:bg-dark-action-selected')}></div>
21
-
17
+ return (<label className={"group flex items-start ".concat(disabled ? 'cursor-not-allowed' : 'cursor-pointer')}>
18
+ {/* Native input keeps keyboard focus, Space to toggle and screen-reader semantics */}
19
+ <input type="checkbox" className="peer sr-only" checked={isChecked} disabled={disabled} onChange={handleChange}/>
20
+ <span className={"relative flex flex-shrink-0 items-center justify-center w-6 h-6 rounded-full ".concat(stateTransition, "\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\n ").concat(disabled ? '' : 'group-hover:bg-light-action-selected dark:group-hover:bg-dark-action-selected')}>
22
21
  {/* Checkbox */}
23
- <div className={"relative z-10 w-4 h-4 border-2 rounded ".concat(disabled
22
+ <span className={"relative w-4 h-4 border-2 rounded ".concat(stateTransition, " ").concat(disabled
24
23
  ? isChecked
25
- ? 'bg-light-text-disabled dark:bg-dark-text-disabled border-none'
24
+ ? 'bg-light-text-disabled dark:bg-dark-text-disabled border-transparent'
26
25
  : 'border-light-text-disabled dark:border-dark-text-disabled'
27
26
  : isChecked
28
- ? 'bg-light-text-primary dark:bg-dark-text-primary border-none'
27
+ ? 'bg-light-text-primary dark:bg-dark-text-primary border-transparent'
29
28
  : 'border-light-text-secondary dark:border-dark-text-secondary', " flex items-center justify-center")}>
30
- {isChecked && (<svg className="w-3 h-3 text-light-text-contrast dark:text-dark-text-contrast" fill="none" stroke="currentColor" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
31
- <path strokeWidth="2" d="M1 6l4 3 6-7"/>
32
- </svg>)}
33
- </div>
34
- </div>
35
- {(label || secondLabel) && (<div className="flex flex-col ml-2">
29
+ {/* Always rendered so the tick can scale in and out */}
30
+ <svg className={"w-3 h-3 text-light-text-contrast dark:text-dark-text-contrast ".concat(popTransition, " ").concat(isChecked ? 'scale-100' : 'scale-0')} fill="none" stroke="currentColor" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" aria-hidden>
31
+ <path strokeWidth="2" d="M1 6l4 3 6-7"/>
32
+ </svg>
33
+ </span>
34
+ </span>
35
+ {(label || secondLabel) && (<span className="flex flex-col ml-2">
36
36
  {label && (<span className={"text-body1 ".concat(disabled
37
37
  ? 'text-light-text-disabled dark:text-dark-text-disabled'
38
38
  : 'text-light-text-primary dark:text-dark-text-primary')}>
@@ -43,6 +43,6 @@ export default function Checkbox(_a) {
43
43
  : 'text-light-text-primary dark:text-dark-text-primary')}>
44
44
  {secondLabel}
45
45
  </span>)}
46
- </div>)}
46
+ </span>)}
47
47
  </label>);
48
48
  }
@@ -3,16 +3,18 @@ import React from 'react';
3
3
  import { AlertCircle } from 'lucide-react';
4
4
  export default function EmptyBox(_a) {
5
5
  var text = _a.text, secondText = _a.secondText, size = _a.size;
6
- var color = 'text-light-text-disabled dark:text-dark-text-disabled';
6
+ // The icon can be faint; the text needs readable contrast
7
+ var iconColor = 'text-light-text-disabled dark:text-dark-text-disabled';
8
+ var textColor = 'text-light-text-secondary dark:text-dark-text-secondary';
7
9
  var height = size === 'small' ? 'py-6' : 'h-full';
8
10
  var iconSize = 'size-6';
9
11
  return (<div className={"flex flex-col space-y-2 justify-center items-center w-full ".concat(height)}>
10
- <AlertCircle className={"".concat(iconSize, " ").concat(color)} strokeWidth={2}/>
12
+ <AlertCircle className={"".concat(iconSize, " ").concat(iconColor)} strokeWidth={2} aria-hidden/>
11
13
  <div className="flex flex-col text-center">
12
- <p className={"text-body1 ".concat(color)}>
14
+ <p className={"text-body1 ".concat(textColor)}>
13
15
  {text}
14
16
  </p>
15
- {secondText && (<p className={"text-body2 opacity-70 ".concat(color)}>
17
+ {secondText && (<p className={"text-body2 ".concat(textColor)}>
16
18
  {secondText}
17
19
  </p>)}
18
20
  </div>
@@ -7,6 +7,9 @@ interface IconButtonProps {
7
7
  iconName: keyof typeof LucideIcons;
8
8
  onClick?: any;
9
9
  loading?: boolean;
10
+ /** Screen-reader label. Defaults to the icon name, e.g. "Settings". */
11
+ ariaLabel?: string;
10
12
  }
11
- export default function IconButton({ color, state, size, iconName, onClick, loading, }: IconButtonProps): React.JSX.Element;
13
+ export default function IconButton({ color, state, size, iconName, onClick, loading, // Default to false
14
+ ariaLabel, }: IconButtonProps): React.JSX.Element;
12
15
  export {};
@@ -1,16 +1,15 @@
1
1
  'use client';
2
- import React, { useState, useEffect } from 'react';
2
+ import React from 'react';
3
3
  import * as LucideIcons from 'lucide-react';
4
4
  import { LoadingSmall } from './loadingScreen';
5
+ import { focusRing, pressFeedback, stateTransition } from './sharedStyles';
5
6
  export default function IconButton(_a) {
6
- var color = _a.color, state = _a.state, size = _a.size, iconName = _a.iconName, onClick = _a.onClick, _b = _a.loading, loading = _b === void 0 ? false : _b;
7
- var _c = useState(null), Icon = _c[0], setIcon = _c[1];
8
- // Load icon from Lucide
9
- useEffect(function () {
10
- if (iconName) {
11
- setIcon(LucideIcons[iconName]);
12
- }
13
- }, [iconName]);
7
+ var color = _a.color, state = _a.state, size = _a.size, iconName = _a.iconName, onClick = _a.onClick, _b = _a.loading, loading = _b === void 0 ? false : _b, // Default to false
8
+ ariaLabel = _a.ariaLabel;
9
+ var Icon = LucideIcons[iconName];
10
+ // "ChevronDown" -> "Chevron Down"
11
+ var label = ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : String(iconName).replace(/([a-z])([A-Z0-9])/g, '$1 $2');
12
+ var isDisabled = state === 'disabled';
14
13
  // Size-based classes
15
14
  var sizeClasses = {
16
15
  large: 'p-2', // 8px padding
@@ -27,45 +26,49 @@ export default function IconButton(_a) {
27
26
  // Base classes (padding and corner radius)
28
27
  var baseClasses = "".concat(sizeClasses[size], " rounded-[8px] leading-none relative");
29
28
  // Background color
30
- var bgColor = state === 'selected'
31
- ? 'bg-light-primary-dark dark:bg-dark-primary-dark' // Selected state
32
- : color === 'primary'
33
- ? 'bg-light-accent-main dark:bg-dark-accent-main' // Primary
34
- : color === 'secondary'
35
- ? 'bg-light-secondary-main dark:bg-dark-secondary-main' // Secondary
36
- : color === 'tertiary'
37
- ? 'bg-light-background-accent100 dark:bg-dark-background-accent100' // Tertiary
38
- : ' '; // iconOnly: none
39
- // Background hover color
29
+ var bgColor = isDisabled
30
+ ? (color === 'iconOnly' ? '' : 'bg-light-actionBackground-disabled dark:bg-dark-actionBackground-disabled')
31
+ : state === 'selected'
32
+ ? 'bg-light-primary-dark dark:bg-dark-primary-dark' // Selected state
33
+ : color === 'primary'
34
+ ? 'bg-light-accent-main dark:bg-dark-accent-main' // Primary
35
+ : color === 'secondary'
36
+ ? 'bg-light-secondary-main dark:bg-dark-secondary-main' // Secondary
37
+ : color === 'tertiary'
38
+ ? 'bg-light-background-accent100 dark:bg-dark-background-accent100' // Tertiary
39
+ : ' '; // iconOnly: none
40
+ // Background hover color; `enabled:` skips it while disabled or loading
40
41
  var bgColorHover = state === 'selected'
41
42
  ? '' // No hover effect when selected
42
43
  : color === 'primary'
43
- ? 'hover:bg-light-accent-dark/75 hover:dark:bg-dark-accent-dark/75' // Primary
44
+ ? 'enabled:hover:bg-light-accent-dark/75 dark:enabled:hover:bg-dark-accent-dark/75' // Primary
44
45
  : color === 'secondary'
45
- ? 'hover:bg-light-secondary-dark/75 hover:dark:bg-dark-secondary-dark/75' // Secondary
46
+ ? 'enabled:hover:bg-light-secondary-dark/75 dark:enabled:hover:bg-dark-secondary-dark/75' // Secondary
46
47
  : color === 'tertiary'
47
- ? 'hover:bg-light-background-accent200/75 hover:dark:bg-dark-background-accent200/75' // Tertiary
48
- : 'hover:bg-light-background-accent200/75 hover:dark:bg-dark-background-accent200/75'; // iconOnly
48
+ ? 'enabled:hover:bg-light-background-accent200/75 dark:enabled:hover:bg-dark-background-accent200/75' // Tertiary
49
+ : 'enabled:hover:bg-light-background-accent200/75 dark:enabled:hover:bg-dark-background-accent200/75'; // iconOnly
49
50
  // Icon color
50
- var iconColor = state === 'selected'
51
- ? 'text-light-text-contrast dark:text-dark-text-contrast' // Selected state
52
- : color === 'primary'
53
- ? 'text-light-text-primary dark:text-dark-text-contrast' // Primary
54
- : color === 'secondary'
55
- ? 'text-light-text-primary dark:text-dark-text-primary' // Secondary
56
- : color === 'tertiary'
57
- ? 'text-light-text-primary dark:text-dark-text-primary' // Tertiary
58
- : 'text-light-text-primary dark:text-dark-text-primary'; // iconOnly
51
+ var iconColor = isDisabled
52
+ ? 'text-light-text-disabled dark:text-dark-text-disabled'
53
+ : state === 'selected'
54
+ ? 'text-light-text-contrast dark:text-dark-text-contrast' // Selected state
55
+ : color === 'primary'
56
+ ? 'text-light-text-primary dark:text-dark-text-contrast' // Primary
57
+ : color === 'secondary'
58
+ ? 'text-light-text-primary dark:text-dark-text-primary' // Secondary
59
+ : color === 'tertiary'
60
+ ? 'text-light-text-primary dark:text-dark-text-primary' // Tertiary
61
+ : 'text-light-text-primary dark:text-dark-text-primary'; // iconOnly
59
62
  // State classes, including loading
60
63
  var stateClasses = loading
61
64
  ? 'cursor-wait' // Show a waiting cursor during loading
62
- : state === 'disabled'
63
- ? 'cursor-not-allowed opacity-50'
65
+ : isDisabled
66
+ ? 'cursor-not-allowed'
64
67
  : state === 'selected'
65
68
  ? 'cursor-pointer'
66
69
  : 'cursor-pointer';
67
- return (<button className={"".concat(baseClasses, " ").concat(bgColor, " ").concat(iconColor, " ").concat(bgColorHover, " ").concat(stateClasses, " transition-colors duration-200 ease-in-out flex items-center justify-center")} disabled={state === 'disabled' || loading} // Disable button during loading
68
- onClick={onClick} aria-label={loading ? 'Loading' : 'Reload'}>
70
+ return (<button type="button" className={"".concat(baseClasses, " ").concat(bgColor, " ").concat(iconColor, " ").concat(bgColorHover, " ").concat(stateClasses, " ").concat(stateTransition, " ").concat(pressFeedback, " ").concat(focusRing, " flex items-center justify-center")} disabled={isDisabled || loading} // Disable button during loading
71
+ onClick={onClick} aria-label={loading ? 'Loading' : label}>
69
72
  {loading ? (<LoadingSmall size={size}/>) : (Icon && <Icon className={iconSizeClasses[size]} strokeWidth={2}/>)}
70
73
  </button>);
71
74
  }
@@ -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
  }