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.

Page canvas — feedback dock is open.

Installation

npx shadcn@latest add https://craftbits.dev/r/admin-feedback-dock.json

Usage

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

  1. Items in, callback out. The dock owns no editing or storage state. You hand it an array of AdminFeedbackDockItem records and receive an onClose ping when the user dismisses the panel.
  2. Header anatomy. The header reads title (defaults to Feedback), a small count badge when items are present, the optional actions slot, and the close glyph. The whole row is a single band so the dock keeps a calm vertical rhythm.
  3. Compose slot. The optional compose slot 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.
  4. Item composition. Every item has a stable id used for both the React key and the data-item-id attribute. The scope label renders as a small uppercase eyebrow, the note as a paragraph (with whitespace-pre-wrap preserving the caller's line breaks), and the tag chips + timestamp share the trailing row.
  5. Tag tones. Tags accept a tone from a six-value semantic palette: neutral, accent, success, warning, error, info. The default is accent so caller code stays terse.
  6. Empty state. When items is empty the body collapses to a single muted line — overridable with the emptyState slot.
  7. 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 when prefers-reduced-motion is set.
  8. Data hooks. The root carries data-cb-admin-feedback-dock, data-side, data-density, and data-item-count. Items carry data-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

PropTypeDefaultDescription
itemsreadonly AdminFeedbackDockItem[]Feedback annotations to render.
onClose() => voidFired when the close button or Escape is pressed.
titleReactNode"Feedback"Heading rendered inside the header.
actionsReactNodeTrailing slot in the header, sits left of the close glyph.
composeReactNodeOptional compose strip rendered between the header and the list.
closeLabelstring"Close feedback dock"Accessible label for the close button.
emptyStateReactNode"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.
classNamestringMerged onto the rendered <aside> via cn().
...restHTMLAttributes<HTMLElement>Any other <aside> attribute.

AdminFeedbackDockItem

FieldTypeDescription
idstringStable identifier — React key + data-item-id.
noteReactNodeFree-form note body. Renders as whitespace-pre-wrap.
scopestringOptional section / heading the note pins to.
tagsreadonly AdminFeedbackDockTag[]Optional accent-tinted pills.
timestampstringOptional pre-formatted timestamp (e.g. "2m ago").

AdminFeedbackDockTag

FieldTypeDescription
labelstringPill 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 via aria-labelledby, so screen readers announce the dock as a self-contained landmark with the correct name.
  • The items container is a role="list" with aria-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 via closeLabel) and a 36 × 36 px hit area with a visible :focus-visible ring.
  • 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-subtle against --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 a ScopePicker overlay, localStorage persistence, 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.