@kushagradhawan/kookie-ui 0.1.39 → 0.1.41

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 (31) hide show
  1. package/README.md +4 -4
  2. package/components.css +214 -44
  3. package/dist/cjs/components/shell.d.ts +2 -0
  4. package/dist/cjs/components/shell.d.ts.map +1 -1
  5. package/dist/cjs/components/shell.js +1 -1
  6. package/dist/cjs/components/shell.js.map +3 -3
  7. package/dist/cjs/hooks/use-body-pointer-events-cleanup.d.ts.map +1 -1
  8. package/dist/cjs/hooks/use-body-pointer-events-cleanup.js +1 -1
  9. package/dist/cjs/hooks/use-body-pointer-events-cleanup.js.map +3 -3
  10. package/dist/esm/components/shell.d.ts +2 -0
  11. package/dist/esm/components/shell.d.ts.map +1 -1
  12. package/dist/esm/components/shell.js +1 -1
  13. package/dist/esm/components/shell.js.map +3 -3
  14. package/dist/esm/hooks/use-body-pointer-events-cleanup.d.ts.map +1 -1
  15. package/dist/esm/hooks/use-body-pointer-events-cleanup.js +1 -1
  16. package/dist/esm/hooks/use-body-pointer-events-cleanup.js.map +3 -3
  17. package/package.json +4 -4
  18. package/src/components/_internal/base-card.css +199 -83
  19. package/src/components/_internal/base-menu.css +25 -17
  20. package/src/components/_internal/base-sidebar-menu.css +9 -9
  21. package/src/components/_internal/base-sidebar.css +12 -12
  22. package/src/components/dialog.css +12 -0
  23. package/src/components/sheet.css +38 -10
  24. package/src/components/shell.css +23 -5
  25. package/src/components/shell.tsx +5 -3
  26. package/src/components/table.css +4 -0
  27. package/src/hooks/use-body-pointer-events-cleanup.ts +76 -45
  28. package/src/styles/tokens/cursor.css +1 -1
  29. package/styles.css +215 -45
  30. package/tokens/base.css +1 -1
  31. package/tokens.css +1 -1
@@ -8,18 +8,18 @@
8
8
  flex-direction: column;
9
9
  box-sizing: border-box;
10
10
  overflow: hidden;
11
-
11
+
12
12
  background-color: var(--color-panel);
13
13
  backdrop-filter: var(--backdrop-filter-panel);
14
14
  box-shadow: var(--shadow-5);
15
15
  transition: var(--transition-background-blur);
16
-
16
+
17
17
  /* GPU optimization: limit paint scope and prevent backdrop-filter layering */
18
18
  contain: paint;
19
19
  isolation: isolate;
20
20
 
21
21
  /* Optimize backdrop-filter performance during animations */
22
- &:where([data-state="open"]) {
22
+ &:where([data-state='open']) {
23
23
  will-change: transform, opacity;
24
24
  }
25
25
 
@@ -188,6 +188,7 @@
188
188
  font-size: var(--font-size-1);
189
189
  line-height: var(--line-height-1);
190
190
  letter-spacing: var(--letter-spacing-1);
191
+ padding-left: calc(var(--base-menu-item-padding-left) + var(--component-gap-2));
191
192
  }
192
193
 
193
194
  & :where(.rt-BaseMenuItemIndicatorIcon, .rt-BaseMenuSubTriggerIcon) {
@@ -231,6 +232,7 @@
231
232
  font-size: var(--font-size-2);
232
233
  line-height: var(--line-height-2);
233
234
  letter-spacing: var(--letter-spacing-2);
235
+ padding-left: calc(var(--base-menu-item-padding-left) + var(--component-gap-3));
234
236
  }
235
237
 
236
238
  & :where(.rt-BaseMenuItemIndicatorIcon, .rt-BaseMenuSubTriggerIcon) {
@@ -274,6 +276,7 @@
274
276
  font-size: var(--font-size-2);
275
277
  line-height: var(--line-height-2);
276
278
  letter-spacing: var(--letter-spacing-2);
279
+ padding-left: calc(var(--base-menu-item-padding-left) + var(--component-gap-4));
277
280
  }
278
281
 
279
282
  & :where(.rt-BaseMenuItemIndicatorIcon, .rt-BaseMenuSubTriggerIcon) {
@@ -308,7 +311,8 @@
308
311
  }
309
312
 
310
313
  /* Ensure gray text appears muted in non-highlighted state */
311
- .rt-BaseMenuItem :where(.rt-Text[data-accent-color='gray'], [data-accent-color='gray']:not(.rt-Badge)) {
314
+ .rt-BaseMenuItem
315
+ :where(.rt-Text[data-accent-color='gray'], [data-accent-color='gray']:not(.rt-Badge)) {
312
316
  color: var(--gray-a10);
313
317
  }
314
318
  .rt-BaseMenuItem:where([data-disabled], [data-highlighted]),
@@ -326,23 +330,27 @@
326
330
  .rt-BaseMenuContent:where(.rt-variant-solid, .rt-variant-soft) {
327
331
  & :where(.rt-BaseMenuSubTrigger) {
328
332
  transition: var(--transition-menu);
329
-
333
+
330
334
  /* Enhanced reduced motion support */
331
335
  @media (prefers-reduced-motion: reduce) {
332
336
  transition: none;
333
337
  backdrop-filter: none; /* Remove backdrop effects for motion-sensitive users */
334
338
  }
335
-
339
+
336
340
  /* Remove backdrop-filter transitions in translucent mode to prevent flickering */
337
341
  :where([data-panel-background='translucent']) & {
338
- transition: background var(--motion-duration-micro) var(--motion-ease-standard), color var(--motion-duration-small) var(--motion-ease-standard);
342
+ transition:
343
+ background var(--motion-duration-micro) var(--motion-ease-standard),
344
+ color var(--motion-duration-small) var(--motion-ease-standard);
339
345
  }
340
346
  }
341
-
347
+
342
348
  & :where(.rt-BaseMenuItem) {
343
349
  /* Remove backdrop-filter transitions in translucent mode to prevent flickering */
344
350
  :where([data-panel-background='translucent']) & {
345
- transition: background var(--motion-duration-micro) var(--motion-ease-standard), color var(--motion-duration-small) var(--motion-ease-standard);
351
+ transition:
352
+ background var(--motion-duration-micro) var(--motion-ease-standard),
353
+ color var(--motion-duration-small) var(--motion-ease-standard);
346
354
  }
347
355
  }
348
356
  }
@@ -352,7 +360,7 @@
352
360
  & :where(.rt-BaseMenuSubTrigger[data-state='open']) {
353
361
  /* Base state: solid gray for solid panels */
354
362
  background-color: var(--gray-3);
355
-
363
+
356
364
  /* Theme-level translucent override */
357
365
  :where([data-panel-background='translucent']) & {
358
366
  background-color: var(--gray-a3);
@@ -371,7 +379,7 @@
371
379
  /* No backdrop-filter here to prevent double-blur with container */
372
380
  }
373
381
  }
374
-
382
+
375
383
  & :where(.rt-BaseMenuItem[data-highlighted]) {
376
384
  background-color: var(--accent-9);
377
385
  color: var(--accent-contrast);
@@ -390,7 +398,7 @@
390
398
  color: inherit !important;
391
399
  }
392
400
  }
393
-
401
+
394
402
  &:where(.rt-high-contrast) {
395
403
  & :where(.rt-BaseMenuItem[data-highlighted]) {
396
404
  background-color: var(--accent-12);
@@ -431,7 +439,7 @@
431
439
  & :where(.rt-BaseMenuSubTrigger[data-state='open']) {
432
440
  /* Base state: solid accent for solid panels */
433
441
  background-color: var(--accent-3);
434
-
442
+
435
443
  /* Theme-level translucent override */
436
444
  :where([data-panel-background='translucent']) & {
437
445
  background-color: var(--accent-a3);
@@ -450,11 +458,11 @@
450
458
  /* No backdrop-filter here to prevent double-blur with container */
451
459
  }
452
460
  }
453
-
461
+
454
462
  & :where(.rt-BaseMenuItem[data-highlighted]) {
455
463
  /* Base state: solid accent for solid panels */
456
464
  background-color: var(--accent-4);
457
-
465
+
458
466
  /* Theme-level translucent override */
459
467
  :where([data-panel-background='translucent']) & {
460
468
  background-color: var(--accent-a4);
@@ -486,11 +494,11 @@
486
494
  outline: 2px solid Highlight;
487
495
  outline-offset: 2px;
488
496
  }
489
-
497
+
490
498
  .rt-BaseMenuContent {
491
499
  border: 1px solid CanvasText;
492
500
  }
493
-
501
+
494
502
  .rt-BaseMenuSeparator {
495
503
  background-color: CanvasText;
496
504
  }
@@ -99,26 +99,26 @@
99
99
  & :where(.rt-SidebarMenuSubTriggerIcon) {
100
100
  transition: transform var(--motion-duration-micro) var(--motion-ease-standard);
101
101
  }
102
-
103
- &:where([data-state="open"]) :where(.rt-SidebarMenuSubTriggerIcon) {
102
+
103
+ &:where([data-state='open']) :where(.rt-SidebarMenuSubTriggerIcon) {
104
104
  transform: rotate(90deg);
105
105
  }
106
106
  }
107
107
 
108
108
  .rt-SidebarMenuSubContent {
109
109
  overflow: hidden;
110
-
110
+
111
111
  /* Allow focus outlines to show */
112
112
  &:where(:focus-within) {
113
113
  overflow: visible;
114
114
  }
115
-
115
+
116
116
  /* Radix Accordion animation support */
117
- &:where([data-state="open"]) {
117
+ &:where([data-state='open']) {
118
118
  animation: rt-sidebar-slide-down var(--motion-duration-small) var(--motion-ease-standard);
119
119
  }
120
-
121
- &:where([data-state="closed"]) {
120
+
121
+ &:where([data-state='closed']) {
122
122
  animation: rt-sidebar-slide-up var(--motion-duration-small) var(--motion-ease-standard);
123
123
  }
124
124
  }
@@ -195,7 +195,7 @@
195
195
  :where(.rt-SidebarContent.rt-r-size-1) & {
196
196
  padding-right: var(--base-menu-item-padding-y); /* Matches top/bottom padding */
197
197
  }
198
-
198
+
199
199
  :where(.rt-SidebarContent.rt-r-size-2) & {
200
200
  padding-right: var(--base-menu-item-padding-y); /* Matches top/bottom padding */
201
201
  }
@@ -220,4 +220,4 @@
220
220
  .rt-SidebarContent :where(.rt-BaseMenuItem) {
221
221
  margin-top: calc(var(--space-1) / 2);
222
222
  margin-bottom: calc(var(--space-1) / 2);
223
- }
223
+ }
@@ -3,12 +3,12 @@
3
3
  /* Sidebar Provider - handles positioning */
4
4
  .rt-SidebarProvider {
5
5
  /* Positioning based on side */
6
- &:where([data-side="left"]) {
6
+ &:where([data-side='left']) {
7
7
  /* Left side is default - no additional positioning needed */
8
8
  order: -1; /* Ensure it appears first in flex container */
9
9
  }
10
-
11
- &:where([data-side="right"]) {
10
+
11
+ &:where([data-side='right']) {
12
12
  /* Position on the right side */
13
13
  order: 999; /* Push to end in flex container */
14
14
  }
@@ -18,7 +18,7 @@
18
18
  .rt-SidebarRoot {
19
19
  --sidebar-width: 16rem; /* Fixed width */
20
20
  --sidebar-base-border-radius: 0; /* Default to no radius */
21
-
21
+
22
22
  width: var(--sidebar-width);
23
23
  height: 100%;
24
24
  flex-shrink: 0;
@@ -34,13 +34,13 @@
34
34
  }
35
35
 
36
36
  /* Floating type - ONLY visual changes */
37
- &:where([data-type="floating"]) {
37
+ &:where([data-type='floating']) {
38
38
  border-radius: var(--sidebar-base-border-radius);
39
39
  margin: var(--space-2);
40
40
  height: calc(100% - var(--space-4));
41
41
  overflow: visible; /* Ensure shadow is not clipped */
42
42
  position: relative; /* Ensure proper stacking context for floating sidebars */
43
-
43
+
44
44
  /* Ensure Theme wrapper has proper border radius in floating mode */
45
45
  & :where(.radix-themes) {
46
46
  border-radius: inherit;
@@ -49,14 +49,14 @@
49
49
  }
50
50
 
51
51
  /* Set border radius for floating sidebars based on size - set on the root where it's used */
52
- .rt-SidebarRoot:where([data-type="floating"]) {
52
+ .rt-SidebarRoot:where([data-type='floating']) {
53
53
  /* Default radius for floating sidebars */
54
54
  --sidebar-base-border-radius: var(--radius-5);
55
-
55
+
56
56
  &:where(.rt-r-size-1) {
57
57
  --sidebar-base-border-radius: var(--radius-5);
58
58
  }
59
-
59
+
60
60
  &:where(.rt-r-size-2) {
61
61
  --sidebar-base-border-radius: var(--radius-6);
62
62
  }
@@ -133,9 +133,9 @@
133
133
  margin: var(--space-2) 0;
134
134
  }
135
135
 
136
- /* Responsive behavior */
136
+ /* Responsive behavior - only hide when not in Sheet overlay mode */
137
137
  @media (max-width: 768px) {
138
- .rt-SidebarRoot {
138
+ .rt-SidebarRoot:not(:where(.rt-SheetContent .rt-SidebarRoot)) {
139
139
  display: none;
140
140
  }
141
- }
141
+ }
@@ -1 +1,13 @@
1
1
  @import './_internal/base-dialog.css';
2
+
3
+ /* Add blur effect to Dialog overlay */
4
+ .rt-DialogOverlay::before {
5
+ backdrop-filter: var(--backdrop-filter-components) !important;
6
+ }
7
+
8
+ /* Safari backdrop-filter fallback */
9
+ @supports not (backdrop-filter: blur(1px)) {
10
+ .rt-DialogOverlay::before {
11
+ backdrop-filter: none !important;
12
+ }
13
+ }
@@ -55,7 +55,17 @@
55
55
  }
56
56
 
57
57
  /* Overlay adjustments: avoid double-fade jank from base dialog */
58
- :where(.rt-SheetOverlay)::before { opacity: 1; }
58
+ .rt-SheetOverlay::before {
59
+ opacity: 1 !important;
60
+ backdrop-filter: var(--backdrop-filter-components) !important;
61
+ }
62
+
63
+ /* Safari backdrop-filter fallback */
64
+ @supports not (backdrop-filter: blur(1px)) {
65
+ .rt-SheetOverlay::before {
66
+ backdrop-filter: none !important;
67
+ }
68
+ }
59
69
 
60
70
  @media (prefers-reduced-motion: no-preference) {
61
71
  /* Override base dialog animations specifically for Sheet */
@@ -72,19 +82,37 @@
72
82
  animation-fill-mode: both;
73
83
  }
74
84
 
75
- .rt-SheetContent:where([data-state='open'][data-side='start']) { animation-name: rt-sheet-open-from-start, rt-fade-in !important; }
76
- .rt-SheetContent:where([data-state='closed'][data-side='start']) { animation-name: rt-sheet-close-to-start, rt-fade-out !important; }
85
+ .rt-SheetContent:where([data-state='open'][data-side='start']) {
86
+ animation-name: rt-sheet-open-from-start, rt-fade-in !important;
87
+ }
88
+ .rt-SheetContent:where([data-state='closed'][data-side='start']) {
89
+ animation-name: rt-sheet-close-to-start, rt-fade-out !important;
90
+ }
77
91
 
78
- .rt-SheetContent:where([data-state='open'][data-side='end']) { animation-name: rt-sheet-open-from-end, rt-fade-in !important; }
79
- .rt-SheetContent:where([data-state='closed'][data-side='end']) { animation-name: rt-sheet-close-to-end, rt-fade-out !important; }
92
+ .rt-SheetContent:where([data-state='open'][data-side='end']) {
93
+ animation-name: rt-sheet-open-from-end, rt-fade-in !important;
94
+ }
95
+ .rt-SheetContent:where([data-state='closed'][data-side='end']) {
96
+ animation-name: rt-sheet-close-to-end, rt-fade-out !important;
97
+ }
80
98
 
81
- .rt-SheetContent:where([data-state='open'][data-side='top']) { animation-name: rt-sheet-open-from-top, rt-fade-in !important; }
82
- .rt-SheetContent:where([data-state='closed'][data-side='top']) { animation-name: rt-sheet-close-to-top, rt-fade-out !important; }
99
+ .rt-SheetContent:where([data-state='open'][data-side='top']) {
100
+ animation-name: rt-sheet-open-from-top, rt-fade-in !important;
101
+ }
102
+ .rt-SheetContent:where([data-state='closed'][data-side='top']) {
103
+ animation-name: rt-sheet-close-to-top, rt-fade-out !important;
104
+ }
83
105
 
84
- .rt-SheetContent:where([data-state='open'][data-side='bottom']) { animation-name: rt-sheet-open-from-bottom, rt-fade-in !important; }
85
- .rt-SheetContent:where([data-state='closed'][data-side='bottom']) { animation-name: rt-sheet-close-to-bottom, rt-fade-out !important; }
106
+ .rt-SheetContent:where([data-state='open'][data-side='bottom']) {
107
+ animation-name: rt-sheet-open-from-bottom, rt-fade-in !important;
108
+ }
109
+ .rt-SheetContent:where([data-state='closed'][data-side='bottom']) {
110
+ animation-name: rt-sheet-close-to-bottom, rt-fade-out !important;
111
+ }
86
112
  }
87
113
 
88
114
  @media (prefers-reduced-motion: reduce) {
89
- .rt-SheetContent { animation: none !important; }
115
+ .rt-SheetContent {
116
+ animation: none !important;
117
+ }
90
118
  }
@@ -18,7 +18,9 @@
18
18
  --shell-overlay-width: auto;
19
19
  }
20
20
  @supports (height: 100dvh) {
21
- .rt-ShellRoot { block-size: 100dvh; }
21
+ .rt-ShellRoot {
22
+ block-size: 100dvh;
23
+ }
22
24
  }
23
25
 
24
26
  /* Global Header (sticky) */
@@ -27,7 +29,8 @@
27
29
  top: 0;
28
30
  inset-inline: 0;
29
31
  z-index: var(--shell-z-header, 10);
30
- block-size: var(--shell-header-height, 64px);
32
+ height: var(--shell-header-height, 64px);
33
+ min-height: var(--shell-header-height, 64px);
31
34
  display: flex;
32
35
  align-items: center;
33
36
  background-color: var(--color-panel);
@@ -85,8 +88,6 @@
85
88
  overflow: hidden;
86
89
  }
87
90
 
88
-
89
-
90
91
  /* Single-markup morph container */
91
92
  .rt-ShellSidebarSingle {
92
93
  /* Staggered animation: width first, then content fade */
@@ -106,7 +107,8 @@
106
107
  /* Exit animation: fade out content first, then collapse width */
107
108
  transition:
108
109
  opacity var(--motion-duration-small) var(--motion-ease-standard),
109
- inline-size var(--motion-duration-small) var(--motion-ease-standard) var(--motion-duration-small);
110
+ inline-size var(--motion-duration-small) var(--motion-ease-standard)
111
+ var(--motion-duration-small);
110
112
  }
111
113
 
112
114
  /* When visible: show content after width settles */
@@ -135,3 +137,19 @@
135
137
  :where(.rt-SheetContent) :where(.rt-ShellSidebarPanel) {
136
138
  transition: none !important;
137
139
  }
140
+
141
+ /* Overlay mode: ensure sidebar content has proper styling when rendered in Sheet */
142
+ :where(.rt-SheetContent) :where(.rt-SidebarRoot) {
143
+ height: 100%;
144
+ width: 100%;
145
+ }
146
+
147
+ :where(.rt-SheetContent) :where(.rt-SidebarContainer) {
148
+ height: 100%;
149
+ width: 100%;
150
+ }
151
+
152
+ :where(.rt-SheetContent) :where(.rt-SidebarContent) {
153
+ height: 100%;
154
+ width: 100%;
155
+ }
@@ -161,6 +161,8 @@ interface ShellRootProps extends React.ComponentPropsWithoutRef<'div'> {
161
161
  onToolChange?: (id: string | null) => void;
162
162
  activeContext?: string | null;
163
163
  onContextChange?: (id: string | null) => void;
164
+ /** Custom cycling order for single-markup sidebars. Defaults to ['panel', 'rail', 'collapsed'] */
165
+ singleViewCycle?: SingleView[];
164
166
  }
165
167
 
166
168
  const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(
@@ -175,6 +177,7 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(
175
177
  onToolChange,
176
178
  activeContext: activeContextProp,
177
179
  onContextChange,
180
+ singleViewCycle = ['panel', 'rail', 'collapsed'],
178
181
  className,
179
182
  style,
180
183
  children,
@@ -237,13 +240,13 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(
237
240
  }, []);
238
241
  const cycleSingleView = React.useCallback(
239
242
  (side: ShellSide) => {
240
- const order: SingleView[] = ['panel', 'rail', 'collapsed'];
243
+ const order = singleViewCycle;
241
244
  const current = singleViewBySide[side];
242
245
  const idx = order.indexOf(current);
243
246
  const next = order[(idx + 1) % order.length];
244
247
  setSingleViewBySide(side, next);
245
248
  },
246
- [singleViewBySide, setSingleViewBySide],
249
+ [singleViewBySide, setSingleViewBySide, singleViewCycle],
247
250
  );
248
251
 
249
252
  // === Active tool coordination state ===
@@ -433,7 +436,6 @@ const Header = React.forwardRef<HTMLElement, ShellHeaderProps>(
433
436
  role="banner"
434
437
  className={classNames('rt-ShellHeader', className)}
435
438
  style={{
436
- ['--shell-header-height' as any]: headerHeight,
437
439
  ['--shell-z-header' as any]: zHeader,
438
440
  ...style,
439
441
  }}
@@ -131,4 +131,8 @@
131
131
  .rt-TableRoot:where(.rt-variant-ghost) {
132
132
  --scrollarea-scrollbar-horizontal-margin-left: 0;
133
133
  --scrollarea-scrollbar-horizontal-margin-right: 0;
134
+
135
+ & :where(.rt-TableBody) :where(.rt-TableRow:last-child) {
136
+ --table-row-box-shadow: none;
137
+ }
134
138
  }
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
 
3
- let bodyCleanupInstalled = false;
3
+ let cleanupInstalled = false;
4
4
 
5
5
  /**
6
6
  * Hook to cleanup stuck pointer-events: none on document.body
@@ -12,70 +12,101 @@ let bodyCleanupInstalled = false;
12
12
  export function useBodyPointerEventsCleanup() {
13
13
  React.useEffect(() => {
14
14
  if (typeof document === 'undefined') return;
15
+ if (cleanupInstalled) return;
15
16
 
16
- let timeoutId: number | undefined;
17
+ cleanupInstalled = true;
17
18
 
18
19
  const hasOpenModal = (): boolean => {
19
- // Detect any open modal dialogs/alertdialogs
20
- return Boolean(
20
+ // Check for open dialogs/alertdialogs
21
+ const hasDialogs = Boolean(
21
22
  document.querySelector(
22
23
  '[role="dialog"][aria-modal="true"], [role="alertdialog"][aria-modal="true"]',
23
24
  ),
24
25
  );
26
+
27
+ // Also check for any Radix overlays that are still mounted
28
+ const hasRadixOverlays = Boolean(
29
+ document.querySelector('[data-radix-popper-content-wrapper], [data-state="open"]'),
30
+ );
31
+
32
+ return hasDialogs || hasRadixOverlays;
25
33
  };
26
34
 
27
- const cleanup = () => {
35
+ const forceCleanup = () => {
36
+ // Aggressive cleanup - remove pointer-events regardless
37
+ if (document.body.style.pointerEvents === 'none') {
38
+ console.log('[KookieUI] Force cleaning stuck pointer-events: none from body');
39
+ document.body.style.pointerEvents = '';
40
+
41
+ // Also remove any scroll-lock related attributes
42
+ document.body.removeAttribute('data-scroll-locked');
43
+ document.body.removeAttribute('data-remove-scroll');
44
+
45
+ // Remove any classes that might be causing issues
46
+ document.body.classList.remove('ReactModal__Body--open');
47
+ }
48
+ };
49
+
50
+ const safeCleanup = () => {
28
51
  if (document.body.style.pointerEvents === 'none' && !hasOpenModal()) {
52
+ console.log('[KookieUI] Safe cleaning stuck pointer-events: none from body');
29
53
  document.body.style.pointerEvents = '';
30
54
  }
31
55
  };
32
56
 
33
- const scheduleCleanup = (delay = 50) => {
34
- if (timeoutId) window.clearTimeout(timeoutId);
35
- timeoutId = window.setTimeout(cleanup, delay);
57
+ // Force cleanup on any click outside modal content
58
+ const onDocumentClick = (event: Event) => {
59
+ const target = event.target as Element;
60
+ if (
61
+ target &&
62
+ !target.closest(
63
+ '[role="dialog"], [role="alertdialog"], [data-radix-popper-content-wrapper]',
64
+ )
65
+ ) {
66
+ // Clicked outside any modal - force cleanup after a short delay
67
+ setTimeout(forceCleanup, 100);
68
+ }
36
69
  };
37
70
 
38
- // Initial run to catch already-stuck state
39
- scheduleCleanup(100);
71
+ // Force cleanup on ESC key
72
+ const onEscapeKey = (event: KeyboardEvent) => {
73
+ if (event.key === 'Escape') {
74
+ setTimeout(forceCleanup, 200);
75
+ }
76
+ };
40
77
 
41
- // If already installed globally, don't re-register listeners/observers
42
- if (bodyCleanupInstalled) {
43
- return () => {
44
- if (timeoutId) window.clearTimeout(timeoutId);
45
- };
46
- }
78
+ // Safe cleanup on other interactions
79
+ const onInteraction = () => {
80
+ setTimeout(safeCleanup, 50);
81
+ };
47
82
 
48
- bodyCleanupInstalled = true;
83
+ // Install global listeners
84
+ document.addEventListener('click', onDocumentClick, true);
85
+ document.addEventListener('keydown', onEscapeKey, true);
86
+ document.addEventListener('pointerup', onInteraction, true);
87
+ document.addEventListener('transitionend', onInteraction, true);
88
+ document.addEventListener('animationend', onInteraction, true);
49
89
 
50
- const onPointer = () => scheduleCleanup(50);
51
- const onKey = (event: KeyboardEvent) => {
52
- if (event.key === 'Escape' || event.key === 'Enter' || event.key === ' ') scheduleCleanup(50);
53
- };
54
- const onVisibility = () => {
55
- if (!document.hidden) scheduleCleanup(50);
56
- };
57
- const onTransitionEnd = () => scheduleCleanup(0);
58
- const onAnimationEnd = () => scheduleCleanup(0);
59
-
60
- // Listen for common interactions that close overlays/menus
61
- document.addEventListener('pointerup', onPointer, true);
62
- document.addEventListener('click', onPointer, true);
63
- document.addEventListener('keydown', onKey, true);
64
- document.addEventListener('keyup', onKey, true);
65
- document.addEventListener('visibilitychange', onVisibility);
66
- document.addEventListener('transitionend', onTransitionEnd, true);
67
- document.addEventListener('animationend', onAnimationEnd, true);
68
-
69
- // Observe body style changes (where pointer-events is applied) and DOM mutations
70
- const bodyObserver = new MutationObserver(() => scheduleCleanup(0));
71
- bodyObserver.observe(document.body, { attributes: true, attributeFilter: ['style'] });
72
-
73
- const domObserver = new MutationObserver(() => scheduleCleanup(0));
74
- domObserver.observe(document, { childList: true, subtree: true });
75
-
76
- // Keep listeners/observers for the app lifetime to ensure cleanup even after overlays unmount
90
+ // Watch for DOM changes that might indicate overlay removal
91
+ const observer = new MutationObserver(() => {
92
+ setTimeout(safeCleanup, 0);
93
+ });
94
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true });
95
+
96
+ // Fallback periodic cleanup
97
+ const intervalId = setInterval(() => {
98
+ if (document.body.style.pointerEvents === 'none' && !hasOpenModal()) {
99
+ console.log('[KookieUI] Periodic cleanup of stuck pointer-events');
100
+ document.body.style.pointerEvents = '';
101
+ }
102
+ }, 1000);
103
+
104
+ // Initial cleanup
105
+ setTimeout(safeCleanup, 100);
106
+
107
+ // Cleanup function (keep listeners for app lifetime)
77
108
  return () => {
78
- if (timeoutId) window.clearTimeout(timeoutId);
109
+ clearInterval(intervalId);
79
110
  };
80
111
  }, []);
81
112
  }
@@ -3,7 +3,7 @@
3
3
  --cursor-checkbox: default;
4
4
  --cursor-disabled: not-allowed;
5
5
  --cursor-link: pointer;
6
- --cursor-menu-item: default;
6
+ --cursor-menu-item: pointer;
7
7
  --cursor-radio: default;
8
8
  --cursor-slider-thumb: default;
9
9
  --cursor-slider-thumb-active: default;