KQ-2110 · BUBBLE/UP
Bubble Up
A project sidebar where what changed floats to the top: a project with new activity shows its count at home, popping in on springs.snap, and after liftDelay the row lifts out with a shadow on springs.flick and glides up on springs.glide into an Updated group that opens a slot for it while the group's count rolls, leaving a dotted placeholder that holds its home for a beat and then closes as the rows below slide up. Opening a project marks it read and sends it back down to its home on glide, and Shift+R sends the whole group home in cascade order. Every row is placed from a pure layout of fixed heights, so the server renders the positions the browser animates and the DOM order never changes; Up and Down walk the rows as they appear, Enter opens, M mutes, and below an 18.5rem container the Updated group folds into a count chip.
1 updated · waiting for activity
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/bubble-up
Props
- liftDelay
- number
- default 500
- How long a row with new activity waits at home, its count showing, before it rises, in ms.
- density
- "compact" | "cozy" | "roomy"
- default "cozy"
- Row height and what each row says: one line, a meta line, or meta and client.
- accent
- "cobalt" | "plum" | "mint" | "ink" | string
- default "cobalt"
- Accent for counts, the chip and the Updated group. A name or any CSS colour.
- projects
- BubbleProject[]
- default defaultBubbleProjects
- The projects, in order within their groups: { id, name, client, group, hue? }.
- groups
- BubbleGroup[]
- default defaultBubbleGroups
- The home groups, in order: { id, label }.
- events
- BubbleEvent[]
- default defaultBubbleEvents
- Activity on the projects, any order: { id, projectId, at, by, kind }. A project is updated while it has events after its read time.
- readAt / defaultReadAt
- Record<string, number>
- default — / defaultBubbleReadAt
- For each project, the time it was read up to, controlled or initial.
- onMarkRead
- (projectIds: string[], readAt: Record<string, number>) => void
- default —
- Projects were marked read, by opening one or by Shift+R, with the whole new read state.
- muted / defaultMuted
- string[]
- default — / []
- Muted project ids, controlled or initial; a muted project never rises.
- onMute
- (projectId: string, muted: boolean) => void
- default —
- A project was muted or unmuted, by its bell or by M.
- onOpen
- (projectId: string) => void
- default —
- A project was opened.
- current / defaultCurrent
- string | null
- default — / null
- The open project's id, controlled or initial.
- placeholderBeat
- number
- default 700
- How long the dotted placeholder holds a risen row's home before it closes, in ms.
- now
- number
- default defaultBubbleNow
- The moment relative times count from, in ms, so server and browser agree.
- title
- string
- default "Fernworks"
- The studio's name in the header.
- subtitle
- string
- default "Client projects"
- The line under the name.
- status
- "ready" | "loading" | "error"
- default "ready"
- Whether the projects have arrived: skeleton rows while loading, a message with Try again on error.
- onRetry
- () => void
- default —
- Try again after an error.
- label
- string
- default —
- The navigation's accessible name. Defaults to "<title> projects".
- className
- string
- default —
- Classes for the outer container.
| Prop | Type | Default | Description |
|---|---|---|---|
| liftDelay | number | 500 | How long a row with new activity waits at home, its count showing, before it rises, in ms. |
| density | "compact" | "cozy" | "roomy" | "cozy" | Row height and what each row says: one line, a meta line, or meta and client. |
| accent | "cobalt" | "plum" | "mint" | "ink" | string | "cobalt" | Accent for counts, the chip and the Updated group. A name or any CSS colour. |
| projects | BubbleProject[] | defaultBubbleProjects | The projects, in order within their groups: { id, name, client, group, hue? }. |
| groups | BubbleGroup[] | defaultBubbleGroups | The home groups, in order: { id, label }. |
| events | BubbleEvent[] | defaultBubbleEvents | Activity on the projects, any order: { id, projectId, at, by, kind }. A project is updated while it has events after its read time. |
| readAt / defaultReadAt | Record<string, number> | — / defaultBubbleReadAt | For each project, the time it was read up to, controlled or initial. |
| onMarkRead | (projectIds: string[], readAt: Record<string, number>) => void | — | Projects were marked read, by opening one or by Shift+R, with the whole new read state. |
| muted / defaultMuted | string[] | — / [] | Muted project ids, controlled or initial; a muted project never rises. |
| onMute | (projectId: string, muted: boolean) => void | — | A project was muted or unmuted, by its bell or by M. |
| onOpen | (projectId: string) => void | — | A project was opened. |
| current / defaultCurrent | string | null | — / null | The open project's id, controlled or initial. |
| placeholderBeat | number | 700 | How long the dotted placeholder holds a risen row's home before it closes, in ms. |
| now | number | defaultBubbleNow | The moment relative times count from, in ms, so server and browser agree. |
| title | string | "Fernworks" | The studio's name in the header. |
| subtitle | string | "Client projects" | The line under the name. |
| status | "ready" | "loading" | "error" | "ready" | Whether the projects have arrived: skeleton rows while loading, a message with Try again on error. |
| onRetry | () => void | — | Try again after an error. |
| label | string | — | The navigation's accessible name. Defaults to "<title> projects". |
| className | string | — | Classes for the outer container. |