KQ-2050 · SPUR/COLUMNS
Spur Columns
An assistant thread where any phrase becomes its own column: choose a dotted phrase in an answer, or select any words and ask about them, and the phrase is lifted out as a chip and carried to the new column's header on springs.glide — landing exactly, because the destination comes from the computed layout rather than a mid-flight measure — while the new column grows in and the older ones narrow to readable spines on the same spring. Every column has its own skeleton, empty and failed states and its own follow-up composer, answered through answerFor (which may return a promise). Left and Right walk the columns, Escape folds the rightmost back into its phrase on the exit ease and returns focus there, and under 520px the columns become a stack that follows a sideways swipe 1:1, rubber-bands at the ends and commits by projection with the trail shown as chips.
Left and Right walk the columns. Escape folds the rightmost column back into its phrase.
ready · 2 columns · active: workspace roles
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/spur-columns
Props
- columns / defaultColumns
- SpurColumn[]
- default — / defaultSpurColumns
- Columns left to right, controlled or initial: { id, phrase?, parentId?, turns: { id, prompt, status: "loading" | "ready" | "empty" | "failed", answer?: { text, phrases? } }[] }.
- onColumnsChange
- (columns) => void
- default —
- Fires with every column after an open, a fold, an ask or an arriving answer.
- answerFor
- (prompt, context) => SpurAnswer | null | Promise<…>
- default defaultSpurAnswerFor
- Answers a spur's phrase or a follow-up ({ columnId, phrase?, kind: "spur" | "ask" }). null shows the empty state; a rejected promise shows the failed state with Try again.
- onAsk
- (text, columnId) => void
- default —
- A follow-up was asked in a column's composer.
- onOpenSpur
- (phrase, fromColumnId, column) => void
- default —
- A spur was opened from a suggested phrase or a selection.
- onCloseSpur
- (columnId) => void
- default —
- A spur, and any spurs opened from it, was folded back (Escape or its close button).
- active / defaultActive
- string
- default — / the last column
- The active column id, controlled or initial. The active column always stays open.
- onActiveChange
- (columnId) => void
- default —
- Fires from the key, spine, chip, phrase or swipe that changed the active column.
- columnWidth
- number
- default 280
- The width older open columns hold, in px — the newest column takes the rest — and the width a column needs to stay open; columns that no longer fit narrow to spines, oldest first.
- spineWidth
- number
- default 40
- A spine's width in px.
- spineStyle
- "label" | "index" | "bar"
- default "label"
- Spines read their phrase set vertically, show a large number with a dot per turn, or a slim accent bar under the number.
- accent
- string
- default "var(--accent-bright)"
- Phrase marks, the carried chip, spines, the active column's edge, trail chips and Send — any CSS colour.
- stackBelow
- number
- default 520
- Below this container width (px) the columns become a swipeable stack with a trail of chips.
- allowSelection
- boolean
- default true
- Selecting words in an answer offers "Ask about this" above the selection.
- maxPhrase
- number
- default 64
- The longest selection, in characters, that can become a spur.
- title
- string
- default "Fernworks onboarding"
- The first column's heading and the region's name.
- status / onRetry
- "ready" | "loading" | "error" / () => void
- default "ready"
- Loading shows skeleton columns; error shows a message and Try again, which calls onRetry.
- height
- number
- default 540
- The surface's height in px; each column scrolls inside it.
- disabled
- boolean
- default false
- Read only: columns can be walked and read; nothing can be opened, folded or asked.
- className
- string
- default —
- Classes for the outer section.
| Prop | Type | Default | Description |
|---|---|---|---|
| columns / defaultColumns | SpurColumn[] | — / defaultSpurColumns | Columns left to right, controlled or initial: { id, phrase?, parentId?, turns: { id, prompt, status: "loading" | "ready" | "empty" | "failed", answer?: { text, phrases? } }[] }. |
| onColumnsChange | (columns) => void | — | Fires with every column after an open, a fold, an ask or an arriving answer. |
| answerFor | (prompt, context) => SpurAnswer | null | Promise<…> | defaultSpurAnswerFor | Answers a spur's phrase or a follow-up ({ columnId, phrase?, kind: "spur" | "ask" }). null shows the empty state; a rejected promise shows the failed state with Try again. |
| onAsk | (text, columnId) => void | — | A follow-up was asked in a column's composer. |
| onOpenSpur | (phrase, fromColumnId, column) => void | — | A spur was opened from a suggested phrase or a selection. |
| onCloseSpur | (columnId) => void | — | A spur, and any spurs opened from it, was folded back (Escape or its close button). |
| active / defaultActive | string | — / the last column | The active column id, controlled or initial. The active column always stays open. |
| onActiveChange | (columnId) => void | — | Fires from the key, spine, chip, phrase or swipe that changed the active column. |
| columnWidth | number | 280 | The width older open columns hold, in px — the newest column takes the rest — and the width a column needs to stay open; columns that no longer fit narrow to spines, oldest first. |
| spineWidth | number | 40 | A spine's width in px. |
| spineStyle | "label" | "index" | "bar" | "label" | Spines read their phrase set vertically, show a large number with a dot per turn, or a slim accent bar under the number. |
| accent | string | "var(--accent-bright)" | Phrase marks, the carried chip, spines, the active column's edge, trail chips and Send — any CSS colour. |
| stackBelow | number | 520 | Below this container width (px) the columns become a swipeable stack with a trail of chips. |
| allowSelection | boolean | true | Selecting words in an answer offers "Ask about this" above the selection. |
| maxPhrase | number | 64 | The longest selection, in characters, that can become a spur. |
| title | string | "Fernworks onboarding" | The first column's heading and the region's name. |
| status / onRetry | "ready" | "loading" | "error" / () => void | "ready" | Loading shows skeleton columns; error shows a message and Try again, which calls onRetry. |
| height | number | 540 | The surface's height in px; each column scrolls inside it. |
| disabled | boolean | false | Read only: columns can be walked and read; nothing can be opened, folded or asked. |
| className | string | — | Classes for the outer section. |