@slithy/base-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.js +612 -0
- package/eslint.config.js +6 -0
- package/package.json +51 -0
- package/src/Dropdown/Dropdown.css +112 -0
- package/src/Dropdown/Dropdown.test.tsx +344 -0
- package/src/Dropdown/Dropdown.tsx +576 -0
- package/src/Dropdown/index.ts +1 -0
- package/src/Tooltip/Tooltip.css +55 -0
- package/src/Tooltip/Tooltip.test.tsx +297 -0
- package/src/Tooltip/Tooltip.tsx +302 -0
- package/src/Tooltip/index.ts +1 -0
- package/src/index.ts +2 -0
- package/src/test-setup.ts +1 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +8 -0
- package/vitest.config.ts +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# @slithy/base-ui
|
|
2
|
+
|
|
3
|
+
Compound UI components built on [Base UI](https://base-ui.com/). Provides accessible, unstyled primitives with sensible defaults, deferred rendering for performance, and full compatibility with animation libraries.
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
|
|
7
|
+
### Tooltip
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { Tooltip } from "@slithy/base-ui";
|
|
11
|
+
|
|
12
|
+
<Tooltip.Provider>
|
|
13
|
+
<Tooltip.Root>
|
|
14
|
+
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
|
|
15
|
+
<Tooltip.Portal>
|
|
16
|
+
<Tooltip.Positioner sideOffset={8}>
|
|
17
|
+
<Tooltip.Popup>
|
|
18
|
+
<Tooltip.Arrow />
|
|
19
|
+
Tooltip content
|
|
20
|
+
</Tooltip.Popup>
|
|
21
|
+
</Tooltip.Positioner>
|
|
22
|
+
</Tooltip.Portal>
|
|
23
|
+
</Tooltip.Root>
|
|
24
|
+
</Tooltip.Provider>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Parts:** `Provider`, `Root`, `Trigger`, `Portal`, `Positioner`, `Popup`, `Arrow`
|
|
28
|
+
|
|
29
|
+
#### Props
|
|
30
|
+
|
|
31
|
+
**`Root`** accepts all Base UI Tooltip.Root props, plus:
|
|
32
|
+
|
|
33
|
+
| Prop | Type | Default | Description |
|
|
34
|
+
|------|------|---------|-------------|
|
|
35
|
+
| `touchDisabled` | `boolean` | `true` | Block activation from touch interactions. Mouse/keyboard still work on hybrid devices. |
|
|
36
|
+
| `unmountOnClose` | `boolean` | `false` | Unmount Base UI after close animation completes, returning to the lightweight pre-activation state. Useful for pages with many tooltips where accumulated mounted instances add up. |
|
|
37
|
+
|
|
38
|
+
**`Trigger`** accepts all Base UI Tooltip.Trigger props, plus:
|
|
39
|
+
|
|
40
|
+
| Prop | Type | Default | Description |
|
|
41
|
+
|------|------|---------|-------------|
|
|
42
|
+
| `render` | `ReactElement \| (props, state) => ReactElement` | — | Replace the default `<button>` with a custom element. Works in both pre- and post-activation states. See [Custom trigger element](#custom-trigger-element). |
|
|
43
|
+
|
|
44
|
+
### Dropdown
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { Dropdown } from "@slithy/base-ui";
|
|
48
|
+
|
|
49
|
+
<Dropdown.Root>
|
|
50
|
+
<Dropdown.Trigger>Open menu</Dropdown.Trigger>
|
|
51
|
+
<Dropdown.Portal>
|
|
52
|
+
<Dropdown.Positioner sideOffset={4}>
|
|
53
|
+
<Dropdown.Popup>
|
|
54
|
+
<Dropdown.Item onClick={handleEdit}>Edit</Dropdown.Item>
|
|
55
|
+
<Dropdown.Item onClick={handleDuplicate}>Duplicate</Dropdown.Item>
|
|
56
|
+
<Dropdown.Separator />
|
|
57
|
+
<Dropdown.Item onClick={handleDelete}>Delete</Dropdown.Item>
|
|
58
|
+
</Dropdown.Popup>
|
|
59
|
+
</Dropdown.Positioner>
|
|
60
|
+
</Dropdown.Portal>
|
|
61
|
+
</Dropdown.Root>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Parts:** `Root`, `Trigger`, `Portal`, `Positioner`, `Popup`, `Arrow`, `Item`, `Separator`, `Group`, `GroupLabel`, `CheckboxItem`, `CheckboxItemIndicator`, `RadioGroup`, `RadioItem`, `RadioItemIndicator`
|
|
65
|
+
|
|
66
|
+
#### Disabling the dropdown
|
|
67
|
+
|
|
68
|
+
Set `disabled` on `Root` to prevent the dropdown from opening while keeping the trigger interactive. This is useful when you want alternate behavior for the same trigger — for example, opening a modal on mobile instead of a dropdown:
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
const isMobile = useIsMobile();
|
|
72
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
73
|
+
|
|
74
|
+
<Dropdown.Root disabled={isMobile}>
|
|
75
|
+
<Dropdown.Trigger onClick={isMobile ? () => setModalOpen(true) : undefined}>
|
|
76
|
+
Options
|
|
77
|
+
</Dropdown.Trigger>
|
|
78
|
+
<Dropdown.Portal>
|
|
79
|
+
<Dropdown.Positioner sideOffset={4}>
|
|
80
|
+
<Dropdown.Popup>
|
|
81
|
+
<Dropdown.Item onClick={handleEdit}>Edit</Dropdown.Item>
|
|
82
|
+
<Dropdown.Item onClick={handleDelete}>Delete</Dropdown.Item>
|
|
83
|
+
</Dropdown.Popup>
|
|
84
|
+
</Dropdown.Positioner>
|
|
85
|
+
</Dropdown.Portal>
|
|
86
|
+
</Dropdown.Root>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Event handlers on `Trigger` (`onClick`, `onPointerDown`, `onKeyDown`) always fire, regardless of the `disabled` state — only the dropdown activation is suppressed.
|
|
90
|
+
|
|
91
|
+
#### Props
|
|
92
|
+
|
|
93
|
+
**`Root`** accepts all Base UI Menu.Root props, plus:
|
|
94
|
+
|
|
95
|
+
| Prop | Type | Default | Description |
|
|
96
|
+
|------|------|---------|-------------|
|
|
97
|
+
| `disabled` | `boolean` | `false` | Prevent the dropdown from opening. The trigger button remains interactive so consumers can attach alternate behavior (e.g. opening a modal on mobile) via event handlers on `Trigger`. |
|
|
98
|
+
| `unmountOnClose` | `boolean` | `false` | Unmount Base UI after close animation completes, returning to the lightweight pre-activation state. Useful for pages with many dropdowns where accumulated mounted instances add up. |
|
|
99
|
+
|
|
100
|
+
**`Trigger`** accepts all Base UI Menu.Trigger props, plus:
|
|
101
|
+
|
|
102
|
+
| Prop | Type | Default | Description |
|
|
103
|
+
|------|------|---------|-------------|
|
|
104
|
+
| `tooltip` | `ReactNode` | — | Show a tooltip on hover. The tooltip is deferred (not mounted until first hover) and automatically dismissed when the dropdown menu opens. |
|
|
105
|
+
| `render` | `ReactElement \| (props, state) => ReactElement` | — | Replace the default `<button>` with a custom element. Works in both pre- and post-activation states. See [Custom trigger element](#custom-trigger-element). |
|
|
106
|
+
|
|
107
|
+
## Deferred rendering
|
|
108
|
+
|
|
109
|
+
Both components use a deferred rendering pattern that avoids mounting Base UI's hooks and floating-ui positioning until the user first interacts with the trigger. Before activation, the trigger renders as a plain `<button>` and portal content is not mounted. This keeps initial JS overhead minimal when rendering many tooltips or dropdowns on a page.
|
|
110
|
+
|
|
111
|
+
The activation latch is one-way by default: once triggered, it stays active so that leave animations can play. Controlled `open` or `defaultOpen` bypass the latch entirely.
|
|
112
|
+
|
|
113
|
+
Set `unmountOnClose` on Root to reset the latch after the close animation completes. This returns the component to its lightweight pre-activation state, freeing Base UI hooks and floating-ui listeners. Useful for long-lived pages with many tooltips or dropdowns where accumulated mounted instances add up.
|
|
114
|
+
|
|
115
|
+
## Custom trigger element
|
|
116
|
+
|
|
117
|
+
Both `Tooltip.Trigger` and `Dropdown.Trigger` accept a `render` prop to replace the default `<button>` with a custom element. This works across both pre- and post-activation states — the deferred rendering layer applies the same prop to whichever element is currently rendered.
|
|
118
|
+
|
|
119
|
+
**Element form** — pass a React element. All required event handlers, `ref`, `className`, `style`, and `disabled` are merged in via `cloneElement`. Existing props on the element are preserved for any key that isn't overridden:
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
<Dropdown.Trigger render={<MyButton variant="ghost" />}>
|
|
123
|
+
Open menu
|
|
124
|
+
</Dropdown.Trigger>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Function form** — receive the full props object and spread it onto your element. Use this form when you need to compose your own handlers alongside the trigger's, or when your component needs additional logic:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<Tooltip.Trigger
|
|
131
|
+
render={(props) => (
|
|
132
|
+
<MyButton
|
|
133
|
+
{...props}
|
|
134
|
+
onFocus={(e) => {
|
|
135
|
+
props.onFocus?.(e);
|
|
136
|
+
trackFocusEvent();
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
>
|
|
141
|
+
Hover me
|
|
142
|
+
</Tooltip.Trigger>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
> **Note:** For the element form, event handlers defined on the render element (e.g. `<MyButton onClick={...} />`) are overridden by the trigger's activation handlers. Use the function form if you need to run your own handlers alongside them.
|
|
146
|
+
|
|
147
|
+
Your custom component must forward `ref` to its underlying DOM element so that focus restoration and synthetic event dispatch work correctly after activation.
|
|
148
|
+
|
|
149
|
+
## Styling
|
|
150
|
+
|
|
151
|
+
Each component applies default class names (`slithy-tooltip-*`, `slithy-dropdown-*`) that can be overridden by passing your own `className`. Default styles use CSS custom properties for theming:
|
|
152
|
+
|
|
153
|
+
```css
|
|
154
|
+
/* Tooltip */
|
|
155
|
+
--slithy-tooltip-bg
|
|
156
|
+
--slithy-tooltip-color
|
|
157
|
+
--slithy-tooltip-font-size
|
|
158
|
+
--slithy-tooltip-padding
|
|
159
|
+
--slithy-tooltip-radius
|
|
160
|
+
--slithy-tooltip-max-width
|
|
161
|
+
|
|
162
|
+
/* Dropdown */
|
|
163
|
+
--slithy-dropdown-bg
|
|
164
|
+
--slithy-dropdown-color
|
|
165
|
+
--slithy-dropdown-font-size
|
|
166
|
+
--slithy-dropdown-padding
|
|
167
|
+
--slithy-dropdown-radius
|
|
168
|
+
--slithy-dropdown-min-width
|
|
169
|
+
--slithy-dropdown-shadow
|
|
170
|
+
--slithy-dropdown-border
|
|
171
|
+
--slithy-dropdown-item-padding
|
|
172
|
+
--slithy-dropdown-item-highlighted-bg
|
|
173
|
+
--slithy-dropdown-separator-color
|
|
174
|
+
--slithy-dropdown-group-label-padding
|
|
175
|
+
--slithy-dropdown-group-label-color
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Positioning
|
|
179
|
+
|
|
180
|
+
Both components pass all props through to Base UI's Positioner, which wraps floating-ui internally. Common positioning props:
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
<Tooltip.Positioner
|
|
184
|
+
side="bottom" // "top" | "bottom" | "left" | "right"
|
|
185
|
+
sideOffset={8} // distance from trigger (px)
|
|
186
|
+
align="center" // "start" | "center" | "end"
|
|
187
|
+
alignOffset={0} // alignment offset (px)
|
|
188
|
+
collisionPadding={5} // viewport edge padding (px)
|
|
189
|
+
/>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Custom animations
|
|
193
|
+
|
|
194
|
+
The `Popup` and `Positioner` components accept a `render` prop (from Base UI) that lets you replace the rendered element. (The `Trigger` `render` prop is documented separately under [Custom trigger element](#custom-trigger-element).) This makes it straightforward to use animation libraries like react-spring:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import { useTransition, animated } from "@react-spring/web";
|
|
198
|
+
|
|
199
|
+
function AnimatedTooltip({ children }: { children: React.ReactNode }) {
|
|
200
|
+
return (
|
|
201
|
+
<Tooltip.Provider>
|
|
202
|
+
<Tooltip.Root>
|
|
203
|
+
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
|
|
204
|
+
<Tooltip.Portal>
|
|
205
|
+
<Tooltip.Positioner sideOffset={8}>
|
|
206
|
+
<Tooltip.Popup
|
|
207
|
+
render={(props) => (
|
|
208
|
+
<animated.div
|
|
209
|
+
{...props}
|
|
210
|
+
style={{
|
|
211
|
+
...props.style,
|
|
212
|
+
opacity: /* your spring value */,
|
|
213
|
+
transform: /* your spring value */,
|
|
214
|
+
}}
|
|
215
|
+
/>
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
{children}
|
|
219
|
+
</Tooltip.Popup>
|
|
220
|
+
</Tooltip.Positioner>
|
|
221
|
+
</Tooltip.Portal>
|
|
222
|
+
</Tooltip.Root>
|
|
223
|
+
</Tooltip.Provider>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Base UI exposes `data-open`, `data-starting-style`, and `data-ending-style` attributes on popup elements, and `onOpenChangeComplete` on Root, so you can drive enter/leave animations from open state and signal completion.
|
|
229
|
+
|
|
230
|
+
The deferred rendering layer does not interfere with animations -- by the time they run, the activation latch has flipped and Base UI is fully mounted.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { Menu } from '@base-ui/react/menu';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
5
|
+
import * as _base_ui_react from '@base-ui/react';
|
|
6
|
+
import { Tooltip as Tooltip$1 } from '@base-ui/react/tooltip';
|
|
7
|
+
|
|
8
|
+
type RootProps$1 = ComponentPropsWithoutRef<typeof Menu.Root> & {
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
/** When `true`, the dropdown will not open. The trigger button remains
|
|
11
|
+
* interactive so consumers can attach alternate behavior (e.g. opening
|
|
12
|
+
* a modal on mobile) via event handlers on `Dropdown.Trigger`. */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/** Unmount Base UI after close animation completes, returning to the
|
|
15
|
+
* lightweight pre-activation state. Defaults to `false`. */
|
|
16
|
+
unmountOnClose?: boolean;
|
|
17
|
+
};
|
|
18
|
+
declare function Root$1({ children, open, defaultOpen, disabled, unmountOnClose, onOpenChangeComplete, ...props }: RootProps$1): react_jsx_runtime.JSX.Element;
|
|
19
|
+
type TriggerProps$1 = ComponentPropsWithoutRef<typeof Menu.Trigger> & {
|
|
20
|
+
/** Show a tooltip on hover. Accepts any ReactNode content. */
|
|
21
|
+
tooltip?: ReactNode;
|
|
22
|
+
};
|
|
23
|
+
declare function Trigger$1({ children, className, style, disabled, tooltip, ...rest }: TriggerProps$1): react_jsx_runtime.JSX.Element;
|
|
24
|
+
type PortalProps$1 = ComponentPropsWithoutRef<typeof Menu.Portal>;
|
|
25
|
+
declare function Portal$1(props: PortalProps$1): react_jsx_runtime.JSX.Element | null;
|
|
26
|
+
type PositionerProps$1 = ComponentPropsWithoutRef<typeof Menu.Positioner>;
|
|
27
|
+
declare function Positioner$1({ className, ...props }: PositionerProps$1): react_jsx_runtime.JSX.Element;
|
|
28
|
+
type PopupProps$1 = ComponentPropsWithoutRef<typeof Menu.Popup>;
|
|
29
|
+
declare function Popup$1({ className, ...props }: PopupProps$1): react_jsx_runtime.JSX.Element;
|
|
30
|
+
type ArrowProps$1 = ComponentPropsWithoutRef<typeof Menu.Arrow>;
|
|
31
|
+
declare function Arrow$1({ className, ...props }: ArrowProps$1): react_jsx_runtime.JSX.Element;
|
|
32
|
+
type ItemProps = ComponentPropsWithoutRef<typeof Menu.Item>;
|
|
33
|
+
declare function Item({ className, ...props }: ItemProps): react_jsx_runtime.JSX.Element;
|
|
34
|
+
type SeparatorProps = ComponentPropsWithoutRef<typeof Menu.Separator>;
|
|
35
|
+
declare function Separator({ className, ...props }: SeparatorProps): react_jsx_runtime.JSX.Element;
|
|
36
|
+
type GroupProps = ComponentPropsWithoutRef<typeof Menu.Group>;
|
|
37
|
+
declare function Group({ className, ...props }: GroupProps): react_jsx_runtime.JSX.Element;
|
|
38
|
+
type GroupLabelProps = ComponentPropsWithoutRef<typeof Menu.GroupLabel>;
|
|
39
|
+
declare function GroupLabel({ className, ...props }: GroupLabelProps): react_jsx_runtime.JSX.Element;
|
|
40
|
+
type CheckboxItemProps = ComponentPropsWithoutRef<typeof Menu.CheckboxItem>;
|
|
41
|
+
declare function CheckboxItem({ className, ...props }: CheckboxItemProps): react_jsx_runtime.JSX.Element;
|
|
42
|
+
type CheckboxItemIndicatorProps = ComponentPropsWithoutRef<typeof Menu.CheckboxItemIndicator>;
|
|
43
|
+
declare function CheckboxItemIndicator({ className, ...props }: CheckboxItemIndicatorProps): react_jsx_runtime.JSX.Element;
|
|
44
|
+
type RadioGroupProps = ComponentPropsWithoutRef<typeof Menu.RadioGroup>;
|
|
45
|
+
declare function RadioGroup({ className, ...props }: RadioGroupProps): react_jsx_runtime.JSX.Element;
|
|
46
|
+
type RadioItemProps = ComponentPropsWithoutRef<typeof Menu.RadioItem>;
|
|
47
|
+
declare function RadioItem({ className, ...props }: RadioItemProps): react_jsx_runtime.JSX.Element;
|
|
48
|
+
type RadioItemIndicatorProps = ComponentPropsWithoutRef<typeof Menu.RadioItemIndicator>;
|
|
49
|
+
declare function RadioItemIndicator({ className, ...props }: RadioItemIndicatorProps): react_jsx_runtime.JSX.Element;
|
|
50
|
+
declare const Dropdown: {
|
|
51
|
+
Root: typeof Root$1;
|
|
52
|
+
Trigger: typeof Trigger$1;
|
|
53
|
+
Portal: typeof Portal$1;
|
|
54
|
+
Positioner: typeof Positioner$1;
|
|
55
|
+
Popup: typeof Popup$1;
|
|
56
|
+
Arrow: typeof Arrow$1;
|
|
57
|
+
Item: typeof Item;
|
|
58
|
+
Separator: typeof Separator;
|
|
59
|
+
Group: typeof Group;
|
|
60
|
+
GroupLabel: typeof GroupLabel;
|
|
61
|
+
CheckboxItem: typeof CheckboxItem;
|
|
62
|
+
CheckboxItemIndicator: typeof CheckboxItemIndicator;
|
|
63
|
+
RadioGroup: typeof RadioGroup;
|
|
64
|
+
RadioItem: typeof RadioItem;
|
|
65
|
+
RadioItemIndicator: typeof RadioItemIndicator;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type RootProps = ComponentPropsWithoutRef<typeof Tooltip$1.Root> & {
|
|
69
|
+
children?: ReactNode;
|
|
70
|
+
/** Block activation from touch interactions. Defaults to `true`. */
|
|
71
|
+
touchDisabled?: boolean;
|
|
72
|
+
/** Unmount Base UI after close animation completes, returning to the
|
|
73
|
+
* lightweight pre-activation state. Defaults to `false`. */
|
|
74
|
+
unmountOnClose?: boolean;
|
|
75
|
+
};
|
|
76
|
+
declare function Root({ children, open, defaultOpen, disabled, touchDisabled, unmountOnClose, onOpenChangeComplete, ...props }: RootProps): react_jsx_runtime.JSX.Element;
|
|
77
|
+
type TriggerProps = ComponentPropsWithoutRef<typeof Tooltip$1.Trigger>;
|
|
78
|
+
declare function Trigger({ children, className, style, disabled, ...rest }: TriggerProps): react_jsx_runtime.JSX.Element;
|
|
79
|
+
type PortalProps = ComponentPropsWithoutRef<typeof Tooltip$1.Portal>;
|
|
80
|
+
declare function Portal(props: PortalProps): react_jsx_runtime.JSX.Element | null;
|
|
81
|
+
type PositionerProps = ComponentPropsWithoutRef<typeof Tooltip$1.Positioner>;
|
|
82
|
+
declare function Positioner({ className, ...props }: PositionerProps): react_jsx_runtime.JSX.Element;
|
|
83
|
+
type PopupProps = ComponentPropsWithoutRef<typeof Tooltip$1.Popup>;
|
|
84
|
+
declare function Popup({ className, ...props }: PopupProps): react_jsx_runtime.JSX.Element;
|
|
85
|
+
type ArrowProps = ComponentPropsWithoutRef<typeof Tooltip$1.Arrow>;
|
|
86
|
+
declare function Arrow({ className, ...props }: ArrowProps): react_jsx_runtime.JSX.Element;
|
|
87
|
+
declare const Tooltip: {
|
|
88
|
+
Provider: react.FC<_base_ui_react.TooltipProviderProps>;
|
|
89
|
+
Root: typeof Root;
|
|
90
|
+
Trigger: typeof Trigger;
|
|
91
|
+
Portal: typeof Portal;
|
|
92
|
+
Positioner: typeof Positioner;
|
|
93
|
+
Popup: typeof Popup;
|
|
94
|
+
Arrow: typeof Arrow;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export { Dropdown, Tooltip };
|