diff --git a/src/components/forms/form-code-badge.tsx b/src/components/forms/form-code-badge.tsx new file mode 100644 index 000000000..ff68a01b8 --- /dev/null +++ b/src/components/forms/form-code-badge.tsx @@ -0,0 +1,95 @@ +import { cn, codeText } from "@/components/ui-primitives"; + +export type FormCodeBadgeVariant = "sm" | "md" | "hero"; + +// Form codes are mostly short ("1A", "10D") but some carry a trailing qualifier +// ("6B Attachment"). Splitting on the first space lets the badge show the code +// prominently while the qualifier renders as a compact sub-label, so long codes +// no longer overflow the fixed chip. +export function splitFormCode(code: string): { head: string; qualifier: string | null } { + const trimmed = code.trim(); + const spaceIndex = trimmed.indexOf(" "); + if (spaceIndex === -1) return { head: trimmed, qualifier: null }; + return { + head: trimmed.slice(0, spaceIndex), + qualifier: trimmed.slice(spaceIndex + 1).trim() || null, + }; +} + +// Scale the code down as it gets longer so a four-character head ("3A/4B") still +// fits the chip without clipping. The hero variant is responsive to match the +// large detail-page badge. +function headSizeClass(head: string, variant: FormCodeBadgeVariant) { + const length = head.length; + if (variant === "sm") { + if (length <= 2) return "text-lg"; + if (length === 3) return "text-base"; + return "text-sm"; + } + if (variant === "hero") { + if (length <= 2) return "text-xl sm:text-4xl"; + if (length === 3) return "text-lg sm:text-3xl"; + return "text-base sm:text-2xl"; + } + if (length <= 2) return "text-2xl"; + if (length === 3) return "text-xl"; + return "text-base"; +} + +const containerByVariant: Record = { + sm: "h-12 w-12 gap-0 px-0.5", + md: "h-14 w-16 gap-0.5 px-0.5", + hero: "h-14 w-14 gap-0 px-1 sm:h-24 sm:w-24 sm:gap-1", +}; + +const qualifierSizeByVariant: Record = { + sm: "text-4xs", + md: "text-4xs", + hero: "text-3xs sm:text-2xs", +}; + +/** + * Renders a form code as a self-contained chip. The code is split into a + * prominent head ("6B") and an optional qualifier sub-label ("Attachment") so + * long codes never overflow. The full code is exposed once to assistive tech + * while the visual fragments are hidden from the accessibility tree. + */ +export function FormCodeBadge({ + code, + variant = "md", + className, +}: { + code: string; + variant?: FormCodeBadgeVariant; + className?: string; +}) { + const { head, qualifier } = splitFormCode(code); + return ( +
+ {/* Expose the whole code as one string for assistive tech and text queries; + the split visual fragments below are decorative and hidden from the a11y tree. */} + {qualifier ? `${head} ${qualifier}` : head} + + {head} + + {qualifier ? ( + + {qualifier} + + ) : null} +
+ ); +} diff --git a/src/components/forms/form-detail-page.tsx b/src/components/forms/form-detail-page.tsx index ae022e777..a5b092ea5 100644 --- a/src/components/forms/form-detail-page.tsx +++ b/src/components/forms/form-detail-page.tsx @@ -43,6 +43,7 @@ import { toneSuccess, toneWarning, } from "@/components/ui-primitives"; +import { FormCodeBadge } from "@/components/forms/form-code-badge"; import { appModeHomeHref } from "@/lib/app-modes"; import { formCatalogDetails, type FormRecord } from "@/lib/form-ranker"; import type { ServiceChipTone, ServiceContact, ServiceCriterion, ServiceSummaryCard } from "@/lib/service-ranker"; @@ -261,9 +262,9 @@ function PathwayContextCard({

Current

-
- {code} -

{form.title}

+
+ +

{form.title}

You are here @@ -562,14 +563,7 @@ export function FormDetailPage({ form }: { form: FormRecord }) {
-
- {code} -
+

{form.title} diff --git a/src/components/forms/forms-search-results-page.tsx b/src/components/forms/forms-search-results-page.tsx index 7dfbecacc..df1358da1 100644 --- a/src/components/forms/forms-search-results-page.tsx +++ b/src/components/forms/forms-search-results-page.tsx @@ -35,6 +35,7 @@ import { SearchResultsEmptyState, SearchResultsHeaderBand, } from "@/components/clinical-dashboard/search-results-header-band"; +import { FormCodeBadge } from "@/components/forms/form-code-badge"; import { useSearchCommand } from "@/components/clinical-dashboard/search-command-context"; import { recordMatchesCommandScopes } from "@/lib/search-command-surface"; import { sortResultItems, type ResultSortValue } from "@/lib/result-sort"; @@ -66,63 +67,6 @@ function resultCode(match: FormSearchMatch, index: number) { return formCatalogDetails(match.service)?.form ?? String(index + 1); } -// Form codes are mostly short ("1A", "10D") but some carry a trailing qualifier -// ("6B Attachment"). Splitting on the first space lets the badge show the code -// prominently while the qualifier renders as a compact sub-label, so long codes -// no longer overflow the fixed chip. -function splitFormCode(code: string): { head: string; qualifier: string | null } { - const trimmed = code.trim(); - const spaceIndex = trimmed.indexOf(" "); - if (spaceIndex === -1) return { head: trimmed, qualifier: null }; - return { - head: trimmed.slice(0, spaceIndex), - qualifier: trimmed.slice(spaceIndex + 1).trim() || null, - }; -} - -// Scale the code down as it gets longer so a four-character head ("3A/4B") still -// fits the chip without clipping. -function formHeadSizeClass(head: string, variant: "md" | "sm") { - const length = head.length; - if (variant === "sm") { - if (length <= 2) return "text-lg"; - if (length === 3) return "text-base"; - return "text-sm"; - } - if (length <= 2) return "text-2xl"; - if (length === 3) return "text-xl"; - return "text-base"; -} - -function FormCodeBadge({ code, variant = "md" }: { code: string; variant?: "md" | "sm" }) { - const { head, qualifier } = splitFormCode(code); - const isSm = variant === "sm"; - return ( -
- {/* Expose the whole code as one string for assistive tech and text queries; - the split visual fragments below are decorative and hidden from the a11y tree. */} - {qualifier ? `${head} ${qualifier}` : head} - - {head} - - {qualifier ? ( - - {qualifier} - - ) : null} -
- ); -} - function tagToneClass(label: string) { const normalized = label.toLowerCase(); if (normalized.includes("crisis") || normalized.includes("risk") || normalized.includes("safety")) {