pixelize-design-library 2.4.2-beta.35 → 2.4.2-beta.36

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.
@@ -70,11 +70,6 @@ const validate = (apps) => {
70
70
  warnOnce(`url-${app.id || i}`, `app "${app.name}" has no url; it cannot be opened.`);
71
71
  });
72
72
  };
73
- // Past a pointer's pause threshold, so crossing the launcher on the way elsewhere does not open it,
74
- // but short enough that deliberately resting on it feels instant.
75
- const HOVER_OPEN_DELAY = 450;
76
- // Long enough to travel the gap between the launcher and the panel below it.
77
- const HOVER_CLOSE_GRACE = 250;
78
73
  // Flip back to true to restore `searchThreshold` opening the field on its own.
79
74
  const SEARCH_OPENS_BY_COUNT = false;
80
75
  const AppSwitcher = ({ apps, currentAppId, onSelect, onOpenChange, isSearchable, searchThreshold = 8, shortcut = null, showShortcutHint = true, hideWhenAlone = true, emptyMessage = "No app matches that name.", footer, ariaLabel = "Switch app", panelWidth, maxHeight = "24rem", isLoading = false, }) => {
@@ -85,11 +80,8 @@ const AppSwitcher = ({ apps, currentAppId, onSelect, onOpenChange, isSearchable,
85
80
  const [activeIndex, setActiveIndex] = (0, react_1.useState)(0);
86
81
  const searchRef = (0, react_1.useRef)(null);
87
82
  const triggerRef = (0, react_1.useRef)(null);
88
- const panelRef = (0, react_1.useRef)(null);
89
83
  const itemRefs = (0, react_1.useRef)([]);
90
84
  const typeahead = (0, react_1.useRef)({ buffer: "", at: 0 });
91
- const hoverTimer = (0, react_1.useRef)();
92
- const openedByHover = (0, react_1.useRef)(false);
93
85
  (0, react_1.useEffect)(() => validate(apps), [apps]);
94
86
  // Search is off while the panel's design settles: the app count alone no longer opens it. An
95
87
  // explicit `isSearchable` still wins, so nothing about the prop changed.
@@ -106,41 +98,6 @@ const AppSwitcher = ({ apps, currentAppId, onSelect, onOpenChange, isSearchable,
106
98
  }
107
99
  onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(next);
108
100
  }, [onOpen, onClose, onOpenChange, requestExclusiveOpen]);
109
- const cancelHoverTimer = (0, react_1.useCallback)(() => {
110
- if (hoverTimer.current)
111
- clearTimeout(hoverTimer.current);
112
- hoverTimer.current = undefined;
113
- }, []);
114
- (0, react_1.useEffect)(() => cancelHoverTimer, [cancelHoverTimer]);
115
- // Touch and stylus report no hover; a timer there would open the panel on a tap-and-hold.
116
- const pointerHovers = () => {
117
- var _a;
118
- return typeof window !== "undefined" &&
119
- ((_a = window.matchMedia) === null || _a === void 0 ? void 0 : _a.call(window, "(hover: hover) and (pointer: fine)").matches);
120
- };
121
- const handleHoverStart = (0, react_1.useCallback)(() => {
122
- if (isOpen || !pointerHovers())
123
- return;
124
- cancelHoverTimer();
125
- hoverTimer.current = setTimeout(() => {
126
- openedByHover.current = true;
127
- setOpen(true);
128
- }, HOVER_OPEN_DELAY);
129
- }, [isOpen, cancelHoverTimer, setOpen]);
130
- // A click-opened panel is sticky, and a keyboard user inside it is not "hovering away".
131
- const handleHoverEnd = (0, react_1.useCallback)(() => {
132
- cancelHoverTimer();
133
- if (!isOpen || !openedByHover.current)
134
- return;
135
- hoverTimer.current = setTimeout(() => {
136
- var _a;
137
- if ((_a = panelRef.current) === null || _a === void 0 ? void 0 : _a.contains(document.activeElement))
138
- return;
139
- openedByHover.current = false;
140
- setOpen(false);
141
- setQuery("");
142
- }, HOVER_CLOSE_GRACE);
143
- }, [isOpen, cancelHoverTimer, setOpen]);
144
101
  const closeAndRefocus = (0, react_1.useCallback)(() => {
145
102
  var _a;
146
103
  setOpen(false);
@@ -264,7 +221,7 @@ const AppSwitcher = ({ apps, currentAppId, onSelect, onOpenChange, isSearchable,
264
221
  const currentSub = (0, outlineRung_1.liftToContrast)(theme.colors.textMuted, currentBg, accentText_1.AA_NORMAL_TEXT);
265
222
  const width = panelWidth !== null && panelWidth !== void 0 ? panelWidth : "28rem";
266
223
  const hint = shortcut && showShortcutHint ? (0, AppSwitcherProps_1.formatShortcut)(shortcut) : null;
267
- return ((0, jsx_runtime_1.jsxs)(react_2.Popover, { isOpen: isOpen, onOpen: () => setOpen(true), onClose: () => { setOpen(false); setQuery(""); }, placement: "bottom-end", initialFocusRef: showSearch ? searchRef : undefined, isLazy: true, children: [(0, jsx_runtime_1.jsx)(react_2.PopoverTrigger, { children: (0, jsx_runtime_1.jsx)(react_2.Flex, { ref: triggerRef, as: "button", type: "button", "aria-label": ariaLabel, align: "center", justify: "center", boxSize: s.trigger, borderRadius: "lg", border: "1px solid", borderColor: isOpen ? theme.colors.boxborder[500] : theme.colors.boxborder[200], bg: isOpen ? theme.colors.backgroundColor.tertiary : "transparent", color: isOpen ? theme.colors.text[900] : theme.colors.textMuted, cursor: "pointer", transition: "background 0.15s ease, border-color 0.15s ease", _hover: { bg: rowHoverBg, color: theme.colors.text[900] }, onMouseEnter: handleHoverStart, onMouseLeave: handleHoverEnd, onClick: () => { openedByHover.current = false; cancelHoverTimer(); }, onKeyDown: (e) => {
224
+ return ((0, jsx_runtime_1.jsxs)(react_2.Popover, { isOpen: isOpen, onOpen: () => setOpen(true), onClose: () => { setOpen(false); setQuery(""); }, placement: "bottom-end", initialFocusRef: showSearch ? searchRef : undefined, isLazy: true, children: [(0, jsx_runtime_1.jsx)(react_2.PopoverTrigger, { children: (0, jsx_runtime_1.jsx)(react_2.Flex, { ref: triggerRef, as: "button", type: "button", "aria-label": ariaLabel, align: "center", justify: "center", boxSize: s.trigger, borderRadius: "lg", border: "1px solid", borderColor: isOpen ? theme.colors.boxborder[500] : theme.colors.boxborder[200], bg: isOpen ? theme.colors.backgroundColor.tertiary : "transparent", color: isOpen ? theme.colors.text[900] : theme.colors.textMuted, cursor: "pointer", transition: "background 0.15s ease, border-color 0.15s ease", _hover: { bg: rowHoverBg, color: theme.colors.text[900] }, onKeyDown: (e) => {
268
225
  if (!isOpen) {
269
226
  if (e.key === "ArrowDown") {
270
227
  e.preventDefault();
@@ -273,7 +230,7 @@ const AppSwitcher = ({ apps, currentAppId, onSelect, onOpenChange, isSearchable,
273
230
  return;
274
231
  }
275
232
  handlePanelKeyDown(e);
276
- }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Grip, { size: s.icon + 1 }) }) }), (0, jsx_runtime_1.jsx)(react_2.PopoverContent, { ref: panelRef, onMouseEnter: cancelHoverTimer, onMouseLeave: handleHoverEnd, w: width, maxW: "calc(100vw - 1.5rem)", textAlign: "start", onKeyDown: handlePanelKeyDown, bg: theme.colors.backgroundColor.light, borderRadius: "xl", overflow: "hidden", boxShadow: `0 10px 30px ${theme.colors.boxShadow.default}`, _focus: { outline: "none", boxShadow: `0 10px 30px ${theme.colors.boxShadow.default}` }, children: (0, jsx_runtime_1.jsxs)(react_2.PopoverBody, { p: 0, children: [(0, jsx_runtime_1.jsxs)(react_2.Flex, { align: "center", gap: 2, px: 3, pt: 3, pb: showSearch ? 2 : 1, children: [(0, jsx_runtime_1.jsx)(react_2.Text, { fontSize: "0.6875rem", fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: theme.colors.textMuted, children: HEADING }), (0, jsx_runtime_1.jsx)(react_2.Box, { flex: "1" }), hint && ((0, jsx_runtime_1.jsx)(react_2.Text, { as: "kbd", fontSize: "0.6875rem", fontFamily: "inherit", color: theme.colors.textMuted, border: "1px solid", borderRadius: "base", px: 1.5, py: 0.5, children: hint }))] }), showSearch && ((0, jsx_runtime_1.jsx)(react_2.Box, { px: 3, pb: 2, children: (0, jsx_runtime_1.jsxs)(react_2.InputGroup, { size: "sm", children: [(0, jsx_runtime_1.jsx)(react_2.InputLeftElement, { pointerEvents: "none", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, color: theme.colors.textMuted }) }), (0, jsx_runtime_1.jsx)(react_2.Input, { ref: searchRef, value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search apps", borderRadius: "lg", "aria-label": "Search apps", onKeyDown: (e) => {
233
+ }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Grip, { size: s.icon + 1 }) }) }), (0, jsx_runtime_1.jsx)(react_2.PopoverContent, { w: width, maxW: "calc(100vw - 1.5rem)", textAlign: "start", onKeyDown: handlePanelKeyDown, bg: theme.colors.backgroundColor.light, borderRadius: "xl", overflow: "hidden", boxShadow: `0 10px 30px ${theme.colors.boxShadow.default}`, _focus: { outline: "none", boxShadow: `0 10px 30px ${theme.colors.boxShadow.default}` }, children: (0, jsx_runtime_1.jsxs)(react_2.PopoverBody, { p: 0, children: [(0, jsx_runtime_1.jsxs)(react_2.Flex, { align: "center", gap: 2, px: 3, pt: 3, pb: showSearch ? 2 : 1, children: [(0, jsx_runtime_1.jsx)(react_2.Text, { fontSize: "0.6875rem", fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: theme.colors.textMuted, children: HEADING }), (0, jsx_runtime_1.jsx)(react_2.Box, { flex: "1" }), hint && ((0, jsx_runtime_1.jsx)(react_2.Text, { as: "kbd", fontSize: "0.6875rem", fontFamily: "inherit", color: theme.colors.textMuted, border: "1px solid", borderRadius: "base", px: 1.5, py: 0.5, children: hint }))] }), showSearch && ((0, jsx_runtime_1.jsx)(react_2.Box, { px: 3, pb: 2, children: (0, jsx_runtime_1.jsxs)(react_2.InputGroup, { size: "sm", children: [(0, jsx_runtime_1.jsx)(react_2.InputLeftElement, { pointerEvents: "none", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, color: theme.colors.textMuted }) }), (0, jsx_runtime_1.jsx)(react_2.Input, { ref: searchRef, value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search apps", borderRadius: "lg", "aria-label": "Search apps", onKeyDown: (e) => {
277
234
  if (e.key === "ArrowDown" && visible.length > 0) {
278
235
  e.preventDefault();
279
236
  focusItem(0);
@@ -1,2 +1,2 @@
1
1
  import { SegmentedControlProps } from "./SegmentedControlProps";
2
- export default function SegmentedControl<T extends string = string>({ options, value, onChange, size, isFullWidth, "aria-label": ariaLabel, }: SegmentedControlProps<T>): import("react/jsx-runtime").JSX.Element;
2
+ export default function SegmentedControl<T extends string = string>({ options, value, onChange, size, isFullWidth, variant, "aria-label": ariaLabel, }: SegmentedControlProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -5,18 +5,38 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const react_2 = require("@chakra-ui/react");
7
7
  const useCustomTheme_1 = require("../../Theme/useCustomTheme");
8
+ const filledSurface_1 = require("../../Theme/tokens/builders/filledSurface");
8
9
  const SIZES = {
9
10
  sm: { h: "1.625rem", px: "0.625rem", fontSize: "0.75rem" },
10
11
  md: { h: "2rem", px: "0.875rem", fontSize: "0.8125rem" },
11
12
  };
12
- function SegmentedControl({ options, value, onChange, size = "md", isFullWidth = false, "aria-label": ariaLabel, }) {
13
+ function SegmentedControl({ options, value, onChange, size = "md", isFullWidth = false, variant = "default", "aria-label": ariaLabel, }) {
13
14
  const { colors } = (0, useCustomTheme_1.useCustomTheme)();
14
15
  const dims = SIZES[size];
15
16
  const refs = (0, react_1.useRef)([]);
17
+ const trackRef = (0, react_1.useRef)(null);
18
+ const isBrandSlide = variant === "brandSlide";
19
+ const isUnderlineSlide = variant === "underlineSlide";
20
+ const hasIndicator = isBrandSlide || isUnderlineSlide;
21
+ const brandFill = (0, filledSurface_1.resolveFilledSurface)(colors.primary, colors);
22
+ const [pill, setPill] = (0, react_1.useState)(null);
16
23
  const selectedIndex = options.findIndex((o) => o.value === value);
17
24
  const enabled = options.reduce((acc, o, i) => (o.isDisabled ? acc : [...acc, i]), []);
18
25
  // WAI-APG: with nothing selected, the first enabled segment owns the tab stop.
19
26
  const tabIndex = selectedIndex >= 0 ? selectedIndex : enabled[0];
27
+ (0, react_1.useLayoutEffect)(() => {
28
+ if (!hasIndicator)
29
+ return;
30
+ const track = trackRef.current;
31
+ const node = refs.current[selectedIndex];
32
+ if (!track || !node) {
33
+ setPill(null);
34
+ return;
35
+ }
36
+ const trackRect = track.getBoundingClientRect();
37
+ const nodeRect = node.getBoundingClientRect();
38
+ setPill({ left: nodeRect.left - trackRect.left, width: nodeRect.width });
39
+ }, [hasIndicator, selectedIndex, options, isFullWidth, size]);
20
40
  const selectAt = (index) => {
21
41
  var _a;
22
42
  (_a = refs.current[index]) === null || _a === void 0 ? void 0 : _a.focus();
@@ -46,13 +66,23 @@ function SegmentedControl({ options, value, onChange, size = "md", isFullWidth =
46
66
  selectAt(enabled[enabled.length - 1]);
47
67
  }
48
68
  };
49
- return ((0, jsx_runtime_1.jsx)(react_2.Flex, { role: "radiogroup", "aria-label": ariaLabel, onKeyDown: handleKeyDown, display: "inline-flex", align: "center", gap: "0.125rem", p: "0.125rem", borderRadius: "md", bg: colors.background[600], w: isFullWidth ? "100%" : "fit-content", children: options.map((option, index) => {
50
- const isSelected = option.value === value;
51
- return ((0, jsx_runtime_1.jsxs)(react_2.Box, { as: "button", type: "button", ref: (node) => {
52
- refs.current[index] = node;
53
- }, role: "radio", "aria-checked": isSelected, disabled: option.isDisabled, tabIndex: index === tabIndex ? 0 : -1, onClick: () => onChange(option.value), display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "0.375rem", flex: isFullWidth ? "1" : undefined, h: dims.h, px: dims.px, borderRadius: "base", borderWidth: "1px", borderStyle: "solid", borderColor: isSelected ? colors.background[400] : "transparent", bg: isSelected ? colors.background[50] : "transparent", boxShadow: isSelected ? "sm" : "none", color: isSelected ? colors.text[900] : colors.text[600], fontSize: dims.fontSize, fontWeight: isSelected ? 600 : 500, whiteSpace: "nowrap", opacity: option.isDisabled ? 0.5 : 1, cursor: option.isDisabled ? "not-allowed" : "pointer", transition: "background-color 0.2s ease, color 0.2s ease", _hover: option.isDisabled || isSelected ? undefined : { color: colors.text[900] }, _focusVisible: {
54
- outline: "none",
55
- boxShadow: `0 0 0 2px ${colors.primary[300]}`,
56
- }, children: [option.icon, option.label] }, option.value));
57
- }) }));
69
+ return ((0, jsx_runtime_1.jsxs)(react_2.Flex, { ref: trackRef, role: "radiogroup", "aria-label": ariaLabel, onKeyDown: handleKeyDown, position: "relative", display: "inline-flex", align: "center", gap: isUnderlineSlide ? "0" : "0.125rem", p: isUnderlineSlide ? "0" : "0.125rem", borderRadius: isUnderlineSlide ? "none" : "md", borderBottom: isUnderlineSlide ? `0.0625rem solid ${colors.boxborder[200]}` : undefined, bg: isBrandSlide ? colors.primary.opacity[8] : isUnderlineSlide ? "transparent" : colors.background[600], w: isFullWidth ? "100%" : "fit-content", children: [isBrandSlide && pill && ((0, jsx_runtime_1.jsx)(react_2.Box, { "aria-hidden": "true", position: "absolute", top: "0.125rem", left: 0, h: dims.h, w: `${pill.width}px`, borderRadius: "base", bg: brandFill.fill, boxShadow: `0 0.125rem 0.5rem ${colors.boxShadow.primary}`, transform: `translateX(${pill.left}px)`, transition: "transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1), width 0.24s cubic-bezier(0.2, 0.8, 0.2, 1)" })), isUnderlineSlide && pill && ((0, jsx_runtime_1.jsx)(react_2.Box, { "aria-hidden": "true", position: "absolute", bottom: "-0.0625rem", left: 0, h: "0.125rem", w: `${pill.width}px`, borderRadius: "full", bg: colors.primary[500], transform: `translateX(${pill.left}px)`, transition: "transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1), width 0.24s cubic-bezier(0.2, 0.8, 0.2, 1)" })), options.map((option, index) => {
70
+ const isSelected = option.value === value;
71
+ return ((0, jsx_runtime_1.jsxs)(react_2.Box, { as: "button", type: "button", ref: (node) => {
72
+ refs.current[index] = node;
73
+ }, role: "radio", "aria-checked": isSelected, disabled: option.isDisabled, tabIndex: index === tabIndex ? 0 : -1, onClick: () => onChange(option.value), position: "relative", zIndex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "0.375rem", flex: isFullWidth ? "1" : undefined, h: dims.h, px: dims.px, borderRadius: isUnderlineSlide ? "none" : "base", borderWidth: "1px", borderStyle: "solid", borderColor: hasIndicator
74
+ ? "transparent"
75
+ : isSelected
76
+ ? colors.primary.opacity[32]
77
+ : "transparent", bg: hasIndicator ? "transparent" : isSelected ? colors.backgroundColor.light : "transparent", boxShadow: hasIndicator ? "none" : isSelected ? "sm" : "none", color: isBrandSlide
78
+ ? isSelected
79
+ ? brandFill.color
80
+ : colors.text[600]
81
+ : isSelected
82
+ ? colors.accentText
83
+ : colors.text[600], fontSize: dims.fontSize, fontWeight: isSelected ? 600 : 500, whiteSpace: "nowrap", opacity: option.isDisabled ? 0.5 : 1, cursor: option.isDisabled ? "not-allowed" : "pointer", transition: "color 0.2s ease", _hover: option.isDisabled || isSelected ? undefined : { color: colors.text[900] }, _focusVisible: {
84
+ outline: "none",
85
+ boxShadow: `0 0 0 2px ${colors.primary[300]}`,
86
+ }, children: [option.icon, option.label] }, option.value));
87
+ })] }));
58
88
  }
@@ -1,4 +1,5 @@
1
1
  export type SegmentedControlSize = "sm" | "md";
2
+ export type SegmentedControlVariant = "default" | "brandSlide" | "underlineSlide";
2
3
  export type SegmentedControlOption<T extends string = string> = {
3
4
  value: T;
4
5
  label: string;
@@ -12,5 +13,7 @@ export type SegmentedControlProps<T extends string = string> = {
12
13
  onChange: (value: T) => void;
13
14
  size?: SegmentedControlSize;
14
15
  isFullWidth?: boolean;
16
+ /** brandSlide = primary view switcher; underlineSlide = dense toolbars/headers. */
17
+ variant?: SegmentedControlVariant;
15
18
  "aria-label": string;
16
19
  };
@@ -4,45 +4,95 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const jsx_runtime_1 = require("react/jsx-runtime");
7
- const react_1 = require("@chakra-ui/react");
7
+ const react_1 = require("react");
8
+ const react_2 = require("@chakra-ui/react");
8
9
  const lucide_react_1 = require("lucide-react");
9
10
  const useCustomTheme_1 = require("../../Theme/useCustomTheme");
10
11
  const outlineRung_1 = require("../../Theme/tokens/builders/outlineRung");
12
+ const filledSurface_1 = require("../../Theme/tokens/builders/filledSurface");
11
13
  const ToolTip_1 = __importDefault(require("../ToolTip/ToolTip"));
12
- const TableToggle = ({ value, onChange }) => {
14
+ const MODES = ["grid", "list"];
15
+ const TableToggle = ({ value, onChange, variant = "default" }) => {
13
16
  const theme = (0, useCustomTheme_1.useCustomTheme)();
14
17
  const { colors } = theme;
18
+ const isBrandSlide = variant === "brandSlide";
19
+ const isUnderlineSlide = variant === "underlineSlide";
20
+ const hasIndicator = isBrandSlide || isUnderlineSlide;
21
+ const brandFill = (0, filledSurface_1.resolveFilledSurface)(colors.primary, colors);
15
22
  const restInk = (0, outlineRung_1.liftToContrast)(colors.background[800], colors.background[600], outlineRung_1.AA_NON_TEXT);
16
23
  const hoverInk = (0, outlineRung_1.liftToContrast)(colors.background[900], colors.background[400], outlineRung_1.AA_NON_TEXT);
24
+ const trackRef = (0, react_1.useRef)(null);
25
+ const refs = (0, react_1.useRef)([]);
26
+ const [pill, setPill] = (0, react_1.useState)(null);
27
+ const selectedIndex = MODES.indexOf(value);
28
+ (0, react_1.useLayoutEffect)(() => {
29
+ if (!hasIndicator)
30
+ return;
31
+ const track = trackRef.current;
32
+ const node = refs.current[selectedIndex];
33
+ if (!track || !node) {
34
+ setPill(null);
35
+ return;
36
+ }
37
+ const trackRect = track.getBoundingClientRect();
38
+ const nodeRect = node.getBoundingClientRect();
39
+ setPill({ left: nodeRect.left - trackRect.left, width: nodeRect.width });
40
+ }, [hasIndicator, selectedIndex]);
17
41
  const styles = {
18
42
  container: {
19
43
  display: "flex",
20
44
  alignItems: "center",
21
- bg: colors.background[600],
22
- p: "0.125rem",
23
- borderRadius: "md",
45
+ bg: isBrandSlide ? colors.primary.opacity[8] : isUnderlineSlide ? "transparent" : colors.background[600],
46
+ p: isUnderlineSlide ? "0" : "0.125rem",
47
+ borderRadius: isUnderlineSlide ? "none" : "md",
48
+ borderBottom: isUnderlineSlide ? `0.0625rem solid ${colors.boxborder[200]}` : undefined,
24
49
  height: "2rem",
25
- gap: "0.125rem",
50
+ gap: isUnderlineSlide ? "0" : "0.125rem",
26
51
  width: "6.3rem",
52
+ position: "relative",
27
53
  },
28
54
  button: (mode) => ({
29
- bg: value === mode ? colors.background[50] : "transparent",
30
- boxShadow: value === mode ? "md" : "none",
31
- color: value === mode ? colors.text[900] : restInk,
32
- borderRadius: "base",
55
+ position: "relative",
56
+ zIndex: 1,
57
+ bg: hasIndicator ? "transparent" : value === mode ? colors.backgroundColor.light : "transparent",
58
+ boxShadow: hasIndicator ? "none" : value === mode ? "md" : "none",
59
+ color: isBrandSlide
60
+ ? value === mode
61
+ ? brandFill.color
62
+ : restInk
63
+ : value === mode
64
+ ? colors.accentText
65
+ : restInk,
66
+ borderRadius: isUnderlineSlide ? "none" : "base",
33
67
  height: "1.75rem",
34
- border: "0.031rem solid",
35
- borderColor: value === mode ? colors.background[400] : "transparent",
68
+ border: hasIndicator ? "none" : "0.031rem solid",
69
+ borderColor: hasIndicator
70
+ ? "transparent"
71
+ : value === mode
72
+ ? colors.primary.opacity[32]
73
+ : "transparent",
36
74
  padding: "0.313rem 0.813rem",
37
75
  _hover: {
38
- bg: value === mode
39
- ? colors.background[50]
40
- : colors.background[400],
41
- color: value === mode ? colors.text[900] : hoverInk,
76
+ bg: hasIndicator
77
+ ? "transparent"
78
+ : value === mode
79
+ ? colors.backgroundColor.light
80
+ : colors.background[400],
81
+ color: isBrandSlide
82
+ ? value === mode
83
+ ? brandFill.color
84
+ : hoverInk
85
+ : value === mode
86
+ ? colors.accentText
87
+ : hoverInk,
42
88
  },
43
- transition: "all 0.3s ease-in-out",
89
+ transition: "color 0.2s ease-in-out",
44
90
  }),
45
91
  };
46
- return ((0, jsx_runtime_1.jsxs)(react_1.Box, { ...styles.container, children: [(0, jsx_runtime_1.jsx)(ToolTip_1.default, { label: "Table View", placement: "top", fontSize: 12, hasArrow: true, children: (0, jsx_runtime_1.jsx)(react_1.IconButton, { "aria-label": "Grid View", icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Table2, { size: "1.25rem" }), variant: "ghost", onClick: () => onChange("grid"), ...styles.button("grid") }) }), (0, jsx_runtime_1.jsx)(ToolTip_1.default, { label: "Kanban View", placement: "top", fontSize: 12, hasArrow: true, children: (0, jsx_runtime_1.jsx)(react_1.IconButton, { "aria-label": "List View", icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Columns3, { size: "1.25rem" }), variant: "ghost", onClick: () => onChange("list"), ...styles.button("list") }) })] }));
92
+ return ((0, jsx_runtime_1.jsxs)(react_2.Box, { ref: trackRef, ...styles.container, children: [isBrandSlide && pill && ((0, jsx_runtime_1.jsx)(react_2.Box, { "aria-hidden": "true", position: "absolute", top: "0.125rem", left: 0, h: "1.75rem", w: `${pill.width}px`, borderRadius: "base", bg: brandFill.fill, boxShadow: `0 0.125rem 0.5rem ${colors.boxShadow.primary}`, transform: `translateX(${pill.left}px)`, transition: "transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1), width 0.24s cubic-bezier(0.2, 0.8, 0.2, 1)" })), isUnderlineSlide && pill && ((0, jsx_runtime_1.jsx)(react_2.Box, { "aria-hidden": "true", position: "absolute", bottom: "-0.0625rem", left: 0, h: "0.125rem", w: `${pill.width}px`, borderRadius: "full", bg: colors.primary[500], transform: `translateX(${pill.left}px)`, transition: "transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1), width 0.24s cubic-bezier(0.2, 0.8, 0.2, 1)" })), (0, jsx_runtime_1.jsx)(ToolTip_1.default, { label: "Table View", placement: "top", fontSize: 12, hasArrow: true, children: (0, jsx_runtime_1.jsx)(react_2.IconButton, { ref: (node) => {
93
+ refs.current[0] = node;
94
+ }, "aria-label": "Grid View", icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Table2, { size: "1.25rem" }), variant: "ghost", onClick: () => onChange("grid"), ...styles.button("grid") }) }), (0, jsx_runtime_1.jsx)(ToolTip_1.default, { label: "Kanban View", placement: "top", fontSize: 12, hasArrow: true, children: (0, jsx_runtime_1.jsx)(react_2.IconButton, { ref: (node) => {
95
+ refs.current[1] = node;
96
+ }, "aria-label": "List View", icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Columns3, { size: "1.25rem" }), variant: "ghost", onClick: () => onChange("list"), ...styles.button("list") }) })] }));
47
97
  };
48
98
  exports.default = TableToggle;
@@ -1,5 +1,8 @@
1
1
  export type ViewMode = "grid" | "list";
2
+ export type TableToggleVariant = "default" | "brandSlide" | "underlineSlide";
2
3
  export type ViewToggleProps = {
3
4
  value: ViewMode;
4
5
  onChange: (val: ViewMode) => void;
6
+ /** brandSlide = primary view switcher; underlineSlide = dense toolbars/headers. */
7
+ variant?: TableToggleVariant;
5
8
  };
@@ -14,5 +14,5 @@ function ToggleSwitch({ isChecked, onChange, isDisabled = false, size = "md", id
14
14
  return ((0, jsx_runtime_1.jsx)(react_1.Box, { as: "button", type: "button", id: id, role: "switch", "aria-checked": isChecked, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, disabled: isDisabled, onClick: () => onChange(!isChecked), position: "relative", display: "inline-flex", alignItems: "center", flexShrink: 0, w: dims.w, h: dims.h, p: 0, borderRadius: "full", borderWidth: "1px", borderStyle: "solid", borderColor: isChecked ? colors.primary[500] : colors.boxborder[500], bg: isChecked ? colors.primary[500] : colors.background[600], opacity: isDisabled ? 0.5 : 1, cursor: isDisabled ? "not-allowed" : "pointer", transition: "background-color 0.2s ease, border-color 0.2s ease", _focusVisible: {
15
15
  outline: "none",
16
16
  boxShadow: `0 0 0 2px ${colors.primary[300]}`,
17
- }, children: (0, jsx_runtime_1.jsx)(react_1.Box, { "aria-hidden": "true", position: "absolute", left: "0.125rem", w: dims.knob, h: dims.knob, borderRadius: "full", bg: colors.background[50], boxShadow: "sm", transition: "transform 0.2s ease", transform: isChecked ? `translateX(${dims.travel})` : "translateX(0)" }) }));
17
+ }, children: (0, jsx_runtime_1.jsx)(react_1.Box, { "aria-hidden": "true", position: "absolute", left: "0.125rem", w: dims.knob, h: dims.knob, borderRadius: "full", bg: colors.white, boxShadow: "sm", transition: "transform 0.2s ease", transform: isChecked ? `translateX(${dims.travel})` : "translateX(0)" }) }));
18
18
  }
@@ -35,11 +35,6 @@ const validate = (apps) => {
35
35
  warnOnce(`url-${app.id || i}`, `app "${app.name}" has no url; it cannot be opened.`);
36
36
  });
37
37
  };
38
- // Past a pointer's pause threshold, so crossing the launcher on the way elsewhere does not open it,
39
- // but short enough that deliberately resting on it feels instant.
40
- const HOVER_OPEN_DELAY = 450;
41
- // Long enough to travel the gap between the launcher and the panel below it.
42
- const HOVER_CLOSE_GRACE = 250;
43
38
  // Flip back to true to restore `searchThreshold` opening the field on its own.
44
39
  const SEARCH_OPENS_BY_COUNT = false;
45
40
  const AppSwitcher = ({ apps, currentAppId, onSelect, onOpenChange, isSearchable, searchThreshold = 8, shortcut = null, showShortcutHint = true, hideWhenAlone = true, emptyMessage = "No app matches that name.", footer, ariaLabel = "Switch app", panelWidth, maxHeight = "24rem", isLoading = false, }) => {
@@ -50,11 +45,8 @@ const AppSwitcher = ({ apps, currentAppId, onSelect, onOpenChange, isSearchable,
50
45
  const [activeIndex, setActiveIndex] = useState(0);
51
46
  const searchRef = useRef(null);
52
47
  const triggerRef = useRef(null);
53
- const panelRef = useRef(null);
54
48
  const itemRefs = useRef([]);
55
49
  const typeahead = useRef({ buffer: "", at: 0 });
56
- const hoverTimer = useRef();
57
- const openedByHover = useRef(false);
58
50
  useEffect(() => validate(apps), [apps]);
59
51
  // Search is off while the panel's design settles: the app count alone no longer opens it. An
60
52
  // explicit `isSearchable` still wins, so nothing about the prop changed.
@@ -71,41 +63,6 @@ const AppSwitcher = ({ apps, currentAppId, onSelect, onOpenChange, isSearchable,
71
63
  }
72
64
  onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(next);
73
65
  }, [onOpen, onClose, onOpenChange, requestExclusiveOpen]);
74
- const cancelHoverTimer = useCallback(() => {
75
- if (hoverTimer.current)
76
- clearTimeout(hoverTimer.current);
77
- hoverTimer.current = undefined;
78
- }, []);
79
- useEffect(() => cancelHoverTimer, [cancelHoverTimer]);
80
- // Touch and stylus report no hover; a timer there would open the panel on a tap-and-hold.
81
- const pointerHovers = () => {
82
- var _a;
83
- return typeof window !== "undefined" &&
84
- ((_a = window.matchMedia) === null || _a === void 0 ? void 0 : _a.call(window, "(hover: hover) and (pointer: fine)").matches);
85
- };
86
- const handleHoverStart = useCallback(() => {
87
- if (isOpen || !pointerHovers())
88
- return;
89
- cancelHoverTimer();
90
- hoverTimer.current = setTimeout(() => {
91
- openedByHover.current = true;
92
- setOpen(true);
93
- }, HOVER_OPEN_DELAY);
94
- }, [isOpen, cancelHoverTimer, setOpen]);
95
- // A click-opened panel is sticky, and a keyboard user inside it is not "hovering away".
96
- const handleHoverEnd = useCallback(() => {
97
- cancelHoverTimer();
98
- if (!isOpen || !openedByHover.current)
99
- return;
100
- hoverTimer.current = setTimeout(() => {
101
- var _a;
102
- if ((_a = panelRef.current) === null || _a === void 0 ? void 0 : _a.contains(document.activeElement))
103
- return;
104
- openedByHover.current = false;
105
- setOpen(false);
106
- setQuery("");
107
- }, HOVER_CLOSE_GRACE);
108
- }, [isOpen, cancelHoverTimer, setOpen]);
109
66
  const closeAndRefocus = useCallback(() => {
110
67
  var _a;
111
68
  setOpen(false);
@@ -229,7 +186,7 @@ const AppSwitcher = ({ apps, currentAppId, onSelect, onOpenChange, isSearchable,
229
186
  const currentSub = liftToContrast(theme.colors.textMuted, currentBg, AA_NORMAL_TEXT);
230
187
  const width = panelWidth !== null && panelWidth !== void 0 ? panelWidth : "28rem";
231
188
  const hint = shortcut && showShortcutHint ? formatShortcut(shortcut) : null;
232
- return (_jsxs(Popover, { isOpen: isOpen, onOpen: () => setOpen(true), onClose: () => { setOpen(false); setQuery(""); }, placement: "bottom-end", initialFocusRef: showSearch ? searchRef : undefined, isLazy: true, children: [_jsx(PopoverTrigger, { children: _jsx(Flex, { ref: triggerRef, as: "button", type: "button", "aria-label": ariaLabel, align: "center", justify: "center", boxSize: s.trigger, borderRadius: "lg", border: "1px solid", borderColor: isOpen ? theme.colors.boxborder[500] : theme.colors.boxborder[200], bg: isOpen ? theme.colors.backgroundColor.tertiary : "transparent", color: isOpen ? theme.colors.text[900] : theme.colors.textMuted, cursor: "pointer", transition: "background 0.15s ease, border-color 0.15s ease", _hover: { bg: rowHoverBg, color: theme.colors.text[900] }, onMouseEnter: handleHoverStart, onMouseLeave: handleHoverEnd, onClick: () => { openedByHover.current = false; cancelHoverTimer(); }, onKeyDown: (e) => {
189
+ return (_jsxs(Popover, { isOpen: isOpen, onOpen: () => setOpen(true), onClose: () => { setOpen(false); setQuery(""); }, placement: "bottom-end", initialFocusRef: showSearch ? searchRef : undefined, isLazy: true, children: [_jsx(PopoverTrigger, { children: _jsx(Flex, { ref: triggerRef, as: "button", type: "button", "aria-label": ariaLabel, align: "center", justify: "center", boxSize: s.trigger, borderRadius: "lg", border: "1px solid", borderColor: isOpen ? theme.colors.boxborder[500] : theme.colors.boxborder[200], bg: isOpen ? theme.colors.backgroundColor.tertiary : "transparent", color: isOpen ? theme.colors.text[900] : theme.colors.textMuted, cursor: "pointer", transition: "background 0.15s ease, border-color 0.15s ease", _hover: { bg: rowHoverBg, color: theme.colors.text[900] }, onKeyDown: (e) => {
233
190
  if (!isOpen) {
234
191
  if (e.key === "ArrowDown") {
235
192
  e.preventDefault();
@@ -238,7 +195,7 @@ const AppSwitcher = ({ apps, currentAppId, onSelect, onOpenChange, isSearchable,
238
195
  return;
239
196
  }
240
197
  handlePanelKeyDown(e);
241
- }, children: _jsx(Grip, { size: s.icon + 1 }) }) }), _jsx(PopoverContent, { ref: panelRef, onMouseEnter: cancelHoverTimer, onMouseLeave: handleHoverEnd, w: width, maxW: "calc(100vw - 1.5rem)", textAlign: "start", onKeyDown: handlePanelKeyDown, bg: theme.colors.backgroundColor.light, borderRadius: "xl", overflow: "hidden", boxShadow: `0 10px 30px ${theme.colors.boxShadow.default}`, _focus: { outline: "none", boxShadow: `0 10px 30px ${theme.colors.boxShadow.default}` }, children: _jsxs(PopoverBody, { p: 0, children: [_jsxs(Flex, { align: "center", gap: 2, px: 3, pt: 3, pb: showSearch ? 2 : 1, children: [_jsx(Text, { fontSize: "0.6875rem", fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: theme.colors.textMuted, children: HEADING }), _jsx(Box, { flex: "1" }), hint && (_jsx(Text, { as: "kbd", fontSize: "0.6875rem", fontFamily: "inherit", color: theme.colors.textMuted, border: "1px solid", borderRadius: "base", px: 1.5, py: 0.5, children: hint }))] }), showSearch && (_jsx(Box, { px: 3, pb: 2, children: _jsxs(InputGroup, { size: "sm", children: [_jsx(InputLeftElement, { pointerEvents: "none", children: _jsx(Search, { size: 15, color: theme.colors.textMuted }) }), _jsx(Input, { ref: searchRef, value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search apps", borderRadius: "lg", "aria-label": "Search apps", onKeyDown: (e) => {
198
+ }, children: _jsx(Grip, { size: s.icon + 1 }) }) }), _jsx(PopoverContent, { w: width, maxW: "calc(100vw - 1.5rem)", textAlign: "start", onKeyDown: handlePanelKeyDown, bg: theme.colors.backgroundColor.light, borderRadius: "xl", overflow: "hidden", boxShadow: `0 10px 30px ${theme.colors.boxShadow.default}`, _focus: { outline: "none", boxShadow: `0 10px 30px ${theme.colors.boxShadow.default}` }, children: _jsxs(PopoverBody, { p: 0, children: [_jsxs(Flex, { align: "center", gap: 2, px: 3, pt: 3, pb: showSearch ? 2 : 1, children: [_jsx(Text, { fontSize: "0.6875rem", fontWeight: 600, letterSpacing: "0.08em", textTransform: "uppercase", color: theme.colors.textMuted, children: HEADING }), _jsx(Box, { flex: "1" }), hint && (_jsx(Text, { as: "kbd", fontSize: "0.6875rem", fontFamily: "inherit", color: theme.colors.textMuted, border: "1px solid", borderRadius: "base", px: 1.5, py: 0.5, children: hint }))] }), showSearch && (_jsx(Box, { px: 3, pb: 2, children: _jsxs(InputGroup, { size: "sm", children: [_jsx(InputLeftElement, { pointerEvents: "none", children: _jsx(Search, { size: 15, color: theme.colors.textMuted }) }), _jsx(Input, { ref: searchRef, value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search apps", borderRadius: "lg", "aria-label": "Search apps", onKeyDown: (e) => {
242
199
  if (e.key === "ArrowDown" && visible.length > 0) {
243
200
  e.preventDefault();
244
201
  focusItem(0);
@@ -1,19 +1,39 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { useRef } from "react";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef, useState } from "react";
3
3
  import { Box, Flex } from "@chakra-ui/react";
4
4
  import { useCustomTheme } from "../../Theme/useCustomTheme.js";
5
+ import { resolveFilledSurface } from "../../Theme/tokens/builders/filledSurface.js";
5
6
  const SIZES = {
6
7
  sm: { h: "1.625rem", px: "0.625rem", fontSize: "0.75rem" },
7
8
  md: { h: "2rem", px: "0.875rem", fontSize: "0.8125rem" },
8
9
  };
9
- export default function SegmentedControl({ options, value, onChange, size = "md", isFullWidth = false, "aria-label": ariaLabel, }) {
10
+ export default function SegmentedControl({ options, value, onChange, size = "md", isFullWidth = false, variant = "default", "aria-label": ariaLabel, }) {
10
11
  const { colors } = useCustomTheme();
11
12
  const dims = SIZES[size];
12
13
  const refs = useRef([]);
14
+ const trackRef = useRef(null);
15
+ const isBrandSlide = variant === "brandSlide";
16
+ const isUnderlineSlide = variant === "underlineSlide";
17
+ const hasIndicator = isBrandSlide || isUnderlineSlide;
18
+ const brandFill = resolveFilledSurface(colors.primary, colors);
19
+ const [pill, setPill] = useState(null);
13
20
  const selectedIndex = options.findIndex((o) => o.value === value);
14
21
  const enabled = options.reduce((acc, o, i) => (o.isDisabled ? acc : [...acc, i]), []);
15
22
  // WAI-APG: with nothing selected, the first enabled segment owns the tab stop.
16
23
  const tabIndex = selectedIndex >= 0 ? selectedIndex : enabled[0];
24
+ useLayoutEffect(() => {
25
+ if (!hasIndicator)
26
+ return;
27
+ const track = trackRef.current;
28
+ const node = refs.current[selectedIndex];
29
+ if (!track || !node) {
30
+ setPill(null);
31
+ return;
32
+ }
33
+ const trackRect = track.getBoundingClientRect();
34
+ const nodeRect = node.getBoundingClientRect();
35
+ setPill({ left: nodeRect.left - trackRect.left, width: nodeRect.width });
36
+ }, [hasIndicator, selectedIndex, options, isFullWidth, size]);
17
37
  const selectAt = (index) => {
18
38
  var _a;
19
39
  (_a = refs.current[index]) === null || _a === void 0 ? void 0 : _a.focus();
@@ -43,13 +63,23 @@ export default function SegmentedControl({ options, value, onChange, size = "md"
43
63
  selectAt(enabled[enabled.length - 1]);
44
64
  }
45
65
  };
46
- return (_jsx(Flex, { role: "radiogroup", "aria-label": ariaLabel, onKeyDown: handleKeyDown, display: "inline-flex", align: "center", gap: "0.125rem", p: "0.125rem", borderRadius: "md", bg: colors.background[600], w: isFullWidth ? "100%" : "fit-content", children: options.map((option, index) => {
47
- const isSelected = option.value === value;
48
- return (_jsxs(Box, { as: "button", type: "button", ref: (node) => {
49
- refs.current[index] = node;
50
- }, role: "radio", "aria-checked": isSelected, disabled: option.isDisabled, tabIndex: index === tabIndex ? 0 : -1, onClick: () => onChange(option.value), display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "0.375rem", flex: isFullWidth ? "1" : undefined, h: dims.h, px: dims.px, borderRadius: "base", borderWidth: "1px", borderStyle: "solid", borderColor: isSelected ? colors.background[400] : "transparent", bg: isSelected ? colors.background[50] : "transparent", boxShadow: isSelected ? "sm" : "none", color: isSelected ? colors.text[900] : colors.text[600], fontSize: dims.fontSize, fontWeight: isSelected ? 600 : 500, whiteSpace: "nowrap", opacity: option.isDisabled ? 0.5 : 1, cursor: option.isDisabled ? "not-allowed" : "pointer", transition: "background-color 0.2s ease, color 0.2s ease", _hover: option.isDisabled || isSelected ? undefined : { color: colors.text[900] }, _focusVisible: {
51
- outline: "none",
52
- boxShadow: `0 0 0 2px ${colors.primary[300]}`,
53
- }, children: [option.icon, option.label] }, option.value));
54
- }) }));
66
+ return (_jsxs(Flex, { ref: trackRef, role: "radiogroup", "aria-label": ariaLabel, onKeyDown: handleKeyDown, position: "relative", display: "inline-flex", align: "center", gap: isUnderlineSlide ? "0" : "0.125rem", p: isUnderlineSlide ? "0" : "0.125rem", borderRadius: isUnderlineSlide ? "none" : "md", borderBottom: isUnderlineSlide ? `0.0625rem solid ${colors.boxborder[200]}` : undefined, bg: isBrandSlide ? colors.primary.opacity[8] : isUnderlineSlide ? "transparent" : colors.background[600], w: isFullWidth ? "100%" : "fit-content", children: [isBrandSlide && pill && (_jsx(Box, { "aria-hidden": "true", position: "absolute", top: "0.125rem", left: 0, h: dims.h, w: `${pill.width}px`, borderRadius: "base", bg: brandFill.fill, boxShadow: `0 0.125rem 0.5rem ${colors.boxShadow.primary}`, transform: `translateX(${pill.left}px)`, transition: "transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1), width 0.24s cubic-bezier(0.2, 0.8, 0.2, 1)" })), isUnderlineSlide && pill && (_jsx(Box, { "aria-hidden": "true", position: "absolute", bottom: "-0.0625rem", left: 0, h: "0.125rem", w: `${pill.width}px`, borderRadius: "full", bg: colors.primary[500], transform: `translateX(${pill.left}px)`, transition: "transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1), width 0.24s cubic-bezier(0.2, 0.8, 0.2, 1)" })), options.map((option, index) => {
67
+ const isSelected = option.value === value;
68
+ return (_jsxs(Box, { as: "button", type: "button", ref: (node) => {
69
+ refs.current[index] = node;
70
+ }, role: "radio", "aria-checked": isSelected, disabled: option.isDisabled, tabIndex: index === tabIndex ? 0 : -1, onClick: () => onChange(option.value), position: "relative", zIndex: 1, display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "0.375rem", flex: isFullWidth ? "1" : undefined, h: dims.h, px: dims.px, borderRadius: isUnderlineSlide ? "none" : "base", borderWidth: "1px", borderStyle: "solid", borderColor: hasIndicator
71
+ ? "transparent"
72
+ : isSelected
73
+ ? colors.primary.opacity[32]
74
+ : "transparent", bg: hasIndicator ? "transparent" : isSelected ? colors.backgroundColor.light : "transparent", boxShadow: hasIndicator ? "none" : isSelected ? "sm" : "none", color: isBrandSlide
75
+ ? isSelected
76
+ ? brandFill.color
77
+ : colors.text[600]
78
+ : isSelected
79
+ ? colors.accentText
80
+ : colors.text[600], fontSize: dims.fontSize, fontWeight: isSelected ? 600 : 500, whiteSpace: "nowrap", opacity: option.isDisabled ? 0.5 : 1, cursor: option.isDisabled ? "not-allowed" : "pointer", transition: "color 0.2s ease", _hover: option.isDisabled || isSelected ? undefined : { color: colors.text[900] }, _focusVisible: {
81
+ outline: "none",
82
+ boxShadow: `0 0 0 2px ${colors.primary[300]}`,
83
+ }, children: [option.icon, option.label] }, option.value));
84
+ })] }));
55
85
  }
@@ -1,43 +1,93 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef, useState } from "react";
2
3
  import { Box, IconButton } from "@chakra-ui/react";
3
4
  import { Columns3, Table2 } from "lucide-react";
4
5
  import { useCustomTheme } from "../../Theme/useCustomTheme.js";
5
6
  import { AA_NON_TEXT, liftToContrast } from "../../Theme/tokens/builders/outlineRung.js";
7
+ import { resolveFilledSurface } from "../../Theme/tokens/builders/filledSurface.js";
6
8
  import ToolTip from "../ToolTip/ToolTip.js";
7
- const TableToggle = ({ value, onChange }) => {
9
+ const MODES = ["grid", "list"];
10
+ const TableToggle = ({ value, onChange, variant = "default" }) => {
8
11
  const theme = useCustomTheme();
9
12
  const { colors } = theme;
13
+ const isBrandSlide = variant === "brandSlide";
14
+ const isUnderlineSlide = variant === "underlineSlide";
15
+ const hasIndicator = isBrandSlide || isUnderlineSlide;
16
+ const brandFill = resolveFilledSurface(colors.primary, colors);
10
17
  const restInk = liftToContrast(colors.background[800], colors.background[600], AA_NON_TEXT);
11
18
  const hoverInk = liftToContrast(colors.background[900], colors.background[400], AA_NON_TEXT);
19
+ const trackRef = useRef(null);
20
+ const refs = useRef([]);
21
+ const [pill, setPill] = useState(null);
22
+ const selectedIndex = MODES.indexOf(value);
23
+ useLayoutEffect(() => {
24
+ if (!hasIndicator)
25
+ return;
26
+ const track = trackRef.current;
27
+ const node = refs.current[selectedIndex];
28
+ if (!track || !node) {
29
+ setPill(null);
30
+ return;
31
+ }
32
+ const trackRect = track.getBoundingClientRect();
33
+ const nodeRect = node.getBoundingClientRect();
34
+ setPill({ left: nodeRect.left - trackRect.left, width: nodeRect.width });
35
+ }, [hasIndicator, selectedIndex]);
12
36
  const styles = {
13
37
  container: {
14
38
  display: "flex",
15
39
  alignItems: "center",
16
- bg: colors.background[600],
17
- p: "0.125rem",
18
- borderRadius: "md",
40
+ bg: isBrandSlide ? colors.primary.opacity[8] : isUnderlineSlide ? "transparent" : colors.background[600],
41
+ p: isUnderlineSlide ? "0" : "0.125rem",
42
+ borderRadius: isUnderlineSlide ? "none" : "md",
43
+ borderBottom: isUnderlineSlide ? `0.0625rem solid ${colors.boxborder[200]}` : undefined,
19
44
  height: "2rem",
20
- gap: "0.125rem",
45
+ gap: isUnderlineSlide ? "0" : "0.125rem",
21
46
  width: "6.3rem",
47
+ position: "relative",
22
48
  },
23
49
  button: (mode) => ({
24
- bg: value === mode ? colors.background[50] : "transparent",
25
- boxShadow: value === mode ? "md" : "none",
26
- color: value === mode ? colors.text[900] : restInk,
27
- borderRadius: "base",
50
+ position: "relative",
51
+ zIndex: 1,
52
+ bg: hasIndicator ? "transparent" : value === mode ? colors.backgroundColor.light : "transparent",
53
+ boxShadow: hasIndicator ? "none" : value === mode ? "md" : "none",
54
+ color: isBrandSlide
55
+ ? value === mode
56
+ ? brandFill.color
57
+ : restInk
58
+ : value === mode
59
+ ? colors.accentText
60
+ : restInk,
61
+ borderRadius: isUnderlineSlide ? "none" : "base",
28
62
  height: "1.75rem",
29
- border: "0.031rem solid",
30
- borderColor: value === mode ? colors.background[400] : "transparent",
63
+ border: hasIndicator ? "none" : "0.031rem solid",
64
+ borderColor: hasIndicator
65
+ ? "transparent"
66
+ : value === mode
67
+ ? colors.primary.opacity[32]
68
+ : "transparent",
31
69
  padding: "0.313rem 0.813rem",
32
70
  _hover: {
33
- bg: value === mode
34
- ? colors.background[50]
35
- : colors.background[400],
36
- color: value === mode ? colors.text[900] : hoverInk,
71
+ bg: hasIndicator
72
+ ? "transparent"
73
+ : value === mode
74
+ ? colors.backgroundColor.light
75
+ : colors.background[400],
76
+ color: isBrandSlide
77
+ ? value === mode
78
+ ? brandFill.color
79
+ : hoverInk
80
+ : value === mode
81
+ ? colors.accentText
82
+ : hoverInk,
37
83
  },
38
- transition: "all 0.3s ease-in-out",
84
+ transition: "color 0.2s ease-in-out",
39
85
  }),
40
86
  };
41
- return (_jsxs(Box, { ...styles.container, children: [_jsx(ToolTip, { label: "Table View", placement: "top", fontSize: 12, hasArrow: true, children: _jsx(IconButton, { "aria-label": "Grid View", icon: _jsx(Table2, { size: "1.25rem" }), variant: "ghost", onClick: () => onChange("grid"), ...styles.button("grid") }) }), _jsx(ToolTip, { label: "Kanban View", placement: "top", fontSize: 12, hasArrow: true, children: _jsx(IconButton, { "aria-label": "List View", icon: _jsx(Columns3, { size: "1.25rem" }), variant: "ghost", onClick: () => onChange("list"), ...styles.button("list") }) })] }));
87
+ return (_jsxs(Box, { ref: trackRef, ...styles.container, children: [isBrandSlide && pill && (_jsx(Box, { "aria-hidden": "true", position: "absolute", top: "0.125rem", left: 0, h: "1.75rem", w: `${pill.width}px`, borderRadius: "base", bg: brandFill.fill, boxShadow: `0 0.125rem 0.5rem ${colors.boxShadow.primary}`, transform: `translateX(${pill.left}px)`, transition: "transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1), width 0.24s cubic-bezier(0.2, 0.8, 0.2, 1)" })), isUnderlineSlide && pill && (_jsx(Box, { "aria-hidden": "true", position: "absolute", bottom: "-0.0625rem", left: 0, h: "0.125rem", w: `${pill.width}px`, borderRadius: "full", bg: colors.primary[500], transform: `translateX(${pill.left}px)`, transition: "transform 0.24s cubic-bezier(0.2, 0.8, 0.2, 1), width 0.24s cubic-bezier(0.2, 0.8, 0.2, 1)" })), _jsx(ToolTip, { label: "Table View", placement: "top", fontSize: 12, hasArrow: true, children: _jsx(IconButton, { ref: (node) => {
88
+ refs.current[0] = node;
89
+ }, "aria-label": "Grid View", icon: _jsx(Table2, { size: "1.25rem" }), variant: "ghost", onClick: () => onChange("grid"), ...styles.button("grid") }) }), _jsx(ToolTip, { label: "Kanban View", placement: "top", fontSize: 12, hasArrow: true, children: _jsx(IconButton, { ref: (node) => {
90
+ refs.current[1] = node;
91
+ }, "aria-label": "List View", icon: _jsx(Columns3, { size: "1.25rem" }), variant: "ghost", onClick: () => onChange("list"), ...styles.button("list") }) })] }));
42
92
  };
43
93
  export default TableToggle;
@@ -11,5 +11,5 @@ export default function ToggleSwitch({ isChecked, onChange, isDisabled = false,
11
11
  return (_jsx(Box, { as: "button", type: "button", id: id, role: "switch", "aria-checked": isChecked, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, disabled: isDisabled, onClick: () => onChange(!isChecked), position: "relative", display: "inline-flex", alignItems: "center", flexShrink: 0, w: dims.w, h: dims.h, p: 0, borderRadius: "full", borderWidth: "1px", borderStyle: "solid", borderColor: isChecked ? colors.primary[500] : colors.boxborder[500], bg: isChecked ? colors.primary[500] : colors.background[600], opacity: isDisabled ? 0.5 : 1, cursor: isDisabled ? "not-allowed" : "pointer", transition: "background-color 0.2s ease, border-color 0.2s ease", _focusVisible: {
12
12
  outline: "none",
13
13
  boxShadow: `0 0 0 2px ${colors.primary[300]}`,
14
- }, children: _jsx(Box, { "aria-hidden": "true", position: "absolute", left: "0.125rem", w: dims.knob, h: dims.knob, borderRadius: "full", bg: colors.background[50], boxShadow: "sm", transition: "transform 0.2s ease", transform: isChecked ? `translateX(${dims.travel})` : "translateX(0)" }) }));
14
+ }, children: _jsx(Box, { "aria-hidden": "true", position: "absolute", left: "0.125rem", w: dims.knob, h: dims.knob, borderRadius: "full", bg: colors.white, boxShadow: "sm", transition: "transform 0.2s ease", transform: isChecked ? `translateX(${dims.travel})` : "translateX(0)" }) }));
15
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixelize-design-library",
3
- "version": "2.4.2-beta.35",
3
+ "version": "2.4.2-beta.36",
4
4
  "description": "React component library for Pixelize apps: themeable Chakra-based components, design tokens and light/dark brand theming.",
5
5
  "keywords": [
6
6
  "react",