@ahrowe/ui 0.40.0 → 0.41.1

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 (98) hide show
  1. package/README.md +13 -13
  2. package/dist/cjs-types/common/ahroweProvider/ahroweProvider.d.ts +4 -0
  3. package/dist/cjs-types/common/ahroweProvider/ahroweProvider.types.d.ts +12 -0
  4. package/dist/cjs-types/common/ahroweProvider/index.d.ts +2 -0
  5. package/dist/cjs-types/common/bodyEnd/portalHostContext.d.ts +6 -0
  6. package/dist/cjs-types/common/errorBoundary/errorBoundary.types.d.ts +2 -2
  7. package/dist/cjs-types/common/hooks/useOverlay.d.ts +4 -1
  8. package/dist/cjs-types/common/popover/popover.types.d.ts +1 -1
  9. package/dist/cjs-types/common/themeProvider/index.d.ts +1 -1
  10. package/dist/cjs-types/common/themeProvider/theme.types.d.ts +8 -0
  11. package/dist/cjs-types/common/themeProvider/themeProvider.d.ts +7 -9
  12. package/dist/cjs-types/common/themeProvider/themeVariablesContext.d.ts +2 -2
  13. package/dist/cjs-types/common/toast/index.d.ts +1 -2
  14. package/dist/cjs-types/common/toast/toast.types.d.ts +4 -4
  15. package/dist/cjs-types/common/toast/toastHost.d.ts +4 -0
  16. package/dist/cjs-types/common/toast/toastService.d.ts +1 -1
  17. package/dist/cjs-types/index.d.ts +2 -1
  18. package/dist/esm/common/ahroweProvider/ahroweProvider.mjs +2 -0
  19. package/dist/esm/common/ahroweProvider/ahroweProvider.mjs.map +1 -0
  20. package/dist/esm/common/bodyEnd/bodyEnd.mjs +1 -1
  21. package/dist/esm/common/bodyEnd/bodyEnd.mjs.map +1 -1
  22. package/dist/esm/common/bodyEnd/portalHostContext.mjs +2 -0
  23. package/dist/esm/common/bodyEnd/portalHostContext.mjs.map +1 -0
  24. package/dist/esm/common/calendar/calendar.mjs +1 -1
  25. package/dist/esm/common/calendar/calendar.mjs.map +1 -1
  26. package/dist/esm/common/carousel/carousel.module.mjs.map +1 -1
  27. package/dist/esm/common/errorBoundary/errorBoundary.mjs +2 -2
  28. package/dist/esm/common/hooks/useAnchorTracking.mjs.map +1 -1
  29. package/dist/esm/common/hooks/useOverlay.mjs +1 -1
  30. package/dist/esm/common/hooks/useOverlay.mjs.map +1 -1
  31. package/dist/esm/common/modal/modal.mjs +1 -1
  32. package/dist/esm/common/modal/modal.mjs.map +1 -1
  33. package/dist/esm/common/otpInput/otpInput.mjs +1 -1
  34. package/dist/esm/common/otpInput/otpInput.mjs.map +1 -1
  35. package/dist/esm/common/popover/popover.mjs +1 -1
  36. package/dist/esm/common/popover/popover.mjs.map +1 -1
  37. package/dist/esm/common/popover/popover.types.mjs.map +1 -1
  38. package/dist/esm/common/scrollbarProvider/scrollbarProvider.module.mjs.map +1 -1
  39. package/dist/esm/common/themeProvider/themeProvider.mjs +1 -1
  40. package/dist/esm/common/themeProvider/themeProvider.mjs.map +1 -1
  41. package/dist/esm/common/themeProvider/themeVariablesContext.mjs.map +1 -1
  42. package/dist/esm/common/timer/timer.mjs +1 -1
  43. package/dist/esm/common/timer/timer.mjs.map +1 -1
  44. package/dist/esm/common/toast/toastHost.mjs +2 -0
  45. package/dist/esm/common/toast/toastHost.mjs.map +1 -0
  46. package/dist/esm/common/toast/toastService.mjs +1 -1
  47. package/dist/esm/common/toast/toastService.mjs.map +1 -1
  48. package/dist/esm/common/tooltip/tooltip.module.mjs.map +1 -1
  49. package/dist/esm/common/virtualList/virtualList.mjs +1 -1
  50. package/dist/esm/common/virtualList/virtualList.mjs.map +1 -1
  51. package/dist/esm/index.mjs +1 -1
  52. package/dist/index.cjs +3 -3
  53. package/dist/index.cjs.map +1 -1
  54. package/dist/mcp.js +2 -2
  55. package/dist/style.css +1 -1
  56. package/dist/types/common/ahroweProvider/ahroweProvider.d.ts +4 -0
  57. package/dist/types/common/ahroweProvider/ahroweProvider.types.d.ts +12 -0
  58. package/dist/types/common/ahroweProvider/index.d.ts +2 -0
  59. package/dist/types/common/bodyEnd/portalHostContext.d.ts +6 -0
  60. package/dist/types/common/errorBoundary/errorBoundary.types.d.ts +2 -2
  61. package/dist/types/common/hooks/useOverlay.d.ts +4 -1
  62. package/dist/types/common/popover/popover.types.d.ts +1 -1
  63. package/dist/types/common/themeProvider/index.d.ts +1 -1
  64. package/dist/types/common/themeProvider/theme.types.d.ts +8 -0
  65. package/dist/types/common/themeProvider/themeProvider.d.ts +7 -9
  66. package/dist/types/common/themeProvider/themeVariablesContext.d.ts +2 -2
  67. package/dist/types/common/toast/index.d.ts +1 -2
  68. package/dist/types/common/toast/toast.types.d.ts +4 -4
  69. package/dist/types/common/toast/toastHost.d.ts +4 -0
  70. package/dist/types/common/toast/toastService.d.ts +1 -1
  71. package/dist/types/index.d.ts +2 -1
  72. package/docs/AhroweProvider.md +90 -0
  73. package/docs/BodyEnd.md +5 -12
  74. package/docs/Breadcrumb.md +0 -2
  75. package/docs/CLAUDE.md +14 -11
  76. package/docs/CommandPalette.md +0 -2
  77. package/docs/ConfigProvider.md +11 -8
  78. package/docs/ConfirmModal.md +0 -2
  79. package/docs/ErrorBoundary.md +5 -3
  80. package/docs/FormValidatorGroup.md +1 -1
  81. package/docs/KlipyPicker.md +1 -1
  82. package/docs/Menu.md +0 -2
  83. package/docs/Modal.md +1 -3
  84. package/docs/MultiSelect.md +0 -2
  85. package/docs/OtpInput.md +0 -2
  86. package/docs/Popover.md +2 -4
  87. package/docs/RadioGroup.md +1 -1
  88. package/docs/ScrollbarProvider.md +4 -0
  89. package/docs/SplitButton.md +0 -2
  90. package/docs/ThemeProvider.md +13 -10
  91. package/docs/Toast.md +18 -17
  92. package/docs/Tooltip.md +1 -1
  93. package/docs/VirtualList.md +0 -2
  94. package/package.json +1 -1
  95. package/dist/cjs-types/common/toast/toastProvider.d.ts +0 -8
  96. package/dist/esm/common/toast/toastProvider.mjs +0 -2
  97. package/dist/esm/common/toast/toastProvider.mjs.map +0 -1
  98. package/dist/types/common/toast/toastProvider.d.ts +0 -8
@@ -0,0 +1,4 @@
1
+ import { ReactElement } from 'react';
2
+ import { AhroweProviderProps } from './ahroweProvider.types.js';
3
+ declare function AhroweProvider({ children, themes, currentThemeId, defaultProps, presentation, toast, scrollbars, }: AhroweProviderProps): ReactElement;
4
+ export default AhroweProvider;
@@ -0,0 +1,12 @@
1
+ import { ConfigProviderProps } from '../configProvider/configProvider.types.js';
2
+ import { ThemeProviderProps } from '../themeProvider/theme.types.js';
3
+ import { ToastContextConfig } from '../toast/toast.types.js';
4
+ export interface AhroweProviderProps extends ConfigProviderProps, ThemeProviderProps {
5
+ /** Defaults for every toast `showToast` shows. Read by the outermost provider only. */
6
+ toast?: ToastContextConfig;
7
+ /**
8
+ * Styles every scrollbar inside the app, portalled panels included, in the theme's colours.
9
+ * Defaults to `true`. Read by the outermost provider only.
10
+ */
11
+ scrollbars?: boolean;
12
+ }
@@ -0,0 +1,2 @@
1
+ export { default } from './ahroweProvider.js';
2
+ export * from './ahroweProvider.types.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The element `AhroweProvider` portals into. `undefined` means there is no provider, so BodyEnd
3
+ * falls back to `#bodyEnd`; `null` means a provider that cannot portal yet, on the server and
4
+ * while hydrating, and must not be reported missing.
5
+ */
6
+ export declare const PortalHostContext: import('react').Context<HTMLElement | null | undefined>;
@@ -19,9 +19,9 @@ export interface ErrorBoundaryLabels {
19
19
  copied?: string;
20
20
  /** Reset button label. Default: "Try again". */
21
21
  reset?: string;
22
- /** Toast shown after a successful copy (only visible if `ToastProvider` is mounted). */
22
+ /** Toast shown after a successful copy, when the boundary is inside an `AhroweProvider`. */
23
23
  copySuccessToast?: string;
24
- /** Toast shown if the copy fails (only visible if `ToastProvider` is mounted). */
24
+ /** Toast shown if the copy fails, when the boundary is inside an `AhroweProvider`. */
25
25
  copyErrorToast?: string;
26
26
  }
27
27
  export interface ErrorBoundaryProps extends Omit<HtmlProps, 'title' | 'onError'> {
@@ -6,7 +6,10 @@ export interface UseOverlayArgs {
6
6
  containerRef: RefObject<HTMLElement | null>;
7
7
  /** Called when Escape is pressed while this is the topmost overlay. Omit to not handle Escape. */
8
8
  onEscape?: () => void;
9
- /** Keep Tab inside the panel and restore focus to the previously focused element on close. */
9
+ /**
10
+ * Keep Tab inside the panel and restore focus to the previously focused element on close, unless
11
+ * an overlay opened after this one is still open.
12
+ */
10
13
  trapFocus?: boolean;
11
14
  /** Prevent the page behind the overlay from scrolling. */
12
15
  lockScroll?: boolean;
@@ -53,7 +53,7 @@ export interface PopoverProps extends Omit<HtmlProps, 'content'> {
53
53
  offset?: number;
54
54
  /** Render the arrow pointing at the trigger (default `true`). */
55
55
  withArrow?: boolean;
56
- /** Close when clicking outside the panel (default `true`). */
56
+ /** Close on a click outside, or when focus leaves the trigger and panel (default `true`). */
57
57
  closeOnOutsideClick?: boolean;
58
58
  /** Close when pressing Escape (default `true`). */
59
59
  closeOnEscape?: boolean;
@@ -1,4 +1,4 @@
1
1
  export { default } from './themeProvider.js';
2
2
  export { defaultTheme } from './defaultTheme.js';
3
3
  export { lightTheme } from './lightTheme.js';
4
- export type { Theme, ThemeVariables } from './theme.types.js';
4
+ export type { Theme, ThemeProviderProps, ThemeVariables } from './theme.types.js';
@@ -1,3 +1,4 @@
1
+ import { ReactNode } from 'react';
1
2
  /**
2
3
  * All CSS custom properties used by the component library.
3
4
  * Every key is optional so a theme only needs to declare the variables it overrides.
@@ -278,3 +279,10 @@ export interface Theme {
278
279
  baseThemeId?: string;
279
280
  variables: ThemeVariables;
280
281
  }
282
+ export interface ThemeProviderProps {
283
+ children: ReactNode;
284
+ /** The themes to choose from. Defaults to `[defaultTheme]`. */
285
+ themes?: Theme[];
286
+ /** The `id` of the theme to apply, from `themes`. */
287
+ currentThemeId?: string;
288
+ }
@@ -1,10 +1,6 @@
1
1
  import { default as React, Component } from 'react';
2
- import { Theme } from './theme.types.js';
3
- interface Props {
4
- children: React.ReactNode;
5
- themes?: Theme[];
6
- currentThemeId?: string;
7
- }
2
+ import { Theme, ThemeProviderProps } from './theme.types.js';
3
+ import { ThemeVariablesContext } from './themeVariablesContext.js';
8
4
  interface State {
9
5
  resolvedVariables: Record<string, string>;
10
6
  colorFadeActive: boolean;
@@ -15,15 +11,17 @@ interface State {
15
11
  currentThemeId: string | undefined;
16
12
  };
17
13
  }
18
- declare class ThemeProvider extends Component<Props, State> {
14
+ declare class ThemeProvider extends Component<ThemeProviderProps, State> {
15
+ static contextType: React.Context<Record<string, string> | null>;
16
+ context: React.ContextType<typeof ThemeVariablesContext>;
19
17
  state: State;
20
18
  private colorFadeTimeout;
21
19
  private wrapperRef;
22
20
  private themeColorMeta;
23
21
  private createdThemeColorMeta;
24
- static getDerivedStateFromProps(props: Props, state: State): Partial<State> | null;
22
+ static getDerivedStateFromProps(props: ThemeProviderProps, state: State): Partial<State> | null;
25
23
  componentDidMount(): void;
26
- componentDidUpdate(prevProps: Props, prevState: State): void;
24
+ componentDidUpdate(prevProps: ThemeProviderProps, prevState: State): void;
27
25
  componentWillUnmount(): void;
28
26
  /**
29
27
  * Replaces the former react-helmet usage: keep a single
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The nearest ThemeProvider's resolved variables, for content that renders outside its DOM
3
3
  * subtree. Theme variables reach a component by CSS inheritance, which a portal breaks: a
4
- * `BodyEnd` child lands wherever `#bodyEnd` happens to be, so it picks up whatever provider
5
- * wraps *that* node rather than the one it was written under.
4
+ * `BodyEnd` child lands wherever the portal target is, so it picks up whatever provider wraps
5
+ * *that* node rather than the one it was written under.
6
6
  *
7
7
  * Deliberately its own module rather than a member of `themeProvider.tsx`: BodyEnd needs the
8
8
  * context and nothing else, and importing the component for it would pull the whole
@@ -1,3 +1,2 @@
1
- export { default } from './toastProvider.js';
2
1
  export { showToast } from './toastService.js';
3
- export type { ToastProviderProps, ToastContextConfig, ToastOptions, ToastType, ToastPosition, ToastSlots, } from './toast.types.js';
2
+ export type { ToastContextConfig, ToastLabels, ToastOptions, ToastType, ToastPosition, ToastSlots, } from './toast.types.js';
@@ -19,17 +19,17 @@ export interface ToastContextConfig {
19
19
  /** Pause the auto-dismiss timer while the toast is hovered or focused. */
20
20
  pauseOnHover?: boolean;
21
21
  /**
22
- * Overrides the text a toast renders of its own. Set on `ToastProvider` for
23
- * the whole app, or per toast through `showToast` options.
22
+ * Overrides the text a toast renders of its own. Set in `AhroweProvider`'s
23
+ * `toast` for the whole app, or per toast through `showToast` options.
24
24
  */
25
25
  labels?: ToastLabels;
26
26
  }
27
- export interface ToastOptions extends ToastContextConfig {
27
+ export interface ToastOptions extends Omit<ToastContextConfig, 'allowStacking'> {
28
28
  type?: ToastType;
29
29
  classNames?: SlotClassNames<ToastSlots>;
30
30
  styles?: SlotStyles<ToastSlots>;
31
31
  }
32
- export interface ToastProviderProps extends ToastContextConfig {
32
+ export interface ToastHostProps extends ToastContextConfig {
33
33
  children: ReactNode;
34
34
  }
35
35
  export interface ToastItem extends Pick<ToastOptions, 'labels'> {
@@ -0,0 +1,4 @@
1
+ import { default as React } from 'react';
2
+ import { ToastHostProps } from './toast.types.js';
3
+ declare function ToastHost({ children, position, duration, showRemainingTime, showCloseButton, allowStacking, pauseOnHover, labels, }: ToastHostProps): React.JSX.Element;
4
+ export default ToastHost;
@@ -2,6 +2,6 @@ import { ReactNode } from 'react';
2
2
  import { ToastOptions } from './toast.types.js';
3
3
  type ShowToastFn = (content: ReactNode, options?: ToastOptions) => void;
4
4
  export declare function _registerToastHandler(fn: ShowToastFn): void;
5
- export declare function _unregisterToastHandler(): void;
5
+ export declare function _unregisterToastHandler(fn: ShowToastFn): void;
6
6
  export declare function showToast(content: ReactNode, options?: ToastOptions): void;
7
7
  export {};
@@ -3,6 +3,8 @@ export * from './common/accordion/index.js';
3
3
  export * from './common/actionButtons/index.js';
4
4
  export { default as ActionIcon } from './common/actionIcon/index.js';
5
5
  export * from './common/actionIcon/index.js';
6
+ export { default as AhroweProvider } from './common/ahroweProvider/index.js';
7
+ export * from './common/ahroweProvider/index.js';
6
8
  export { default as Alert } from './common/alert/index.js';
7
9
  export * from './common/alert/index.js';
8
10
  export { default as AnimatedLogo } from './common/animatedLogo/index.js';
@@ -144,7 +146,6 @@ export { default as Timeline } from './common/timeline/index.js';
144
146
  export * from './common/timeline/index.js';
145
147
  export { default as Timer } from './common/timer/index.js';
146
148
  export * from './common/timer/index.js';
147
- export { default as ToastProvider } from './common/toast/index.js';
148
149
  export * from './common/toast/index.js';
149
150
  export { default as Tooltip } from './common/tooltip/index.js';
150
151
  export * from './common/tooltip/index.js';
@@ -0,0 +1,90 @@
1
+ # AhroweProvider
2
+
3
+ **When to use:** The one wrapper an app needs, at its root: it applies the theme, sets default props for every component, shows toasts and renders the element that Modal, Tooltip, Popover, Menu and Toast portal into. Nest another one for a section with its own theme or defaults.
4
+
5
+ **Keywords:** setup, getting started, install, bootstrap, app shell, dark mode, notifications
6
+
7
+ **Import:** `import { AhroweProvider } from '@ahrowe/ui'`
8
+ **Types:** `import type { AhroweProviderProps } from '@ahrowe/ui'`
9
+
10
+ ```tsx
11
+ import '@ahrowe/ui/style.css';
12
+ import { AhroweProvider, defaultTheme, lightTheme, useTheme } from '@ahrowe/ui';
13
+
14
+ // Keep `themes` stable, here at module scope: a new array on every render reads as a theme change.
15
+ const THEMES = [defaultTheme, lightTheme];
16
+
17
+ function Root() {
18
+ const { currentThemeId } = useTheme();
19
+
20
+ return (
21
+ <AhroweProvider
22
+ themes={THEMES}
23
+ currentThemeId={currentThemeId}
24
+ defaultProps={{ Input: { alwaysFloatLabel: true } }}
25
+ toast={{ position: 'bottom-right', duration: 3000 }}
26
+ >
27
+ <App />
28
+ </AhroweProvider>
29
+ );
30
+ }
31
+ ```
32
+
33
+ Every prop is optional. `<AhroweProvider><App /></AhroweProvider>` applies `defaultTheme` and shows toasts with their built-in settings.
34
+
35
+ **Key props:**
36
+
37
+ | Prop | Type | Default | Description |
38
+ |------|------|---------|-------------|
39
+ | `themes` | `Theme[]` | `[defaultTheme]` | The themes to choose from. See [ThemeProvider.md](ThemeProvider.md) |
40
+ | `currentThemeId` | `string` | the first theme without a `baseThemeId` | The `id` of the theme to apply |
41
+ | `defaultProps` | `ComponentDefaults` | | Default props per component. See [ConfigProvider.md](ConfigProvider.md) |
42
+ | `presentation` | `Presentation` | | How every sheet-capable component presents. See [ConfigProvider.md](ConfigProvider.md) |
43
+ | `toast` | `ToastContextConfig` | | Settings for every toast `showToast` shows. See [Toast.md](Toast.md) |
44
+ | `scrollbars` | `boolean` | `true` | Styles every scrollbar inside the app in the theme's colours. See **Scrollbars** |
45
+
46
+ ## What it renders
47
+
48
+ The theme's wrapper `div`. Inside it, in a `display: contents` element that styles the scrollbars (unless `scrollbars={false}`), come an empty `<template>`, your app and, after it, the element everything portalled renders into. That is why no `<div id="bodyEnd" />` is needed. Portalled panels stay inside the theme's wrapper and come after the app, so they paint above it. Each one gets the font and colours of the theme it was written under, a nested one included.
49
+
50
+ On the server, and while hydrating, nothing is portalled: the server has nowhere to put it. Panels appear on the render right after hydration.
51
+
52
+ ## Nesting
53
+
54
+ A nested `AhroweProvider` applies its `themes`, `currentThemeId`, `defaultProps` and `presentation` to its section. Its `defaultProps` merge with the outer ones per component, the same as nested `ConfigProvider`s. Toasts, scrollbars and the portal element stay with the outermost provider, so a nested `toast` or `scrollbars` is ignored, with a warning.
55
+
56
+ Only a provider inside another `AhroweProvider` counts as nested. Put the outer one at the root first: one placed around a section of an app that has none renders the portal element inside that section, where an `overflow: hidden` or a `transform` around it clips every panel.
57
+
58
+ `ThemeProvider` and `ConfigProvider` still work on their own, for a section that needs only one of the two.
59
+
60
+ ## Scrollbars
61
+
62
+ Every scrollbar inside the provider, portalled panels included, is thin, with a rounded thumb in the theme's `--background-accent-light`. The page's own scrollbar, when the window itself scrolls, gets the same thumb colour: it belongs to `<html>`, outside the provider, so the provider hands the colour up in `--ahrowe-page-scrollbar-thumb`. A browser without `scrollbar-color` keeps its own page scrollbar.
63
+
64
+ None of this outweighs a rule of yours. Every one of these rules has zero specificity, so any scrollbar rule on an element, on `<html>` or on `:root` wins. `scrollbars={false}` turns all of it off; `ScrollbarProvider` then styles one region at a time.
65
+
66
+ ## Migrating from separate providers
67
+
68
+ ```tsx
69
+ // Before
70
+ <ThemeProvider themes={THEMES} currentThemeId={currentThemeId}>
71
+ <ConfigProvider defaultProps={defaults}>
72
+ <ToastProvider position="bottom-right">
73
+ <App />
74
+ </ToastProvider>
75
+ </ConfigProvider>
76
+ <div id="bodyEnd" />
77
+ </ThemeProvider>
78
+
79
+ // After
80
+ <AhroweProvider
81
+ themes={THEMES}
82
+ currentThemeId={currentThemeId}
83
+ defaultProps={defaults}
84
+ toast={{ position: 'bottom-right' }}
85
+ >
86
+ <App />
87
+ </AhroweProvider>
88
+ ```
89
+
90
+ Delete the `<div id="bodyEnd" />`, including one in `index.html`: inside `AhroweProvider` nothing renders into it. Delete a `ScrollbarProvider` around the app too, or, if the app had none, pass `scrollbars={false}` to keep the browser's scrollbars. `ToastProvider` was removed in 0.41, and its props are the keys of `toast`.
package/docs/BodyEnd.md CHANGED
@@ -1,19 +1,12 @@
1
1
  # BodyEnd
2
2
 
3
- **When to use:** Renders children via a React portal into a `#bodyEnd` DOM node. Use for modals, toasts, tooltips, or any UI that must escape the stacking context of its parent.
3
+ **When to use:** Renders children through a React portal, to the end of the app. Use for modals, toasts, tooltips, or any UI that must escape the stacking context of its parent.
4
4
 
5
5
  **Keywords:** teleport
6
6
 
7
7
  **Import:** `import { BodyEnd } from '@ahrowe/ui'`
8
8
 
9
- **Setup required:** Add `<div id="bodyEnd"></div>` inside your topmost `ThemeProvider` (usually as the last child) so portalled content inherits the theme's CSS variables.
10
-
11
- ```tsx
12
- <ThemeProvider>
13
- <App />
14
- <div id="bodyEnd"></div>
15
- </ThemeProvider>
16
- ```
9
+ **Setup:** none inside [AhroweProvider](AhroweProvider.md), which renders the element BodyEnd portals into. Without it, BodyEnd looks for an element with `id="bodyEnd"`: put `<div id="bodyEnd"></div>` inside your topmost `ThemeProvider`, as the last child, so portalled content inherits the theme.
17
10
 
18
11
  ```tsx
19
12
  import { BodyEnd } from '@ahrowe/ui';
@@ -27,9 +20,9 @@ import { BodyEnd } from '@ahrowe/ui';
27
20
 
28
21
  | Prop | Type | Description |
29
22
  |------|------|-------------|
30
- | `children` | `ReactNode` | Content rendered into `#bodyEnd` |
23
+ | `children` | `ReactNode` | Content rendered at the end of the app |
31
24
 
32
- **Note:** Returns `null` silently if `#bodyEnd` doesn't exist in the DOM.
25
+ **Note:** Outside `AhroweProvider` and without a `#bodyEnd`, it renders nothing and logs an error in development.
33
26
 
34
- **Theming crosses the portal.** Children keep the variables of the nearest `ThemeProvider` they were *written* under, not the one that happens to wrap `#bodyEnd`. BodyEnd applies them through a `display: contents` wrapper, which generates no box — positioning, stacking and clipping are unchanged — while custom properties still inherit. Variables that theme leaves unset fall through to whatever wraps `#bodyEnd`, the same cascade the children would have seen rendered in place. With no `ThemeProvider` above, no wrapper is added at all.
27
+ **Theming crosses the portal.** Children keep the variables of the nearest `ThemeProvider` they were *written* under, not the one that happens to wrap the element it portals into. BodyEnd applies them through a `display: contents` wrapper, which generates no box — positioning, stacking and clipping are unchanged — while custom properties still inherit. Variables that theme leaves unset fall through to whatever wraps that element, the same cascade the children would have seen rendered in place. With no `ThemeProvider` above, no wrapper is added at all.
35
28
 
@@ -80,8 +80,6 @@ function Example() {
80
80
  />
81
81
  ```
82
82
 
83
- **Requires:** `<div id="bodyEnd"></div>` in your HTML when `maxItems` is used and the trail actually collapses (the hidden-items menu is a `Popover`, which renders via portal).
84
-
85
83
  **BreadcrumbItem:**
86
84
 
87
85
  | Field | Type | Description |
package/docs/CLAUDE.md CHANGED
@@ -9,13 +9,14 @@ enums and slot names. Do not infer props from a component's name.
9
9
 
10
10
  ## Rules for every component
11
11
 
12
- - **`ThemeProvider` is required.** It injects the CSS variables every component paints with;
13
- without it they render unstyled. Wrap the app once. Two themes ship, `defaultTheme` (dark) and
14
- `lightTheme`; a custom theme is `{ id, variables: { '--primary-color': '#6200ea', … } }` passed
15
- through `themes`, selected with `currentThemeId`. `useTheme()` switches at runtime. The variable
16
- list is in ThemeProvider.md.
17
- - **Portals need a host.** Modal, Tooltip, Popover, Toast and Menu render into `<div id="bodyEnd" />`.
18
- Put it inside `ThemeProvider`, once.
12
+ - **`AhroweProvider` is required.** Wrap the app in it once (AhroweProvider.md). It injects the
13
+ CSS variables every component paints with, so without it they render unstyled; it renders the
14
+ element Modal, Tooltip, Popover, Toast and Menu portal into; and it shows toasts. Two themes
15
+ ship, `defaultTheme` (dark) and `lightTheme`; a custom theme is
16
+ `{ id, variables: { '--primary-color': '#6200ea', … } }` passed through `themes`, selected with
17
+ `currentThemeId`. `useTheme()` switches at runtime. The variable list is in ThemeProvider.md.
18
+ It already is a `ThemeProvider`, a `ConfigProvider`, a `ScrollbarProvider` and the toast host,
19
+ so do not add those or a `<div id="bodyEnd" />` beside it. A section with its own theme or defaults nests another one.
19
20
  - **Form components** (Input, NumberInput, Dropdown, MultiSelect, InputDropdown, Checkbox, Switch,
20
21
  RadioGroup, DatePicker, TimeInput, OtpInput, IconPicker) take `formValidator`, which then owns
21
22
  the value: `new FormValidator('', [Validators.required()])`. Whole forms use
@@ -29,7 +30,8 @@ enums and slot names. Do not infer props from a component's name.
29
30
  hit named inner elements. Slot names are on each doc's **Slots** line. Every native attribute of
30
31
  the root element passes through (`id`, `aria-*`, `data-*`, `on*`).
31
32
  - **Text a component renders itself** (close buttons, empty messages, page labels) is set with its
32
- `labels` prop. App-wide defaults for any prop go through `ConfigProvider` (ConfigProvider.md).
33
+ `labels` prop. App-wide defaults for any prop go through `AhroweProvider`'s `defaultProps`
34
+ (ConfigProvider.md).
33
35
  - **Variants are enums**, not strings: `ButtonStyleType.Primary`, `ButtonSize.Small`. Each doc lists
34
36
  them under **Enums**.
35
37
  - **`icon` props** take a FontAwesome `IconDefinition` or any React element.
@@ -50,12 +52,13 @@ enums and slot names. Do not infer props from a component's name.
50
52
  | Accordion.md | Accordion | Collapsible sections for progressive disclosure — FAQs, settings panels, long-form content that users expand on demand |
51
53
  | ActionButtons.md | ActionButtons | Standardised Submit / Cancel button pair for forms, dialogs, and drawer footers |
52
54
  | ActionIcon.md | ActionIcon | Clickable icon button without a label — toolbar actions, table row actions, icon-only controls |
55
+ | AhroweProvider.md | AhroweProvider | The one wrapper an app needs, at its root: it applies the theme, sets default props for every component, shows toasts and renders the element that Modal, Tooltip, Popover, Menu and Toast portal into |
53
56
  | Alert.md | Alert, AlertStyleType | A persistent inline status message, used for form-level errors, page/section banners, empty-config warnings, and "here's what changed" notices |
54
57
  | AnimatedLogo.md | AnimatedLogo | A self-drawing SVG mark for splash/loading screens, empty states, or anywhere a brand or icon mark should feel alive rather than static |
55
58
  | AnimatedText.md | AnimatedText, AnimatedTextVariant, AnimatedTextUnit | Decorative text entrance or emphasis effect, for hero headlines, empty-state messages, "premium"/"new" callouts, loading/decoding states |
56
59
  | Avatar.md | Avatar, AvatarGroup | User or entity image — profile pictures, comment authors, member lists |
57
60
  | Badge.md | Badge, BadgeStyleType, BadgePosition | Small count or status indicator — unread counts, notification dots, "new" markers |
58
- | BodyEnd.md | BodyEnd | Renders children via a React portal into a `#bodyEnd` DOM node |
61
+ | BodyEnd.md | BodyEnd | Renders children through a React portal, to the end of the app |
59
62
  | Breadcrumb.md | Breadcrumb | A trail showing the current page's position within a hierarchy, with links back to each ancestor, such as a file browser path, a category/subcategory drill-down, or a multi-level settings page |
60
63
  | Button.md | Button, ButtonStyleType, ButtonSize, ButtonType | Primary interactive action trigger |
61
64
  | ButtonGroup.md | ButtonGroup, ButtonGroupOrientation | Joins a row (or column) of buttons into a single seamless control, merging their borders and rounding only the outer corners |
@@ -118,12 +121,12 @@ enums and slot names. Do not infer props from a component's name.
118
121
  | Switch.md | Switch | Sliding boolean toggle — settings panels, feature flags, preference lists |
119
122
  | TabHeader.md | TabHeader | A horizontal row of tabs to switch between sibling views or sections — settings panels, dashboards, detail pages |
120
123
  | Textarea.md | Textarea | Multiline text input — comments, descriptions, notes, code snippets, long-form content |
121
- | ThemeProvider.md | ThemeProvider | Required wrapper for the entire component library |
124
+ | ThemeProvider.md | ThemeProvider | Applies a theme to its subtree, injecting its CSS custom properties as inline styles so every component inside can use them |
122
125
  | Tilt.md | Tilt, TiltGroup | Wrap content that should tilt toward the cursor in 3D |
123
126
  | TimeInput.md | TimeInput | Time-of-day selection — appointment times, opening hours, shift starts, reminders |
124
127
  | Timeline.md | Timeline, TimelineAlignment, TimelineItemStatus | A vertical list of dated events, connected by a line, for audit logs, activity feeds, order status history, and deploy logs |
125
128
  | Timer.md | Timer | A countdown timer with a depleting ring, add/remove-time and pause/cancel controls, and an imperative ref API |
126
- | Toast.md | ToastProvider, showToast | Non-blocking notifications — save confirmations, error alerts, info messages, warnings |
129
+ | Toast.md | showToast | Non-blocking notifications — save confirmations, error alerts, info messages, warnings |
127
130
  | Tooltip.md | Tooltip | Contextual help, info icons, field explanations, error pointers |
128
131
  | Tree.md | Tree, flattenTree | A hierarchical list the user can expand, collapse and pick from: categories, nested locations, folder structures, org charts |
129
132
  | VirtualList.md | VirtualList | Efficiently render large lists or tables — thousands of rows rendered with a virtualised scroll window so only visible rows are in the DOM |
@@ -7,8 +7,6 @@
7
7
  **Import:** `import { CommandPalette } from '@ahrowe/ui'`
8
8
  **Types:** `import type { CommandPaletteProps, CommandPaletteLabels } from '@ahrowe/ui'`
9
9
 
10
- **Requires:** `<div id="bodyEnd"></div>` in your app: the palette is a [Modal](Modal.md) and portals there.
11
-
12
10
  ```tsx
13
11
  import { CommandPalette, menuHotkeys, useHotkeys } from '@ahrowe/ui';
14
12
  import type { MenuEntry } from '@ahrowe/ui';
@@ -7,16 +7,19 @@
7
7
  **Import:** `import { ConfigProvider } from '@ahrowe/ui'`
8
8
  **Types:** `import type { ComponentDefaults, ConfigProviderProps } from '@ahrowe/ui'`
9
9
 
10
- Place it inside `ThemeProvider` (it's a separate concern from theming — theme = CSS variables, config = default props):
10
+ At the app root, pass `defaultProps` and `presentation` to [AhroweProvider](AhroweProvider.md), which includes a ConfigProvider. Use ConfigProvider on its own for a section with defaults of its own:
11
11
 
12
12
  ```tsx
13
- import { ThemeProvider, ConfigProvider } from '@ahrowe/ui';
13
+ import { AhroweProvider, ConfigProvider } from '@ahrowe/ui';
14
14
 
15
- <ThemeProvider>
16
- <ConfigProvider defaultProps={{ Input: { alwaysFloatLabel: true } }}>
17
- <App />
18
- </ConfigProvider>
19
- </ThemeProvider>
15
+ <AhroweProvider defaultProps={{ Input: { alwaysFloatLabel: true } }}>
16
+ <App />
17
+ </AhroweProvider>
18
+
19
+ // Inside App: one form that wants different defaults
20
+ <ConfigProvider defaultProps={{ Input: { alwaysFloatLabel: false } }}>
21
+ <SignupForm />
22
+ </ConfigProvider>
20
23
  ```
21
24
 
22
25
  ## How defaults resolve
@@ -135,6 +138,6 @@ Every string a component renders of its own is set through its `labels` prop, an
135
138
 
136
139
  **Labels merge per key**, unlike every other default here. An instance that overrides one string keeps the provider's translations for the rest, so `<Alert labels={{ dismiss: 'Weg damit' }} />` under the provider above does not lose anything else Alert might have been given. Anything the component does not produce itself, a `title`, a column `label`, a button's children, is already yours and never appears in `labels`.
137
140
 
138
- `ColorPicker` and `VirtualList` appear in `ComponentDefaults` with their labels only: the first is configured elsewhere, the second has generic props that a per-key default map cannot express. `Calendar` is generic too, so its entry is the props that do not depend on the event type: `labels`, `locale`, `weekStartsOn`, `slotMinutes`, `views`, `hiddenDays` and `scrollToHour`. `Toast` is not there at all, because `ToastProvider` is already its app-wide configuration and carries `labels` itself.
141
+ `ColorPicker` and `VirtualList` appear in `ComponentDefaults` with their labels only: the first is configured elsewhere, the second has generic props that a per-key default map cannot express. `Calendar` is generic too, so its entry is the props that do not depend on the event type: `labels`, `locale`, `weekStartsOn`, `slotMinutes`, `views`, `hiddenDays` and `scrollToHour`. `Toast` is not there at all, because `AhroweProvider`'s `toast` prop is already its app-wide configuration and carries `labels` itself.
139
142
 
140
143
  Each component's doc lists its own keys under **Labels:**.
@@ -6,8 +6,6 @@
6
6
 
7
7
  **Import:** `import { ConfirmModal } from '@ahrowe/ui'`
8
8
 
9
- **Requires:** `<div id="bodyEnd"></div>` in your HTML (renders via portal).
10
-
11
9
  ```tsx
12
10
  import { ConfirmModal } from '@ahrowe/ui';
13
11
 
@@ -65,7 +65,9 @@ import { ErrorBoundary } from '@ahrowe/ui';
65
65
  </ErrorBoundary>
66
66
  ```
67
67
 
68
- The default fallback is a centered card with a tinted icon badge, an `Accordion` for the collapsible stack trace (with a `CopyButton` that also fires a `Toast` confirmation if `ToastProvider` is mounted), and a `Button` for "Try again".
68
+ The default fallback is a centered card with a tinted icon badge, an `Accordion` for the collapsible stack trace (with a `CopyButton` that also fires a `Toast` confirmation when the boundary is inside `AhroweProvider`), and a `Button` for "Try again".
69
+
70
+ Put an app-wide boundary inside `AhroweProvider`, not around it. Around it, the error has unmounted the provider by the time the fallback shows, so the fallback renders unthemed and its copy confirmation never appears.
69
71
 
70
72
  **Key props:**
71
73
 
@@ -89,7 +91,7 @@ The default fallback is a centered card with a tinted icon badge, an `Accordion`
89
91
  | `copy` | `"Copy"` | Copy button tooltip and accessible name before copying |
90
92
  | `copied` | `"Copied"` | Copy button tooltip and accessible name right after a successful copy |
91
93
  | `reset` | `"Try again"` | Reset button label |
92
- | `copySuccessToast` | `"Stack trace copied to clipboard"` | Toast shown after a successful copy (only visible if `ToastProvider` is mounted) |
93
- | `copyErrorToast` | `"Could not copy stack trace"` | Toast shown if the copy fails |
94
+ | `copySuccessToast` | `"Stack trace copied to clipboard"` | Toast shown after a successful copy, inside `AhroweProvider` only |
95
+ | `copyErrorToast` | `"Could not copy stack trace"` | Toast shown if the copy fails, inside `AhroweProvider` only |
94
96
 
95
97
  **Slots:** `root` `header` `badge` `icon` `title` `description` `stack` `stackHeader` `stackLabel` `stackTrace` `copyButton` `actions` `resetButton`
@@ -71,7 +71,7 @@ const editForm = useFormValidatorGroup(fields, { initialValues: user, onSubmit:
71
71
  5. Otherwise calls `onSubmit(values, group)`. If it throws, the error is passed to `parseError(err, true)` to surface field-level or generic errors.
72
72
  6. Sets `isSubmitting` false.
73
73
 
74
- By default `onSubmitError` shows the first error as an error **toast** (requires a `ToastProvider` in the tree — see `Toast`). Override it via `options.onSubmitError`.
74
+ By default `onSubmitError` shows the first error as an error **toast** (see `Toast`). Override it via `options.onSubmitError`.
75
75
 
76
76
  ## Options
77
77
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  **Import:** `import { KlipyPicker } from '@ahrowe/ui'`
8
8
 
9
- **Requires:** `<div id="bodyEnd"></div>` in your HTML when `isModal` is used (Modal renders via portal). A Klipy API token, passed as the `token` prop. There is no built-in key: without one the picker renders but every request fails.
9
+ **Requires:** a Klipy API token, passed as the `token` prop. There is no built-in key: without one the picker renders but every request fails.
10
10
 
11
11
  ```tsx
12
12
  import { KlipyPicker } from '@ahrowe/ui';
package/docs/Menu.md CHANGED
@@ -7,8 +7,6 @@
7
7
  **Import:** `import { Menu, menuHotkeys } from '@ahrowe/ui'`
8
8
  **Types:** `import type { MenuProps, MenuEntry, MenuAction } from '@ahrowe/ui'`
9
9
 
10
- **Requires:** `<div id="bodyEnd"></div>` in your app: the menu is portalled there, like every other anchored panel in the library.
11
-
12
10
  ```tsx
13
11
  import { Menu } from '@ahrowe/ui';
14
12
  import type { MenuEntry } from '@ahrowe/ui';
package/docs/Modal.md CHANGED
@@ -6,11 +6,9 @@
6
6
 
7
7
  **Import:** `import { Modal } from '@ahrowe/ui'`
8
8
 
9
- **Requires:** `<div id="bodyEnd"></div>` in your HTML (renders via portal).
10
-
11
9
  The panel itself never scrolls: the header is fixed and the children scroll inside the `body` slot, so the title and close button stay visible no matter how long the content is. Pass `footer` for anything that should stay put below the content, typically the action buttons.
12
10
 
13
- While open, the modal traps focus (focus moves into the panel and returns to whatever was focused before on close), locks scrolling on the page behind it, and closes on Escape. Modals nest correctly: Escape closes only the topmost one, and the page stays locked until the last one closes. The same applies to anything opened from inside a modal — a `Menu`, `Dropdown` or `Popover` counts as the layer above it, so Escape closes that first, and the modal's focus trap leaves focus alone while it's open (all three render through the same portal, so their DOM sits beside the modal's panel rather than inside it).
11
+ While open, the modal traps focus (focus moves into the panel and returns to whatever was focused before on close), locks scrolling on the page behind it, and closes on Escape. A key already held down when it opens does not repeat into it, so holding Enter on the button that opens it does not go on to press whatever the modal focused first. Modals nest correctly: Escape closes only the topmost one, and the page stays locked until the last one closes. The same applies to anything opened from inside a modal — a `Menu`, `Dropdown` or `Popover` counts as the layer above it, so Escape closes that first, and the modal's focus trap leaves focus alone while it's open (all three render through the same portal, so their DOM sits beside the modal's panel rather than inside it).
14
12
 
15
13
  ```tsx
16
14
  import { Modal, ActionButtons } from '@ahrowe/ui';
@@ -7,8 +7,6 @@
7
7
  **Import:** `import { MultiSelect } from '@ahrowe/ui'`
8
8
  **Types:** `import type { MultiSelectProps, MultiSelectItem, MultiSelectValue } from '@ahrowe/ui'`
9
9
 
10
- **Requires:** `<div id="bodyEnd"></div>` in your app — the panel renders through a portal.
11
-
12
10
  ```tsx
13
11
  import { useState } from 'react';
14
12
  import { MultiSelect } from '@ahrowe/ui';
package/docs/OtpInput.md CHANGED
@@ -6,8 +6,6 @@
6
6
 
7
7
  **Import:** `import { OtpInput, OtpInputCharset } from '@ahrowe/ui'`
8
8
 
9
- **Requires:** `<div id="bodyEnd"></div>` in your HTML (the error message renders via `Tooltip`, which portals).
10
-
11
9
  **Charset:** `OtpInputCharset.Numeric` (default) | `OtpInputCharset.Alphanumeric`
12
10
 
13
11
  ```tsx
package/docs/Popover.md CHANGED
@@ -65,7 +65,7 @@ const [anchor, setAnchor] = useState<HTMLElement | null>(null);
65
65
  | `onOpenChange` | `(isOpen, { fromKeyboard }) => void` | — | Open-state change callback. `fromKeyboard` says whether a key caused it, which is what a menu uses to decide between putting the focus on an entry and leaving it on the trigger |
66
66
  | `offset` | `number` | `10` | Gap in px between trigger and panel |
67
67
  | `withArrow` | `boolean` | `true` | Render the arrow pointing at the trigger |
68
- | `closeOnOutsideClick` | `boolean` | `true` | Close on click outside (ignored for hover trigger) |
68
+ | `closeOnOutsideClick` | `boolean` | `true` | Close on a click outside, or when focus leaves the trigger and panel (ignored for hover trigger) |
69
69
  | `closeOnEscape` | `boolean` | `true` | Close on Escape |
70
70
  | `openDelay` | `number` | `100` | Hover open delay in ms |
71
71
  | `closeDelay` | `number` | `120` | Hover close delay in ms |
@@ -88,10 +88,8 @@ Only a `Click` trigger presents as a sheet. `Hover` and `Focus` open something s
88
88
 
89
89
  **Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ Popover: { presentation: Presentation.Auto } }}`, or `presentation={Presentation.Auto}` on the provider itself to set every sheet-capable component at once. See [ConfigProvider.md](ConfigProvider.md).
90
90
 
91
- Requires a `<div id="bodyEnd"></div>` at the app root — the panel is portaled there, same as `Tooltip` and `Menu`.
92
-
93
91
  **Scrolling:** where the browser supports CSS anchor positioning, including the trigger's transforms, the panel is anchored to the trigger in CSS, so it moves with a scrolling trigger in the same frame instead of catching up a frame later. Firefox resolves anchors without transforms, so it keeps the pixel positioning. While it is open the trigger carries an inline `anchor-name`, added to any inline one it already has; an `anchor-name` your stylesheet gives the trigger is overridden for that time.
94
92
 
95
- **Keyboard & focus (click trigger):** the panel is portaled to the end of the DOM, so Tab can't reach it naturally. When the panel has focusable content and the keyboard opened it (Enter or Space on the trigger, or ArrowDown/ArrowUp), focus moves to the first focusable element and the panel gets `role="dialog"`; a pointer click leaves the focus where it already is. `focusOnOpen` forces either. Tabbing past the last element closes the popover and moves focus to the element after the trigger; Shift+Tab past the first element, and Escape, close it and return focus to the trigger. This is non-modal: focus is not trapped, and hover/focus triggers don't steal focus. Escape closes only the innermost open layer, so a popover inside a `Modal` closes on the first press and leaves the modal open. Popovers with only static content don't manage focus or claim the dialog role.
93
+ **Keyboard & focus (click trigger):** the panel is portaled to the end of the DOM, so Tab can't reach it naturally. When the panel has focusable content and the keyboard opened it (Enter or Space on the trigger, or ArrowDown/ArrowUp), focus moves to the first focusable element and the panel gets `role="dialog"`; a pointer click leaves the focus where it already is. `focusOnOpen` forces either. Tabbing past the last element closes the popover and moves focus to the element after the trigger; Shift+Tab past the first element, and Escape, close it and return focus to the trigger. However else it closes, such as a button inside it setting `isOpen` to `false`, focus that is still inside the panel goes back to the trigger too, as the native `hidePopover()` does; focus that has already moved elsewhere, or onto the page after a click outside, stays where it is. A key already held down when the panel opens does not repeat into it, so a held Enter does not go on to pick the first entry; in the field it opened from, such as one showing suggestions while you type, it keeps repeating. This is non-modal: focus is not trapped, and hover/focus triggers don't steal focus. Escape closes only the innermost open layer, so a popover inside a `Modal` closes on the first press and leaves the modal open. Popovers with only static content don't manage focus or claim the dialog role.
96
94
 
97
95
  **Anchored (`anchor`):** your code opens the panel, not a key event Popover sees, so the default `focusOnOpen` looks at the click that opened it: Enter or Space on the anchor or on something inside it (a click with `detail === 0`) moves the focus into the panel, a pointer click leaves it where it is, and an open with no click at all, such as suggestions appearing while the user types into an anchored field, never takes it. Escape, Tab and Shift+Tab out of the panel continue from the anchor as they would from a trigger: from the anchor itself when it takes focus, otherwise from the first focusable element inside it.
@@ -71,7 +71,7 @@ interface RadioOption {
71
71
  | `errorMessage` | `string` | Manual error message (when not using a `formValidator`) |
72
72
  | `isValid` | `boolean` | Manual valid state (default `true`) |
73
73
 
74
- **Validation:** like `Input`, an errored group shows an error `Tooltip` and turns its unselected circles red. With a `formValidator` the error surfaces only after the field is *touched* (focus leaves the group), then on focus or hover; requires `<div id="bodyEnd"></div>` at the app root for the tooltip portal.
74
+ **Validation:** like `Input`, an errored group shows an error `Tooltip` and turns its unselected circles red. With a `formValidator` the error surfaces only after the field is *touched* (focus leaves the group), then on focus or hover.
75
75
 
76
76
  **Global defaults:** adopts `ConfigProvider` — e.g. `defaultProps={{ RadioGroup: { orientation: RadioGroupOrientation.Horizontal } }}`. See [ConfigProvider.md](ConfigProvider.md).
77
77
 
@@ -20,3 +20,7 @@ import { ScrollbarProvider } from '@ahrowe/ui';
20
20
  |------|------|-------------|
21
21
  | `className` | `string` | Additional class on the wrapper |
22
22
  | `children` | `ReactNode` | Scrollable content |
23
+
24
+ **Your own rules win.** Its rules have zero specificity, so any scrollbar rule of a component's or of yours, on an element inside it, takes precedence.
25
+
26
+ **The whole app:** `AhroweProvider` already styles every scrollbar in the app, portalled panels included, unless it is given `scrollbars={false}`. Use `ScrollbarProvider` for one region in an app that turned that off.
@@ -8,8 +8,6 @@
8
8
  **Types:** `import type { SplitButtonAction, SplitButtonProps } from '@ahrowe/ui'`
9
9
  **Enums:** `import { PopoverAlign } from '@ahrowe/ui'` for `align`.
10
10
 
11
- **Requires:** `<div id="bodyEnd"></div>` in your HTML (the actions menu is a `Menu`, which portals).
12
-
13
11
  ```tsx
14
12
  import { SplitButton } from '@ahrowe/ui';
15
13
  import type { SplitButtonAction } from '@ahrowe/ui';