@underverse-ui/underverse 1.0.32 → 1.0.34

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.
package/AGENTS.md CHANGED
@@ -51,13 +51,23 @@ import { OverlayScrollbarProvider } from "@underverse-ui/underverse";
51
51
  export function App() {
52
52
  return (
53
53
  <>
54
- <OverlayScrollbarProvider />
54
+ <OverlayScrollbarProvider
55
+ enabled
56
+ theme="os-theme-underverse"
57
+ autoHide="leave"
58
+ />
55
59
  {/* app */}
56
60
  </>
57
61
  );
58
62
  }
59
63
  ```
60
64
 
65
+ Behavior:
66
+
67
+ - Provider initializes globally by default on common scroll selectors (`.overflow-*`, `textarea`) and `[data-os-scrollbar]`.
68
+ - Provider can run globally via custom `selector` (for example: `.overflow-auto, .overflow-y-auto, .overflow-x-auto, [data-os-scrollbar]`).
69
+ - Use `data-os-ignore` on a node to opt out.
70
+
61
71
  ## i18n Notes
62
72
 
63
73
  - Components work without `next-intl` using fallback translations.
package/CHANGELOG.md ADDED
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@underverse-ui/underverse` are documented in this file.
4
+
5
+ ## [1.0.32] - 2026-02-24
6
+
7
+ ### Changed
8
+
9
+ - Standardized OverlayScrollbars initialization to explicit marker targeting only:
10
+ - Provider now initializes only on `[data-os-scrollbar]`.
11
+ - Removed generic overflow class scanning (`.overflow-*`).
12
+ - Hardened provider behavior for production:
13
+ - No initialization on `document.body` / `document.documentElement`.
14
+ - Excludes portal / modal / toast trees:
15
+ - `[data-radix-portal]`
16
+ - `[role="dialog"]`
17
+ - `[aria-modal="true"]`
18
+ - `[data-sonner-toaster]`
19
+ - Supports node-level opt-out with `data-os-ignore`.
20
+ - Added provider configuration props:
21
+ - `enabled` (default `true`)
22
+ - `theme` (default `os-theme-underverse`)
23
+ - `visibility`
24
+ - `autoHide`
25
+ - `autoHideDelay`
26
+ - `dragScroll`
27
+ - `clickScroll`
28
+ - `selector` (default `.overflow-auto, .overflow-y-auto, .overflow-x-auto, .overflow-scroll, .overflow-y-scroll, .overflow-x-scroll, textarea, [data-os-scrollbar]`)
29
+ - `exclude` (default `html, body, [data-os-ignore], [data-radix-portal], [role='dialog'], [aria-modal='true'], [data-sonner-toaster]`)
30
+ - Exported provider prop type:
31
+ - `OverlayScrollbarProviderProps`
32
+
33
+ ### Updated Components
34
+
35
+ - Provider now covers common scrollable surfaces by default via global selector, so Underverse components do not require per-component manual marker wiring.
36
+
37
+ ### Internal
38
+
39
+ - `Popover` now sets `role="dialog"` only when `modal=true`, avoiding accidental exclusion for non-modal popovers.
40
+ - Moved to default global selector behavior, so apps no longer need to add `data-os-scrollbar` manually to each Underverse component.
41
+
42
+ ### Testing
43
+
44
+ - Added controller-level tests for:
45
+ - selector initialization
46
+ - exclude behavior
47
+ - dynamic add/remove cleanup
48
+ - portal safety with wide selectors
49
+ - destroy cleanup (memory leak prevention)
50
+
51
+ ### Migration
52
+
53
+ - Mount a single `OverlayScrollbarProvider` from the package at app root.
54
+ - Remove app-local DOM-scanning scrollbar providers.
55
+ - Keep `overlayscrollbars/overlayscrollbars.css` imported globally.
56
+ - Default is already global selector mode. Override `selector` only if you need custom scope.
57
+ - For app-specific opt-out nodes, use `data-os-ignore`.
package/README.md CHANGED
@@ -138,6 +138,7 @@ import { Form, FormField, FormItem, FormLabel, FormMessage } from "@underverse-u
138
138
  ### Overlay Scrollbars (Optional, Recommended)
139
139
 
140
140
  Use `OverlayScrollbarProvider` to get overlay scrollbars (no layout space taken) across your app and Underverse components.
141
+ See release notes in `CHANGELOG.md` for migration details by version.
141
142
 
142
143
  ```tsx
143
144
  import "overlayscrollbars/overlayscrollbars.css";
@@ -146,13 +147,52 @@ import { OverlayScrollbarProvider } from "@underverse-ui/underverse";
146
147
  function App() {
147
148
  return (
148
149
  <>
149
- <OverlayScrollbarProvider />
150
+ <OverlayScrollbarProvider
151
+ enabled
152
+ theme="os-theme-underverse"
153
+ autoHide="leave"
154
+ autoHideDelay={600}
155
+ />
150
156
  {/* your app */}
151
157
  </>
152
158
  );
153
159
  }
154
160
  ```
155
161
 
162
+ Provider behavior:
163
+
164
+ - Initializes globally by default on common scroll containers (`.overflow-*`, `textarea`) and `[data-os-scrollbar]`.
165
+ - Does **not** initialize on `document.body` / `document.documentElement`.
166
+ - Skips portal/dialog trees (`[data-radix-portal]`, `[role="dialog"]`, `[aria-modal="true"]`, `[data-sonner-toaster]`).
167
+ - Per-node opt-out is available via `data-os-ignore`.
168
+
169
+ Provider props:
170
+
171
+ - `enabled?: boolean`
172
+ - `theme?: string`
173
+ - `visibility?: "visible" | "hidden" | "auto"`
174
+ - `autoHide?: "never" | "scroll" | "leave" | "move"`
175
+ - `autoHideDelay?: number`
176
+ - `dragScroll?: boolean`
177
+ - `clickScroll?: boolean`
178
+ - `selector?: string` default: `.overflow-auto, .overflow-y-auto, .overflow-x-auto, .overflow-scroll, .overflow-y-scroll, .overflow-x-scroll, textarea, [data-os-scrollbar]`
179
+ - `exclude?: string` default: `html, body, [data-os-ignore], [data-radix-portal], [role='dialog'], [aria-modal='true'], [data-sonner-toaster]`
180
+
181
+ Custom selector mode example:
182
+
183
+ ```tsx
184
+ <OverlayScrollbarProvider
185
+ selector=".overflow-auto, .overflow-y-auto, .overflow-x-auto, [data-os-scrollbar]"
186
+ />
187
+ ```
188
+
189
+ Migration notes:
190
+
191
+ - Remove any local DOM-scanning scrollbar provider in your app.
192
+ - Keep a single `OverlayScrollbarProvider` mounted once at app root.
193
+ - You no longer need to add `data-os-scrollbar` to every Underverse component manually.
194
+ - Use `data-os-scrollbar` only for custom scroll nodes that are not covered by your selector.
195
+
156
196
  ### Standalone React (Vite, CRA, etc.)
157
197
 
158
198
  ```tsx
@@ -26,7 +26,12 @@
26
26
  {
27
27
  "id": "overlay-scrollbar-provider",
28
28
  "title": "Enable overlay scrollbars",
29
- "code": "import \"overlayscrollbars/overlayscrollbars.css\";\nimport { OverlayScrollbarProvider } from \"@underverse-ui/underverse\";\n\nexport function App(){\n return <><OverlayScrollbarProvider />{/* app */}</>;\n}\n"
29
+ "code": "import \"overlayscrollbars/overlayscrollbars.css\";\nimport { OverlayScrollbarProvider } from \"@underverse-ui/underverse\";\n\nexport function App(){\n return <><OverlayScrollbarProvider enabled theme=\"os-theme-underverse\" autoHide=\"leave\" />{/* app */}</>;\n}\n"
30
+ },
31
+ {
32
+ "id": "overlay-scrollbar-global-selector",
33
+ "title": "Enable global selector mode",
34
+ "code": "import \"overlayscrollbars/overlayscrollbars.css\";\nimport { OverlayScrollbarProvider } from \"@underverse-ui/underverse\";\n\nexport function App(){\n return <><OverlayScrollbarProvider selector=\".overflow-auto, .overflow-y-auto, .overflow-x-auto, [data-os-scrollbar]\" />{/* app */}</>;\n}\n"
30
35
  },
31
36
  {
32
37
  "id": "api-discovery-order",
@@ -39,4 +44,3 @@
39
44
  }
40
45
  ]
41
46
  }
42
-
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "package": "@underverse-ui/underverse",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "sourceEntry": "src/index.ts",
5
- "totalExports": 206,
5
+ "totalExports": 207,
6
6
  "exports": [
7
7
  {
8
8
  "name": "*",
@@ -862,6 +862,13 @@
862
862
  "local": false,
863
863
  "aliasOf": "default"
864
864
  },
865
+ {
866
+ "name": "OverlayScrollbarProviderProps",
867
+ "kind": "type",
868
+ "source": "../../../components/ui/OverlayScrollbarProvider",
869
+ "reexport": true,
870
+ "local": false
871
+ },
865
872
  {
866
873
  "name": "PageLoading",
867
874
  "kind": "value",
package/dist/index.cjs CHANGED
@@ -3820,6 +3820,8 @@ var Popover = ({
3820
3820
  children,
3821
3821
  className,
3822
3822
  contentClassName,
3823
+ contentProps,
3824
+ contentScrollable = false,
3823
3825
  placement = "bottom",
3824
3826
  modal = false,
3825
3827
  disabled = false,
@@ -3991,7 +3993,7 @@ var Popover = ({
3991
3993
  {
3992
3994
  ref: panelRef,
3993
3995
  "data-state": "open",
3994
- role: "dialog",
3996
+ role: modal ? "dialog" : void 0,
3995
3997
  "aria-modal": modal || void 0,
3996
3998
  style: {
3997
3999
  transformOrigin: getTransformOrigin2(initialPlacement.side, initialPlacement.align)
@@ -4004,12 +4006,14 @@ var Popover = ({
4004
4006
  children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4005
4007
  "div",
4006
4008
  {
4009
+ ...contentProps,
4007
4010
  className: cn(
4008
4011
  "rounded-2xl md:rounded-3xl border bg-popover text-popover-foreground shadow-md",
4009
4012
  "backdrop-blur-sm bg-popover/95 border-border/60 p-4",
4013
+ contentProps?.className,
4010
4014
  contentClassName
4011
4015
  ),
4012
- tabIndex: -1,
4016
+ tabIndex: contentProps?.tabIndex ?? -1,
4013
4017
  children
4014
4018
  }
4015
4019
  )
@@ -5828,7 +5832,7 @@ var ScrollArea = (0, import_react14.forwardRef)(
5828
5832
  className
5829
5833
  ),
5830
5834
  ...props,
5831
- children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: cn("h-full w-full overflow-y-auto scroll-area-viewport custom-scrollbar", contentClassName), "data-os-scrollbar": true, children })
5835
+ children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: cn("h-full w-full overflow-y-auto scroll-area-viewport custom-scrollbar", contentClassName), children })
5832
5836
  }
5833
5837
  );
5834
5838
  }
@@ -11903,7 +11907,6 @@ function CalendarTimeline({
11903
11907
  {
11904
11908
  ref: bodyRef,
11905
11909
  className: "relative flex-1 overflow-auto custom-scrollbar",
11906
- "data-os-scrollbar": true,
11907
11910
  onPointerMove,
11908
11911
  onPointerUp,
11909
11912
  onPointerLeave: () => setHoverCell(null),
@@ -14122,7 +14125,6 @@ function CategoryTreeSelect(props) {
14122
14125
  "p-2",
14123
14126
  "animate-in fade-in-0 zoom-in-95 slide-in-from-top-2 duration-300"
14124
14127
  ),
14125
- "data-os-scrollbar": true,
14126
14128
  children: [
14127
14129
  renderSearch(),
14128
14130
  renderTreeContent()
@@ -19018,81 +19020,161 @@ var LoadingBar = ({
19018
19020
  // ../../components/ui/OverlayScrollbarProvider.tsx
19019
19021
  var import_react33 = require("react");
19020
19022
  var import_overlayscrollbars = require("overlayscrollbars");
19021
- var EXPLICIT_SELECTOR = [".thin-scrollbar", ".scrollbar-thin", ".custom-scrollbar", "[data-os-scrollbar]"].join(", ");
19022
- var PORTAL_EXCLUDE_SELECTOR = [
19023
+
19024
+ // ../../components/ui/overlay-scrollbar-controller.ts
19025
+ var DEFAULT_OVERLAY_SCROLLBAR_SELECTOR = [
19026
+ ".overflow-auto",
19027
+ ".overflow-y-auto",
19028
+ ".overflow-x-auto",
19029
+ ".overflow-scroll",
19030
+ ".overflow-y-scroll",
19031
+ ".overflow-x-scroll",
19032
+ "textarea",
19033
+ "[data-os-scrollbar]"
19034
+ ].join(", ");
19035
+ var DEFAULT_OVERLAY_SCROLLBAR_EXCLUDE = [
19036
+ "html",
19037
+ "body",
19038
+ "[data-os-ignore]",
19023
19039
  "[data-radix-portal]",
19024
19040
  "[role='dialog']",
19025
19041
  "[aria-modal='true']",
19026
19042
  "[data-sonner-toaster]"
19027
19043
  ].join(", ");
19028
- var OPTIONS = {
19029
- scrollbars: {
19030
- theme: "os-theme-underverse",
19031
- visibility: "auto",
19032
- autoHide: "leave",
19033
- autoHideDelay: 600,
19034
- dragScroll: true,
19035
- clickScroll: false
19044
+ function splitSelectorList(selectorList) {
19045
+ return selectorList.split(",").map((part) => part.trim()).filter(Boolean);
19046
+ }
19047
+ function safeMatches(element, selector) {
19048
+ try {
19049
+ return element.matches(selector);
19050
+ } catch {
19051
+ return false;
19036
19052
  }
19037
- };
19038
- function shouldSkip(element) {
19039
- if (element === document.body || element === document.documentElement) return true;
19040
- if (element.hasAttribute("data-os-ignore")) return true;
19053
+ }
19054
+ function safeClosest(element, selector) {
19055
+ try {
19056
+ return element.closest(selector);
19057
+ } catch {
19058
+ return null;
19059
+ }
19060
+ }
19061
+ function shouldSkipElement(element, excludeSelectors, ancestorExcludeSelectors) {
19062
+ const tagName = element.tagName?.toLowerCase?.() ?? "";
19063
+ if (tagName === "body" || tagName === "html") return true;
19064
+ if (element.classList.contains("scrollbar-none")) return true;
19041
19065
  if (element.hasAttribute("data-overlayscrollbars")) return true;
19042
- if (element.closest(PORTAL_EXCLUDE_SELECTOR)) return true;
19066
+ if (excludeSelectors.some((selector) => safeMatches(element, selector))) return true;
19067
+ if (ancestorExcludeSelectors.some((selector) => safeClosest(element, selector))) return true;
19043
19068
  return false;
19044
19069
  }
19045
- function OverlayScrollbarProvider() {
19046
- (0, import_react33.useEffect)(() => {
19047
- const instances = /* @__PURE__ */ new Map();
19048
- let rafId = 0;
19049
- const init = (element) => {
19050
- if (shouldSkip(element)) return;
19051
- if (instances.has(element)) return;
19052
- instances.set(element, (0, import_overlayscrollbars.OverlayScrollbars)(element, OPTIONS));
19053
- };
19054
- const scan = (root) => {
19055
- if (root instanceof HTMLElement && root.matches(EXPLICIT_SELECTOR)) {
19056
- init(root);
19070
+ function createOverlayScrollbarController({
19071
+ selector,
19072
+ exclude,
19073
+ options,
19074
+ createInstance,
19075
+ root = document.body,
19076
+ createObserver,
19077
+ requestAnimationFrameImpl = requestAnimationFrame,
19078
+ cancelAnimationFrameImpl = cancelAnimationFrame
19079
+ }) {
19080
+ const instances = /* @__PURE__ */ new Map();
19081
+ const excludeSelectors = splitSelectorList(exclude);
19082
+ const ancestorExcludeSelectors = excludeSelectors.filter((item) => item !== "html" && item !== "body");
19083
+ let rafId = 0;
19084
+ const init = (element) => {
19085
+ if (shouldSkipElement(element, excludeSelectors, ancestorExcludeSelectors)) return;
19086
+ if (instances.has(element)) return;
19087
+ instances.set(element, createInstance(element, options));
19088
+ };
19089
+ const scan = (root2) => {
19090
+ if (root2 instanceof HTMLElement && safeMatches(root2, selector)) {
19091
+ init(root2);
19092
+ }
19093
+ if (!("querySelectorAll" in root2)) return;
19094
+ try {
19095
+ root2.querySelectorAll(selector).forEach(init);
19096
+ } catch {
19097
+ }
19098
+ };
19099
+ const cleanup = () => {
19100
+ instances.forEach((instance, element) => {
19101
+ if (!element.isConnected) {
19102
+ instance.destroy();
19103
+ instances.delete(element);
19057
19104
  }
19058
- root.querySelectorAll(EXPLICIT_SELECTOR).forEach(init);
19059
- };
19060
- const cleanup = () => {
19061
- instances.forEach((instance, element) => {
19062
- if (!element.isConnected) {
19063
- instance.destroy();
19064
- instances.delete(element);
19105
+ });
19106
+ };
19107
+ scan(root);
19108
+ const onMutations = (mutations) => {
19109
+ if (rafId) return;
19110
+ rafId = requestAnimationFrameImpl(() => {
19111
+ rafId = 0;
19112
+ const roots = /* @__PURE__ */ new Set();
19113
+ mutations.forEach((mutation) => {
19114
+ if (mutation.target && (typeof mutation.target.querySelectorAll === "function" || mutation.target instanceof HTMLElement)) {
19115
+ roots.add(mutation.target);
19065
19116
  }
19066
- });
19067
- };
19068
- scan(document.body);
19069
- const observer = new MutationObserver((mutations) => {
19070
- if (rafId) return;
19071
- rafId = requestAnimationFrame(() => {
19072
- rafId = 0;
19073
- const scanRoots = /* @__PURE__ */ new Set();
19074
- mutations.forEach((mutation) => {
19075
- if (mutation.target instanceof HTMLElement || mutation.target instanceof Document || mutation.target instanceof DocumentFragment) {
19076
- scanRoots.add(mutation.target);
19117
+ Array.from(mutation.addedNodes).forEach((node) => {
19118
+ if (node instanceof HTMLElement) {
19119
+ roots.add(node);
19077
19120
  }
19078
- mutation.addedNodes.forEach((node) => {
19079
- if (node instanceof HTMLElement) {
19080
- scanRoots.add(node);
19081
- }
19082
- });
19083
19121
  });
19084
- scanRoots.forEach(scan);
19085
- cleanup();
19086
19122
  });
19123
+ roots.forEach(scan);
19124
+ cleanup();
19125
+ });
19126
+ };
19127
+ const observer = createObserver ? createObserver(onMutations) : new MutationObserver(onMutations);
19128
+ observer.observe(root, { childList: true, subtree: true, attributes: true });
19129
+ const destroy = () => {
19130
+ if (rafId) cancelAnimationFrameImpl(rafId);
19131
+ observer.disconnect();
19132
+ instances.forEach((instance) => instance.destroy());
19133
+ instances.clear();
19134
+ };
19135
+ return {
19136
+ destroy,
19137
+ scan,
19138
+ cleanup,
19139
+ getInstanceCount: () => instances.size
19140
+ };
19141
+ }
19142
+
19143
+ // ../../components/ui/OverlayScrollbarProvider.tsx
19144
+ function OverlayScrollbarProvider({
19145
+ enabled = true,
19146
+ theme = "os-theme-underverse",
19147
+ visibility = "auto",
19148
+ autoHide = "leave",
19149
+ autoHideDelay = 600,
19150
+ dragScroll = true,
19151
+ clickScroll = false,
19152
+ selector = DEFAULT_OVERLAY_SCROLLBAR_SELECTOR,
19153
+ exclude = DEFAULT_OVERLAY_SCROLLBAR_EXCLUDE
19154
+ } = {}) {
19155
+ (0, import_react33.useEffect)(() => {
19156
+ if (typeof window === "undefined") return;
19157
+ if (!enabled) return;
19158
+ const options = {
19159
+ scrollbars: {
19160
+ theme,
19161
+ visibility,
19162
+ autoHide,
19163
+ autoHideDelay,
19164
+ dragScroll,
19165
+ clickScroll
19166
+ }
19167
+ };
19168
+ const controller = createOverlayScrollbarController({
19169
+ selector,
19170
+ exclude,
19171
+ options,
19172
+ createInstance: (element, instanceOptions) => (0, import_overlayscrollbars.OverlayScrollbars)(element, instanceOptions)
19087
19173
  });
19088
- observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class"] });
19089
19174
  return () => {
19090
- if (rafId) cancelAnimationFrame(rafId);
19091
- observer.disconnect();
19092
- instances.forEach((instance) => instance.destroy());
19093
- instances.clear();
19175
+ controller.destroy();
19094
19176
  };
19095
- }, []);
19177
+ }, [enabled, theme, visibility, autoHide, autoHideDelay, dragScroll, clickScroll, selector, exclude]);
19096
19178
  return null;
19097
19179
  }
19098
19180
  var OverlayScrollbarProvider_default = OverlayScrollbarProvider;
@@ -19114,7 +19196,6 @@ var Table = import_react34.default.forwardRef(({ className, containerClassName,
19114
19196
  "backdrop-blur-sm transition-all duration-300",
19115
19197
  containerClassName
19116
19198
  ),
19117
- "data-os-scrollbar": true,
19118
19199
  children: /* @__PURE__ */ (0, import_jsx_runtime65.jsx)("table", { ref, className: cn("w-full caption-bottom text-sm", className), ...props })
19119
19200
  }
19120
19201
  );
@@ -20046,13 +20127,8 @@ function DataTable({
20046
20127
  children: /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(
20047
20128
  "div",
20048
20129
  {
20049
- className: "custom-scrollbar w-full",
20050
- "data-os-scrollbar": true,
20051
- style: stickyHeader ? {
20052
- maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
20053
- overflowY: "auto",
20054
- overflowX: "auto"
20055
- } : { overflowX: "auto" },
20130
+ className: cn("w-full overflow-x-auto", stickyHeader && "overflow-y-auto"),
20131
+ style: stickyHeader ? { maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight } : void 0,
20056
20132
  children: /* @__PURE__ */ (0, import_jsx_runtime68.jsxs)(
20057
20133
  Table,
20058
20134
  {