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
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,6 +1,6 @@
1
1
  import React from 'react';
2
2
  interface ButtonProps {
3
- size: 'small' | 'medium' | 'large';
3
+ size: 'tiny' | 'small' | 'medium' | 'large';
4
4
  type: 'primary' | 'secondary' | 'tertiary' | 'textOnly';
5
5
  color?: 'default' | 'danger';
6
6
  state: 'enabled' | 'hovered' | 'focused' | 'disabled' | 'selected';
@@ -1,104 +1,24 @@
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',
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',
97
16
  }[size];
98
17
  var sizeIcon = {
99
18
  large: 'w-6 h-6',
100
19
  medium: 'w-5 h-5',
101
20
  small: 'w-4 h-4',
21
+ tiny: 'w-3.5 h-3.5',
102
22
  }[size];
103
23
  var baseTypeClasses = {
104
24
  default: {
@@ -110,19 +30,20 @@ export default function Button(_a) {
110
30
  danger: {
111
31
  primary: 'bg-light-error-main dark:bg-dark-error-main text-light-error-contrast dark:text-dark-error-contrast',
112
32
  secondary: 'bg-light-error-alertBg dark:bg-dark-error-alertBg text-light-error-dark dark:text-dark-error-light',
113
- 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',
114
35
  textOnly: 'text-light-error-main dark:text-dark-error-main',
115
36
  },
116
37
  }[color][type];
117
38
  var hoverTypeClasses = {
118
39
  default: {
119
- 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',
120
41
  secondary: 'hover:bg-light-secondary-dark dark:hover:bg-dark-secondary-dark',
121
- tertiary: 'hover:bg-light-background-accent200 hover:dark:bg-dark-background-accent200',
122
- 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',
123
44
  },
124
45
  danger: {
125
- 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',
126
47
  secondary: 'hover:bg-light-error-light/60 dark:hover:bg-dark-error-dark/60',
127
48
  tertiary: 'hover:bg-light-error-main/10 dark:hover:bg-dark-error-main/10',
128
49
  textOnly: 'hover:bg-light-error-main/10 dark:hover:bg-dark-error-main/10',
@@ -139,20 +60,23 @@ export default function Button(_a) {
139
60
  : 'cursor-not-allowed text-light-text-disabled dark:text-dark-text-disabled bg-light-actionBackground-disabled dark:bg-dark-actionBackground-disabled',
140
61
  selected: 'cursor-pointer bg-light-primary-dark dark:bg-dark-primary-dark text-light-text-contrast dark:text-dark-text-contrast',
141
62
  };
142
- 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 ");
143
- return (<button className={buttonClasses} onMouseEnter={function () { if (state !== 'disabled')
144
- setIsHovered(true); }} onMouseLeave={function () { if (state !== 'disabled')
145
- 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}>
146
66
  {/* Group IconLeft and label in a flex container for left alignment */}
147
- <div className="flex items-center">
148
- {IconLeft && <IconLeft className={sizeIcon} strokeWidth={2}/>}
149
- <div className="flex flex-col text-center 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"}>
150
70
  <span className="whitespace-nowrap overflow-hidden truncate">{label}</span>
151
- {secondLabel && (<span className="whitespace-nowrap overflow-hidden truncate text-body2 opacity-70">{secondLabel}</span>)}
71
+ {secondLabel && (<span className={"whitespace-nowrap overflow-hidden truncate ".concat(size === 'tiny' ? 'text-caption' : 'text-body2', " opacity-70")}>{secondLabel}</span>)}
152
72
  </div>
153
73
  </div>
154
- {IconRight && (<div onClick={onClickActionIcon} className="cursor-pointer">
155
- <IconRight className={sizeIcon} strokeWidth={2}/>
156
- </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>)}
157
81
  </button>);
158
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>
@@ -3,7 +3,7 @@ import { icons } from 'lucide-react';
3
3
  type IconName = keyof typeof icons;
4
4
  interface IconProps {
5
5
  name: IconName;
6
- size?: 'small' | 'medium' | 'large' | number;
6
+ size?: 'tiny' | 'small' | 'medium' | 'large' | number;
7
7
  color?: 'primary' | 'secondary' | 'disabled' | 'error' | 'warning' | 'success' | 'info' | 'accent' | 'inherit';
8
8
  strokeWidth?: number;
9
9
  className?: string;
@@ -2,6 +2,7 @@
2
2
  import React from 'react';
3
3
  import { icons } from 'lucide-react';
4
4
  var sizeMap = {
5
+ tiny: 12,
5
6
  small: 16,
6
7
  medium: 24,
7
8
  large: 32,
@@ -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,17 +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(false), isHovered = _c[0], setIsHovered = _c[1];
8
- var _d = useState(null), Icon = _d[0], setIcon = _d[1];
9
- // Load icon from Lucide
10
- useEffect(function () {
11
- if (iconName) {
12
- setIcon(LucideIcons[iconName]);
13
- }
14
- }, [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';
15
13
  // Size-based classes
16
14
  var sizeClasses = {
17
15
  large: 'p-2', // 8px padding
@@ -28,47 +26,49 @@ export default function IconButton(_a) {
28
26
  // Base classes (padding and corner radius)
29
27
  var baseClasses = "".concat(sizeClasses[size], " rounded-[8px] leading-none relative");
30
28
  // Background color
31
- var bgColor = 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
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
41
41
  var bgColorHover = state === 'selected'
42
42
  ? '' // No hover effect when selected
43
43
  : color === 'primary'
44
- ? 'hover:bg-light-accent-dark hover:dark:bg-dark-accent-dark' // Primary
44
+ ? 'enabled:hover:bg-light-accent-dark/75 dark:enabled:hover:bg-dark-accent-dark/75' // Primary
45
45
  : color === 'secondary'
46
- ? 'hover:bg-light-secondary-dark hover:dark:bg-dark-secondary-dark' // Secondary
46
+ ? 'enabled:hover:bg-light-secondary-dark/75 dark:enabled:hover:bg-dark-secondary-dark/75' // Secondary
47
47
  : color === 'tertiary'
48
- ? 'hover:bg-light-background-accent200 hover:dark:bg-dark-background-accent200' // Tertiary
49
- : 'hover:bg-light-background-accent200 hover:dark:bg-dark-background-accent200'; // 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
50
50
  // Icon color
51
- var iconColor = state === 'selected'
52
- ? 'text-light-text-contrast dark:text-dark-text-contrast' // Selected state
53
- : color === 'primary'
54
- ? 'text-light-text-primary dark:text-dark-text-contrast' // Primary
55
- : color === 'secondary'
56
- ? 'text-light-text-primary dark:text-dark-text-primary' // Secondary
57
- : color === 'tertiary'
58
- ? 'text-light-text-primary dark:text-dark-text-primary' // Tertiary
59
- : '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
60
62
  // State classes, including loading
61
63
  var stateClasses = loading
62
64
  ? 'cursor-wait' // Show a waiting cursor during loading
63
- : state === 'disabled'
64
- ? 'cursor-not-allowed opacity-50'
65
+ : isDisabled
66
+ ? 'cursor-not-allowed'
65
67
  : state === 'selected'
66
68
  ? 'cursor-pointer'
67
- : isHovered
68
- ? 'cursor-pointer hover:bg-opacity-75'
69
- : 'cursor-pointer';
70
- 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
71
- onMouseEnter={function () { return setIsHovered(true); }} onMouseLeave={function () { return setIsHovered(false); }} onClick={onClick} aria-label={loading ? 'Loading' : 'Reload'}>
69
+ : 'cursor-pointer';
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}>
72
72
  {loading ? (<LoadingSmall size={size}/>) : (Icon && <Icon className={iconSizeClasses[size]} strokeWidth={2}/>)}
73
73
  </button>);
74
74
  }