Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/components/forms/form-code-badge.tsx
Original file line number Diff line number Diff line change
@@ -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<FormCodeBadgeVariant, string> = {
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<FormCodeBadgeVariant, string> = {
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 (
<div
className={cn(
"flex shrink-0 flex-col items-center justify-center overflow-hidden rounded-xl border border-[color:var(--clinical-accent-border)] bg-gradient-to-b from-[color:var(--clinical-accent-soft)] to-[color-mix(in_srgb,var(--clinical-accent-soft)_55%,var(--surface))] text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)]",
containerByVariant[variant],
className,
)}
>
{/* 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. */}
<span className="sr-only">{qualifier ? `${head} ${qualifier}` : head}</span>
<span aria-hidden className={cn("font-extrabold leading-none", headSizeClass(head, variant), codeText)}>
{head}
</span>
{qualifier ? (
<span
aria-hidden
title={qualifier}
className={cn(
"w-full truncate text-center font-bold uppercase leading-none tracking-tight opacity-75",
qualifierSizeByVariant[variant],
)}
>
{qualifier}
</span>
) : null}
</div>
);
}
16 changes: 5 additions & 11 deletions src/components/forms/form-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -261,9 +262,9 @@ function PathwayContextCard({
<div className="relative rounded-lg border border-[color:var(--clinical-accent)] bg-[color:var(--clinical-accent-soft)]/35 p-3">
<span className="absolute -left-[1.55rem] top-4 h-4 w-4 rounded-full border-2 border-[color:var(--surface)] bg-[color:var(--clinical-accent)]" />
<p className="mb-2 text-2xs font-bold uppercase text-[color:var(--text-soft)]">Current</p>
<div className="flex items-center gap-2">
<span className={cn("text-2xl font-bold text-[color:var(--clinical-accent)]", codeText)}>{code}</span>
<p className="text-sm font-semibold text-[color:var(--text-heading)]">{form.title}</p>
<div className="flex items-center gap-2.5">
<FormCodeBadge code={code} variant="sm" />
<p className="min-w-0 text-sm font-semibold text-[color:var(--text-heading)]">{form.title}</p>
</div>
<span className="mt-2 inline-flex min-h-6 items-center rounded-full bg-[color:var(--clinical-accent-soft)] px-2 text-2xs font-bold text-[color:var(--clinical-accent)]">
You are here
Expand Down Expand Up @@ -562,14 +563,7 @@ export function FormDetailPage({ form }: { form: FormRecord }) {
<div className="min-w-0 space-y-4">
<section className="rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] p-3 shadow-[var(--shadow-inset)] sm:p-5">
<div className="grid grid-cols-[3.75rem_minmax(0,1fr)_2.75rem] gap-x-3 gap-y-2.5 sm:grid-cols-[6rem_minmax(0,1fr)_auto] sm:gap-x-4 sm:gap-y-3 xl:grid-cols-[auto_minmax(0,1fr)_auto] xl:items-start">
<div
className={cn(
"grid h-14 w-14 shrink-0 place-items-center rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--surface)] text-xl font-bold text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)] sm:h-24 sm:w-24 sm:text-4xl",
codeText,
)}
>
{code}
</div>
<FormCodeBadge code={code} variant="hero" />
<div className="min-w-0">
<h1 className="max-w-4xl text-3xl font-extrabold leading-[1.05] text-[color:var(--text-heading)] sm:text-4xl">
{form.title}
Expand Down
58 changes: 1 addition & 57 deletions src/components/forms/forms-search-results-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
<div
className={cn(
"flex shrink-0 flex-col items-center justify-center overflow-hidden rounded-xl border border-[color:var(--clinical-accent-border)] bg-gradient-to-b from-[color:var(--clinical-accent-soft)] to-[color-mix(in_srgb,var(--clinical-accent-soft)_55%,var(--surface))] text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)]",
isSm ? "h-12 w-12 gap-0 px-0.5" : "h-14 w-16 gap-0.5 px-0.5",
)}
>
{/* 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. */}
<span className="sr-only">{qualifier ? `${head} ${qualifier}` : head}</span>
<span aria-hidden className={cn("font-extrabold leading-none", formHeadSizeClass(head, variant), codeText)}>
{head}
</span>
{qualifier ? (
<span
aria-hidden
title={qualifier}
className="w-full truncate text-center text-4xs font-bold uppercase leading-none tracking-tight opacity-75"
>
{qualifier}
</span>
) : null}
</div>
);
}

function tagToneClass(label: string) {
const normalized = label.toLowerCase();
if (normalized.includes("crisis") || normalized.includes("risk") || normalized.includes("safety")) {
Expand Down