Admin Feedback Dock
A floating dock for surfacing admin-facing feedback annotations on top of a live page. Each entry pairs a free-form note with an optional scope label (which section / heading the note pins to), a row of tag chips, and a timestamp. The dock is presentation-only — the caller owns capture, persistence, and editing.
Installation
npx shadcn@latest add https://craftbits.dev/r/admin-feedback-dock.jsonUsage
AdminFeedbackDock renders a fixed <aside> landmark anchored to the right (default) or left edge of the viewport. Pass an array of items and a close callback; the dock takes care of the header, count badge, scroll area, empty state, and Escape-to-close keyboard handling.
import { AdminFeedbackDock, type AdminFeedbackDockItem } from "@craft-bits/core";
const items: AdminFeedbackDockItem[] = [
{
id: "fb-1",
scope: "Hero",
note: "Accent star wobble feels too loud.",
tags: [{ label: "animation", tone: "accent" }],
timestamp: "just now",
},
];
<AdminFeedbackDock
items={items}
onClose={() => setOpen(false)}
/>Compose a custom header by passing the actions slot — your buttons render to the left of the default close glyph:
<AdminFeedbackDock
items={items}
onClose={() => setOpen(false)}
actions={<button onClick={copyAll}>Copy all</button>}
/>Drop a compose textarea above the list with the compose slot — the dock renders it inside a <header> strip with a divider:
<AdminFeedbackDock
items={items}
onClose={() => setOpen(false)}
compose={<textarea placeholder="What's off?" />}
/>Understanding the component
- Items in, callback out. The dock owns no editing or storage state. You hand it an array of
AdminFeedbackDockItemrecords and receive anonCloseping when the user dismisses the panel. - Header anatomy. The header reads
title(defaults toFeedback), a small count badge when items are present, the optionalactionsslot, and the close glyph. The whole row is a single band so the dock keeps a calm vertical rhythm. - Compose slot. The optional
composeslot lives between the header and the list. It's intended for an inline compose textarea or a context summary — render whatever the caller needs, the dock just provides the strip and the divider. - Item composition. Every item has a stable
idused for both the React key and thedata-item-idattribute. The scope label renders as a small uppercase eyebrow, the note as a paragraph (withwhitespace-pre-wrappreserving the caller's line breaks), and the tag chips + timestamp share the trailing row. - Tag tones. Tags accept a
tonefrom a six-value semantic palette:neutral,accent,success,warning,error,info. The default isaccentso caller code stays terse. - Empty state. When
itemsis empty the body collapses to a single muted line — overridable with theemptyStateslot. - Motion. The panel slides in from the anchored edge using
SPRINGS.smooth; items fade-and-drop using the same spring. Both collapse to instant transitions whenprefers-reduced-motionis set. - Data hooks. The root carries
data-cb-admin-feedback-dock,data-side,data-density, anddata-item-count. Items carrydata-item-id. Both are stable styling / QA hooks.
Variants
Right side, comfortable density (default)
<AdminFeedbackDock items={items} onClose={close} />Anchored to the left edge
<AdminFeedbackDock items={items} onClose={close} side="left" />Compact density
<AdminFeedbackDock items={items} onClose={close} density="compact" />Custom empty state
<AdminFeedbackDock
items={[]}
onClose={close}
emptyState={<span>Nothing flagged this session.</span>}
/>Props
AdminFeedbackDock
| Prop | Type | Default | Description |
|---|---|---|---|
items | readonly AdminFeedbackDockItem[] | — | Feedback annotations to render. |
onClose | () => void | — | Fired when the close button or Escape is pressed. |
title | ReactNode | "Feedback" | Heading rendered inside the header. |
actions | ReactNode | — | Trailing slot in the header, sits left of the close glyph. |
compose | ReactNode | — | Optional compose strip rendered between the header and the list. |
closeLabel | string | "Close feedback dock" | Accessible label for the close button. |
emptyState | ReactNode | "No feedback yet." | Replaces the default empty-state copy. |
side | 'right' | 'left' | 'right' | Edge of the viewport the dock anchors to. |
density | 'compact' | 'comfortable' | 'comfortable' | Panel width preset. |
className | string | — | Merged onto the rendered <aside> via cn(). |
...rest | HTMLAttributes<HTMLElement> | — | Any other <aside> attribute. |
AdminFeedbackDockItem
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier — React key + data-item-id. |
note | ReactNode | Free-form note body. Renders as whitespace-pre-wrap. |
scope | string | Optional section / heading the note pins to. |
tags | readonly AdminFeedbackDockTag[] | Optional accent-tinted pills. |
timestamp | string | Optional pre-formatted timestamp (e.g. "2m ago"). |
AdminFeedbackDockTag
| Field | Type | Description |
|---|---|---|
label | string | Pill text. |
tone | 'neutral' | 'accent' | 'success' | 'warning' | 'error' | 'info' | Optional tone preset. Defaults to accent. |
Accessibility
- The root renders as a
<aside role="complementary">with the title wired viaaria-labelledby, so screen readers announce the dock as a self-contained landmark with the correct name. - The items container is a
role="list"witharia-live="polite", so new annotations are announced as they arrive. - The empty state renders as a
role="status"region so an assistive-technology user is told the dock is empty when first opened. - The close button has an explicit
aria-label(overridable viacloseLabel) and a 36 × 36 px hit area with a visible:focus-visiblering. - Escape closes the dock. The handler stops propagation only after firing, so parent listeners stay quiet.
- Motion respects
prefers-reduced-motion— the slide-in and item enter / exit collapse to instant transitions. - Colour contrast: all text uses
--cb-fg,--cb-fg-muted, or--cb-fg-subtleagainst--cb-bg-elevated; tag chips use their semantic background at 10–15% alpha behind the matching foreground token. All combinations pass WCAG AA on the default light and dark themes.
Credits
- Extracted from:
algoflashcards(src/platform/ui/AdminFeedbackDock.tsx). The original was a 950-line DEV-only notebook that owned aScopePickeroverlay,localStoragepersistence, audio cues, a hard-coded tag palette, a two-tap clear confirmation, and an inline compose textarea. craft-bits keeps only the dock — the floating panel that lists annotations and emits a close callback — and exposes the compose strip, the actions slot, and the tag tones as slots / variants so callers can rebuild any of the original behaviours on top.