Review workspace for a project's concepts — a rail of deliverables, rounds and options as segment tabs, compare and full screen, the artwork as the hero, and a footer with the round's outcome and the decisions.
npx shadcn@latest add https://sdk-components.thesqd.com/r/concept-approval.jsonimport { ConceptApproval } from "@/components/blocks/concept-approval";import { ConceptApproval } from "@/components/blocks/concept-approval";
import { AnnotatedCanvas } from "@/components/custom/annotated-canvas";
// Your own stage: pass children instead of files. The block keeps the rail,
// the round/option tabs, compare, full screen and the footer.
export function ReviewPage() {
return (
<ConceptApproval
items={deliverables}
rounds={rounds}
options={[{ id: "c1", label: "Concept 1", badge: { label: "Chosen", tone: "open" } }]}
statuses={[{ label: "2 unresolved", tone: "changes" }]}
meta="Sent Mar 4"
>
<AnnotatedCanvas src={src} pins={pins} onPlace={place} />
</ConceptApproval>
);
}ConceptApproval — review workspace for a project's concepts
├── rail (items, 1+) — Item list: thumbnail, name, status dot + line
└── card — card-surface
├── toolbar
│ ├── rounds — Tabs (segment), a status dot per round
│ ├── toolbar slot
│ ├── Compare rounds — Button, pressed while comparing
│ └── Full screen — icon Button → Dialog with the stage
├── stage
│ ├── options (2+) — Tabs (segment) above the artwork, optional xxs Badge
│ └── children, or the active option's file
└── footer
├── statuses + meta + note — left; badge first
└── comments link + actions[] — right
While comparing, pass the shared <CompareSlider> (components/ui/compare-slider)
as the stage: before/after split by a draggable line, arrow keys step it.ConceptApproval
| Prop | Type | Default | Description |
|---|---|---|---|
items | ConceptApprovalItem[] | — | The left rail, shown for one item too. Omit to hide it. |
activeItemId / onItemChange | string / (id) => void | — | Controlled rail selection. Uncontrolled when omitted. |
rounds | ConceptApprovalRound[] | — | The open deliverable's rounds, oldest first — one segment tab each. |
round / onRoundChange | number / (round) => void | — | Controlled round. Defaults to the last one. |
options | ConceptApprovalOption[] | — | The open round's options. Defaults to one per file; tabs show for 2+. |
activeOptionId / onOptionChange | string / (id) => void | — | Controlled option. |
files | ConceptApprovalFile[] | — | The default stage draws the active option's image or video. |
children | React.ReactNode | — | Your own stage (an annotated canvas, a comparison) in place of `files`. |
fullscreen | React.ReactNode | false | — | Full-screen dialog content. Defaults to the stage; `false` hides the button. |
toolbar | React.ReactNode | — | Extra toolbar controls, before Compare. |
onCompare | () => void | — | Shows the Compare rounds button. |
comparing | boolean | false | Pressed state of Compare. |
compareDisabled | boolean | false | Disable Compare (e.g. on the first round). |
compareLabel / stopComparingLabel | string | "Compare rounds" / "Stop comparing" | |
statuses | ConceptApprovalStatus[] | — | xs status badges, first on the footer's left. |
meta | React.ReactNode | — | Muted text after the badges ("Sent Mar 4"). |
note | React.ReactNode | — | One line after the badges and meta, on the left. |
actions | ConceptApprovalAction[] | — | Footer buttons. Icon-only sizes get a Tooltip. |
commentsLabel / onCommentsClick | ReactNode / () => void | — | Footer link, shown when both are passed. |
onFullscreenChange | (open: boolean) => void | — | The full-screen dialog opened or closed — e.g. to move something floating into it. |
spotlight | boolean | false | Dim and blur the whole screen except the card (the rail dims too) — e.g. while writing about a spot on the work. Uses <Spotlight> (components/ui/spotlight). |
spotlightKeep | () => Element[] | — | More elements to leave lit under the spotlight, such as a comments panel. |
onSpotlightDismiss | () => void | — | A click on the dimmed area. |
railLabel / fullscreenLabel | string | "Deliverables" / "Open full screen" | |
className | string | — | Merged onto the outer section. |
ConceptApprovalItem
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | |
name | React.ReactNode | — | |
thumbnail | string | — | Drawn contained, never cropped. |
status | React.ReactNode | — | One line under the name. |
tone | ConceptApprovalTone | — | Colour of the status dot. |
ConceptApprovalRound
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | — | |
label | React.ReactNode | "Round {value}" | |
tone | ConceptApprovalTone | — | Colour of the tab's dot. |
ConceptApprovalOption
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | |
label | React.ReactNode | — | |
badge | { label, tone } | — | xxs badge after the label ("Approved", "Chosen"). |
ConceptApprovalFile
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Matches an option id. |
kind | "image" | "video" | "image" | |
src | string | — | |
poster | string | — | Poster image for videos. |
label | string | — | Option label when `options` is not passed. |
alt | string | — |
ConceptApprovalStatus
| Prop | Type | Default | Description |
|---|---|---|---|
label | React.ReactNode | — | |
tone | "approved" | "changes" | "open" | "unused" | — | success / warning / brand / muted. |
ConceptApprovalAction
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | |
label | string | — | Button label, or the tooltip on an icon-only size. |
icon | React.ReactNode | — | |
onClick | () => void | — | |
variant | ButtonProps["variant"] | "outline" | |
size | ButtonProps["size"] | "sm" |