@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 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.
@@ -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 };