"use client" import * as React from "react" import type { ComponentDocSpec } from "@/lib/design-system/component-doc-types" import { BadgeCountIndicatorPreview, BadgeCountOnlyPreview, BadgeCountOverlayPreview, BadgeCountPreview, BadgeSizesPreview, BadgeVariantsPreview, BadgeWithIconPreview, StatusBadgeActionablePreview, StatusBadgeProductPreview, StatusBadgeSemanticPreview, } from "@/components/design-system/feedback-previews" function ex( section: Omit, children: React.ReactNode, ) { return { ...section, children } } export const badgeComponentDoc: ComponentDocSpec = { slug: "badge", summary: "Two chips on one page: Badge for generic tags, filters, and counts; StatusBadge for entity workflow status and product marketing chrome (New, Beta).", extraImports: [ { label: "StatusCell", path: "@exxatdesignux/ui/components/data-views" }, { label: "Status tints", path: "@exxatdesignux/ui/lib/status-badge-tints" }, { label: "Domain status map", path: "@/lib/list-status-badges" }, ], sections: [ ex({ id: "variants", title: "Variants" }, ), ex({ id: "sizes", title: "Size" }, ), ex({ id: "with-icon", title: "With icon" }, ), ex({ id: "count-indicator", title: "Indicator" }, ), ex({ id: "count-overlay", title: "Count overlay" }, ), ex({ id: "count-only", title: "Count only" }, ), ex({ id: "count", title: "Count with label" }, ), ex({ id: "status", title: "Status" }, ), ex({ id: "actionable-status", title: "Actionable status" }, ), ex({ id: "product-status", title: "Product status" }, ), ], anatomy: [ { part: "Badge", description: "Generic inline chip: variant, optional icons via data-icon, tabular counts." }, { part: "StatusBadge (semantic)", description: "label + tone + icon + size. Presentational only." }, { part: "StatusCell", description: "The column wrapper. Adds options + onChange to make status editable in place." }, { part: "StatusBadge (product)", description: "status prop for nav/tab/card marketing chips; pill or dot variant." }, { part: "STATUS_BADGE_TONE_CLASS", description: "Five semantic tints (success, warning, info, danger, neutral)." }, ], features: [ { group: "Badge", icon: "fa-tag", items: [ { part: "variant", description: "Visual style for tags, filters, and non-status labels." }, { part: "data-icon", description: "Inline-start or inline-end FA icons with automatic padding." }, { part: "asChild", description: "Merge chip chrome onto a child element (e.g. Link)." }, { part: "indicator overlay", description: "Unread dot anchored top-end on icon triggers (relative wrapper + absolute dot)." }, { part: "count overlay", description: "Numeric Badge top-end on icon triggers; not beside the control." }, { part: "BadgeCount", description: "Red count-only chip — bulk bar, selection tallies; caps at 99+." }, { part: "variant count", description: "Solid destructive fill for numeric-only badges." }, { part: "tabular-nums", description: "Add on count badges for stable digit width." }, ], }, { group: "StatusBadge", icon: "fa-circle-check", items: [ { part: "tone", description: "Maps workflow meaning onto five chip washes." }, { part: "size sm", description: "Dense rows and table cells. Renders the icon." }, { part: "size md", description: "Cards, headers, and inspectors. Roomier shell." }, { part: "status", description: "Product marketing modes with uppercase labels." }, { part: "variant dot", description: "Presence-style product dot with sr-only label." }, ], }, { group: "StatusCell", icon: "fa-table-columns", items: [ { part: "static", description: "Renders StatusBadge unchanged. The default for any status column." }, { part: "options + onChange", description: "Turns the chip into a menu trigger that advances the record in place." }, { part: "chevron", description: "Appears only when actionable, so the affordance never lies." }, { part: "min-h-6", description: "Trigger clears the 24px target floor that badge padding alone misses." }, { part: "radio menu", description: "DropdownMenuRadioGroup gives pick-one semantics and aria-checked." }, ], }, ], api: [ { prop: "Badge.variant", type: "default | secondary | outline | destructive | ghost | link | count", defaultValue: "default", description: "Generic chip style. Not for entity lifecycle status.", }, { prop: "Badge.asChild", type: "boolean", defaultValue: "false", description: "Render as child element while keeping badge styles.", }, { prop: "StatusBadge.label", type: "string", description: "Semantic mode: sentence-case status text (Published, In review).", }, { prop: "StatusBadge.tone", type: "success | warning | info | danger | neutral", description: "Semantic tint from STATUS_BADGE_TONE_CLASS.", }, { prop: "StatusBadge.icon", type: "string (FA suffix)", description: "fa-light glyph, rendered at sm and md. Pair one with every tone so color is not the only signal.", }, { prop: "StatusBadge.size", type: "sm | md", defaultValue: "sm", description: "sm for tables and dense rows; md for cards and detail headers.", }, { prop: "StatusBadge.status", type: "new | beta | alpha | preview | deprecated", description: "Product marketing chip. Mutually exclusive with label + tone.", }, { prop: "StatusBadge.variant", type: "pill | dot", defaultValue: "pill", description: "Product mode only. dot hides visible text; use aria-label.", }, { prop: "StatusBadge.tintClassName", type: "string", description: "Escape hatch for one-off tints. Prefer tone + STATUS_BADGE_TONE_CLASS.", }, ], ux: { job: "Surface record state or categorical labels at a glance without stealing focus from the primary action on the row or card.", budgets: [ { label: "Semantic tones", value: "5", rationale: "Map every domain status onto success, warning, info, danger, or neutral before adding colors.", }, { label: "Tone + icon", value: "always paired", rationale: "Color alone fails WCAG 1.4.1. Every status column ships a glyph beside its tint.", }, { label: "Label case", value: "sentence", rationale: 'Semantic labels: "Due soon", "In review". Product chips stay uppercase.', }, { label: "Chips per row", value: "1 status", rationale: "One workflow status chip per record row; use Badge for secondary tags.", }, ], principles: ["P6", "P8", "P13", "P19"], modernReferences: [ "Linear issue status chips (M1, M4)", "Notion property tags (M1, M4)", "Stripe subscription status pills (M4, M11)", ], patternDoc: "apps/web/docs/table-column-cells-pattern.md", rulePath: ".cursor/rules/exxat-table-column-cells.mdc", whenToUse: [ "Hub table status columns with StatusCell (tone + label + icon).", "Status the reviewer may advance in place: StatusCell + options + onChange.", "Board cards and detail headers with StatusBadge md.", "Product marketing on nav items, tabs, or feature cards (status=\"beta\").", "Badge outline/secondary for filters, categories, and non-workflow tags.", "Destructive Badge variant for overdue or at-risk labels that are not the primary status chip.", "Count badges on toolbar icons, inbox rows, or exam progress (tabular-nums).", ], whenNotToUse: [ "Entity lifecycle status with raw Badge variant + uppercase. Use StatusBadge + tone.", "An editable status chip built by wrapping StatusBadge in your own button. Use StatusCell.", "Toast or banner feedback after an action. LocalBanner or inline status.", "Multiple competing status chips on one row. Pick the single workflow state.", "Hand-rolled bg-emerald-* classes per page. Import STATUS_BADGE_TONE_CLASS.", ], }, guidelines: { do: [ "Map domain statuses in lib/list-status-badges.ts (or product equivalent) onto STATUS_BADGE_TONE_CLASS.", "Table cells: StatusCell with label + tone. Add options + onChange when the user may advance the record.", "Board cards and inspectors: StatusBadge size md with icon when it aids scanning.", "Tables and StatusCell: StatusBadge size sm (default) with icon — never omit the glyph when tone carries meaning (WCAG 1.4.1).", "Product chrome: StatusBadge status=\"new\" | \"beta\" on nav, tabs, and feature cards.", "Badge with icon: fa-light suffix, data-icon inline-start | inline-end, aria-hidden on decorative icons.", "Icon overlays: wrap trigger in relative inline-flex; dot or Badge at top-end. Never place count beside the icon.", "Indicator dot: size-2, border-background hairline, destructive fill for unread.", "Count overlay: h-4 min-w-4 tabular-nums Badge at -top-1.5 -end-1.5 on icon-sm triggers.", "Count only: BadgeCount or variant=count for bulk bar and inline selection tallies.", ], dont: [ "Use Badge default/destructive for Published, Draft, or compliance workflow states.", "Wrap StatusBadge in your own button to make status editable. Use StatusCell options + onChange.", "Uppercase semantic status labels. Product status chips are the only uppercase case.", "Invent a sixth semantic color without design review and token addition.", "Place notification count badges beside icon buttons. Use top-end overlay on the trigger.", "Stack StatusBadge + colored Badge for the same workflow fact.", "Use ListHubStatusBadge in new code. Import StatusCell for columns, StatusBadge elsewhere.", ], }, accessibility: [ { principle: "perceivable", criterion: "1.4.1", criterionTitle: "Use of Color", level: "A", guidance: "Semantic status pairs tint with label text plus an icon at either size. Never signal state by color alone.", }, { principle: "perceivable", criterion: "1.4.3", criterionTitle: "Contrast (Minimum)", level: "AA", guidance: "Tint washes use locked-L status-badge pairs (fill ≈95%, ink ≈38.5%) via STATUS_BADGE_TONE_CLASS. Neutral is true grey (chroma 0).", }, { principle: "perceivable", criterion: "1.4.11", criterionTitle: "Non-text Contrast", level: "AA", guidance: "Product dot variant (variant=\"dot\") includes sr-only text; the dot is not the sole programmatic name.", }, { principle: "understandable", criterion: "2.4.6", criterionTitle: "Headings and Labels", level: "AA", guidance: "StatusBadge sets aria-label from label (semantic) or product status name. Override only when visible text differs.", }, { principle: "robust", criterion: "4.1.2", criterionTitle: "Name, Role, Value", level: "A", guidance: "Badge icons are decorative (aria-hidden). Status text is exposed on the span; dot variant uses sr-only copy.", }, { principle: "operable", criterion: "2.5.8", criterionTitle: "Target Size (Minimum)", level: "AA", guidance: "An actionable StatusCell trigger adds min-h-6 because badge padding alone computes under 24px, and paints its focus ring inset so the clipped table cell cannot hide it.", }, { principle: "robust", criterion: "4.1.2", criterionTitle: "Name, Role, Value (actionable)", level: "A", guidance: "An actionable StatusCell names the action and the current state (\"Change status. Currently In review.\") on both aria-label and Tip, and its menu uses role=menuitemradio with aria-checked on the current option.", }, ], relatedSlugs: ["status-badge", "status-cell", "table", "banner", "progress-cell"], }