@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 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