@dreamtree-org/twreact-ui 1.3.9 → 1.3.11

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 (128) hide show
  1. package/README.md +32 -0
  2. package/ai-skills/dreamtree-ui.md +12 -7
  3. package/dist/ai-schema.json +156 -29
  4. package/dist/charts/index.js +195 -66
  5. package/dist/components/blocks/AppShell.d.ts.map +1 -1
  6. package/dist/components/charts/CartesianChart.d.ts.map +1 -1
  7. package/dist/components/charts/ChartFrame.d.ts.map +1 -1
  8. package/dist/components/charts/useChartLayout.d.ts +25 -0
  9. package/dist/components/charts/useChartLayout.d.ts.map +1 -0
  10. package/dist/components/core/Accordion.d.ts.map +1 -1
  11. package/dist/components/core/Button.d.ts.map +1 -1
  12. package/dist/components/core/Checkbox.d.ts.map +1 -1
  13. package/dist/components/core/ColorPicker.d.ts.map +1 -1
  14. package/dist/components/core/DatePicker.d.ts.map +1 -1
  15. package/dist/components/core/Form.d.ts.map +1 -1
  16. package/dist/components/core/Input.d.ts.map +1 -1
  17. package/dist/components/core/Loader.d.ts.map +1 -1
  18. package/dist/components/core/LocationPicker.d.ts.map +1 -1
  19. package/dist/components/core/PriceRangePicker.d.ts +0 -29
  20. package/dist/components/core/PriceRangePicker.d.ts.map +1 -1
  21. package/dist/components/core/ProgressBar.d.ts.map +1 -1
  22. package/dist/components/core/Radio.d.ts.map +1 -1
  23. package/dist/components/core/Rate.d.ts.map +1 -1
  24. package/dist/components/core/RoundedTag.d.ts.map +1 -1
  25. package/dist/components/core/Skeleton.d.ts.map +1 -1
  26. package/dist/components/core/Slider.d.ts.map +1 -1
  27. package/dist/components/core/SpeechToText.d.ts.map +1 -1
  28. package/dist/components/core/Switch.d.ts.map +1 -1
  29. package/dist/components/core/Table/ColumnMenu.d.ts.map +1 -1
  30. package/dist/components/core/Table/Pagination.d.ts.map +1 -1
  31. package/dist/components/core/Table/TableRow.d.ts.map +1 -1
  32. package/dist/components/core/Table/helpers.d.ts +2 -0
  33. package/dist/components/core/Table/helpers.d.ts.map +1 -1
  34. package/dist/components/core/Tabs.d.ts +2 -5
  35. package/dist/components/core/Tabs.d.ts.map +1 -1
  36. package/dist/components/core/TextToSpeech.d.ts +2 -7
  37. package/dist/components/core/TextToSpeech.d.ts.map +1 -1
  38. package/dist/components/core/Textarea.d.ts.map +1 -1
  39. package/dist/components/core/Tooltip.d.ts.map +1 -1
  40. package/dist/components/feedback/Alert.d.ts.map +1 -1
  41. package/dist/components/feedback/Dialog.d.ts.map +1 -1
  42. package/dist/components/feedback/Toast.d.ts.map +1 -1
  43. package/dist/components/navigation/Breadcrumbs.d.ts.map +1 -1
  44. package/dist/components/navigation/FootNav.d.ts.map +1 -1
  45. package/dist/components/navigation/Sidebar.d.ts.map +1 -1
  46. package/dist/components/utility/Avatar.d.ts +8 -0
  47. package/dist/components/utility/Avatar.d.ts.map +1 -1
  48. package/dist/components/utility/AvatarGroup.d.ts +4 -0
  49. package/dist/components/utility/AvatarGroup.d.ts.map +1 -0
  50. package/dist/components/utility/Badge.d.ts.map +1 -1
  51. package/dist/components/utility/Card.d.ts.map +1 -1
  52. package/dist/components/utility/Carousel.d.ts.map +1 -1
  53. package/dist/components/utility/FileUpload.d.ts.map +1 -1
  54. package/dist/components/utility/Pagination.d.ts.map +1 -1
  55. package/dist/components/utility/Sortable.d.ts.map +1 -1
  56. package/dist/components/utility/Stepper.d.ts.map +1 -1
  57. package/dist/esm/components/blocks/AppShell.js +24 -18
  58. package/dist/esm/components/blocks/EmptyState.js +1 -1
  59. package/dist/esm/components/blocks/ErrorState.js +1 -1
  60. package/dist/esm/components/blocks/LoadingState.js +4 -4
  61. package/dist/esm/components/blocks/StatCard.js +6 -6
  62. package/dist/esm/components/blocks/StatusPill.js +5 -8
  63. package/dist/esm/components/charts/CartesianChart.js +85 -62
  64. package/dist/esm/components/charts/ChartFrame.js +12 -5
  65. package/dist/esm/components/charts/useChartLayout.js +105 -0
  66. package/dist/esm/components/core/Accordion.js +40 -9
  67. package/dist/esm/components/core/Button.js +49 -25
  68. package/dist/esm/components/core/Checkbox.js +71 -45
  69. package/dist/esm/components/core/ColorPicker.js +49 -48
  70. package/dist/esm/components/core/DatePicker.js +97 -58
  71. package/dist/esm/components/core/Form.js +35 -8
  72. package/dist/esm/components/core/Input.js +89 -20
  73. package/dist/esm/components/core/Loader.js +30 -44
  74. package/dist/esm/components/core/LocationPicker.js +82 -16
  75. package/dist/esm/components/core/PriceRangePicker.js +205 -218
  76. package/dist/esm/components/core/ProgressBar.js +8 -12
  77. package/dist/esm/components/core/Radio.js +43 -15
  78. package/dist/esm/components/core/Rate.js +109 -50
  79. package/dist/esm/components/core/RoundedTag.js +27 -10
  80. package/dist/esm/components/core/Skeleton.js +13 -14
  81. package/dist/esm/components/core/Slider.js +24 -6
  82. package/dist/esm/components/core/SpeechToText.js +68 -16
  83. package/dist/esm/components/core/Switch.js +88 -16
  84. package/dist/esm/components/core/Table/ColumnMenu.js +2 -4
  85. package/dist/esm/components/core/Table/Pagination.js +15 -18
  86. package/dist/esm/components/core/Table/TableRow.js +2 -2
  87. package/dist/esm/components/core/Table/helpers.js +33 -1
  88. package/dist/esm/components/core/Tabs.js +185 -37
  89. package/dist/esm/components/core/TextToSpeech.js +121 -28
  90. package/dist/esm/components/core/Textarea.js +10 -2
  91. package/dist/esm/components/core/Tooltip.js +100 -28
  92. package/dist/esm/components/feedback/Alert.js +21 -11
  93. package/dist/esm/components/feedback/Dialog.js +44 -100
  94. package/dist/esm/components/feedback/Toast.js +24 -5
  95. package/dist/esm/components/navigation/Breadcrumbs.js +97 -35
  96. package/dist/esm/components/navigation/FootNav.js +102 -13
  97. package/dist/esm/components/navigation/Sidebar.js +13 -61
  98. package/dist/esm/components/utility/Avatar.js +37 -22
  99. package/dist/esm/components/utility/AvatarGroup.js +56 -0
  100. package/dist/esm/components/utility/Badge.js +43 -23
  101. package/dist/esm/components/utility/Card.js +32 -8
  102. package/dist/esm/components/utility/Carousel.js +165 -38
  103. package/dist/esm/components/utility/FileUpload.js +106 -31
  104. package/dist/esm/components/utility/Pagination.js +33 -15
  105. package/dist/esm/components/utility/Sortable.js +6 -2
  106. package/dist/esm/components/utility/Stepper.js +110 -47
  107. package/dist/esm/hooks/useFocusTrap.js +10 -2
  108. package/dist/esm/hooks/useSliderValues.js +8 -2
  109. package/dist/esm/hooks/useSortable.js +107 -5
  110. package/dist/esm/index.js +1 -0
  111. package/dist/esm/utils/touchTarget.js +4 -1
  112. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  113. package/dist/hooks/useSliderValues.d.ts +6 -2
  114. package/dist/hooks/useSliderValues.d.ts.map +1 -1
  115. package/dist/hooks/useSortable.d.ts +11 -1
  116. package/dist/hooks/useSortable.d.ts.map +1 -1
  117. package/dist/index.css +1 -1
  118. package/dist/index.d.ts +3 -2
  119. package/dist/index.esm.css +1 -1
  120. package/dist/index.esm.js +3734 -2174
  121. package/dist/index.js +3732 -2171
  122. package/dist/types/charts.d.ts +3 -1
  123. package/dist/types/charts.d.ts.map +1 -1
  124. package/dist/types/index.d.ts +64 -9
  125. package/dist/types/index.d.ts.map +1 -1
  126. package/doc/README.md +1 -0
  127. package/mcp/catalog.snapshot.json +350 -121
  128. package/package.json +1 -1
@@ -1,32 +1,60 @@
1
1
  'use client';
2
- import { objectWithoutProperties as _objectWithoutProperties, toConsumableArray as _toConsumableArray, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
- import { jsx, jsxs } from 'react/jsx-runtime';
4
- import React from 'react';
2
+ import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, toConsumableArray as _toConsumableArray, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+ import React, { useState, useRef, useEffect } from 'react';
5
5
  import { cn } from '../../utils/cn.js';
6
+ import { TOUCH_TARGET } from '../../utils/touchTarget.js';
6
7
  import { ChevronRight, Home } from 'lucide-react';
7
8
 
8
- var _excluded = ["items", "separator", "showHome", "homeIcon", "onHomeClick", "className"];
9
+ var _excluded = ["items", "separator", "showHome", "homeIcon", "onHomeClick", "maxItems", "className"];
9
10
  // Items without an `id` still need a stable, unique React key.
10
11
  var itemKey = function itemKey(item, index) {
11
12
  var _ref, _item$href;
12
13
  return item.id != null ? "id:".concat(item.id) : "idx:".concat(index, ":").concat((_ref = (_item$href = item.href) !== null && _item$href !== void 0 ? _item$href : item.label) !== null && _ref !== void 0 ? _ref : '');
13
14
  };
14
- var Breadcrumbs = /*#__PURE__*/React.forwardRef(function (_ref2, ref) {
15
- var _ref2$items = _ref2.items,
16
- items = _ref2$items === void 0 ? [] : _ref2$items,
17
- _ref2$separator = _ref2.separator,
18
- separator = _ref2$separator === void 0 ? jsx(ChevronRight, {
15
+ // Long labels truncate with an ellipsis; the full text stays available as a tooltip.
16
+ var LABEL_CLASSES = 'min-w-0 max-w-[10rem] truncate sm:max-w-[16rem]';
17
+ var FOCUS_RING = 'rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-1 dark:focus-visible:ring-offset-gray-900';
18
+ var labelTitle = function labelTitle(label) {
19
+ return typeof label === 'string' || typeof label === 'number' ? String(label) : undefined;
20
+ };
21
+ function ItemContent(_ref2) {
22
+ var item = _ref2.item;
23
+ return jsxs(Fragment, {
24
+ children: [item.icon && jsx("span", {
25
+ className: "mr-1 shrink-0",
26
+ children: item.icon
27
+ }), jsx("span", {
28
+ className: LABEL_CLASSES,
29
+ title: labelTitle(item.label),
30
+ children: item.label
31
+ })]
32
+ });
33
+ }
34
+ var Breadcrumbs = /*#__PURE__*/React.forwardRef(function (_ref3, ref) {
35
+ var _ref3$items = _ref3.items,
36
+ items = _ref3$items === void 0 ? [] : _ref3$items,
37
+ _ref3$separator = _ref3.separator,
38
+ separator = _ref3$separator === void 0 ? jsx(ChevronRight, {
19
39
  className: "h-4 w-4"
20
- }) : _ref2$separator,
21
- _ref2$showHome = _ref2.showHome,
22
- showHome = _ref2$showHome === void 0 ? true : _ref2$showHome,
23
- _ref2$homeIcon = _ref2.homeIcon,
24
- homeIcon = _ref2$homeIcon === void 0 ? jsx(Home, {
40
+ }) : _ref3$separator,
41
+ _ref3$showHome = _ref3.showHome,
42
+ showHome = _ref3$showHome === void 0 ? true : _ref3$showHome,
43
+ _ref3$homeIcon = _ref3.homeIcon,
44
+ homeIcon = _ref3$homeIcon === void 0 ? jsx(Home, {
25
45
  className: "h-4 w-4"
26
- }) : _ref2$homeIcon,
27
- onHomeClick = _ref2.onHomeClick,
28
- className = _ref2.className,
29
- props = _objectWithoutProperties(_ref2, _excluded);
46
+ }) : _ref3$homeIcon,
47
+ onHomeClick = _ref3.onHomeClick,
48
+ maxItems = _ref3.maxItems,
49
+ className = _ref3.className,
50
+ props = _objectWithoutProperties(_ref3, _excluded);
51
+ var _useState = useState(false),
52
+ _useState2 = _slicedToArray(_useState, 2),
53
+ expanded = _useState2[0],
54
+ setExpanded = _useState2[1];
55
+ // After expanding, focus moves to the first revealed crumb instead of being lost.
56
+ var focusRevealed = useRef(false);
57
+ var firstHiddenRef = useRef(null);
30
58
  var allItems = showHome ? [{
31
59
  id: 'home',
32
60
  label: 'Home',
@@ -34,37 +62,71 @@ var Breadcrumbs = /*#__PURE__*/React.forwardRef(function (_ref2, ref) {
34
62
  icon: homeIcon,
35
63
  onClick: onHomeClick
36
64
  }].concat(_toConsumableArray(items)) : items;
65
+ // Collapse keeps the first crumb and the last (maxItems - 1); needs at least 2 to make sense.
66
+ var limit = Number.isFinite(maxItems) ? Math.max(2, Math.floor(maxItems)) : Infinity;
67
+ var collapsed = !expanded && allItems.length > limit;
68
+ var hiddenFrom = 1;
69
+ var hiddenTo = allItems.length - (limit - 1); // exclusive
70
+ var hiddenCount = collapsed ? hiddenTo - hiddenFrom : 0;
71
+ useEffect(function () {
72
+ if (expanded && focusRevealed.current) {
73
+ var _firstHiddenRef$curre;
74
+ focusRevealed.current = false;
75
+ (_firstHiddenRef$curre = firstHiddenRef.current) === null || _firstHiddenRef$curre === void 0 || _firstHiddenRef$curre.focus();
76
+ }
77
+ }, [expanded]);
78
+ var renderSeparator = function renderSeparator() {
79
+ return jsx("span", {
80
+ className: "shrink-0 text-primary-400 dark:text-primary-500",
81
+ "aria-hidden": "true",
82
+ children: separator
83
+ });
84
+ };
37
85
  return jsx("nav", _objectSpread2(_objectSpread2({
38
86
  ref: ref,
39
- className: cn('flex items-center space-x-1 text-sm', className),
87
+ className: cn('flex min-w-0 items-center space-x-1 text-sm', className),
40
88
  "aria-label": "Breadcrumb"
41
89
  }, props), {}, {
42
90
  children: allItems.map(function (item, index) {
91
+ if (collapsed && index >= hiddenFrom && index < hiddenTo) {
92
+ if (index !== hiddenFrom) return null;
93
+ return jsxs(React.Fragment, {
94
+ children: [renderSeparator(), jsx("button", {
95
+ type: "button",
96
+ className: cn('inline-flex items-center justify-center px-1 text-primary-500 transition-colors duration-fast motion-reduce:transition-none', 'hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300', FOCUS_RING, TOUCH_TARGET),
97
+ "aria-label": "Show ".concat(hiddenCount, " more breadcrumb").concat(hiddenCount === 1 ? '' : 's'),
98
+ onClick: function onClick() {
99
+ focusRevealed.current = true;
100
+ setExpanded(true);
101
+ },
102
+ children: jsx("span", {
103
+ "aria-hidden": "true",
104
+ children: "\u2026"
105
+ })
106
+ })]
107
+ }, "breadcrumbs-collapsed");
108
+ }
109
+ var isLast = index === allItems.length - 1;
43
110
  return jsxs(React.Fragment, {
44
- children: [index > 0 && jsx("span", {
45
- className: "text-primary-400",
46
- "aria-hidden": "true",
47
- children: separator
48
- }), index === allItems.length - 1 ? jsxs("span", {
49
- className: "font-medium text-primary-900",
111
+ children: [index > 0 && renderSeparator(), isLast ? jsx("span", {
112
+ className: "flex min-w-0 items-center font-medium text-primary-900 dark:text-primary-100",
50
113
  "aria-current": "page",
51
- children: [item.icon && jsx("span", {
52
- className: "mr-1",
53
- children: item.icon
54
- }), item.label]
55
- }) : jsxs("a", {
114
+ children: jsx(ItemContent, {
115
+ item: item
116
+ })
117
+ }) : jsx("a", {
118
+ ref: expanded && index === hiddenFrom ? firstHiddenRef : undefined,
56
119
  href: item.href,
57
- className: "flex items-center text-primary-500 transition-colors",
120
+ className: cn('flex min-w-0 items-center text-primary-500 underline-offset-4 transition-colors duration-fast motion-reduce:transition-none', 'hover:text-primary-700 hover:underline dark:text-primary-400 dark:hover:text-primary-300', FOCUS_RING),
58
121
  onClick: function onClick(e) {
59
122
  if (item.onClick) {
60
123
  e.preventDefault();
61
124
  item.onClick(item);
62
125
  }
63
126
  },
64
- children: [item.icon && jsx("span", {
65
- className: "mr-1",
66
- children: item.icon
67
- }), item.label]
127
+ children: jsx(ItemContent, {
128
+ item: item
129
+ })
68
130
  })]
69
131
  }, itemKey(item, index));
70
132
  })
@@ -1,10 +1,12 @@
1
1
  'use client';
2
- import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2, slicedToArray as _slicedToArray } from '../../_virtual/_rollupPluginBabelHelpers.js';
2
+ import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
3
  import { jsx, jsxs } from 'react/jsx-runtime';
4
4
  import React from 'react';
5
5
  import { cn } from '../../utils/cn.js';
6
+ import { PRESS_FEEDBACK } from '../../utils/touchTarget.js';
6
7
 
7
- var _excluded = ["items", "className"];
8
+ var _excluded = ["items", "linkAs", "hideOnScroll", "badgeMax", "className", "onFocus"],
9
+ _excluded2 = ["className"];
8
10
  var hasBadge = function hasBadge(badge) {
9
11
  return badge === 0 || Boolean(badge);
10
12
  };
@@ -17,34 +19,100 @@ var passthrough = function passthrough(item) {
17
19
  return /^(aria|data)-/.test(key);
18
20
  }));
19
21
  };
22
+ // Numeric counts above `max` read `${max}+` so a big number can't widen the tab.
23
+ var formatBadge = function formatBadge(badge, max) {
24
+ return typeof badge === 'number' && typeof max === 'number' && badge > max ? "".concat(max, "+") : badge;
25
+ };
20
26
  var Badge = function Badge(_ref3) {
21
- var badge = _ref3.badge;
27
+ var badge = _ref3.badge,
28
+ max = _ref3.max;
22
29
  return jsx("span", {
23
- className: "absolute -top-1.5 left-full -ml-2 flex h-4 min-w-4 items-center justify-center rounded-full bg-error-600 px-1 text-micro font-semibold text-white",
30
+ className: "absolute -top-1.5 left-full -ml-2 flex h-4 min-w-4 items-center justify-center whitespace-nowrap rounded-full bg-error-600 px-1 text-micro font-semibold tabular-nums text-white",
24
31
  "data-slot": "badge",
25
- children: badge
32
+ children: formatBadge(badge, max)
26
33
  });
27
34
  };
35
+ // Scroll distance (px) that counts as a direction change, and the offset
36
+ // before the bar may hide (so it never hides at the very top of the page).
37
+ var SCROLL_DELTA = 8;
38
+ var HIDE_AFTER = 64;
39
+ // Hide on scroll down, show on scroll up. Passive listener; SSR-safe (the
40
+ // effect only runs in the browser).
41
+ function useHideOnScroll(enabled) {
42
+ var _React$useState = React.useState(false),
43
+ _React$useState2 = _slicedToArray(_React$useState, 2),
44
+ hidden = _React$useState2[0],
45
+ setHidden = _React$useState2[1];
46
+ React.useEffect(function () {
47
+ if (!enabled || typeof window === 'undefined') {
48
+ setHidden(false);
49
+ return undefined;
50
+ }
51
+ var lastY = window.scrollY || 0;
52
+ var onScroll = function onScroll() {
53
+ var y = window.scrollY || 0;
54
+ var delta = y - lastY;
55
+ if (Math.abs(delta) < SCROLL_DELTA) return;
56
+ setHidden(delta > 0 && y > HIDE_AFTER);
57
+ lastY = y;
58
+ };
59
+ window.addEventListener('scroll', onScroll, {
60
+ passive: true
61
+ });
62
+ return function () {
63
+ return window.removeEventListener('scroll', onScroll);
64
+ };
65
+ }, [enabled]);
66
+ return [hidden, setHidden];
67
+ }
28
68
  var FootNav = /*#__PURE__*/React.forwardRef(function (_ref4, ref) {
29
69
  var _ref4$items = _ref4.items,
30
70
  items = _ref4$items === void 0 ? [] : _ref4$items,
71
+ linkAs = _ref4.linkAs,
72
+ _ref4$hideOnScroll = _ref4.hideOnScroll,
73
+ hideOnScroll = _ref4$hideOnScroll === void 0 ? false : _ref4$hideOnScroll,
74
+ _ref4$badgeMax = _ref4.badgeMax,
75
+ badgeMax = _ref4$badgeMax === void 0 ? 99 : _ref4$badgeMax,
31
76
  className = _ref4.className,
77
+ onFocus = _ref4.onFocus,
32
78
  props = _objectWithoutProperties(_ref4, _excluded);
79
+ var _useHideOnScroll = useHideOnScroll(hideOnScroll),
80
+ _useHideOnScroll2 = _slicedToArray(_useHideOnScroll, 2),
81
+ hidden = _useHideOnScroll2[0],
82
+ setHidden = _useHideOnScroll2[1];
83
+ // Keyboard focus moving into a hidden bar brings it back.
84
+ var handleFocus = function handleFocus(e) {
85
+ if (hidden) setHidden(false);
86
+ onFocus === null || onFocus === void 0 || onFocus(e);
87
+ };
33
88
  return jsx("nav", _objectSpread2(_objectSpread2({
34
89
  ref: ref,
35
- className: cn('fixed bottom-0 left-0 right-0 bg-surface border-t border-line px-2 py-2 md:hidden', className)
90
+ "aria-label": "Primary",
91
+ "data-hidden": hideOnScroll ? String(hidden) : undefined,
92
+ className: cn('fixed bottom-0 left-0 right-0 z-sticky border-t border-line px-2 pb-safe md:hidden',
93
+ // Translucent where backdrop blur is supported, solid surface otherwise.
94
+ 'bg-surface supports-[backdrop-filter]:bg-surface/80 supports-[backdrop-filter]:backdrop-blur', hideOnScroll && 'transition-transform duration-base ease-standard motion-reduce:transition-none', hideOnScroll && hidden && 'translate-y-full', className),
95
+ onFocus: handleFocus
36
96
  }, props), {}, {
37
97
  children: jsx("div", {
38
- className: "flex items-stretch justify-around gap-1",
98
+ className: "flex items-stretch justify-around gap-1 py-2",
39
99
  children: items.map(function (item, index) {
40
- var _item$id;
100
+ var _item$to, _item$id;
41
101
  var isAction = item.variant === 'action';
42
- return jsxs("button", _objectSpread2(_objectSpread2({
43
- type: "button",
102
+ var isLink = item.href !== undefined || item.to !== undefined;
103
+ var Item = isLink ? linkAs || 'a' : 'button';
104
+ var linkProps = isLink ? _objectSpread2({
105
+ href: item.href
106
+ }, linkAs ? {
107
+ to: (_item$to = item.to) !== null && _item$to !== void 0 ? _item$to : item.href
108
+ } : {}) : {
109
+ type: 'button'
110
+ };
111
+ return jsxs(Item, _objectSpread2(_objectSpread2(_objectSpread2({}, linkProps), {}, {
44
112
  "aria-current": item.active ? 'page' : undefined,
45
113
  "data-variant": isAction ? 'action' : undefined
46
114
  }, passthrough(item)), {}, {
47
- className: cn('relative flex min-w-0 flex-1 flex-col items-center gap-1 rounded-md px-1 py-2 text-xs transition-colors', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-1 motion-reduce:transition-none', isAction ? 'group justify-start py-0 font-medium text-muted' : item.active ? 'bg-primary-50 font-semibold text-primary-700 dark:bg-primary-900/40 dark:text-primary-300' : 'font-medium text-muted hover:bg-gray-100 hover:text-foreground dark:hover:bg-gray-800'),
115
+ className: cn('relative flex min-w-0 flex-1 flex-col items-center gap-1 rounded-md px-1 py-2 text-xs no-underline', PRESS_FEEDBACK, 'transition duration-fast ease-standard motion-reduce:transition-none', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-1 dark:focus-visible:ring-offset-gray-900', isAction ? 'group justify-start py-0 font-medium text-muted' : item.active ? 'bg-primary-50 font-semibold text-primary-700 dark:bg-primary-900/40 dark:text-primary-300' : 'font-medium text-muted hover:bg-gray-100 hover:text-foreground dark:hover:bg-gray-800'),
48
116
  onClick: function onClick() {
49
117
  var _item$onClick;
50
118
  return (_item$onClick = item.onClick) === null || _item$onClick === void 0 ? void 0 : _item$onClick.call(item, item);
@@ -56,13 +124,15 @@ var FootNav = /*#__PURE__*/React.forwardRef(function (_ref4, ref) {
56
124
  className: "relative -mt-7 inline-flex h-14 w-14 items-center justify-center rounded-full bg-primary-600 text-2xl text-white shadow-lg ring-4 ring-surface transition-colors group-hover:bg-primary-700 motion-reduce:transition-none",
57
125
  "data-slot": "icon",
58
126
  children: [item.icon, hasBadge(item.badge) && jsx(Badge, {
59
- badge: item.badge
127
+ badge: item.badge,
128
+ max: badgeMax
60
129
  })]
61
130
  }) : jsxs("span", {
62
131
  className: "relative inline-flex text-lg",
63
132
  "data-slot": "icon",
64
133
  children: [item.icon, hasBadge(item.badge) && jsx(Badge, {
65
- badge: item.badge
134
+ badge: item.badge,
135
+ max: badgeMax
66
136
  })]
67
137
  }), jsx("span", {
68
138
  className: "block max-w-full truncate",
@@ -74,5 +144,24 @@ var FootNav = /*#__PURE__*/React.forwardRef(function (_ref4, ref) {
74
144
  }));
75
145
  });
76
146
  FootNav.displayName = 'FootNav';
147
+ // Reserves the bar's height plus the bottom safe area at the end of the page
148
+ // content, so nothing scrolls under the fixed bar. Hidden from `md` up, like
149
+ // the bar itself.
150
+ var FootNavSpacer = /*#__PURE__*/React.forwardRef(function (_ref5, ref) {
151
+ var className = _ref5.className,
152
+ props = _objectWithoutProperties(_ref5, _excluded2);
153
+ return jsx("div", _objectSpread2(_objectSpread2({
154
+ ref: ref,
155
+ "aria-hidden": "true",
156
+ "data-slot": "footnav-spacer",
157
+ className: cn('pb-safe md:hidden', className)
158
+ }, props), {}, {
159
+ children: jsx("div", {
160
+ className: "h-20"
161
+ })
162
+ }));
163
+ });
164
+ FootNavSpacer.displayName = 'FootNav.Spacer';
165
+ FootNav.Spacer = FootNavSpacer;
77
166
 
78
167
  export { FootNav as default };
@@ -6,6 +6,8 @@ import { cn } from '../../utils/cn.js';
6
6
  import { ChevronRight, ChevronLeft, ChevronDown } from 'lucide-react';
7
7
  import Tooltip from '../core/Tooltip.js';
8
8
  import { useLatestRef } from '../../hooks/useLatestRef.js';
9
+ import { useFocusTrap } from '../../hooks/useFocusTrap.js';
10
+ import { useScrollLock } from '../../hooks/useScrollLock.js';
9
11
 
10
12
  var _excluded = ["items", "collapsed", "onToggle", "className", "containerClassName", "stickyOffset", "logo", "footer", "user", "onUserClick", "drawerPosition", "isMobileOpen", "setIsMobileOpen", "showCollapsedTooltips", "tooltipOptions", "linkAs", "expandedIds", "defaultExpandedIds", "onExpandedChange"];
11
13
  // useLayoutEffect warns during SSR; fall back to useEffect on the server.
@@ -15,13 +17,6 @@ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect
15
17
  var isModifiedClick = function isModifiedClick(e) {
16
18
  return Boolean(e && (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || typeof e.button === "number" && e.button !== 0));
17
19
  };
18
- // Focus trap for the mobile drawer (same selector as feedback/Dialog).
19
- var FOCUSABLE_SELECTOR = ["a[href]", "button:not([disabled])", "textarea:not([disabled])", "input:not([disabled])", "select:not([disabled])", '[tabindex]:not([tabindex="-1"])'].join(",");
20
- var getFocusable = function getFocusable(root) {
21
- return root ? Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter(function (el) {
22
- return el.getAttribute("aria-hidden") !== "true";
23
- }) : [];
24
- };
25
20
  var hasChildItems = function hasChildItems(item) {
26
21
  return Array.isArray(item.children) && item.children.length > 0;
27
22
  };
@@ -171,12 +166,8 @@ var Sidebar = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
171
166
  var setIsMobileOpenRef = useLatestRef(setIsMobileOpen);
172
167
  var mobileOpen = isMobileOpen !== null && isMobileOpen !== void 0 ? isMobileOpen : false;
173
168
  var effectiveCollapsed = mobileOpen ? false : collapsed;
174
- useEffect(function () {
175
- if (mobileOpen) document.body.style.overflow = "hidden";else document.body.style.overflow = "";
176
- return function () {
177
- document.body.style.overflow = "";
178
- };
179
- }, [mobileOpen]);
169
+ // Ref-counted, so a Dialog/Drawer stacked on top never unlocks the page early.
170
+ useScrollLock(mobileOpen);
180
171
  // The drawer stays mounted (it slides in/out), so when it is shut it must
181
172
  // leave the tab order and the a11y tree. `inert` is set imperatively: React
182
173
  // 18 does not know the attribute and React 19 treats `inert=""` as false.
@@ -185,54 +176,15 @@ var Sidebar = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
185
176
  if (!drawer) return;
186
177
  if (mobileOpen) drawer.removeAttribute("inert");else drawer.setAttribute("inert", "");
187
178
  }, [mobileOpen]);
188
- // Open drawer = modal dialog: initial focus inside, Tab trapped, Escape
189
- // closes, focus returns to the opener on close (WAI-ARIA APG dialog).
190
- useEffect(function () {
191
- var _ref2, _ref2$focus, _focusables$;
192
- if (!mobileOpen) return undefined;
193
- var drawer = drawerRef.current;
194
- var previouslyFocused = document.activeElement;
195
- var focusables = getFocusable(drawer);
196
- (_ref2 = (_focusables$ = focusables[0]) !== null && _focusables$ !== void 0 ? _focusables$ : drawer) === null || _ref2 === void 0 || (_ref2$focus = _ref2.focus) === null || _ref2$focus === void 0 || _ref2$focus.call(_ref2);
197
- var handleKeyDown = function handleKeyDown(e) {
198
- if (e.key === "Escape") {
199
- var _setIsMobileOpenRef$c;
200
- (_setIsMobileOpenRef$c = setIsMobileOpenRef.current) === null || _setIsMobileOpenRef$c === void 0 || _setIsMobileOpenRef$c.call(setIsMobileOpenRef, false);
201
- return;
202
- }
203
- if (e.key !== "Tab" || !drawer) return;
204
- // md+ viewports hide the drawer (md:hidden): nothing to trap.
205
- if (window.getComputedStyle(drawer).display === "none") return;
206
- var nodes = getFocusable(drawer);
207
- if (nodes.length === 0) {
208
- e.preventDefault();
209
- drawer.focus();
210
- return;
211
- }
212
- var first = nodes[0];
213
- var last = nodes[nodes.length - 1];
214
- var active = document.activeElement;
215
- var outside = !drawer.contains(active) || active === drawer;
216
- if (e.shiftKey && (active === first || outside)) {
217
- e.preventDefault();
218
- last.focus();
219
- } else if (!e.shiftKey && (active === last || outside)) {
220
- e.preventDefault();
221
- first.focus();
222
- }
223
- };
224
- document.addEventListener("keydown", handleKeyDown);
225
- return function () {
226
- document.removeEventListener("keydown", handleKeyDown);
227
- // Restore focus only if it is still in the drawer (or was dropped to
228
- // <body> when the drawer went inert) — never steal it from elsewhere.
229
- var active = document.activeElement;
230
- var focusLost = !active || active === document.body || (drawer === null || drawer === void 0 ? void 0 : drawer.contains(active));
231
- if (focusLost && previouslyFocused && previouslyFocused !== document.body && typeof previouslyFocused.focus === "function" && document.contains(previouslyFocused)) {
232
- previouslyFocused.focus();
233
- }
234
- };
235
- }, [mobileOpen, setIsMobileOpenRef]);
179
+ // Open drawer = modal dialog: initial focus inside, Tab trapped (skipped
180
+ // while md:hidden hides it), Escape closes, focus returns to the opener.
181
+ useFocusTrap(drawerRef, {
182
+ active: mobileOpen,
183
+ onEscape: function onEscape() {
184
+ var _setIsMobileOpenRef$c;
185
+ return (_setIsMobileOpenRef$c = setIsMobileOpenRef.current) === null || _setIsMobileOpenRef$c === void 0 ? void 0 : _setIsMobileOpenRef$c.call(setIsMobileOpenRef, false);
186
+ }
187
+ });
236
188
  var closeMobile = function closeMobile() {
237
189
  if (mobileOpen && setIsMobileOpen) setIsMobileOpen(false);
238
190
  };
@@ -1,11 +1,20 @@
1
1
  'use client';
2
- import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
2
+ import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
3
  import { jsxs, jsx } from 'react/jsx-runtime';
4
4
  import React from 'react';
5
5
  import { cn } from '../../utils/cn.js';
6
6
  import { User } from 'lucide-react';
7
7
 
8
8
  var _excluded = ["src", "alt", "name", "size", "shape", "status", "className"];
9
+ // Internal (not exported from the package): shared with AvatarGroup's "+N" tile.
10
+ var AVATAR_SIZES = {
11
+ xs: 'h-6 w-6 text-xs',
12
+ sm: 'h-8 w-8 text-sm',
13
+ md: 'h-10 w-10 text-base',
14
+ lg: 'h-12 w-12 text-lg',
15
+ xl: 'h-16 w-16 text-xl',
16
+ '2xl': 'h-20 w-20 text-2xl'
17
+ };
9
18
  var Avatar = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
10
19
  var src = _ref.src,
11
20
  alt = _ref.alt,
@@ -17,14 +26,23 @@ var Avatar = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
17
26
  status = _ref.status,
18
27
  className = _ref.className,
19
28
  props = _objectWithoutProperties(_ref, _excluded);
20
- var sizes = {
21
- xs: 'h-6 w-6 text-xs',
22
- sm: 'h-8 w-8 text-sm',
23
- md: 'h-10 w-10 text-base',
24
- lg: 'h-12 w-12 text-lg',
25
- xl: 'h-16 w-16 text-xl',
26
- '2xl': 'h-20 w-20 text-2xl'
27
- };
29
+ // State-driven fallback: a broken image swaps to initials / icon without
30
+ // touching the DOM directly. Reset whenever `src` changes
31
+ // (adjust-during-render pattern).
32
+ var _React$useState = React.useState(false),
33
+ _React$useState2 = _slicedToArray(_React$useState, 2),
34
+ imgFailed = _React$useState2[0],
35
+ setImgFailed = _React$useState2[1];
36
+ var _React$useState3 = React.useState(src),
37
+ _React$useState4 = _slicedToArray(_React$useState3, 2),
38
+ prevSrc = _React$useState4[0],
39
+ setPrevSrc = _React$useState4[1];
40
+ if (src !== prevSrc) {
41
+ setPrevSrc(src);
42
+ setImgFailed(false);
43
+ }
44
+ var showImage = Boolean(src) && !imgFailed;
45
+ var sizes = AVATAR_SIZES;
28
46
  var getInitials = function getInitials(name) {
29
47
  if (!name) return '';
30
48
  return name.split(' ').map(function (word) {
@@ -33,7 +51,7 @@ var Avatar = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
33
51
  };
34
52
  var statusColors = {
35
53
  online: 'bg-success-500',
36
- offline: 'bg-gray-400',
54
+ offline: 'bg-gray-400 dark:bg-gray-500',
37
55
  away: 'bg-warning-500',
38
56
  busy: 'bg-error-500'
39
57
  };
@@ -48,14 +66,14 @@ var Avatar = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
48
66
  return jsxs("div", {
49
67
  ref: ref,
50
68
  className: "relative inline-block",
51
- children: [jsxs("div", _objectSpread2(_objectSpread2({
52
- className: cn('flex items-center justify-center bg-gray-100 text-primary-600 font-medium', sizes[size], {
69
+ children: [jsx("div", _objectSpread2(_objectSpread2({
70
+ className: cn('flex items-center justify-center bg-gray-100 text-primary-600 font-medium dark:bg-gray-800 dark:text-primary-300', sizes[size], {
53
71
  'rounded-full': shape === 'circle',
54
72
  'rounded-md': shape === 'square',
55
73
  'rounded-lg': shape === 'rounded'
56
74
  }, className)
57
75
  }, props), {}, {
58
- children: [src ? jsx("img", {
76
+ children: showImage ? jsx("img", {
59
77
  src: src,
60
78
  alt: alt || name,
61
79
  className: cn('h-full w-full object-cover', {
@@ -63,27 +81,24 @@ var Avatar = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
63
81
  'rounded-md': shape === 'square',
64
82
  'rounded-lg': shape === 'rounded'
65
83
  }),
66
- onError: function onError(e) {
67
- e.target.style.display = 'none';
68
- e.target.nextSibling.style.display = 'flex';
84
+ onError: function onError() {
85
+ return setImgFailed(true);
69
86
  }
70
- }) : null, jsx("div", {
87
+ }) : jsx("div", {
71
88
  className: cn('flex items-center justify-center h-full w-full', {
72
89
  'rounded-full': shape === 'circle',
73
90
  'rounded-md': shape === 'square',
74
91
  'rounded-lg': shape === 'rounded'
75
- }, {
76
- 'hidden': src
77
92
  }),
78
93
  children: name ? getInitials(name) : jsx(User, {
79
94
  className: "h-1/2 w-1/2"
80
95
  })
81
- })]
96
+ })
82
97
  })), status && jsx("span", {
83
- className: cn('absolute bottom-0 right-0 rounded-full border-2 border-white', statusColors[status], statusSizes[size])
98
+ className: cn('absolute bottom-0 right-0 rounded-full border-2 border-surface', statusColors[status], statusSizes[size])
84
99
  })]
85
100
  });
86
101
  });
87
102
  Avatar.displayName = 'Avatar';
88
103
 
89
- export { Avatar as default };
104
+ export { AVATAR_SIZES, Avatar as default };
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+ import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
3
+ import { jsxs } from 'react/jsx-runtime';
4
+ import React from 'react';
5
+ import { cn } from '../../utils/cn.js';
6
+ import { AVATAR_SIZES } from './Avatar.js';
7
+
8
+ var _excluded = ["children", "max", "size", "className"];
9
+ // Overlap grows with the avatar so the stack reads the same at every size.
10
+ var OVERLAP = {
11
+ xs: '-space-x-1.5',
12
+ sm: '-space-x-2',
13
+ md: '-space-x-2',
14
+ lg: '-space-x-3',
15
+ xl: '-space-x-4',
16
+ '2xl': '-space-x-5'
17
+ };
18
+ // Each avatar is ringed in the surface colour so it reads as cut out of the
19
+ // one behind it (dark-mode aware).
20
+ var RING = 'ring-2 ring-surface';
21
+ var AvatarGroup = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
22
+ var children = _ref.children,
23
+ max = _ref.max,
24
+ _ref$size = _ref.size,
25
+ size = _ref$size === void 0 ? 'md' : _ref$size,
26
+ className = _ref.className,
27
+ props = _objectWithoutProperties(_ref, _excluded);
28
+ var avatars = React.Children.toArray(children).filter(React.isValidElement);
29
+ var limit = typeof max === 'number' && max >= 0 ? max : avatars.length;
30
+ var visible = avatars.slice(0, limit);
31
+ var overflow = avatars.length - visible.length;
32
+ var sizeKey = AVATAR_SIZES[size] ? size : 'md';
33
+ return jsxs("div", _objectSpread2(_objectSpread2({
34
+ ref: ref,
35
+ role: "group",
36
+ className: cn('flex items-center', OVERLAP[sizeKey], className)
37
+ }, props), {}, {
38
+ children: [visible.map(function (child) {
39
+ var _child$props$size;
40
+ return /*#__PURE__*/React.cloneElement(child, {
41
+ // An avatar's own `size` wins; otherwise the group's size applies.
42
+ size: (_child$props$size = child.props.size) !== null && _child$props$size !== void 0 ? _child$props$size : size,
43
+ className: cn(RING, child.props.className)
44
+ });
45
+ }), overflow > 0 && jsxs("span", {
46
+ role: "img",
47
+ "aria-label": "".concat(overflow, " more"),
48
+ "data-slot": "overflow",
49
+ className: cn('relative inline-flex shrink-0 items-center justify-center rounded-full bg-gray-200 font-medium text-gray-700 dark:bg-gray-700 dark:text-gray-100', RING, AVATAR_SIZES[sizeKey]),
50
+ children: ["+", overflow]
51
+ })]
52
+ }));
53
+ });
54
+ AvatarGroup.displayName = 'AvatarGroup';
55
+
56
+ export { AvatarGroup as default };