@diabolic/hangover 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/LICENSE +21 -0
- package/README.md +980 -0
- package/dist/hangover.css +547 -0
- package/dist/index.cjs.js +4269 -0
- package/dist/index.esm.js +4263 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
# hangover
|
|
2
|
+
|
|
3
|
+
A React 18 compound-component Dropdown / Field Picker library.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@diabolic/hangover)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
[Live Demo](https://bugrakaan.github.io/hangover/)
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Compound Components** - Composable `Dropdown.Trigger`, `Panel`, `Navigation`, `Section`, `Group`, `Item` API
|
|
13
|
+
- **Fuzzy Search** - Built-in fuzzy filtering across items (powered by fuse.js)
|
|
14
|
+
- **Two Display Modes** - Scroll-spy with smooth scroll or one-section-at-a-time tab mode
|
|
15
|
+
- **Left Navigation** - Optional nav column with auto-collapse and single-section auto-transform
|
|
16
|
+
- **Checkbox Items** - Multi-select with select-all support
|
|
17
|
+
- **Dark Mode** - Token-based theming with a built-in dark theme
|
|
18
|
+
- **Smart Positioning** - Portal-rendered panel with placement variants and auto-placement
|
|
19
|
+
- **Controlled & Uncontrolled** - Full control or sensible defaults out of the box
|
|
20
|
+
- **Imperative API** - Open, close, and drive state via `ref` with an optional external anchor
|
|
21
|
+
- **Config-driven Rendering** - Build entire menus from a single `fromConfig` object
|
|
22
|
+
- **React 18** - Compound, accessible, headless-style components
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @diabolic/hangover
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```jsx
|
|
37
|
+
import { Dropdown } from '@diabolic/hangover'
|
|
38
|
+
import '@diabolic/hangover/styles'
|
|
39
|
+
|
|
40
|
+
export default function App() {
|
|
41
|
+
return (
|
|
42
|
+
<Dropdown>
|
|
43
|
+
<Dropdown.Trigger>
|
|
44
|
+
<button>Open</button>
|
|
45
|
+
</Dropdown.Trigger>
|
|
46
|
+
<Dropdown.Panel>
|
|
47
|
+
<Dropdown.Content>
|
|
48
|
+
<Dropdown.Section>
|
|
49
|
+
<Dropdown.Group label="Fruits">
|
|
50
|
+
<Dropdown.Item id="apple">Apple</Dropdown.Item>
|
|
51
|
+
<Dropdown.Item id="banana">Banana</Dropdown.Item>
|
|
52
|
+
</Dropdown.Group>
|
|
53
|
+
</Dropdown.Section>
|
|
54
|
+
</Dropdown.Content>
|
|
55
|
+
</Dropdown.Panel>
|
|
56
|
+
</Dropdown>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use the exported styles subpath above. Do not import from `dist/...` directly.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Component Reference
|
|
66
|
+
|
|
67
|
+
### `<Dropdown>`
|
|
68
|
+
|
|
69
|
+
Root provider. All state lives here.
|
|
70
|
+
|
|
71
|
+
| Prop | Type | Default | Description |
|
|
72
|
+
|---|---|---|---|
|
|
73
|
+
| `displayMode` | `"scroll" \| "tab"` | `"scroll"` | How sections are navigated. `"scroll"` uses scroll-spy + smooth scroll; `"tab"` shows one section at a time. |
|
|
74
|
+
| `defaultOpen` | `boolean` | `false` | Panel starts open. |
|
|
75
|
+
| `defaultGroupExpanded` | `"first" \| true \| false` | `true` | Default expand state for all groups. `true` expands all; `false` collapses all; `"first"` expands only the first group across all sections. |
|
|
76
|
+
| `hideOnSelection` | `boolean` | `true` | Close the panel automatically when a `type="click"` item is selected. Set to `false` to keep the panel open. |
|
|
77
|
+
| `darkMode` | `boolean` | `false` | Enable dark mode. Applies `hangoverDropdown--dark` CSS class which overrides all color tokens. |
|
|
78
|
+
| `searchQuery` | `string` | — | Controlled search query. When provided, the internal search state is kept in sync with this value. Use together with `onEvent` (`type: "search"`) to handle changes. |
|
|
79
|
+
| `defaultSearchQuery` | `string` | `""` | Uncontrolled initial search query. Only applied on first render. |
|
|
80
|
+
| `onEvent` | `(event) => any` | — | Central event handler. See [Events](#events). |
|
|
81
|
+
| `ref` | `React.Ref` | — | Exposes imperative API. See [Imperative API](#imperative-api). |
|
|
82
|
+
| `...rest` | `any` | — | Any additional props (e.g. `data-*`, `className`, `style`) are forwarded to the root `<div>`. |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### `<Dropdown.Trigger>`
|
|
87
|
+
|
|
88
|
+
Wraps any single child element and turns it into the toggle trigger.
|
|
89
|
+
|
|
90
|
+
Injects `ref`, `onClick`, `aria-expanded`, `aria-haspopup` onto the child automatically — no extra props needed.
|
|
91
|
+
|
|
92
|
+
```jsx
|
|
93
|
+
<Dropdown.Trigger>
|
|
94
|
+
<button>Open</button>
|
|
95
|
+
</Dropdown.Trigger>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`Dropdown.Trigger` is optional. When you cannot add markup inside `<Dropdown>`, use an external `anchor` ref with the imperative API instead. See [Without a Trigger](#without-a-trigger).
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### `<Dropdown.Panel>`
|
|
103
|
+
|
|
104
|
+
Renders into a portal on `document.body`. Handles positioning, outside-click, and Escape key closing.
|
|
105
|
+
|
|
106
|
+
| Prop | Type | Default | Description |
|
|
107
|
+
|---|---|---|---|
|
|
108
|
+
| `placement` | `string` | `"bottom-start"` | Panel position relative to the trigger. Supported: `"bottom-start"`, `"bottom-end"`, `"bottom"`, `"top-start"`, `"top-end"`, `"top"`. |
|
|
109
|
+
| `title` | `string` | — | Optional title bar rendered at the top of the panel (above the nav/content area). Uses the same muted uppercase style as section headings. |
|
|
110
|
+
| `offset` | `number \| string` | `8` | Distance between trigger and panel. Accepts a number (`10`) or a px string (`"10px"`). |
|
|
111
|
+
| `anchor` | `React.RefObject` | — | Ref to an external DOM element used as the positioning anchor. Overrides the built-in trigger ref. Use together with the imperative API when `Dropdown.Trigger` is not in the markup. |
|
|
112
|
+
| `component` | `React component` | — | Custom wrapper component. |
|
|
113
|
+
| `...rest` | `any` | — | Any additional props are forwarded to the panel `<div>` (or `component`). |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### `<Dropdown.Navigation>`
|
|
118
|
+
|
|
119
|
+
Left navigation column. When present, the panel switches to a two-column layout.
|
|
120
|
+
|
|
121
|
+
| Prop | Type | Default | Description |
|
|
122
|
+
|---|---|---|---|
|
|
123
|
+
| `showAll` | `boolean` | `false` | Automatically prepends an **All** nav item with id `"__all__"`. |
|
|
124
|
+
| `allLabel` | `string` | `"All"` | Label for the auto-prepended All item. |
|
|
125
|
+
| `allIcon` | `ReactNode \| FC` | — | Icon for the auto-prepended All item. |
|
|
126
|
+
| `collapsed` | `boolean` | `false` | Start the nav column in collapsed state. |
|
|
127
|
+
| `autoCollapse` | `boolean` | `false` | Automatically collapse the nav column when the viewport is too narrow to fit the full panel width (derived from `--hangover-nav-width` + `--hangover-content-max-width`). |
|
|
128
|
+
| `component` | `React component` | — | Custom wrapper component. |
|
|
129
|
+
| `...rest` | `any` | — | Any additional props are forwarded to the nav column wrapper `<div>` (or `component`). |
|
|
130
|
+
|
|
131
|
+
> **Single-section auto-transform** — When `<Dropdown.Navigation>` has exactly one child item (excluding the auto-prepended All item), the nav column is hidden automatically and section titles are suppressed. No extra prop is needed; it is detected at render time.
|
|
132
|
+
|
|
133
|
+
```jsx
|
|
134
|
+
{/* Nav column and section title disappear automatically */}
|
|
135
|
+
<Dropdown.Navigation showAll>
|
|
136
|
+
<Dropdown.NavigationItem id="metrics">Metrics</Dropdown.NavigationItem>
|
|
137
|
+
</Dropdown.Navigation>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Renders `Dropdown.NavigationItem` children inside a `forNavigation` column.
|
|
141
|
+
|
|
142
|
+
Use `showAll` to automatically prepend an **All** item:
|
|
143
|
+
|
|
144
|
+
```jsx
|
|
145
|
+
<Dropdown.Navigation showAll>
|
|
146
|
+
<Dropdown.NavigationItem id="fruits" icon={<IconFruits />}>Fruits</Dropdown.NavigationItem>
|
|
147
|
+
</Dropdown.Navigation>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Or provide a custom label/icon for the All item:
|
|
151
|
+
|
|
152
|
+
```jsx
|
|
153
|
+
<Dropdown.Navigation showAll allLabel="Everything" allIcon={IconAll}>
|
|
154
|
+
<Dropdown.NavigationItem id="fruits">Fruits</Dropdown.NavigationItem>
|
|
155
|
+
</Dropdown.Navigation>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### `<Dropdown.NavigationItem>`
|
|
161
|
+
|
|
162
|
+
A single item in the navigation column.
|
|
163
|
+
|
|
164
|
+
| Prop | Type | Default | Description |
|
|
165
|
+
|---|---|---|---|
|
|
166
|
+
| `id` | `string` | **required** | Must match the `forId` of a `Dropdown.Section`. |
|
|
167
|
+
| `icon` | `ReactNode \| FC` | — | Icon displayed left of the label. |
|
|
168
|
+
| `component` | `React component` | — | Custom component. Receives `isActive`, `onClick`, `id`. |
|
|
169
|
+
| `...rest` | `any` | — | Any additional props are forwarded to the `<button>` (or `component`). `onClick` is composed with the internal nav handler. |
|
|
170
|
+
|
|
171
|
+
In `displayMode="scroll"`, clicking a nav item smooth-scrolls to the matching section. The active item is updated automatically as the user scrolls (scroll spy).
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
### `<Dropdown.Content>`
|
|
176
|
+
|
|
177
|
+
Right content column. Contains the search bar and the scrollable item list.
|
|
178
|
+
|
|
179
|
+
| Prop | Type | Default | Description |
|
|
180
|
+
|---|---|---|---|
|
|
181
|
+
| `searchPlaceholder` | `string` | `"Search"` | Placeholder text for the search input. |
|
|
182
|
+
| `emptyText` | `string` | `"Nothing to show here"` | Text shown when `Content` has no children at all (empty state). The search bar is also hidden in this state. |
|
|
183
|
+
| `component` | `React component` | — | Custom wrapper component. |
|
|
184
|
+
| `...rest` | `any` | — | Any additional props are forwarded to the content column `<div>` (or `component`). |
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### `<Dropdown.Section>`
|
|
189
|
+
|
|
190
|
+
Groups `Dropdown.Group` / `Dropdown.Item` elements under a nav scope.
|
|
191
|
+
|
|
192
|
+
| Prop | Type | Default | Description |
|
|
193
|
+
|---|---|---|---|
|
|
194
|
+
| `forId` | `string` | `"__all__"` | Matches a `Dropdown.NavigationItem` id. Defaults to `"__all__"`, so it can be omitted when there's no navigation. Also accepts `for` (JSX alias). |
|
|
195
|
+
| `title` | `string` | — | Section heading shown in `displayMode="scroll"`. Sticks to the top of the scroll container while the section is in view. |
|
|
196
|
+
| `...rest` | `any` | — | Any additional props are forwarded to the section wrapper `<div>`. |
|
|
197
|
+
|
|
198
|
+
```jsx
|
|
199
|
+
{/* With nav */}
|
|
200
|
+
<Dropdown.Section forId="fruits" title="Fruits">
|
|
201
|
+
...
|
|
202
|
+
</Dropdown.Section>
|
|
203
|
+
|
|
204
|
+
{/* Without nav — forId can be omitted */}
|
|
205
|
+
<Dropdown.Section>
|
|
206
|
+
...
|
|
207
|
+
</Dropdown.Section>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### `<Dropdown.Group>`
|
|
213
|
+
|
|
214
|
+
A collapsible group of items with a colored left-border accent.
|
|
215
|
+
|
|
216
|
+
| Prop | Type | Default | Description |
|
|
217
|
+
|---|---|---|---|
|
|
218
|
+
| `label` | `string` | **required** | Group heading text. |
|
|
219
|
+
| `id` | `string` | auto | Stable identifier for the imperative API (`ref.current.selectAll(id)`). Auto-derived from `label` if omitted (`"Team Members"` → `"team_members"`). Provide an explicit `id` when using the imperative API. |
|
|
220
|
+
| `icon` | `ReactNode \| FC` | — | Icon displayed left of the group label in the header bar. |
|
|
221
|
+
| `color` | `string` | auto | CSS color for the left accent bar. Auto-assigned from a built-in palette if omitted. |
|
|
222
|
+
| `defaultExpanded` | `boolean` | — | Override the root-level `defaultGroupExpanded` for this specific group. |
|
|
223
|
+
| `showSelectAll` | `boolean` | `false` | Shows a "Select all" checkbox item inside the group. |
|
|
224
|
+
| `selectAllPosition` | `"top" \| "bottom"` | `"bottom"` | Position of the select-all item. |
|
|
225
|
+
| `emptyText` | `string` | `"Nothing to show here"` | Text shown when the group has no children. |
|
|
226
|
+
| `noResultsText` | `string` | `"No results"` | Text shown when a search query returns no matching items inside this group. |
|
|
227
|
+
| `component` | `React component` | — | Custom wrapper component. |
|
|
228
|
+
| `...rest` | `any` | — | Any additional props are forwarded to the group wrapper `<div>` (or `component`). |
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
### `<Dropdown.Item>`
|
|
233
|
+
|
|
234
|
+
A single selectable or checkable item.
|
|
235
|
+
|
|
236
|
+
| Prop | Type | Default | Description |
|
|
237
|
+
|---|---|---|---|
|
|
238
|
+
| `id` | `string` | **required** | Unique identifier for this item. |
|
|
239
|
+
| `type` | `"click" \| "checkbox"` | `"click"` | Interaction mode. `"click"` triggers a `select` event; `"checkbox"` triggers a `check` event. |
|
|
240
|
+
| `icon` | `ReactNode \| FC` | — | Icon displayed left of the item label. |
|
|
241
|
+
| `defaultChecked` | `boolean` | `false` | Initial checked state (uncontrolled, `type="checkbox"` only). |
|
|
242
|
+
| `checkIcon` | `ReactNode \| FC` | built-in ✓ | Custom check icon. |
|
|
243
|
+
| `component` | `React component` | — | Custom component. Receives `isSelected`, `isChecked`, `onClick`. |
|
|
244
|
+
| `...rest` | `any` | — | Any additional props are forwarded to the item `<div>` (or `component`). `onClick` and `onKeyDown` are composed with the internal selection handlers. |
|
|
245
|
+
|
|
246
|
+
Items are automatically filtered when the user types in the search box.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## `fromConfig` — config-driven rendering
|
|
251
|
+
|
|
252
|
+
Pass a plain JS object to the `fromConfig` prop on `<Dropdown>` to render the entire tree without writing JSX. Useful for server-driven UIs or stored configurations.
|
|
253
|
+
|
|
254
|
+
> `fromConfig` and `children` cannot be used together — `fromConfig` takes precedence.
|
|
255
|
+
|
|
256
|
+
```jsx
|
|
257
|
+
<Dropdown fromConfig={config} />
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Config schema
|
|
261
|
+
|
|
262
|
+
Any unknown props at any level are forwarded as-is to the underlying component. This means you can pass `data-*` attributes, `onClick`, `onMouseEnter`, or any other prop directly in the config object:
|
|
263
|
+
|
|
264
|
+
```js
|
|
265
|
+
// Example — data-test and onClick on a specific item
|
|
266
|
+
{ id: 'name', label: 'Name', 'data-test': 'field-name', onClick: handleClick }
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
const config = {
|
|
271
|
+
// Root props (all optional)
|
|
272
|
+
displayMode: 'scroll' | 'tab',
|
|
273
|
+
defaultOpen: boolean,
|
|
274
|
+
defaultGroupExpanded: boolean | 'first',
|
|
275
|
+
hideOnSelection: boolean,
|
|
276
|
+
onEvent: ({ type, payload, prev }) => any,
|
|
277
|
+
// ...any extra props are spread onto <Dropdown>
|
|
278
|
+
|
|
279
|
+
// Trigger — required
|
|
280
|
+
trigger: ReactNode | string | {
|
|
281
|
+
label: string,
|
|
282
|
+
className?: string,
|
|
283
|
+
component?: ComponentType,
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// Panel (optional)
|
|
287
|
+
panel?: {
|
|
288
|
+
placement?: string, // default 'bottom-start'
|
|
289
|
+
offset?: number, // default 8
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
// Navigation column (optional)
|
|
293
|
+
navigation?: { ... }, // legacy alias, still supported
|
|
294
|
+
items?: [ // preferred root config for navigation items
|
|
295
|
+
{
|
|
296
|
+
id?: string,
|
|
297
|
+
label: string,
|
|
298
|
+
icon?: ReactNode | FC,
|
|
299
|
+
title?: string,
|
|
300
|
+
items?: [
|
|
301
|
+
{
|
|
302
|
+
id?: string,
|
|
303
|
+
label?: string,
|
|
304
|
+
icon?: ReactNode | FC,
|
|
305
|
+
color?: string,
|
|
306
|
+
defaultExpanded?: boolean,
|
|
307
|
+
showSelectAll?: boolean,
|
|
308
|
+
selectAllPosition?: 'top' | 'bottom',
|
|
309
|
+
emptyText?: string,
|
|
310
|
+
noResultsText?: string,
|
|
311
|
+
items: [
|
|
312
|
+
{
|
|
313
|
+
id: string,
|
|
314
|
+
label: string,
|
|
315
|
+
icon?: ReactNode | FC,
|
|
316
|
+
type?: 'click' | 'checkbox',
|
|
317
|
+
defaultChecked?: boolean,
|
|
318
|
+
checkIcon?: ReactNode | FC,
|
|
319
|
+
actions?: ReactNode | ((item) => ReactNode),
|
|
320
|
+
component?: ComponentType,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
showAll?: boolean,
|
|
328
|
+
allLabel?: string,
|
|
329
|
+
allIcon?: ReactNode | FC,
|
|
330
|
+
collapsed?: boolean,
|
|
331
|
+
autoCollapse?: boolean,
|
|
332
|
+
|
|
333
|
+
// Content column — required
|
|
334
|
+
content: {
|
|
335
|
+
searchPlaceholder?: string,
|
|
336
|
+
emptyText?: string, // shown when no sections/groups are provided
|
|
337
|
+
sections: [
|
|
338
|
+
{
|
|
339
|
+
for?: string, // optional when section content lives under items[]
|
|
340
|
+
title?: string,
|
|
341
|
+
groups?: [
|
|
342
|
+
{
|
|
343
|
+
id?: string,
|
|
344
|
+
label?: string,
|
|
345
|
+
icon?: ReactNode | FC,
|
|
346
|
+
color?: string,
|
|
347
|
+
defaultExpanded?: boolean,
|
|
348
|
+
showSelectAll?: boolean,
|
|
349
|
+
selectAllPosition?: 'top' | 'bottom',
|
|
350
|
+
emptyText?: string,
|
|
351
|
+
noResultsText?: string,
|
|
352
|
+
items: [
|
|
353
|
+
{
|
|
354
|
+
id: string,
|
|
355
|
+
label: string,
|
|
356
|
+
icon?: ReactNode | FC,
|
|
357
|
+
type?: 'click' | 'checkbox',
|
|
358
|
+
defaultChecked?: boolean,
|
|
359
|
+
checkIcon?: ReactNode | FC,
|
|
360
|
+
actions?: ReactNode | ((item) => ReactNode),
|
|
361
|
+
component?: ComponentType,
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
items?: [
|
|
367
|
+
{
|
|
368
|
+
id?: string,
|
|
369
|
+
label?: string,
|
|
370
|
+
icon?: ReactNode | FC,
|
|
371
|
+
color?: string,
|
|
372
|
+
defaultExpanded?: boolean,
|
|
373
|
+
showSelectAll?: boolean,
|
|
374
|
+
selectAllPosition?: 'top' | 'bottom',
|
|
375
|
+
emptyText?: string,
|
|
376
|
+
noResultsText?: string,
|
|
377
|
+
items: [
|
|
378
|
+
{
|
|
379
|
+
id: string,
|
|
380
|
+
label: string,
|
|
381
|
+
icon?: ReactNode | FC,
|
|
382
|
+
type?: 'click' | 'checkbox',
|
|
383
|
+
defaultChecked?: boolean,
|
|
384
|
+
checkIcon?: ReactNode | FC,
|
|
385
|
+
actions?: ReactNode | ((item) => ReactNode),
|
|
386
|
+
component?: ComponentType,
|
|
387
|
+
// ...any extra props (data-*, onClick, onMouseEnter, …) are forwarded to <DropdownItem>
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Example
|
|
399
|
+
|
|
400
|
+
```jsx
|
|
401
|
+
import { Dropdown } from '@diabolic/hangover'
|
|
402
|
+
|
|
403
|
+
const config = {
|
|
404
|
+
trigger: 'Select fields',
|
|
405
|
+
showAll: true,
|
|
406
|
+
items: [
|
|
407
|
+
{
|
|
408
|
+
id: 'basic',
|
|
409
|
+
label: 'Basic',
|
|
410
|
+
title: 'Basic',
|
|
411
|
+
items: [
|
|
412
|
+
{
|
|
413
|
+
label: 'Identity',
|
|
414
|
+
items: [
|
|
415
|
+
{ id: 'name', label: 'Name' },
|
|
416
|
+
{ id: 'email', label: 'Email' },
|
|
417
|
+
],
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
id: 'advanced',
|
|
423
|
+
label: 'Advanced',
|
|
424
|
+
title: 'Advanced',
|
|
425
|
+
items: [
|
|
426
|
+
{
|
|
427
|
+
label: 'System',
|
|
428
|
+
items: [
|
|
429
|
+
{ id: 'created-at', label: 'Created at' },
|
|
430
|
+
{ id: 'updated-at', label: 'Updated at' },
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
content: {
|
|
437
|
+
searchPlaceholder: 'Search fields...',
|
|
438
|
+
},
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export default function App() {
|
|
442
|
+
return <Dropdown fromConfig={config} />
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Events
|
|
449
|
+
|
|
450
|
+
All events are delivered via the `onEvent` prop on `<Dropdown>`.
|
|
451
|
+
|
|
452
|
+
```jsx
|
|
453
|
+
<Dropdown onEvent={({ type, payload, prev }) => {
|
|
454
|
+
console.log(type, payload)
|
|
455
|
+
}}>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
| Event type | `payload` | `prev` | Description |
|
|
459
|
+
|---|---|---|---|
|
|
460
|
+
| `open` | `{ trigger }` | — | Panel opened. `trigger`: `"click" \| "imperative"`. |
|
|
461
|
+
| `close` | `{ trigger }` | — | Panel closed. `trigger`: `"click" \| "outside" \| "escape" \| "imperative"`. |
|
|
462
|
+
| `select` | `{ id, label, groupId, groupLabel }` | `{ id, label } \| null` | An item was clicked (`type="click"`). |
|
|
463
|
+
| `check` | `{ id, label, groupId, groupLabel }` | `{ checked }` | A checkbox item was toggled. |
|
|
464
|
+
| `selectAll` | `{ groupId, groupLabel, itemIds }` | `{ checked }` | Select-all was toggled. |
|
|
465
|
+
| `navChange` | `{ id }` | `{ id }` | Active nav item changed. |
|
|
466
|
+
| `search` | `{ query }` | `{ query }` | Search input changed. |
|
|
467
|
+
| `groupToggle` | `{ groupId, groupLabel, expanded }` | — | A group was expanded or collapsed. |
|
|
468
|
+
|
|
469
|
+
### Cancelling an event
|
|
470
|
+
|
|
471
|
+
Return `null` from `onEvent` to cancel the state update:
|
|
472
|
+
|
|
473
|
+
```jsx
|
|
474
|
+
<Dropdown onEvent={({ type, payload }) => {
|
|
475
|
+
if (type === 'select' && payload.id === 'locked') return null // cancel
|
|
476
|
+
}}>
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Native DOM events
|
|
480
|
+
|
|
481
|
+
Each event also fires a native `CustomEvent` on the trigger element:
|
|
482
|
+
|
|
483
|
+
```js
|
|
484
|
+
trigger.addEventListener('HO:select', (e) => {
|
|
485
|
+
console.log(e.detail) // { payload, prev }
|
|
486
|
+
})
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## Imperative API
|
|
492
|
+
|
|
493
|
+
Attach a `ref` to `<Dropdown>` to control it programmatically from **outside** the tree:
|
|
494
|
+
|
|
495
|
+
```jsx
|
|
496
|
+
const dropdownRef = useRef()
|
|
497
|
+
|
|
498
|
+
<Dropdown ref={dropdownRef}>
|
|
499
|
+
...
|
|
500
|
+
</Dropdown>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
| Method | Returns | Description |
|
|
504
|
+
|---|---|---|
|
|
505
|
+
| `open()` | — | Open the panel. |
|
|
506
|
+
| `close()` | — | Close the panel. |
|
|
507
|
+
| `toggle()` | — | Toggle open/close. |
|
|
508
|
+
| `isOpen()` | `boolean` | Current open state. |
|
|
509
|
+
| `getSelected()` | `{ id, label } \| null` | Currently selected item. |
|
|
510
|
+
| `getChecked()` | `Map<id, boolean>` | All checkbox states. |
|
|
511
|
+
| `getActiveNavItem()` | `string` | Active nav item id. |
|
|
512
|
+
| `setSearch(query)` | — | Programmatically set the search query. |
|
|
513
|
+
| `selectAll(groupId, checked?)` | — | Toggle or force the select-all state of a group. `groupId` matches the `id` prop on `<Dropdown.Group>`. Omit `checked` to toggle; pass `true`/`false` to force. |
|
|
514
|
+
|
|
515
|
+
```jsx
|
|
516
|
+
const ref = useRef()
|
|
517
|
+
|
|
518
|
+
// toggle
|
|
519
|
+
ref.current.selectAll('metrics')
|
|
520
|
+
|
|
521
|
+
// force on / force off
|
|
522
|
+
ref.current.selectAll('metrics', true)
|
|
523
|
+
ref.current.selectAll('metrics', false)
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## `useDropdown` Hook
|
|
529
|
+
|
|
530
|
+
Use `useDropdown()` to read state and trigger actions from **inside** the dropdown tree — for example inside a custom component passed via the `component` prop.
|
|
531
|
+
|
|
532
|
+
```jsx
|
|
533
|
+
import { useDropdown } from '@diabolic/hangover'
|
|
534
|
+
|
|
535
|
+
function MyCustomItem({ isSelected, onClick, children }) {
|
|
536
|
+
const { searchQuery, activeNavId, close } = useDropdown()
|
|
537
|
+
// ...
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
<Dropdown.Item id="foo" component={MyCustomItem}>Foo</Dropdown.Item>
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Must be called inside a `<Dropdown>` subtree.
|
|
544
|
+
|
|
545
|
+
### Returns
|
|
546
|
+
|
|
547
|
+
**Reactive state** — triggers re-render when the value changes:
|
|
548
|
+
|
|
549
|
+
| Property | Type | Description |
|
|
550
|
+
|---|---|---|
|
|
551
|
+
| `isOpen` | `boolean` | Panel open state. |
|
|
552
|
+
| `selectedItem` | `{ id, label } \| null` | Currently selected item. |
|
|
553
|
+
| `checkedItems` | `Map<id, boolean>` | All checkbox states. |
|
|
554
|
+
| `activeNavId` | `string` | Active navigation item id. |
|
|
555
|
+
| `activeNavLabel` | `string` | Label of the active navigation item. |
|
|
556
|
+
| `searchQuery` | `string` | Current search input value. |
|
|
557
|
+
| `displayMode` | `"scroll" \| "tab"` | Display mode of the dropdown. |
|
|
558
|
+
| `darkMode` | `boolean` | Dark mode state. |
|
|
559
|
+
|
|
560
|
+
**Actions** — stable references, safe to use in `useEffect` / `useCallback` deps:
|
|
561
|
+
|
|
562
|
+
| Method | Description |
|
|
563
|
+
|---|---|
|
|
564
|
+
| `open()` | Open the panel. |
|
|
565
|
+
| `close()` | Close the panel. |
|
|
566
|
+
| `toggle()` | Toggle open/closed. |
|
|
567
|
+
| `setSearch(query)` | Update the search query. |
|
|
568
|
+
| `fireEvent(type, payload)` | Fire any internal event. Useful for advanced / unforeseen scenarios. Return `null` from `onEvent` to cancel. |
|
|
569
|
+
|
|
570
|
+
### Example — custom trigger with current state
|
|
571
|
+
|
|
572
|
+
```jsx
|
|
573
|
+
import { useDropdown } from '@diabolic/hangover'
|
|
574
|
+
|
|
575
|
+
function SmartTrigger() {
|
|
576
|
+
const { isOpen, selectedItem, toggle } = useDropdown()
|
|
577
|
+
|
|
578
|
+
return (
|
|
579
|
+
<button onClick={toggle}>
|
|
580
|
+
{selectedItem ? selectedItem.label : 'Select a field'}
|
|
581
|
+
{isOpen ? ' ▲' : ' ▼'}
|
|
582
|
+
</button>
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
<Dropdown>
|
|
587
|
+
<Dropdown.Trigger>
|
|
588
|
+
<SmartTrigger />
|
|
589
|
+
</Dropdown.Trigger>
|
|
590
|
+
...
|
|
591
|
+
</Dropdown>
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Example — close panel from inside a custom item
|
|
595
|
+
|
|
596
|
+
```jsx
|
|
597
|
+
function MyItem({ isSelected, onClick, children }) {
|
|
598
|
+
const { close } = useDropdown()
|
|
599
|
+
|
|
600
|
+
return (
|
|
601
|
+
<div onClick={() => { onClick(); close() }}>
|
|
602
|
+
{children}
|
|
603
|
+
</div>
|
|
604
|
+
)
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Example — react to search query inside a custom component
|
|
609
|
+
|
|
610
|
+
```jsx
|
|
611
|
+
function MyContent({ children }) {
|
|
612
|
+
const { searchQuery, activeNavLabel } = useDropdown()
|
|
613
|
+
|
|
614
|
+
return (
|
|
615
|
+
<div>
|
|
616
|
+
{searchQuery && <p>Results for "{searchQuery}" in {activeNavLabel}</p>}
|
|
617
|
+
{children}
|
|
618
|
+
</div>
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## Recipes
|
|
626
|
+
|
|
627
|
+
### With icons on groups and items
|
|
628
|
+
|
|
629
|
+
```jsx
|
|
630
|
+
function IconUser() {
|
|
631
|
+
return (
|
|
632
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
633
|
+
<path fillRule="evenodd" clipRule="evenodd" d="M8 2a2.667 2.667 0 1 1 0 5.333A2.667 2.667 0 0 1 8 2Zm0 6.667c-3.2 0-5.333 1.6-5.333 2.666V12h10.666v-.667C13.333 10.267 11.2 8.667 8 8.667Z" fill="currentColor" />
|
|
634
|
+
</svg>
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function IconMail() {
|
|
639
|
+
return (
|
|
640
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
641
|
+
<path fillRule="evenodd" clipRule="evenodd" d="M2 3.333A.667.667 0 0 1 2.667 2.667h10.666A.667.667 0 0 1 14 3.333v9.334a.667.667 0 0 1-.667.666H2.667A.667.667 0 0 1 2 12.667V3.333Zm1.333.92V12h9.334V4.253L8 7.92 3.333 4.253ZM12.24 4H3.76L8 6.747 12.24 4Z" fill="currentColor" />
|
|
642
|
+
</svg>
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
<Dropdown.Group label="Contact" icon={IconUser}>
|
|
647
|
+
<Dropdown.Item id="name" icon={IconUser}>Full Name</Dropdown.Item>
|
|
648
|
+
<Dropdown.Item id="email" icon={IconMail}>Email Address</Dropdown.Item>
|
|
649
|
+
</Dropdown.Group>
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
Icons inherit the item's text color via `currentColor` — they automatically adapt to hover, active, and dark mode states.
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
### Dark mode
|
|
657
|
+
|
|
658
|
+
```jsx
|
|
659
|
+
<Dropdown darkMode>
|
|
660
|
+
<Dropdown.Trigger><button>Open</button></Dropdown.Trigger>
|
|
661
|
+
<Dropdown.Panel>
|
|
662
|
+
...
|
|
663
|
+
</Dropdown.Panel>
|
|
664
|
+
</Dropdown>
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
### Panel title
|
|
670
|
+
|
|
671
|
+
```jsx
|
|
672
|
+
<Dropdown.Panel title="Select a field">
|
|
673
|
+
...
|
|
674
|
+
</Dropdown.Panel>
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
### Empty content state
|
|
680
|
+
|
|
681
|
+
When `<Dropdown.Content>` has no children, the search bar is hidden and an empty message is shown:
|
|
682
|
+
|
|
683
|
+
```jsx
|
|
684
|
+
<Dropdown.Content emptyText="No fields available">
|
|
685
|
+
{/* no children */}
|
|
686
|
+
</Dropdown.Content>
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
---
|
|
690
|
+
|
|
691
|
+
### Search no-results per group
|
|
692
|
+
|
|
693
|
+
```jsx
|
|
694
|
+
<Dropdown.Group label="Metrics" noResultsText="No metrics match your search">
|
|
695
|
+
<Dropdown.Item id="revenue">Revenue</Dropdown.Item>
|
|
696
|
+
<Dropdown.Item id="sessions">Sessions</Dropdown.Item>
|
|
697
|
+
</Dropdown.Group>
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
### Controlled search query
|
|
703
|
+
|
|
704
|
+
Drive the search input from outside — connect it to your own state or a URL param:
|
|
705
|
+
|
|
706
|
+
```jsx
|
|
707
|
+
const [query, setQuery] = useState('')
|
|
708
|
+
|
|
709
|
+
<input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search..." />
|
|
710
|
+
|
|
711
|
+
<Dropdown
|
|
712
|
+
searchQuery={query}
|
|
713
|
+
onEvent={({ type, payload }) => {
|
|
714
|
+
if (type === 'search') setQuery(payload.query)
|
|
715
|
+
}}
|
|
716
|
+
>
|
|
717
|
+
...
|
|
718
|
+
</Dropdown>
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
Or use `defaultSearchQuery` to set the initial value without controlling it:
|
|
722
|
+
|
|
723
|
+
```jsx
|
|
724
|
+
<Dropdown defaultSearchQuery="rev">
|
|
725
|
+
...
|
|
726
|
+
</Dropdown>
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
### With left navigation (scroll mode)
|
|
732
|
+
|
|
733
|
+
```jsx
|
|
734
|
+
<Dropdown displayMode="scroll">
|
|
735
|
+
<Dropdown.Trigger><button>Browse</button></Dropdown.Trigger>
|
|
736
|
+
<Dropdown.Panel>
|
|
737
|
+
<Dropdown.Navigation showAll>
|
|
738
|
+
<Dropdown.NavigationItem id="fruits">Fruits</Dropdown.NavigationItem>
|
|
739
|
+
<Dropdown.NavigationItem id="vegetables">Vegetables</Dropdown.NavigationItem>
|
|
740
|
+
</Dropdown.Navigation>
|
|
741
|
+
<Dropdown.Content>
|
|
742
|
+
<Dropdown.Section forId="fruits" title="Fruits">
|
|
743
|
+
<Dropdown.Group label="Citrus">
|
|
744
|
+
<Dropdown.Item id="orange">Orange</Dropdown.Item>
|
|
745
|
+
<Dropdown.Item id="lemon">Lemon</Dropdown.Item>
|
|
746
|
+
</Dropdown.Group>
|
|
747
|
+
</Dropdown.Section>
|
|
748
|
+
<Dropdown.Section forId="vegetables" title="Vegetables">
|
|
749
|
+
<Dropdown.Group label="Leafy">
|
|
750
|
+
<Dropdown.Item id="spinach">Spinach</Dropdown.Item>
|
|
751
|
+
</Dropdown.Group>
|
|
752
|
+
</Dropdown.Section>
|
|
753
|
+
</Dropdown.Content>
|
|
754
|
+
</Dropdown.Panel>
|
|
755
|
+
</Dropdown>
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
### Checkbox mode with select-all
|
|
759
|
+
|
|
760
|
+
```jsx
|
|
761
|
+
<Dropdown>
|
|
762
|
+
<Dropdown.Trigger><button>Select fields</button></Dropdown.Trigger>
|
|
763
|
+
<Dropdown.Panel>
|
|
764
|
+
<Dropdown.Content>
|
|
765
|
+
<Dropdown.Section>
|
|
766
|
+
<Dropdown.Group label="Metrics" showSelectAll>
|
|
767
|
+
<Dropdown.Item id="revenue" type="checkbox">Revenue</Dropdown.Item>
|
|
768
|
+
<Dropdown.Item id="sessions" type="checkbox">Sessions</Dropdown.Item>
|
|
769
|
+
<Dropdown.Item id="bounce" type="checkbox">Bounce rate</Dropdown.Item>
|
|
770
|
+
</Dropdown.Group>
|
|
771
|
+
</Dropdown.Section>
|
|
772
|
+
</Dropdown.Content>
|
|
773
|
+
</Dropdown.Panel>
|
|
774
|
+
</Dropdown>
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### Controlled — prevent selection
|
|
778
|
+
|
|
779
|
+
```jsx
|
|
780
|
+
<Dropdown onEvent={({ type, payload }) => {
|
|
781
|
+
if (type === 'select' && payload.id === 'locked') {
|
|
782
|
+
alert('This item is locked')
|
|
783
|
+
return null // cancel
|
|
784
|
+
}
|
|
785
|
+
}}>
|
|
786
|
+
...
|
|
787
|
+
</Dropdown>
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### Collapse all groups by default
|
|
791
|
+
|
|
792
|
+
```jsx
|
|
793
|
+
<Dropdown defaultGroupExpanded={false}>
|
|
794
|
+
...
|
|
795
|
+
</Dropdown>
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
### Expand only the first group
|
|
799
|
+
|
|
800
|
+
```jsx
|
|
801
|
+
<Dropdown defaultGroupExpanded="first">
|
|
802
|
+
...
|
|
803
|
+
</Dropdown>
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### Custom panel placement and offset
|
|
807
|
+
|
|
808
|
+
```jsx
|
|
809
|
+
{/* Open above the trigger, aligned to the right edge, 16px gap */}
|
|
810
|
+
<Dropdown.Panel placement="top-end" offset={16}>
|
|
811
|
+
...
|
|
812
|
+
</Dropdown.Panel>
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
### Without a Trigger
|
|
816
|
+
|
|
817
|
+
When you cannot wrap the toggle button inside `<Dropdown>`, use an external `anchor` ref and control the panel via the [Imperative API](#imperative-api):
|
|
818
|
+
|
|
819
|
+
```jsx
|
|
820
|
+
const dropdownRef = useRef()
|
|
821
|
+
const buttonRef = useRef()
|
|
822
|
+
|
|
823
|
+
// The button lives anywhere in the tree — outside <Dropdown> if needed
|
|
824
|
+
<button ref={buttonRef} onClick={() => dropdownRef.current.toggle()}>
|
|
825
|
+
Open
|
|
826
|
+
</button>
|
|
827
|
+
|
|
828
|
+
<Dropdown ref={dropdownRef}>
|
|
829
|
+
<Dropdown.Panel anchor={buttonRef}>
|
|
830
|
+
<Dropdown.Content>
|
|
831
|
+
<Dropdown.Section>
|
|
832
|
+
<Dropdown.Group label="Items">
|
|
833
|
+
<Dropdown.Item id="one">One</Dropdown.Item>
|
|
834
|
+
</Dropdown.Group>
|
|
835
|
+
</Dropdown.Section>
|
|
836
|
+
</Dropdown.Content>
|
|
837
|
+
</Dropdown.Panel>
|
|
838
|
+
</Dropdown>
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
`Dropdown.Trigger` is not needed in this pattern. The `anchor` ref is used for positioning and outside-click detection.
|
|
842
|
+
|
|
843
|
+
---
|
|
844
|
+
|
|
845
|
+
### Collapsible / auto-collapsing navigation
|
|
846
|
+
|
|
847
|
+
```jsx
|
|
848
|
+
{/* Start collapsed */}
|
|
849
|
+
<Dropdown.Navigation collapsed>
|
|
850
|
+
...
|
|
851
|
+
</Dropdown.Navigation>
|
|
852
|
+
|
|
853
|
+
{/* Auto-collapse when the viewport is too narrow */}
|
|
854
|
+
<Dropdown.Navigation autoCollapse>
|
|
855
|
+
...
|
|
856
|
+
</Dropdown.Navigation>
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
The `autoCollapse` threshold is `--hangover-nav-width` + `--hangover-content-max-width`. Override either token to tune the breakpoint.
|
|
860
|
+
|
|
861
|
+
---
|
|
862
|
+
|
|
863
|
+
### Custom component slot
|
|
864
|
+
|
|
865
|
+
Every compound component accepts a `component` prop to swap the root element:
|
|
866
|
+
|
|
867
|
+
```jsx
|
|
868
|
+
<Dropdown.Item id="foo" component={MyCustomRow}>
|
|
869
|
+
Foo
|
|
870
|
+
</Dropdown.Item>
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## CSS Tokens
|
|
876
|
+
|
|
877
|
+
All visual properties are configurable via CSS custom properties:
|
|
878
|
+
|
|
879
|
+
```css
|
|
880
|
+
.hangoverDropdown {
|
|
881
|
+
--hangover-font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
882
|
+
--hangover-font-size-base: 14px;
|
|
883
|
+
--hangover-font-size-sm: 12px;
|
|
884
|
+
|
|
885
|
+
/* Text */
|
|
886
|
+
--hangover-color-text: #0a1551;
|
|
887
|
+
--hangover-color-text-muted: #8a9ab5;
|
|
888
|
+
|
|
889
|
+
/* Backgrounds */
|
|
890
|
+
--hangover-color-bg-panel: #ffffff;
|
|
891
|
+
--hangover-color-bg-title: #eceef5;
|
|
892
|
+
--hangover-color-bg-nav: #F5F6FC;
|
|
893
|
+
--hangover-color-bg-nav-hover: #EAECF5;
|
|
894
|
+
--hangover-color-bg-nav-active: #E0E3EF;
|
|
895
|
+
--hangover-color-bg-hover: #f3f3fe;
|
|
896
|
+
--hangover-color-bg-hover-dark: #eaeaf9;
|
|
897
|
+
--hangover-color-bg-selected: #eef2ff;
|
|
898
|
+
--hangover-color-bg-checked: #EDF8FF;
|
|
899
|
+
|
|
900
|
+
/* Border & misc */
|
|
901
|
+
--hangover-color-border: #e2e8f0;
|
|
902
|
+
--hangover-color-search-ph: #979DC6;
|
|
903
|
+
--hangover-color-search-icon: #343C6A;
|
|
904
|
+
--hangover-color-focus: #3b82f6;
|
|
905
|
+
--hangover-group-default-color: #16a34a;
|
|
906
|
+
|
|
907
|
+
/* Shape */
|
|
908
|
+
--hangover-radius-panel: 4px;
|
|
909
|
+
--hangover-radius-item: 4px;
|
|
910
|
+
--hangover-radius-nav-item: 4px;
|
|
911
|
+
|
|
912
|
+
--hangover-shadow-panel:
|
|
913
|
+
0 8px 16px 0 rgba(84, 95, 111, 0.16),
|
|
914
|
+
0 2px 4px 0 rgba(37, 45, 91, 0.04);
|
|
915
|
+
|
|
916
|
+
/* Layout */
|
|
917
|
+
--hangover-nav-width: 172px;
|
|
918
|
+
--hangover-content-max-width: 240px;
|
|
919
|
+
--hangover-list-max-height: 280px;
|
|
920
|
+
|
|
921
|
+
--hangover-transition: 330ms ease;
|
|
922
|
+
}
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
### Dark mode
|
|
926
|
+
|
|
927
|
+
Pass `darkMode` to `<Dropdown>` to apply a pre-built dark palette:
|
|
928
|
+
|
|
929
|
+
```jsx
|
|
930
|
+
<Dropdown darkMode>
|
|
931
|
+
...
|
|
932
|
+
</Dropdown>
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
To customise dark mode colors, override the tokens inside `.hangoverDropdown--dark`:
|
|
936
|
+
|
|
937
|
+
```css
|
|
938
|
+
.hangoverDropdown--dark {
|
|
939
|
+
--hangover-color-bg-panel: #1a1d2e;
|
|
940
|
+
--hangover-color-text: #dde1f5;
|
|
941
|
+
/* ... */
|
|
942
|
+
}
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
---
|
|
946
|
+
|
|
947
|
+
## Storybook
|
|
948
|
+
|
|
949
|
+
Explore all components and interactions in the [live demo](https://bugrakaan.github.io/hangover/).
|
|
950
|
+
|
|
951
|
+
Run locally:
|
|
952
|
+
|
|
953
|
+
```bash
|
|
954
|
+
npm run storybook
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
Build the static demo site (output in `storybook-static/`):
|
|
958
|
+
|
|
959
|
+
```bash
|
|
960
|
+
npm run build-storybook
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
Stories are located in `src/stories/`:
|
|
964
|
+
|
|
965
|
+
| Story | Description |
|
|
966
|
+
|---|---|
|
|
967
|
+
| 1. Basic | Basic usage, with/without nav, single-section auto-transform, open by default, empty state |
|
|
968
|
+
| 2. Scroll Spy | Scroll-spy with nav icons, collapsible navigation, long text stress test |
|
|
969
|
+
| 3. Tab Mode | `displayMode="tab"` |
|
|
970
|
+
| 4. Checkbox | Checkbox items, select-all |
|
|
971
|
+
| 5. Controlled | Controlled state, external anchor + imperative API |
|
|
972
|
+
| 6. Events | Live event stream, cancel event |
|
|
973
|
+
| 7. Placement | All panel placement variants, auto-placement with scroll |
|
|
974
|
+
| 8. From Config | `fromConfig` prop — config-driven rendering with and without auto-collapse |
|
|
975
|
+
|
|
976
|
+
---
|
|
977
|
+
|
|
978
|
+
## License
|
|
979
|
+
|
|
980
|
+
MIT
|