@longd/layout-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 +450 -0
- package/dist/chunk-CZ3IMHZ6.js +1083 -0
- package/dist/chunk-CZ3IMHZ6.js.map +1 -0
- package/dist/index.cjs +1098 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/select.cjs +1095 -0
- package/dist/layout/select.cjs.map +1 -0
- package/dist/layout/select.d.cts +102 -0
- package/dist/layout/select.d.ts +102 -0
- package/dist/layout/select.js +9 -0
- package/dist/layout/select.js.map +1 -0
- package/package.json +122 -0
package/README.md
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# @longd/layout-ui
|
|
2
|
+
|
|
3
|
+
A React component library featuring a powerful, feature-rich select component with virtualization, drag-and-drop sorting, grouped options, and intelligent chip overflow management. Built with **Tailwind CSS v4**.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Single & multiple selection** with chip-based display
|
|
8
|
+
- **Virtualized list** via `@tanstack/react-virtual` — handles 10,000+ options smoothly
|
|
9
|
+
- **Drag-and-drop sorting** (flat & grouped) via `@dnd-kit`
|
|
10
|
+
- **Grouped options** with visual headers
|
|
11
|
+
- **Intelligent chip overflow** — auto-detects available space, shows partial (truncated) chips, and collapses into a `+N` overflow badge with tooltip
|
|
12
|
+
- **Async option loading** via `queryFn`
|
|
13
|
+
- **Search / filter** built-in
|
|
14
|
+
- **Keyboard accessible** — full keyboard navigation
|
|
15
|
+
- **Fully typed** — comprehensive TypeScript definitions
|
|
16
|
+
- **Tailwind CSS v4** — themeable via CSS custom properties (shadcn/ui compatible)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @longd/layout-ui
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Peer dependencies
|
|
27
|
+
|
|
28
|
+
The library requires **React 18+** (installed as peer dependency):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install react react-dom
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Prerequisites
|
|
37
|
+
|
|
38
|
+
### 1. Tailwind CSS v4
|
|
39
|
+
|
|
40
|
+
This library uses Tailwind CSS utility classes. Your project must have **Tailwind CSS v4** set up.
|
|
41
|
+
|
|
42
|
+
Tell Tailwind to scan the library's dist files so it generates the necessary utility classes. Add a `@source` directive in your main CSS file:
|
|
43
|
+
|
|
44
|
+
```css
|
|
45
|
+
@import 'tailwindcss';
|
|
46
|
+
@source "../node_modules/@longd/layout-ui/dist";
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Theme CSS variables
|
|
50
|
+
|
|
51
|
+
The components use semantic color tokens (e.g. `bg-secondary`, `text-muted-foreground`, `border-border`) that map to CSS custom properties. You need these variables defined in your CSS. If you use [shadcn/ui](https://ui.shadcn.com), they are already set up.
|
|
52
|
+
|
|
53
|
+
If not, add the following minimal theme to your global CSS (adjust colors to your design):
|
|
54
|
+
|
|
55
|
+
```css
|
|
56
|
+
:root {
|
|
57
|
+
--background: oklch(1 0 0);
|
|
58
|
+
--foreground: oklch(0.141 0.005 285.823);
|
|
59
|
+
--popover: oklch(1 0 0);
|
|
60
|
+
--popover-foreground: oklch(0.141 0.005 285.823);
|
|
61
|
+
--primary: oklch(0.21 0.006 285.885);
|
|
62
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
63
|
+
--secondary: oklch(0.967 0.001 286.375);
|
|
64
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
65
|
+
--muted: oklch(0.967 0.001 286.375);
|
|
66
|
+
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
67
|
+
--accent: oklch(0.967 0.001 286.375);
|
|
68
|
+
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
69
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
70
|
+
--border: oklch(0.92 0.004 286.32);
|
|
71
|
+
--input: oklch(0.92 0.004 286.32);
|
|
72
|
+
--ring: oklch(0.871 0.006 286.286);
|
|
73
|
+
--radius: 0.625rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@theme inline {
|
|
77
|
+
--color-background: var(--background);
|
|
78
|
+
--color-foreground: var(--foreground);
|
|
79
|
+
--color-popover: var(--popover);
|
|
80
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
81
|
+
--color-primary: var(--primary);
|
|
82
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
83
|
+
--color-secondary: var(--secondary);
|
|
84
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
85
|
+
--color-muted: var(--muted);
|
|
86
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
87
|
+
--color-accent: var(--accent);
|
|
88
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
89
|
+
--color-destructive: var(--destructive);
|
|
90
|
+
--color-border: var(--border);
|
|
91
|
+
--color-input: var(--input);
|
|
92
|
+
--color-ring: var(--ring);
|
|
93
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
94
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
95
|
+
--radius-lg: var(--radius);
|
|
96
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 3. `cn` utility
|
|
101
|
+
|
|
102
|
+
The library bundles its own `cn` helper (`clsx` + `tailwind-merge`) internally. You do **not** need to provide one.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Quick start
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { useState } from 'react'
|
|
110
|
+
import { LayoutSelect } from '@longd/layout-ui'
|
|
111
|
+
import type { IOption } from '@longd/layout-ui'
|
|
112
|
+
|
|
113
|
+
const options: IOption[] = [
|
|
114
|
+
{ label: 'Apple', value: 'apple' },
|
|
115
|
+
{ label: 'Banana', value: 'banana' },
|
|
116
|
+
{ label: 'Cherry', value: 'cherry' },
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
function App() {
|
|
120
|
+
const [value, setValue] = useState<IOption | null>(null)
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<LayoutSelect
|
|
124
|
+
type="single"
|
|
125
|
+
options={options}
|
|
126
|
+
selectValue={value}
|
|
127
|
+
onChange={(val) => setValue(val)}
|
|
128
|
+
placeholder="Pick a fruit"
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Deep import (tree-shaking)
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { LayoutSelect } from '@longd/layout-ui/layout/select'
|
|
138
|
+
import type { IOption } from '@longd/layout-ui/layout/select'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## API reference
|
|
144
|
+
|
|
145
|
+
### `<LayoutSelect>`
|
|
146
|
+
|
|
147
|
+
| Prop | Type | Default | Description |
|
|
148
|
+
| ------------------ | --------------------------------- | --------------- | --------------------------------------------------------------------------- |
|
|
149
|
+
| `type` | `'single' \| 'multiple'` | — | **Required.** Selection mode. |
|
|
150
|
+
| `options` | `IOption[]` | — | **Required.** Available options (may contain nested `children` for groups). |
|
|
151
|
+
| `selectValue` | `IOption \| IOption[] \| null` | — | Controlled value. `IOption` for single, `IOption[]` for multiple. |
|
|
152
|
+
| `onChange` | `(value, selectedOption) => void` | — | Called when selection changes. |
|
|
153
|
+
| `placeholder` | `string` | `'Select item'` | Placeholder text when nothing is selected. |
|
|
154
|
+
| `disabled` | `boolean` | `false` | Disable the entire select. |
|
|
155
|
+
| `readOnly` | `boolean` | `false` | Read-only mode — looks interactive but prevents changes. |
|
|
156
|
+
| `error` | `boolean` | `false` | Marks the select as having a validation error. |
|
|
157
|
+
| `clearable` | `boolean` | `false` | Allow clearing the selection (single mode). |
|
|
158
|
+
| `label` | `string` | — | Label rendered above the select. |
|
|
159
|
+
| `className` | `string` | — | Custom class for the root wrapper. |
|
|
160
|
+
| `triggerClassName` | `string` | — | Custom class for the trigger button. |
|
|
161
|
+
| `popupClassName` | `string` | — | Custom class for the dropdown popup. |
|
|
162
|
+
|
|
163
|
+
#### Multiple-mode props
|
|
164
|
+
|
|
165
|
+
| Prop | Type | Default | Description |
|
|
166
|
+
| ----------------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
167
|
+
| `collapsed` | `boolean` | `false` | When `true`, the trigger expands to show all chips (wraps to multiple rows) instead of collapsing overflow into a `+N` badge. |
|
|
168
|
+
| `showItemsLength` | `number` | — | Force a maximum number of visible chips. |
|
|
169
|
+
|
|
170
|
+
#### Sortable props
|
|
171
|
+
|
|
172
|
+
| Prop | Type | Default | Description |
|
|
173
|
+
| ---------------------- | ------------------------------------ | ------- | ---------------------------------------------------------------- |
|
|
174
|
+
| `sortable` | `boolean` | `false` | Enable drag-and-drop reordering of options in the dropdown list. |
|
|
175
|
+
| `onSortEnd` | `(sortedOptions: IOption[]) => void` | — | Called after reordering. Required when `sortable` is `true`. |
|
|
176
|
+
| `sortableAcrossGroups` | `boolean` | `false` | Allow dragging items across groups. |
|
|
177
|
+
|
|
178
|
+
#### Render overrides
|
|
179
|
+
|
|
180
|
+
| Prop | Type | Description |
|
|
181
|
+
| --------------- | ------------------------------ | ---------------------------------------- |
|
|
182
|
+
| `renderTrigger` | `(props) => ReactNode` | Replace the entire trigger UI. |
|
|
183
|
+
| `renderItem` | `(option, state) => ReactNode` | Replace the default option row renderer. |
|
|
184
|
+
| `listPrefix` | `ReactNode` | Content rendered before the option list. |
|
|
185
|
+
| `listSuffix` | `ReactNode` | Content rendered after the option list. |
|
|
186
|
+
|
|
187
|
+
#### Async loading
|
|
188
|
+
|
|
189
|
+
| Prop | Type | Description |
|
|
190
|
+
| --------- | -------------------------- | ----------------------------------------------------------------------------- |
|
|
191
|
+
| `queryFn` | `() => Promise<IOption[]>` | Called when popup opens. Returned options replace the current `options` prop. |
|
|
192
|
+
|
|
193
|
+
### `IOption`
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
interface IOption {
|
|
197
|
+
label: string
|
|
198
|
+
value: string | number
|
|
199
|
+
icon?: IconProp // ReactNode or () => ReactNode
|
|
200
|
+
disabled?: boolean
|
|
201
|
+
disabledTooltip?: string
|
|
202
|
+
children?: IOption[] // Nested children — rendered as a visual group
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `IconProp`
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
type IconProp = React.ReactNode | (() => React.ReactNode)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The render-function form `() => ReactNode` lets you pass lazy/memoised icons so the component only mounts them when visible (better perf for large lists with heavy SVG icons).
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Examples
|
|
217
|
+
|
|
218
|
+
### Multiple select
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
const [selected, setSelected] = useState<IOption[]>([])
|
|
222
|
+
|
|
223
|
+
<LayoutSelect
|
|
224
|
+
type="multiple"
|
|
225
|
+
options={options}
|
|
226
|
+
selectValue={selected}
|
|
227
|
+
onChange={(val) => setSelected(val)}
|
|
228
|
+
placeholder="Select items"
|
|
229
|
+
/>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Grouped options
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
const groupedOptions: IOption[] = [
|
|
236
|
+
{
|
|
237
|
+
label: 'Fruits',
|
|
238
|
+
value: 'fruits',
|
|
239
|
+
children: [
|
|
240
|
+
{ label: 'Apple', value: 'apple' },
|
|
241
|
+
{ label: 'Banana', value: 'banana' },
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
label: 'Vegetables',
|
|
246
|
+
value: 'vegetables',
|
|
247
|
+
children: [
|
|
248
|
+
{ label: 'Carrot', value: 'carrot' },
|
|
249
|
+
{ label: 'Broccoli', value: 'broccoli' },
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
<LayoutSelect
|
|
255
|
+
type="single"
|
|
256
|
+
options={groupedOptions}
|
|
257
|
+
selectValue={value}
|
|
258
|
+
onChange={(val) => setValue(val)}
|
|
259
|
+
/>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### With icons
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
import { Apple, Cherry } from 'lucide-react'
|
|
266
|
+
|
|
267
|
+
const options: IOption[] = [
|
|
268
|
+
{ label: 'Apple', value: 'apple', icon: <Apple /> },
|
|
269
|
+
{ label: 'Cherry', value: 'cherry', icon: () => <Cherry /> }, // lazy icon
|
|
270
|
+
]
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Sortable list
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
const [options, setOptions] = useState<IOption[]>(initialOptions)
|
|
277
|
+
|
|
278
|
+
<LayoutSelect
|
|
279
|
+
type="multiple"
|
|
280
|
+
options={options}
|
|
281
|
+
selectValue={selected}
|
|
282
|
+
onChange={setSelected}
|
|
283
|
+
sortable
|
|
284
|
+
onSortEnd={(sorted) => setOptions(sorted)}
|
|
285
|
+
/>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Sortable across groups
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
<LayoutSelect
|
|
292
|
+
type="multiple"
|
|
293
|
+
options={groupedOptions}
|
|
294
|
+
selectValue={selected}
|
|
295
|
+
onChange={setSelected}
|
|
296
|
+
sortable
|
|
297
|
+
sortableAcrossGroups
|
|
298
|
+
onSortEnd={(sorted) => setGroupedOptions(sorted)}
|
|
299
|
+
/>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Async options
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
<LayoutSelect
|
|
306
|
+
type="single"
|
|
307
|
+
options={[]}
|
|
308
|
+
selectValue={value}
|
|
309
|
+
onChange={(val) => setValue(val)}
|
|
310
|
+
queryFn={async () => {
|
|
311
|
+
const res = await fetch('/api/options')
|
|
312
|
+
return res.json()
|
|
313
|
+
}}
|
|
314
|
+
/>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Collapsed mode (show all chips)
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
<LayoutSelect
|
|
321
|
+
type="multiple"
|
|
322
|
+
options={options}
|
|
323
|
+
selectValue={selected}
|
|
324
|
+
onChange={setSelected}
|
|
325
|
+
collapsed
|
|
326
|
+
/>
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Adding new components
|
|
332
|
+
|
|
333
|
+
This library is structured for easy expansion. To add a new component:
|
|
334
|
+
|
|
335
|
+
1. **Create the component** in `src/<category>/<component>.tsx`
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
src/
|
|
339
|
+
layout/
|
|
340
|
+
select.tsx ← existing
|
|
341
|
+
data-table.tsx ← new component
|
|
342
|
+
feedback/
|
|
343
|
+
toast.tsx ← new category + component
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
2. **Re-export from** `src/index.ts`
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
// Layout
|
|
350
|
+
export { LayoutSelect } from './layout/select'
|
|
351
|
+
export type { LayoutSelectProps, IOption, IconProp } from './layout/select'
|
|
352
|
+
|
|
353
|
+
// NEW: Layout — DataTable
|
|
354
|
+
export { DataTable } from './layout/data-table'
|
|
355
|
+
export type { DataTableProps } from './layout/data-table'
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
3. **Add a build entry** in `tsup.config.ts`
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
entry: {
|
|
362
|
+
index: 'src/index.ts',
|
|
363
|
+
'layout/select': 'src/layout/select.tsx',
|
|
364
|
+
'layout/data-table': 'src/layout/data-table.tsx', // ← add
|
|
365
|
+
},
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
4. **Add an exports entry** in `package.json`
|
|
369
|
+
|
|
370
|
+
```json
|
|
371
|
+
"exports": {
|
|
372
|
+
".": { ... },
|
|
373
|
+
"./layout/select": { ... },
|
|
374
|
+
"./layout/data-table": {
|
|
375
|
+
"import": {
|
|
376
|
+
"types": "./dist/layout/data-table.d.ts",
|
|
377
|
+
"default": "./dist/layout/data-table.js"
|
|
378
|
+
},
|
|
379
|
+
"require": {
|
|
380
|
+
"types": "./dist/layout/data-table.d.cts",
|
|
381
|
+
"default": "./dist/layout/data-table.cjs"
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
5. **Update `tsconfig.build.json`** `include` if you added a new category folder:
|
|
388
|
+
|
|
389
|
+
```json
|
|
390
|
+
"include": [
|
|
391
|
+
"src/index.ts",
|
|
392
|
+
"src/layout/**/*.ts",
|
|
393
|
+
"src/layout/**/*.tsx",
|
|
394
|
+
"src/feedback/**/*.ts",
|
|
395
|
+
"src/feedback/**/*.tsx",
|
|
396
|
+
"src/lib/**/*.ts"
|
|
397
|
+
]
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
6. **Build and verify**: `npm run build:lib`
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Development
|
|
405
|
+
|
|
406
|
+
The project includes a TanStack Start demo app for local development:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
# Start the dev server (demo app)
|
|
410
|
+
npm run dev
|
|
411
|
+
|
|
412
|
+
# Build the library
|
|
413
|
+
npm run build:lib
|
|
414
|
+
|
|
415
|
+
# Build the demo app
|
|
416
|
+
npm run build
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## Scripts
|
|
422
|
+
|
|
423
|
+
| Script | Description |
|
|
424
|
+
| ------------------- | ------------------------------------------------ |
|
|
425
|
+
| `npm run dev` | Start the TanStack Start dev server on port 3000 |
|
|
426
|
+
| `npm run build:lib` | Build the library to `dist/` (ESM + CJS + types) |
|
|
427
|
+
| `npm run build` | Build the demo app |
|
|
428
|
+
| `npm run test` | Run tests |
|
|
429
|
+
| `npm run lint` | Run ESLint |
|
|
430
|
+
| `npm run check` | Run Prettier + ESLint auto-fix |
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Publishing
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
# Build and publish
|
|
438
|
+
npm publish
|
|
439
|
+
|
|
440
|
+
# Or with a scoped name (update "name" in package.json first)
|
|
441
|
+
npm publish --access public
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
The `prepublishOnly` script automatically runs `build:lib` before publishing.
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## License
|
|
449
|
+
|
|
450
|
+
MIT
|