KQ-1762 · THEME/SORTER
Theme Sorter
A feedback inbox you sort by hand: a card lifts after 4px of travel and follows the pointer 1:1 as a copy above both columns, leaning with its speed, while the theme under it swells on springs.snap. Dropped on a theme it is squeezed in — narrowing on springs.glide and flattening 60ms behind on the same spring toward the theme's centre — the theme gulps on springs.recoil as its tally rolls up on snap, and once it is in the themes re-rank by count with a layout glide; dropped anywhere else it springs home with the release velocity. The inbox is one tab stop with roving focus: Up and Down move, T or Enter opens a theme menu with type-ahead and New theme, A or Delete archives, and a failed tag springs the card back into its slot with a note.
Feedback inbox
9 to sort · 5 themes
- Onboarding4
- Permissions3
- Pricing3
- Exports2
- Mobile app1
Up and Down move between quotes. T or Enter chooses a theme, A or Delete archives. Cards can also be dragged onto a theme.
9 in inbox · 5 themes
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/theme-sorter
Props
- items / defaultItems
- FeedbackItem[]
- default — / defaultThemeSorterItems
- Feedback as { id, quote, customer, company, source: "email" | "call" | "survey" | "chat" | "review", plan, sentiment: "positive" | "neutral" | "negative", at, themeId?, archived? }, controlled or initial. Untagged, unarchived items wait in the inbox; tagged ones count toward their theme.
- onItemsChange
- (items: FeedbackItem[]) => void
- default —
- Fires with every item after a tag, an archive or a failed tag's revert.
- themes / defaultThemes
- FeedbackTheme[]
- default — / defaultThemeSorterThemes
- Themes as { id, name }, controlled or initial; shown most-counted first.
- onThemesChange
- (themes: FeedbackTheme[]) => void
- default —
- Fires with every theme after one is created.
- onTag
- (itemId: string, themeId: string) => void | Promise<unknown>
- default —
- An item was tagged by a drop or the theme menu. Return a promise; a rejection springs the item back into the inbox with a note and rolls the tally back.
- onCreateTheme
- (theme: FeedbackTheme) => void
- default —
- A theme was created from New theme; the item it was opened on is tagged into it.
- onArchive
- (itemId: string) => void
- default —
- An item was archived without a theme.
- bucketStyle
- "tray" | "jar" | "row"
- default "tray"
- How a theme looks: a tray with its count, a share bar coloured by its sentiment mix and a percentage; a jar whose level rises with its count; or a compact row. Chips at narrow widths keep a hint of each.
- density
- "compact" | "regular" | "roomy"
- default "regular"
- Card padding, quote size and gaps.
- sentimentPalette
- "signal" | "duo" | "mono"
- default "signal"
- Sentiment colours: green, ink and red; cobalt, ink and amber; or ink alone, told apart by the dot, ring and diamond marks.
- now
- number | Date
- default defaultThemeSorterNow
- The moment ages count from, so the server and the browser print the same text.
- status
- "ready" | "loading" | "error"
- default "ready"
- Whether the items have arrived: loading shows skeleton cards and themes, error a line with Try again.
- onRetry
- () => void
- default —
- Try again was pressed after the items failed to load.
- title
- string
- default "Feedback inbox"
- The inbox's heading and accessible name.
- maxHeight
- number
- default 540
- Tallest the surface grows before its columns scroll, in px.
- disabled
- boolean
- default false
- Nothing can be dragged, tagged, created or archived.
- className
- string
- default —
- Classes for the surface.
| Prop | Type | Default | Description |
|---|---|---|---|
| items / defaultItems | FeedbackItem[] | — / defaultThemeSorterItems | Feedback as { id, quote, customer, company, source: "email" | "call" | "survey" | "chat" | "review", plan, sentiment: "positive" | "neutral" | "negative", at, themeId?, archived? }, controlled or initial. Untagged, unarchived items wait in the inbox; tagged ones count toward their theme. |
| onItemsChange | (items: FeedbackItem[]) => void | — | Fires with every item after a tag, an archive or a failed tag's revert. |
| themes / defaultThemes | FeedbackTheme[] | — / defaultThemeSorterThemes | Themes as { id, name }, controlled or initial; shown most-counted first. |
| onThemesChange | (themes: FeedbackTheme[]) => void | — | Fires with every theme after one is created. |
| onTag | (itemId: string, themeId: string) => void | Promise<unknown> | — | An item was tagged by a drop or the theme menu. Return a promise; a rejection springs the item back into the inbox with a note and rolls the tally back. |
| onCreateTheme | (theme: FeedbackTheme) => void | — | A theme was created from New theme; the item it was opened on is tagged into it. |
| onArchive | (itemId: string) => void | — | An item was archived without a theme. |
| bucketStyle | "tray" | "jar" | "row" | "tray" | How a theme looks: a tray with its count, a share bar coloured by its sentiment mix and a percentage; a jar whose level rises with its count; or a compact row. Chips at narrow widths keep a hint of each. |
| density | "compact" | "regular" | "roomy" | "regular" | Card padding, quote size and gaps. |
| sentimentPalette | "signal" | "duo" | "mono" | "signal" | Sentiment colours: green, ink and red; cobalt, ink and amber; or ink alone, told apart by the dot, ring and diamond marks. |
| now | number | Date | defaultThemeSorterNow | The moment ages count from, so the server and the browser print the same text. |
| status | "ready" | "loading" | "error" | "ready" | Whether the items have arrived: loading shows skeleton cards and themes, error a line with Try again. |
| onRetry | () => void | — | Try again was pressed after the items failed to load. |
| title | string | "Feedback inbox" | The inbox's heading and accessible name. |
| maxHeight | number | 540 | Tallest the surface grows before its columns scroll, in px. |
| disabled | boolean | false | Nothing can be dragged, tagged, created or archived. |
| className | string | — | Classes for the surface. |