KQ-2113 · SORTED/INSERT
Sorted Insert
A sidebar where New opens an inline field in the list instead of a dialog: every row, header and the field are placed by computed y, so the rows below part on springs.glide to make the field's slot. On Enter the field becomes the new row where it stands, then the row flies on glide to its sorted place — riding 8px aside on a solid lifted card while the rows it passes slide up a slot — settles home on springs.snap and holds a highlight so you see where it went; a refused name nudges the field on a short tween and slides its message in. Rows are one roving tab stop where Up and Down move, F2 renames (a rename that sorts elsewhere flies there too), N opens the field in the focused section and Escape cancels, and under 300px the sidebar becomes a drawer whose field opens at its top.
Enter adds the name, Escape cancels. In the list, Up and Down move, F2 renames and N adds to the focused section.
Alder & Pike
Clients · since Mar 2024
- Projects
- 4
- Added
- Mar 2024
4 projects with Fernworks so far. The list keeps clients in alphabetical order, so a new name always lands in its place.
9 clients · 4 prospects · press n in the list
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/sorted-insert
Props
- ref
- React.Ref<SortedInsertHandle>
- default —
- Drive the field from outside: startCreate(sectionId?, text?), typeDraft(text), submitDraft(), cancelDraft(). startCreate never moves focus.
- sections
- InsertSection[]
- default defaultInsertSections
- The sections, top to bottom: { id, label, noun }.
- items / defaultItems
- InsertItem[]
- default defaultInsertItems
- The rows, controlled or initial: { id, name, section, createdAt (ISO date), projects? }.
- onItemsChange
- (items: InsertItem[]) => void
- default —
- Fires with the whole list after an add, a rename or a failed add taken back out.
- sortBy
- "name" | "recent" | "size"
- default "name"
- The order within each section: A–Z, newest first, or most projects first. Changing it re-sorts every row on glide.
- highlight
- "wash" | "bar" | "ring"
- default "wash"
- How a row shows where it landed, for 1.6 s: an accent wash, a bar on its leading edge that shrinks away, or a ring.
- accent
- "cobalt" | "plum" | "mint" | "ink" | string
- default "cobalt"
- Field, highlight and selection. A name or any CSS colour.
- maxNameLength
- number
- default 40
- The longest name the field accepts.
- validateName
- (name: string, sectionId: string, items: InsertItem[]) => string | null
- default —
- Your own naming rule, run after the built-in ones (not empty, not too long, not a duplicate in the section): return a message to refuse.
- onCreate
- (name: string, sectionId: string) => void | Promise<unknown>
- default —
- A name was accepted. Return a promise and the row shows as adding until it settles; a rejection takes the row back out and reopens the field with the name.
- onRename
- (id: string, name: string, previous: string) => void
- default —
- A row was renamed.
- onCancel
- (kind: "create" | "rename", target: string) => void
- default —
- The field closed without saving: Escape, or an empty field losing focus.
- selected / defaultSelected
- string
- default the first item
- The chosen row, controlled or initial.
- onSelect
- (id: string) => void
- default —
- A row was chosen.
- now
- string
- default "2026-10-14"
- Today as an ISO date: the createdAt of new rows.
- status
- "ready" | "loading" | "error"
- default "ready"
- Whether the items have arrived: still skeleton rows while loading, a message and Retry on error.
- onRetry
- () => void
- default —
- Retry was pressed after an error.
- rowHeight
- number
- default 34
- Every row's height in px.
- studio
- { name: string; initials: string }
- default { name: "Fernworks", initials: "Fw" }
- The studio in the header.
- frameHeight
- number
- default 520
- The frame's height in px; the list scrolls inside it and scrolls a landing slot into view.
- title
- string
- default "Clients"
- The sidebar's heading and accessible name.
- disabled
- boolean
- default false
- Read only: nothing can be added, renamed or chosen.
- className
- string
- default —
- Classes for the frame.
| Prop | Type | Default | Description |
|---|---|---|---|
| ref | React.Ref<SortedInsertHandle> | — | Drive the field from outside: startCreate(sectionId?, text?), typeDraft(text), submitDraft(), cancelDraft(). startCreate never moves focus. |
| sections | InsertSection[] | defaultInsertSections | The sections, top to bottom: { id, label, noun }. |
| items / defaultItems | InsertItem[] | defaultInsertItems | The rows, controlled or initial: { id, name, section, createdAt (ISO date), projects? }. |
| onItemsChange | (items: InsertItem[]) => void | — | Fires with the whole list after an add, a rename or a failed add taken back out. |
| sortBy | "name" | "recent" | "size" | "name" | The order within each section: A–Z, newest first, or most projects first. Changing it re-sorts every row on glide. |
| highlight | "wash" | "bar" | "ring" | "wash" | How a row shows where it landed, for 1.6 s: an accent wash, a bar on its leading edge that shrinks away, or a ring. |
| accent | "cobalt" | "plum" | "mint" | "ink" | string | "cobalt" | Field, highlight and selection. A name or any CSS colour. |
| maxNameLength | number | 40 | The longest name the field accepts. |
| validateName | (name: string, sectionId: string, items: InsertItem[]) => string | null | — | Your own naming rule, run after the built-in ones (not empty, not too long, not a duplicate in the section): return a message to refuse. |
| onCreate | (name: string, sectionId: string) => void | Promise<unknown> | — | A name was accepted. Return a promise and the row shows as adding until it settles; a rejection takes the row back out and reopens the field with the name. |
| onRename | (id: string, name: string, previous: string) => void | — | A row was renamed. |
| onCancel | (kind: "create" | "rename", target: string) => void | — | The field closed without saving: Escape, or an empty field losing focus. |
| selected / defaultSelected | string | the first item | The chosen row, controlled or initial. |
| onSelect | (id: string) => void | — | A row was chosen. |
| now | string | "2026-10-14" | Today as an ISO date: the createdAt of new rows. |
| status | "ready" | "loading" | "error" | "ready" | Whether the items have arrived: still skeleton rows while loading, a message and Retry on error. |
| onRetry | () => void | — | Retry was pressed after an error. |
| rowHeight | number | 34 | Every row's height in px. |
| studio | { name: string; initials: string } | { name: "Fernworks", initials: "Fw" } | The studio in the header. |
| frameHeight | number | 520 | The frame's height in px; the list scrolls inside it and scrolls a landing slot into view. |
| title | string | "Clients" | The sidebar's heading and accessible name. |
| disabled | boolean | false | Read only: nothing can be added, renamed or chosen. |
| className | string | — | Classes for the frame. |