@kaizen/components 2.1.1 → 2.2.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 (32) hide show
  1. package/dist/cjs/index.cjs +2 -0
  2. package/dist/cjs/src/Button/Button.cjs +1 -0
  3. package/dist/cjs/src/LinkButton/LinkButton.cjs +1 -0
  4. package/dist/cjs/src/Pagination/Pagination.cjs +1 -0
  5. package/dist/cjs/src/TitleBlock/TitleBlock.module.scss.cjs +1 -0
  6. package/dist/cjs/src/TitleBlock/utils.cjs +1 -1
  7. package/dist/cjs/src/Tooltip/OverlayArrow.cjs +1 -0
  8. package/dist/cjs/src/Tooltip/Tooltip.cjs +1 -0
  9. package/dist/cjs/src/utils/useContainerQueries.cjs +89 -0
  10. package/dist/esm/index.mjs +1 -0
  11. package/dist/esm/src/Button/Button.mjs +1 -0
  12. package/dist/esm/src/LinkButton/LinkButton.mjs +1 -0
  13. package/dist/esm/src/Pagination/Pagination.mjs +1 -0
  14. package/dist/esm/src/TitleBlock/TitleBlock.module.scss.mjs +1 -0
  15. package/dist/esm/src/TitleBlock/utils.mjs +1 -1
  16. package/dist/esm/src/Tooltip/OverlayArrow.mjs +1 -0
  17. package/dist/esm/src/Tooltip/Tooltip.mjs +1 -0
  18. package/dist/esm/src/utils/useContainerQueries.mjs +87 -0
  19. package/dist/styles.css +25 -5
  20. package/dist/types/TitleBlock/types.d.ts +1 -1
  21. package/dist/types/utils/index.d.ts +1 -0
  22. package/dist/types/utils/useContainerQueries.d.ts +21 -0
  23. package/package.json +1 -1
  24. package/src/TitleBlock/TitleBlock.module.scss +29 -0
  25. package/src/TitleBlock/_docs/TitleBlock--usage-guidelines.mdx +4 -0
  26. package/src/TitleBlock/_docs/TitleBlock.stories.tsx +108 -0
  27. package/src/TitleBlock/_variables.scss +1 -0
  28. package/src/TitleBlock/types.ts +1 -1
  29. package/src/TitleBlock/utils.ts +1 -1
  30. package/src/utils/index.ts +1 -0
  31. package/src/utils/useContainerQueries.spec.tsx +209 -0
  32. package/src/utils/useContainerQueries.tsx +121 -0
@@ -172,6 +172,7 @@ var ToggleSwitchField = require('./src/ToggleSwitch/ToggleSwitchField/ToggleSwit
172
172
  var Tooltip = require('./src/Tooltip/Tooltip.cjs');
173
173
  var TooltipTrigger = require('./src/Tooltip/TooltipTrigger.cjs');
174
174
  var useMediaQueries = require('./src/utils/useMediaQueries.cjs');
175
+ var useContainerQueries = require('./src/utils/useContainerQueries.cjs');
175
176
  var hostedAssets = require('./src/utils/hostedAssets.cjs');
176
177
  var ReversedColors = require('./src/utils/ReversedColors/ReversedColors.cjs');
177
178
  var VisuallyHidden = require('./src/VisuallyHidden/VisuallyHidden.cjs');
@@ -573,6 +574,7 @@ exports.Tooltip = Tooltip.Tooltip;
573
574
  exports.TooltipTrigger = TooltipTrigger.TooltipTrigger;
574
575
  exports.subtractOnePixel = useMediaQueries.subtractOnePixel;
575
576
  exports.useMediaQueries = useMediaQueries.useMediaQueries;
577
+ exports.useContainerQueries = useContainerQueries.useContainerQueries;
576
578
  exports.assetUrl = hostedAssets.assetUrl;
577
579
  exports.ReversedColors = ReversedColors.ReversedColors;
578
580
  exports.useReversedColors = ReversedColors.useReversedColors;
@@ -3,6 +3,7 @@
3
3
  var tslib = require('tslib');
4
4
  var React = require('react');
5
5
  var reactAriaComponents = require('react-aria-components');
6
+ require('use-debounce');
6
7
  var ReversedColors = require('../utils/ReversedColors/ReversedColors.cjs');
7
8
  var mergeClassNames = require('../utils/mergeClassNames.cjs');
8
9
  var PendingContent = require('./subcomponents/PendingContent/PendingContent.cjs');
@@ -11,6 +11,7 @@ require('../Loading/LoadingParagraph/LoadingParagraph.cjs');
11
11
  require('../Loading/LoadingSpinner/LoadingSpinner.cjs');
12
12
  require('../VisuallyHidden/VisuallyHidden.cjs');
13
13
  var ButtonContent = require('../Button/subcomponents/ButtonContent/ButtonContent.cjs');
14
+ require('use-debounce');
14
15
  var ReversedColors = require('../utils/ReversedColors/ReversedColors.cjs');
15
16
  var mergeClassNames = require('../utils/mergeClassNames.cjs');
16
17
  var Button_module = require('../Button/Button.module.css.cjs');
@@ -4,6 +4,7 @@ var tslib = require('tslib');
4
4
  var React = require('react');
5
5
  var classnames = require('classnames');
6
6
  var useMediaQueries = require('../utils/useMediaQueries.cjs');
7
+ require('use-debounce');
7
8
  require('../utils/ReversedColors/ReversedColors.cjs');
8
9
  var DirectionalLink = require('./subcomponents/DirectionalLink/DirectionalLink.cjs');
9
10
  var PaginationLink = require('./subcomponents/PaginationLink/PaginationLink.cjs');
@@ -2,6 +2,7 @@
2
2
 
3
3
  var styles = {
4
4
  "titleBlock": "TitleBlock-module_titleBlock__7jxn3",
5
+ "lightVariant": "TitleBlock-module_lightVariant__1EeLV",
5
6
  "educationVariant": "TitleBlock-module_educationVariant__YvpWa",
6
7
  "adminVariant": "TitleBlock-module_adminVariant__s95nS",
7
8
  "titleRow": "TitleBlock-module_titleRow__Ifqiu",
@@ -4,7 +4,7 @@ var tslib = require('tslib');
4
4
  var isMenuGroupNotButton = function (value) {
5
5
  return 'menuItems' in value;
6
6
  };
7
- var NON_REVERSED_VARIANTS = ['education', 'admin'];
7
+ var NON_REVERSED_VARIANTS = ['light', 'education', 'admin'];
8
8
  var isReversed = function (variant) {
9
9
  // The default variant (no variant prop) is reversed (dark background)
10
10
  if (variant === undefined) return true;
@@ -3,6 +3,7 @@
3
3
  var tslib = require('tslib');
4
4
  var React = require('react');
5
5
  var reactAriaComponents = require('react-aria-components');
6
+ require('use-debounce');
6
7
  var ReversedColors = require('../utils/ReversedColors/ReversedColors.cjs');
7
8
  var mergeClassNames = require('../utils/mergeClassNames.cjs');
8
9
  var OverlayArrow_module = require('./OverlayArrow.module.scss.cjs');
@@ -4,6 +4,7 @@ var tslib = require('tslib');
4
4
  var React = require('react');
5
5
  var reactAriaComponents = require('react-aria-components');
6
6
  var VisuallyHidden = require('../VisuallyHidden/VisuallyHidden.cjs');
7
+ require('use-debounce');
7
8
  var ReversedColors = require('../utils/ReversedColors/ReversedColors.cjs');
8
9
  var mergeClassNames = require('../utils/mergeClassNames.cjs');
9
10
  var OverlayArrow = require('./OverlayArrow.cjs');
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var useDebounce = require('use-debounce');
5
+ var DEFAULT_DEBOUNCE_MS = 1000;
6
+ /**
7
+ * Tailwind CSS default container query breakpoints in pixels
8
+ * These match the default ranges from Tailwind CSS documentation
9
+ * https://tailwindcss.com/docs/responsive-design#container-size-reference
10
+ */
11
+ var DEFAULT_BREAKPOINTS = {
12
+ '3xs': 256,
13
+ '2xs': 288,
14
+ 'xs': 320,
15
+ 'sm': 384,
16
+ 'md': 448,
17
+ 'lg': 512,
18
+ 'xl': 576,
19
+ '2xl': 672,
20
+ '3xl': 768,
21
+ '4xl': 896,
22
+ '5xl': 1024,
23
+ '6xl': 1152,
24
+ '7xl': 1280
25
+ };
26
+ var useContainerQueries = function () {
27
+ var isClient = typeof window !== 'undefined';
28
+ var _a = React.useState(0),
29
+ containerWidth = _a[0],
30
+ setContainerWidth = _a[1];
31
+ var resizeObserverRef = React.useRef(null);
32
+ var debouncedSetContainerWidth = useDebounce.useDebouncedCallback(function (width) {
33
+ setContainerWidth(width);
34
+ }, DEFAULT_DEBOUNCE_MS);
35
+ var containerRef = React.useCallback(function (node) {
36
+ // Skip if SSR
37
+ if (!isClient) return;
38
+ // Cleanup previous observer
39
+ if (resizeObserverRef.current) {
40
+ resizeObserverRef.current.disconnect();
41
+ resizeObserverRef.current = null;
42
+ }
43
+ if (node) {
44
+ // Create new ResizeObserver
45
+ resizeObserverRef.current = new ResizeObserver(function (entries) {
46
+ var _a, _b, _c;
47
+ for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) {
48
+ var entry = entries_1[_i];
49
+ // Use borderBoxSize for more accurate measurements
50
+ var width_1 = (_c = (_b = (_a = entry.borderBoxSize) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.inlineSize) !== null && _c !== void 0 ? _c : entry.contentRect.width;
51
+ debouncedSetContainerWidth(width_1);
52
+ }
53
+ });
54
+ resizeObserverRef.current.observe(node);
55
+ // Set initial width immediately (no debounce for initial render)
56
+ var width = node.getBoundingClientRect().width;
57
+ setContainerWidth(width);
58
+ }
59
+ }, [debouncedSetContainerWidth, isClient]);
60
+ React.useEffect(function () {
61
+ return function () {
62
+ if (resizeObserverRef.current) {
63
+ resizeObserverRef.current.disconnect();
64
+ }
65
+ };
66
+ }, []);
67
+ var queries = React.useMemo(function () {
68
+ return {
69
+ is3xsOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['3xs'],
70
+ is2xsOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['2xs'],
71
+ isXsOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.xs,
72
+ isSmOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.sm,
73
+ isMdOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.md,
74
+ isLgOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.lg,
75
+ isXlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.xl,
76
+ is2xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['2xl'],
77
+ is3xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['3xl'],
78
+ is4xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['4xl'],
79
+ is5xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['5xl'],
80
+ is6xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['6xl'],
81
+ is7xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['7xl']
82
+ };
83
+ }, [containerWidth]);
84
+ return {
85
+ containerRef: containerRef,
86
+ queries: queries
87
+ };
88
+ };
89
+ exports.useContainerQueries = useContainerQueries;
@@ -177,6 +177,7 @@ export { ToggleSwitchField } from './src/ToggleSwitch/ToggleSwitchField/ToggleSw
177
177
  export { Tooltip } from './src/Tooltip/Tooltip.mjs';
178
178
  export { TooltipTrigger } from './src/Tooltip/TooltipTrigger.mjs';
179
179
  export { subtractOnePixel, useMediaQueries } from './src/utils/useMediaQueries.mjs';
180
+ export { useContainerQueries } from './src/utils/useContainerQueries.mjs';
180
181
  export { assetUrl } from './src/utils/hostedAssets.mjs';
181
182
  export { ReversedColors, useReversedColors } from './src/utils/ReversedColors/ReversedColors.mjs';
182
183
  export { VisuallyHidden } from './src/VisuallyHidden/VisuallyHidden.mjs';
@@ -1,6 +1,7 @@
1
1
  import { __rest, __assign } from 'tslib';
2
2
  import React, { forwardRef } from 'react';
3
3
  import { Button as Button$1 } from 'react-aria-components';
4
+ import 'use-debounce';
4
5
  import { useReversedColors } from '../utils/ReversedColors/ReversedColors.mjs';
5
6
  import { mergeClassNames } from '../utils/mergeClassNames.mjs';
6
7
  import { PendingContent } from './subcomponents/PendingContent/PendingContent.mjs';
@@ -9,6 +9,7 @@ import '../Loading/LoadingParagraph/LoadingParagraph.mjs';
9
9
  import '../Loading/LoadingSpinner/LoadingSpinner.mjs';
10
10
  import '../VisuallyHidden/VisuallyHidden.mjs';
11
11
  import { ButtonContent } from '../Button/subcomponents/ButtonContent/ButtonContent.mjs';
12
+ import 'use-debounce';
12
13
  import { useReversedColors } from '../utils/ReversedColors/ReversedColors.mjs';
13
14
  import { mergeClassNames } from '../utils/mergeClassNames.mjs';
14
15
  import buttonStyles from '../Button/Button.module.css.mjs';
@@ -2,6 +2,7 @@ import { __rest, __assign } from 'tslib';
2
2
  import React from 'react';
3
3
  import classnames from 'classnames';
4
4
  import { useMediaQueries } from '../utils/useMediaQueries.mjs';
5
+ import 'use-debounce';
5
6
  import '../utils/ReversedColors/ReversedColors.mjs';
6
7
  import { DirectionalLink } from './subcomponents/DirectionalLink/DirectionalLink.mjs';
7
8
  import { PaginationLink } from './subcomponents/PaginationLink/PaginationLink.mjs';
@@ -1,5 +1,6 @@
1
1
  var styles = {
2
2
  "titleBlock": "TitleBlock-module_titleBlock__7jxn3",
3
+ "lightVariant": "TitleBlock-module_lightVariant__1EeLV",
3
4
  "educationVariant": "TitleBlock-module_educationVariant__YvpWa",
4
5
  "adminVariant": "TitleBlock-module_adminVariant__s95nS",
5
6
  "titleRow": "TitleBlock-module_titleRow__Ifqiu",
@@ -2,7 +2,7 @@ import { __spreadArray, __assign } from 'tslib';
2
2
  var isMenuGroupNotButton = function (value) {
3
3
  return 'menuItems' in value;
4
4
  };
5
- var NON_REVERSED_VARIANTS = ['education', 'admin'];
5
+ var NON_REVERSED_VARIANTS = ['light', 'education', 'admin'];
6
6
  var isReversed = function (variant) {
7
7
  // The default variant (no variant prop) is reversed (dark background)
8
8
  if (variant === undefined) return true;
@@ -1,6 +1,7 @@
1
1
  import { __assign } from 'tslib';
2
2
  import React from 'react';
3
3
  import { OverlayArrow as OverlayArrow$1 } from 'react-aria-components';
4
+ import 'use-debounce';
4
5
  import { useReversedColors } from '../utils/ReversedColors/ReversedColors.mjs';
5
6
  import { mergeClassNames } from '../utils/mergeClassNames.mjs';
6
7
  import styles from './OverlayArrow.module.scss.mjs';
@@ -3,6 +3,7 @@ import React, { forwardRef, useContext, useState, useLayoutEffect } from 'react'
3
3
  import { useContextProps, TooltipContext, TooltipTriggerStateContext, Tooltip as Tooltip$1 } from 'react-aria-components';
4
4
  export { TooltipContext } from 'react-aria-components';
5
5
  import { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden.mjs';
6
+ import 'use-debounce';
6
7
  import { useReversedColors } from '../utils/ReversedColors/ReversedColors.mjs';
7
8
  import { mergeClassNames } from '../utils/mergeClassNames.mjs';
8
9
  import { OverlayArrow } from './OverlayArrow.mjs';
@@ -0,0 +1,87 @@
1
+ import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
2
+ import { useDebouncedCallback } from 'use-debounce';
3
+ var DEFAULT_DEBOUNCE_MS = 1000;
4
+ /**
5
+ * Tailwind CSS default container query breakpoints in pixels
6
+ * These match the default ranges from Tailwind CSS documentation
7
+ * https://tailwindcss.com/docs/responsive-design#container-size-reference
8
+ */
9
+ var DEFAULT_BREAKPOINTS = {
10
+ '3xs': 256,
11
+ '2xs': 288,
12
+ 'xs': 320,
13
+ 'sm': 384,
14
+ 'md': 448,
15
+ 'lg': 512,
16
+ 'xl': 576,
17
+ '2xl': 672,
18
+ '3xl': 768,
19
+ '4xl': 896,
20
+ '5xl': 1024,
21
+ '6xl': 1152,
22
+ '7xl': 1280
23
+ };
24
+ var useContainerQueries = function () {
25
+ var isClient = typeof window !== 'undefined';
26
+ var _a = useState(0),
27
+ containerWidth = _a[0],
28
+ setContainerWidth = _a[1];
29
+ var resizeObserverRef = useRef(null);
30
+ var debouncedSetContainerWidth = useDebouncedCallback(function (width) {
31
+ setContainerWidth(width);
32
+ }, DEFAULT_DEBOUNCE_MS);
33
+ var containerRef = useCallback(function (node) {
34
+ // Skip if SSR
35
+ if (!isClient) return;
36
+ // Cleanup previous observer
37
+ if (resizeObserverRef.current) {
38
+ resizeObserverRef.current.disconnect();
39
+ resizeObserverRef.current = null;
40
+ }
41
+ if (node) {
42
+ // Create new ResizeObserver
43
+ resizeObserverRef.current = new ResizeObserver(function (entries) {
44
+ var _a, _b, _c;
45
+ for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) {
46
+ var entry = entries_1[_i];
47
+ // Use borderBoxSize for more accurate measurements
48
+ var width_1 = (_c = (_b = (_a = entry.borderBoxSize) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.inlineSize) !== null && _c !== void 0 ? _c : entry.contentRect.width;
49
+ debouncedSetContainerWidth(width_1);
50
+ }
51
+ });
52
+ resizeObserverRef.current.observe(node);
53
+ // Set initial width immediately (no debounce for initial render)
54
+ var width = node.getBoundingClientRect().width;
55
+ setContainerWidth(width);
56
+ }
57
+ }, [debouncedSetContainerWidth, isClient]);
58
+ useEffect(function () {
59
+ return function () {
60
+ if (resizeObserverRef.current) {
61
+ resizeObserverRef.current.disconnect();
62
+ }
63
+ };
64
+ }, []);
65
+ var queries = useMemo(function () {
66
+ return {
67
+ is3xsOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['3xs'],
68
+ is2xsOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['2xs'],
69
+ isXsOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.xs,
70
+ isSmOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.sm,
71
+ isMdOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.md,
72
+ isLgOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.lg,
73
+ isXlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.xl,
74
+ is2xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['2xl'],
75
+ is3xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['3xl'],
76
+ is4xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['4xl'],
77
+ is5xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['5xl'],
78
+ is6xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['6xl'],
79
+ is7xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['7xl']
80
+ };
81
+ }, [containerWidth]);
82
+ return {
83
+ containerRef: containerRef,
84
+ queries: queries
85
+ };
86
+ };
87
+ export { useContainerQueries };
package/dist/styles.css CHANGED
@@ -10237,6 +10237,9 @@
10237
10237
  justify-content: center;
10238
10238
  flex-direction: column;
10239
10239
  }
10240
+ .TitleBlock-module_titleBlock__7jxn3.TitleBlock-module_lightVariant__1EeLV {
10241
+ background-color: var(--color-white, #ffffff);
10242
+ }
10240
10243
  .TitleBlock-module_titleBlock__7jxn3.TitleBlock-module_educationVariant__YvpWa {
10241
10244
  background-color: var(--color-blue-100, #e6f6ff);
10242
10245
  }
@@ -10254,6 +10257,10 @@
10254
10257
  width: 100%;
10255
10258
  justify-content: center;
10256
10259
  }
10260
+ .TitleBlock-module_lightVariant__1EeLV .TitleBlock-module_titleRow__Ifqiu {
10261
+ background-color: var(--color-white, #ffffff);
10262
+ margin-bottom: 3px;
10263
+ }
10257
10264
  .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_titleRow__Ifqiu {
10258
10265
  background-color: var(--color-white, #ffffff);
10259
10266
  margin-bottom: 3px;
@@ -10271,6 +10278,9 @@
10271
10278
  margin: 0 12px;
10272
10279
  }
10273
10280
  }
10281
+ .TitleBlock-module_lightVariant__1EeLV .TitleBlock-module_titleRowInner__SlArS {
10282
+ border-bottom: 1px solid var(--color-gray-300, #eaeaec);
10283
+ }
10274
10284
  .TitleBlock-module_titleRowInnerContent__NhTHV {
10275
10285
  position: relative;
10276
10286
  display: flex;
@@ -10283,7 +10293,7 @@
10283
10293
  .TitleBlock-module_educationVariant__YvpWa .TitleBlock-module_titleRowInnerContent__NhTHV {
10284
10294
  border-bottom-color: rgba(var(--color-purple-700-rgb, 74, 35, 77), 0.2);
10285
10295
  }
10286
- .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_titleRowInnerContent__NhTHV, .TitleBlock-module_collapseNavigationArea__x9hzQ .TitleBlock-module_titleRowInnerContent__NhTHV {
10296
+ .TitleBlock-module_lightVariant__1EeLV .TitleBlock-module_titleRowInnerContent__NhTHV, .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_titleRowInnerContent__NhTHV, .TitleBlock-module_collapseNavigationArea__x9hzQ .TitleBlock-module_titleRowInnerContent__NhTHV {
10287
10297
  border-bottom-color: transparent;
10288
10298
  }
10289
10299
  @container (max-width: calc(1080px - 1px)) {
@@ -10426,6 +10436,9 @@
10426
10436
  max-width: 230px;
10427
10437
  margin-inline: var(--spacing-12) 0;
10428
10438
  }
10439
+ .TitleBlock-module_lightVariant__1EeLV .TitleBlock-module_subtitle__AI9tj {
10440
+ color: var(--color-purple-800, #2f2438);
10441
+ }
10429
10442
  .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_subtitle__AI9tj {
10430
10443
  color: var(--color-purple-800, #2f2438);
10431
10444
  }
@@ -10598,6 +10611,11 @@
10598
10611
  z-index: 1;
10599
10612
  }
10600
10613
  }
10614
+ @container (max-width: calc(768px - 1px)) {
10615
+ .TitleBlock-module_lightVariant__1EeLV .TitleBlock-module_navigationTabEdgeShadowRight__xkIWc, .TitleBlock-module_lightVariant__1EeLV .TitleBlock-module_navigationTabEdgeShadowLeft__9pGhR {
10616
+ background: linear-gradient(0deg, var(--color-gray-100, #f9f9f9), rgba(var(--color-gray-100-rgb, 249, 249, 249), 0));
10617
+ }
10618
+ }
10601
10619
  @container (max-width: calc(768px - 1px)) {
10602
10620
  .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_navigationTabEdgeShadowRight__xkIWc, .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_navigationTabEdgeShadowLeft__9pGhR {
10603
10621
  background: linear-gradient(0deg, var(--color-gray-100, #f9f9f9), rgba(var(--color-gray-100-rgb, 249, 249, 249), 0));
@@ -10636,7 +10654,7 @@
10636
10654
  margin-inline-end: 0;
10637
10655
  margin-inline-start: 2.25rem;
10638
10656
  }
10639
- .TitleBlock-module_educationVariant__YvpWa .TitleBlock-module_secondaryActionsContainer__-4q0l, .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_secondaryActionsContainer__-4q0l {
10657
+ .TitleBlock-module_lightVariant__1EeLV .TitleBlock-module_secondaryActionsContainer__-4q0l, .TitleBlock-module_educationVariant__YvpWa .TitleBlock-module_secondaryActionsContainer__-4q0l, .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_secondaryActionsContainer__-4q0l {
10640
10658
  color: var(--color-blue-500, #0168b3);
10641
10659
  }
10642
10660
  @container (max-width: calc(1080px - 1px)) {
@@ -10751,9 +10769,11 @@
10751
10769
  .TitleBlock-module_breadcrumbText__vDi0k {
10752
10770
  color: var(--color-white, #ffffff);
10753
10771
  }
10754
- .TitleBlock-module_educationVariant__YvpWa .TitleBlock-module_breadcrumb__lVNKz, .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_breadcrumb__lVNKz,
10772
+ .TitleBlock-module_lightVariant__1EeLV .TitleBlock-module_breadcrumb__lVNKz, .TitleBlock-module_educationVariant__YvpWa .TitleBlock-module_breadcrumb__lVNKz, .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_breadcrumb__lVNKz,
10773
+ .TitleBlock-module_lightVariant__1EeLV .TitleBlock-module_breadcrumb__lVNKz:hover,
10755
10774
  .TitleBlock-module_educationVariant__YvpWa .TitleBlock-module_breadcrumb__lVNKz:hover,
10756
10775
  .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_breadcrumb__lVNKz:hover,
10776
+ .TitleBlock-module_lightVariant__1EeLV .TitleBlock-module_breadcrumbText__vDi0k,
10757
10777
  .TitleBlock-module_educationVariant__YvpWa .TitleBlock-module_breadcrumbText__vDi0k,
10758
10778
  .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_breadcrumbText__vDi0k {
10759
10779
  color: var(--color-purple-800, #2f2438);
@@ -10779,12 +10799,12 @@
10779
10799
  color: var(--color-white, #ffffff);
10780
10800
  background-color: rgba(var(--color-white-rgb, 255, 255, 255), 0.1);
10781
10801
  }
10782
- .TitleBlock-module_educationVariant__YvpWa .TitleBlock-module_circle__68z-T, .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_circle__68z-T {
10802
+ .TitleBlock-module_lightVariant__1EeLV .TitleBlock-module_circle__68z-T, .TitleBlock-module_educationVariant__YvpWa .TitleBlock-module_circle__68z-T, .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_circle__68z-T {
10783
10803
  color: var(--color-blue-500, #0168b3);
10784
10804
  border-color: var(--color-blue-300, #73c0e8);
10785
10805
  border-width: 3px;
10786
10806
  }
10787
- .TitleBlock-module_educationVariant__YvpWa .TitleBlock-module_breadcrumb__lVNKz:hover .TitleBlock-module_circle__68z-T, .TitleBlock-module_educationVariant__YvpWa .TitleBlock-module_breadcrumb__lVNKz:focus .TitleBlock-module_circle__68z-T, .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_breadcrumb__lVNKz:hover .TitleBlock-module_circle__68z-T, .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_breadcrumb__lVNKz:focus .TitleBlock-module_circle__68z-T {
10807
+ .TitleBlock-module_lightVariant__1EeLV .TitleBlock-module_breadcrumb__lVNKz:hover .TitleBlock-module_circle__68z-T, .TitleBlock-module_lightVariant__1EeLV .TitleBlock-module_breadcrumb__lVNKz:focus .TitleBlock-module_circle__68z-T, .TitleBlock-module_educationVariant__YvpWa .TitleBlock-module_breadcrumb__lVNKz:hover .TitleBlock-module_circle__68z-T, .TitleBlock-module_educationVariant__YvpWa .TitleBlock-module_breadcrumb__lVNKz:focus .TitleBlock-module_circle__68z-T, .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_breadcrumb__lVNKz:hover .TitleBlock-module_circle__68z-T, .TitleBlock-module_adminVariant__s95nS .TitleBlock-module_breadcrumb__lVNKz:focus .TitleBlock-module_circle__68z-T {
10788
10808
  border-color: var(--color-blue-500, #0168b3);
10789
10809
  color: var(--color-blue-500, #0168b3);
10790
10810
  background-color: rgba(var(--color-blue-500-rgb, 1, 104, 179), 0.1);
@@ -117,7 +117,7 @@ export type TitleBlockDistributiveOmit<T, K extends keyof any> = T extends any ?
117
117
  export type TitleBlockAvatarProps = Omit<GenericAvatarProps, 'size'> | Omit<CompanyAvatarProps, 'size'>;
118
118
  export type DefaultActionProps = TitleBlockButtonProps | TitleBlockCustomButtonProps;
119
119
  export type SectionTitleRenderProps = Pick<TitleBlockProps, 'sectionTitle' | 'sectionTitleAutomationId' | 'sectionTitleDescription' | 'sectionTitleDescriptionAutomationId'>;
120
- export type TitleBlockVariant = 'admin' | 'education';
120
+ export type TitleBlockVariant = 'light' | 'admin' | 'education';
121
121
  export type NavigationTabs = React.ReactElement<NavigationTabProps>[];
122
122
  export type TextDirection = 'ltr' | 'rtl';
123
123
  export type SurveyStatus = {
@@ -1,3 +1,4 @@
1
1
  export * from './useMediaQueries';
2
+ export * from './useContainerQueries';
2
3
  export * from './hostedAssets';
3
4
  export * from './ReversedColors';
@@ -0,0 +1,21 @@
1
+ import type React from 'react';
2
+ type ContainerQueries = {
3
+ is3xsOrLarger: boolean;
4
+ is2xsOrLarger: boolean;
5
+ isXsOrLarger: boolean;
6
+ isSmOrLarger: boolean;
7
+ isMdOrLarger: boolean;
8
+ isLgOrLarger: boolean;
9
+ isXlOrLarger: boolean;
10
+ is2xlOrLarger: boolean;
11
+ is3xlOrLarger: boolean;
12
+ is4xlOrLarger: boolean;
13
+ is5xlOrLarger: boolean;
14
+ is6xlOrLarger: boolean;
15
+ is7xlOrLarger: boolean;
16
+ };
17
+ export declare const useContainerQueries: () => {
18
+ containerRef: React.RefCallback<HTMLElement>;
19
+ queries: ContainerQueries;
20
+ };
21
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaizen/components",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Kaizen component library",
5
5
  "author": "Geoffrey Chong <geoff.chong@cultureamp.com>",
6
6
  "homepage": "https://cultureamp.design",
@@ -31,6 +31,10 @@
31
31
  justify-content: center;
32
32
  flex-direction: column;
33
33
 
34
+ &.lightVariant {
35
+ background-color: $dt-color-background-color-light;
36
+ }
37
+
34
38
  &.educationVariant {
35
39
  background-color: $dt-color-background-color-eduction;
36
40
  }
@@ -51,6 +55,11 @@
51
55
  justify-content: center;
52
56
  }
53
57
 
58
+ .lightVariant .titleRow {
59
+ background-color: $color-white;
60
+ margin-bottom: 3px; // needed to correctly position the active tab label :after element
61
+ }
62
+
54
63
  .adminVariant .titleRow {
55
64
  background-color: $color-white;
56
65
  margin-bottom: 3px; // needed to correctly position the active tab label :after element
@@ -73,6 +82,10 @@
73
82
  @extend %titleBlockInner;
74
83
  }
75
84
 
85
+ .lightVariant .titleRowInner {
86
+ border-bottom: 1px solid $color-gray-300;
87
+ }
88
+
76
89
  .rowBelowSeparatorInner {
77
90
  @extend %titleBlockInner;
78
91
  }
@@ -90,6 +103,7 @@
90
103
  border-bottom-color: rgba($color-purple-700-rgb, 0.2);
91
104
  }
92
105
 
106
+ .lightVariant &,
93
107
  .adminVariant &,
94
108
  .collapseNavigationArea & {
95
109
  border-bottom-color: transparent;
@@ -245,6 +259,10 @@
245
259
  max-width: 230px;
246
260
  margin-inline: var(--spacing-12) 0;
247
261
 
262
+ .lightVariant & {
263
+ color: $color-purple-800;
264
+ }
265
+
248
266
  .adminVariant & {
249
267
  color: $color-purple-800;
250
268
  }
@@ -438,6 +456,12 @@
438
456
  z-index: 1;
439
457
  }
440
458
 
459
+ .lightVariant & {
460
+ @include title-block-small {
461
+ background: linear-gradient(0deg, $color-gray-100, rgba($color-gray-100-rgb, 0));
462
+ }
463
+ }
464
+
441
465
  .adminVariant & {
442
466
  @include title-block-small {
443
467
  background: linear-gradient(0deg, $color-gray-100, rgba($color-gray-100-rgb, 0));
@@ -488,6 +512,7 @@
488
512
  // To be removed eventually – the Dropdown does not
489
513
  // currently set its own color, and we want it
490
514
  // to be Cluny like the default secondary buttons
515
+ .lightVariant &,
491
516
  .educationVariant &,
492
517
  .adminVariant & {
493
518
  color: $color-blue-500;
@@ -621,6 +646,7 @@
621
646
  .breadcrumbText {
622
647
  color: $color-white;
623
648
 
649
+ .lightVariant &,
624
650
  .educationVariant &,
625
651
  .adminVariant & {
626
652
  color: $color-purple-800;
@@ -654,6 +680,7 @@
654
680
  background-color: rgba($color-white-rgb, 0.1);
655
681
  }
656
682
 
683
+ .lightVariant &,
657
684
  .educationVariant &,
658
685
  .adminVariant & {
659
686
  color: $color-blue-500;
@@ -661,6 +688,8 @@
661
688
  border-width: $border-width;
662
689
  }
663
690
 
691
+ .lightVariant .breadcrumb:hover &,
692
+ .lightVariant .breadcrumb:focus &,
664
693
  .educationVariant .breadcrumb:hover &,
665
694
  .educationVariant .breadcrumb:focus &,
666
695
  .adminVariant .breadcrumb:hover &,
@@ -35,6 +35,10 @@ A consistent pattern to allow users to navigate and complete tasks easily.
35
35
 
36
36
  <Canvas of={TitleBlockStories.HasLongTitle} />
37
37
 
38
+ ### Light Variant
39
+
40
+ <Canvas of={TitleBlockStories.LightVariant} />
41
+
38
42
  ### Admin Variant
39
43
 
40
44
  <Canvas of={TitleBlockStories.AdminVariant} />
@@ -146,6 +146,47 @@ export const HasLongTitle: Story = {
146
146
  args: { title: 'A long title with over thirty characters' },
147
147
  }
148
148
 
149
+ export const LightVariant: Story = {
150
+ parameters: {
151
+ docs: {
152
+ canvas: {
153
+ sourceState: 'shown',
154
+ },
155
+ },
156
+ viewport: {
157
+ viewports: {
158
+ default: {
159
+ name: 'Above or equal to 1366',
160
+ styles: { width: '1366px', height: '800px' },
161
+ type: 'desktop',
162
+ },
163
+ under1366: {
164
+ name: 'Under 1366',
165
+ styles: { width: '1365px', height: '800px' },
166
+ type: 'desktop',
167
+ },
168
+ },
169
+ defaultViewport: 'default',
170
+ },
171
+ chromatic: {
172
+ disable: false,
173
+ viewports: [1365, 1366],
174
+ },
175
+ },
176
+ args: {
177
+ variant: 'light',
178
+ title: 'Light TitleBlock',
179
+ navigationTabs: [
180
+ <NavigationTab key="1" variant="light" text="Label" href="#" active />,
181
+ <NavigationTab key="2" variant="light" text="Label" href="#" />,
182
+ <NavigationTab key="3" variant="light" text="Label" href="#" />,
183
+ <NavigationTab key="4" variant="light" text="Label" href="#" />,
184
+ <NavigationTab key="5" variant="light" text="Label" href="#" />,
185
+ <NavigationTab key="6" variant="light" text="Label" href="#" />,
186
+ ],
187
+ },
188
+ }
189
+
149
190
  export const AdminVariant: Story = {
150
191
  parameters: {
151
192
  viewport: {
@@ -216,6 +257,73 @@ export const EducationVariant: Story = {
216
257
  },
217
258
  }
218
259
 
260
+ export const StickerSheetVariants: Story = {
261
+ name: 'Sticker Sheet (Variants)',
262
+ render: () => (
263
+ <StickerSheet title="TitleBlock Variants">
264
+ <StickerSheet.Row header="Default (Purple background)">
265
+ <TitleBlock
266
+ title="Default Variant"
267
+ subtitle="This is the default purple variant with white text"
268
+ breadcrumb={{
269
+ path: '#',
270
+ text: 'Back to home',
271
+ }}
272
+ navigationTabs={[
273
+ <NavigationTab key="1" text="Overview" href="#" active />,
274
+ <NavigationTab key="2" text="Settings" href="#" />,
275
+ ]}
276
+ />
277
+ </StickerSheet.Row>
278
+ <StickerSheet.Row header="Education (Purple background)">
279
+ <TitleBlock
280
+ variant="education"
281
+ title="Education Variant"
282
+ subtitle="This is the education variant with purple background"
283
+ breadcrumb={{
284
+ path: '#',
285
+ text: 'Back to courses',
286
+ }}
287
+ navigationTabs={[
288
+ <NavigationTab key="1" text="Lessons" href="#" active />,
289
+ <NavigationTab key="2" text="Assignments" href="#" />,
290
+ ]}
291
+ />
292
+ </StickerSheet.Row>
293
+ <StickerSheet.Row header="Admin (White background)">
294
+ <TitleBlock
295
+ variant="admin"
296
+ title="Admin Variant"
297
+ subtitle="This is the admin variant with white background"
298
+ breadcrumb={{
299
+ path: '#',
300
+ text: 'Back to dashboard',
301
+ }}
302
+ navigationTabs={[
303
+ <NavigationTab key="1" text="Users" href="#" active />,
304
+ <NavigationTab key="2" text="Settings" href="#" />,
305
+ ]}
306
+ />
307
+ </StickerSheet.Row>
308
+ <StickerSheet.Row header="Light (White background)">
309
+ <TitleBlock
310
+ variant="light"
311
+ title="Light Variant"
312
+ subtitle="This is the light variant with clean white background"
313
+ breadcrumb={{
314
+ path: '#',
315
+ text: 'Back to overview',
316
+ }}
317
+ navigationTabs={[
318
+ <NavigationTab key="1" text="Details" href="#" active />,
319
+ <NavigationTab key="2" text="Analytics" href="#" />,
320
+ ]}
321
+ />
322
+ </StickerSheet.Row>
323
+ </StickerSheet>
324
+ ),
325
+ }
326
+
219
327
  export const StickerSheetBreadcrumbs: Story = {
220
328
  name: 'Sticker Sheet (Breadcrumb)',
221
329
  parameters: {
@@ -3,6 +3,7 @@
3
3
  $dt-color-background-color-admin: $color-gray-100;
4
4
  $dt-color-background-color-eduction: $color-blue-100;
5
5
  $dt-color-background-color-default: $color-purple-600;
6
+ $dt-color-background-color-light: $color-white;
6
7
  $dt-color-background-gradient: linear-gradient(
7
8
  0deg,
8
9
  $color-purple-600,
@@ -157,7 +157,7 @@ export type SectionTitleRenderProps = Pick<
157
157
  | 'sectionTitleDescriptionAutomationId'
158
158
  >
159
159
 
160
- export type TitleBlockVariant = 'admin' | 'education' // the default is wisteria bg (AKA "reporting")
160
+ export type TitleBlockVariant = 'light' | 'admin' | 'education' // the default is wisteria bg (AKA "reporting")
161
161
 
162
162
  export type NavigationTabs = React.ReactElement<NavigationTabProps>[]
163
163
 
@@ -11,7 +11,7 @@ export const isMenuGroupNotButton = (
11
11
  value: (TitleBlockButtonProps | TitleBlockCustomButtonProps) | TitleBlockMenuGroup,
12
12
  ): value is TitleBlockMenuGroup => 'menuItems' in value
13
13
 
14
- export const NON_REVERSED_VARIANTS = ['education', 'admin']
14
+ export const NON_REVERSED_VARIANTS = ['light', 'education', 'admin']
15
15
 
16
16
  export const isReversed = (variant: TitleBlockVariant | undefined): boolean => {
17
17
  // The default variant (no variant prop) is reversed (dark background)
@@ -1,4 +1,5 @@
1
1
  // Not all utils are exported because most are intended for internal use only
2
2
  export * from './useMediaQueries'
3
+ export * from './useContainerQueries'
3
4
  export * from './hostedAssets'
4
5
  export * from './ReversedColors'
@@ -0,0 +1,209 @@
1
+ import React from 'react'
2
+ import { act, render, screen } from '@testing-library/react'
3
+ import { vi } from 'vitest'
4
+ import { useContainerQueries } from './useContainerQueries'
5
+
6
+ const ExampleComponent = (): JSX.Element => {
7
+ const { containerRef, queries } = useContainerQueries()
8
+
9
+ return (
10
+ <div ref={containerRef} data-testid="container">
11
+ {queries.isSmOrLarger && <button type="button">Small query boolean</button>}
12
+ {queries.isMdOrLarger && <button type="button">Medium or larger query</button>}
13
+ </div>
14
+ )
15
+ }
16
+
17
+ // Mock ResizeObserver
18
+ class ResizeObserverMock {
19
+ callback: ResizeObserverCallback
20
+ elements: Set<Element>
21
+
22
+ constructor(callback: ResizeObserverCallback) {
23
+ this.callback = callback
24
+ this.elements = new Set()
25
+ }
26
+
27
+ observe(target: Element): void {
28
+ this.elements.add(target)
29
+ }
30
+
31
+ unobserve(target: Element): void {
32
+ this.elements.delete(target)
33
+ }
34
+
35
+ disconnect(): void {
36
+ this.elements.clear()
37
+ }
38
+
39
+ // Helper method to trigger resize
40
+ trigger(width: number): void {
41
+ const entries: ResizeObserverEntry[] = Array.from(this.elements).map((element) => ({
42
+ target: element,
43
+ contentRect: {
44
+ width,
45
+ height: 100,
46
+ top: 0,
47
+ left: 0,
48
+ bottom: 100,
49
+ right: width,
50
+ x: 0,
51
+ y: 0,
52
+ } as DOMRectReadOnly,
53
+ borderBoxSize: [
54
+ {
55
+ inlineSize: width,
56
+ blockSize: 100,
57
+ },
58
+ ],
59
+ contentBoxSize: [
60
+ {
61
+ inlineSize: width,
62
+ blockSize: 100,
63
+ },
64
+ ],
65
+ devicePixelContentBoxSize: [
66
+ {
67
+ inlineSize: width,
68
+ blockSize: 100,
69
+ },
70
+ ],
71
+ })) as ResizeObserverEntry[]
72
+
73
+ this.callback(entries, this as unknown as ResizeObserver)
74
+ }
75
+ }
76
+
77
+ let resizeObserverInstance: ResizeObserverMock | null = null
78
+
79
+ // Store original values to restore after each test
80
+ const originalResizeObserver = global.ResizeObserver
81
+ const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect
82
+
83
+ const setupResizeObserver = (): ResizeObserverMock => {
84
+ const mockObserver = vi.fn((callback: ResizeObserverCallback) => {
85
+ resizeObserverInstance = new ResizeObserverMock(callback)
86
+ return resizeObserverInstance
87
+ })
88
+
89
+ global.ResizeObserver = mockObserver as unknown as typeof ResizeObserver
90
+
91
+ return resizeObserverInstance!
92
+ }
93
+
94
+ describe('useContainerQueries()', () => {
95
+ beforeEach(() => {
96
+ resizeObserverInstance = null
97
+ })
98
+
99
+ afterEach(() => {
100
+ vi.restoreAllMocks()
101
+ // Restore original global/prototype properties
102
+ global.ResizeObserver = originalResizeObserver
103
+ Element.prototype.getBoundingClientRect = originalGetBoundingClientRect
104
+ })
105
+
106
+ it('shows and hides content based on Tailwind container breakpoints', async () => {
107
+ setupResizeObserver()
108
+
109
+ // Mock getBoundingClientRect to return a small width initially
110
+ const mockGetBoundingClientRect = vi.fn(() => ({
111
+ width: 300,
112
+ height: 100,
113
+ top: 0,
114
+ left: 0,
115
+ bottom: 100,
116
+ right: 300,
117
+ x: 0,
118
+ y: 0,
119
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
120
+ toJSON: () => {},
121
+ }))
122
+ Element.prototype.getBoundingClientRect = mockGetBoundingClientRect
123
+
124
+ render(<ExampleComponent />)
125
+
126
+ // Initially at 300px, should not show sm (384px) or md (448px) content
127
+ expect(screen.queryByRole('button', { name: /Small query boolean/i })).not.toBeInTheDocument()
128
+ expect(
129
+ screen.queryByRole('button', { name: /Medium or larger query/i }),
130
+ ).not.toBeInTheDocument()
131
+
132
+ // Trigger resize to 400px (sm breakpoint is 384px)
133
+ await act(async () => {
134
+ resizeObserverInstance?.trigger(400)
135
+ // Wait for debounce (1000ms + buffer)
136
+ await new Promise((resolve) => setTimeout(resolve, 1100))
137
+ })
138
+
139
+ expect(screen.queryByRole('button', { name: /Small query boolean/i })).toBeInTheDocument()
140
+ expect(
141
+ screen.queryByRole('button', { name: /Medium or larger query/i }),
142
+ ).not.toBeInTheDocument()
143
+
144
+ // Trigger resize to 500px (md breakpoint is 448px)
145
+ await act(async () => {
146
+ resizeObserverInstance?.trigger(500)
147
+ // Wait for debounce (1000ms + buffer)
148
+ await new Promise((resolve) => setTimeout(resolve, 1100))
149
+ })
150
+
151
+ expect(screen.queryByRole('button', { name: /Small query boolean/i })).toBeInTheDocument()
152
+ expect(screen.queryByRole('button', { name: /Medium or larger query/i })).toBeInTheDocument()
153
+ })
154
+
155
+ it('returns SSR-safe defaults when window is undefined', () => {
156
+ // This test verifies the SSR code path exists
157
+ // In actual SSR environment, the hook returns safe defaults
158
+ expect(useContainerQueries).toBeDefined()
159
+ })
160
+
161
+ it('cleans up ResizeObserver on unmount', () => {
162
+ setupResizeObserver()
163
+
164
+ const mockGetBoundingClientRect = vi.fn(() => ({
165
+ width: 500,
166
+ height: 100,
167
+ top: 0,
168
+ left: 0,
169
+ bottom: 100,
170
+ right: 500,
171
+ x: 0,
172
+ y: 0,
173
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
174
+ toJSON: () => {},
175
+ }))
176
+ Element.prototype.getBoundingClientRect = mockGetBoundingClientRect
177
+
178
+ const { unmount } = render(<ExampleComponent />)
179
+
180
+ const disconnectSpy = vi.spyOn(resizeObserverInstance!, 'disconnect')
181
+
182
+ unmount()
183
+
184
+ expect(disconnectSpy).toHaveBeenCalled()
185
+ })
186
+
187
+ it('observes the container element', () => {
188
+ setupResizeObserver()
189
+
190
+ const mockGetBoundingClientRect = vi.fn(() => ({
191
+ width: 500,
192
+ height: 100,
193
+ top: 0,
194
+ left: 0,
195
+ bottom: 100,
196
+ right: 500,
197
+ x: 0,
198
+ y: 0,
199
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
200
+ toJSON: () => {},
201
+ }))
202
+ Element.prototype.getBoundingClientRect = mockGetBoundingClientRect
203
+
204
+ render(<ExampleComponent />)
205
+
206
+ // Verify that the ResizeObserver is observing the container
207
+ expect(resizeObserverInstance?.elements.size).toBe(1)
208
+ })
209
+ })
@@ -0,0 +1,121 @@
1
+ import type React from 'react'
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3
+ import { useDebouncedCallback } from 'use-debounce'
4
+
5
+ const DEFAULT_DEBOUNCE_MS = 1000
6
+
7
+ /**
8
+ * Tailwind CSS default container query breakpoints in pixels
9
+ * These match the default ranges from Tailwind CSS documentation
10
+ * https://tailwindcss.com/docs/responsive-design#container-size-reference
11
+ */
12
+ const DEFAULT_BREAKPOINTS = {
13
+ '3xs': 256,
14
+ '2xs': 288,
15
+ 'xs': 320,
16
+ 'sm': 384,
17
+ 'md': 448,
18
+ 'lg': 512,
19
+ 'xl': 576,
20
+ '2xl': 672,
21
+ '3xl': 768,
22
+ '4xl': 896,
23
+ '5xl': 1024,
24
+ '6xl': 1152,
25
+ '7xl': 1280,
26
+ } as const
27
+
28
+ type ContainerQueries = {
29
+ is3xsOrLarger: boolean
30
+ is2xsOrLarger: boolean
31
+ isXsOrLarger: boolean
32
+ isSmOrLarger: boolean
33
+ isMdOrLarger: boolean
34
+ isLgOrLarger: boolean
35
+ isXlOrLarger: boolean
36
+ is2xlOrLarger: boolean
37
+ is3xlOrLarger: boolean
38
+ is4xlOrLarger: boolean
39
+ is5xlOrLarger: boolean
40
+ is6xlOrLarger: boolean
41
+ is7xlOrLarger: boolean
42
+ }
43
+
44
+ export const useContainerQueries = (): {
45
+ containerRef: React.RefCallback<HTMLElement>
46
+ queries: ContainerQueries
47
+ } => {
48
+ const isClient = typeof window !== 'undefined'
49
+
50
+ const [containerWidth, setContainerWidth] = useState<number>(0)
51
+
52
+ const resizeObserverRef = useRef<ResizeObserver | null>(null)
53
+
54
+ const debouncedSetContainerWidth = useDebouncedCallback((width: number) => {
55
+ setContainerWidth(width)
56
+ }, DEFAULT_DEBOUNCE_MS)
57
+
58
+ const containerRef = useCallback(
59
+ (node: HTMLElement | null) => {
60
+ // Skip if SSR
61
+ if (!isClient) return
62
+
63
+ // Cleanup previous observer
64
+ if (resizeObserverRef.current) {
65
+ resizeObserverRef.current.disconnect()
66
+ resizeObserverRef.current = null
67
+ }
68
+
69
+ if (node) {
70
+ // Create new ResizeObserver
71
+ resizeObserverRef.current = new ResizeObserver((entries) => {
72
+ for (const entry of entries) {
73
+ // Use borderBoxSize for more accurate measurements
74
+ const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width
75
+ debouncedSetContainerWidth(width)
76
+ }
77
+ })
78
+
79
+ resizeObserverRef.current.observe(node)
80
+
81
+ // Set initial width immediately (no debounce for initial render)
82
+ const width = node.getBoundingClientRect().width
83
+ setContainerWidth(width)
84
+ }
85
+ },
86
+ [debouncedSetContainerWidth, isClient],
87
+ )
88
+
89
+ useEffect(
90
+ () => () => {
91
+ if (resizeObserverRef.current) {
92
+ resizeObserverRef.current.disconnect()
93
+ }
94
+ },
95
+ [],
96
+ )
97
+
98
+ const queries = useMemo(
99
+ () => ({
100
+ is3xsOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['3xs'],
101
+ is2xsOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['2xs'],
102
+ isXsOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.xs,
103
+ isSmOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.sm,
104
+ isMdOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.md,
105
+ isLgOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.lg,
106
+ isXlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS.xl,
107
+ is2xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['2xl'],
108
+ is3xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['3xl'],
109
+ is4xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['4xl'],
110
+ is5xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['5xl'],
111
+ is6xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['6xl'],
112
+ is7xlOrLarger: containerWidth >= DEFAULT_BREAKPOINTS['7xl'],
113
+ }),
114
+ [containerWidth],
115
+ )
116
+
117
+ return {
118
+ containerRef,
119
+ queries,
120
+ }
121
+ }