chore(audit-drain): rip out next-intl, RTL lint, sweeps, polish

Drain the long-tail audit queue captured in alpha-uat-master.md.

- next-intl ripped out (zero useTranslations callers ever existed):
  package.json, next.config.ts plugin wrap, src/i18n/, messages/, and
  the layout NextIntlClientProvider all gone; <html lang="en"> hardcoded.
- RTL lint nudge added: warn-only no-restricted-syntax on physical
  Tailwind utilities (ml-/mr-/pl-/pr-/text-left/text-right/border-l/
  border-r/rounded-l-/rounded-r-) inside JSX className literals.
  Existing ~1,000 sites grandfathered; new code trends toward logical.
- Icon-only button accessibility lint: jsx-a11y/control-has-associated-
  label enabled at warn; 4 empty <th>/<td> action placeholders gain
  sr-only labels.
- Currency: SUPPORTED_CURRENCIES drops the hardcoded English labels;
  new currencyLabel(code, locale?) helper resolves via Intl.DisplayNames.
  CurrencySelect + settings-manager migrated.
- Date locale sweep: 7 surfaces flip from toLocaleString('en-GB'|'en-US')
  to toLocaleString(undefined, ...) so dates honour runtime locale.
- Dialog/Sheet width: 10 document/EOI/entity-form dialogs gain a
  lg:max-w-4xl or lg:max-w-5xl step so wide desktops get breathing room.
- PaymentsSection collapsed-bar: slim one-line bar showing
  "Payments - Not received yet" or "Payments - \$X received - N payments
  - Expand"; per-interest collapse state persists in localStorage; the
  RecordPayment flow auto-expands.
- muted-foreground opacity sweep: 10 text-bearing
  text-muted-foreground/{60,70,80} hits dropped to plain
  text-muted-foreground for AA contrast on muted bg. Icon-only
  (aria-hidden) opacity hits left as-is.
- Micro-type bump: text-[10px] and text-[11px] -> text-xs (12px)
  across 87 files in src/components + src/app. Pure mechanical sweep.
- Audit-doc cleanup: alpha-uat-master.md stale 2026-05-25 summary
  rewritten with cumulative state through today. Items genuinely still
  open are now a short long-tail list.
- New docs/marketing-site-followups.md: Umami Phase 4a/3/5, email
  pixel E2E verification, and website-cutover work parked here so
  they don't get lost in the CRM audit doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 18:48:46 +02:00
parent 353a31323e
commit e9509dc45c
115 changed files with 528 additions and 776 deletions

View File

@@ -77,7 +77,7 @@ export function AssignedToChip({
type="button"
aria-label={`Change deal owner (currently ${label})`}
className={cn(
'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium transition-colors',
'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium transition-colors',
currentAssignedTo
? 'border-sky-200 bg-sky-50 text-sky-800 hover:bg-sky-100'
: 'border-border bg-muted/50 text-muted-foreground hover:bg-muted',

View File

@@ -45,7 +45,7 @@ export function DealPulseChip({ interest }: { interest: DealHealthInput }) {
<button
type="button"
className={cn(
'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium transition-colors cursor-pointer',
'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium transition-colors cursor-pointer',
tint,
)}
aria-label={`Deal pulse: ${label}, score ${health.score}/100. Click for breakdown.`}
@@ -65,7 +65,7 @@ export function DealPulseChip({ interest }: { interest: DealHealthInput }) {
</div>
<div>
<p className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
What pushed the score
</p>
{health.signals.length === 0 ? (
@@ -79,7 +79,7 @@ export function DealPulseChip({ interest }: { interest: DealHealthInput }) {
<li key={s.id} className="flex items-start gap-2">
<span
className={cn(
'shrink-0 rounded px-1.5 py-0.5 text-[10px] font-semibold tabular-nums',
'shrink-0 rounded px-1.5 py-0.5 text-xs font-semibold tabular-nums',
s.delta > 0 ? 'bg-emerald-100 text-emerald-800' : 'bg-rose-100 text-rose-800',
)}
>
@@ -92,7 +92,7 @@ export function DealPulseChip({ interest }: { interest: DealHealthInput }) {
)}
</div>
<div className="rounded-md bg-muted/40 p-2.5 text-[11px] text-muted-foreground">
<div className="rounded-md bg-muted/40 p-2.5 text-xs text-muted-foreground">
<p className="font-medium text-foreground/80">How this is calculated</p>
<p className="mt-0.5">
Every signal above traces to a specific date or pipeline stage on this deal. Recent

View File

@@ -237,7 +237,7 @@ export function ExternalEoiUploadDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl">
<DialogContent className="sm:max-w-2xl lg:max-w-4xl">
<DialogHeader>
<DialogTitle>Upload externally-signed EOI</DialogTitle>
<DialogDescription>

View File

@@ -161,7 +161,7 @@ export function InterestCard({ interest, portSlug, onEdit, onArchive }: Interest
key={b.id}
title={b.detail}
className={cn(
'inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium',
'inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium',
b.className,
)}
>
@@ -185,7 +185,7 @@ export function InterestCard({ interest, portSlug, onEdit, onArchive }: Interest
) : null}
{lastActivity ? (
<p className="mt-1.5 text-[11px] text-muted-foreground tabular-nums">
<p className="mt-1.5 text-xs text-muted-foreground tabular-nums">
Last activity {lastActivity}
</p>
) : null}

View File

@@ -227,7 +227,7 @@ export function getInterestColumns({
key={b.id}
title={b.detail}
aria-label={b.detail}
className={`inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium ${b.className}`}
className={`inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium ${b.className}`}
>
{b.label}
</span>

View File

@@ -230,7 +230,7 @@ function ContactLogRow({
<div className="min-w-0 flex-1 space-y-1.5">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-foreground">{channelMeta.label}</span>
<Badge variant="outline" className="text-[10px] capitalize">
<Badge variant="outline" className="text-xs capitalize">
{entry.direction}
</Badge>
<span className="text-xs text-muted-foreground">
@@ -511,7 +511,7 @@ function ComposeDialogBody({
}
onClick={() => (voice.isListening ? voice.stop() : voice.start())}
className={cn(
'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-medium transition-colors',
'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium transition-colors',
voice.isListening
? 'border-rose-300 bg-rose-50 text-rose-800 animate-pulse'
: 'border-border bg-muted/40 text-muted-foreground hover:bg-muted',
@@ -532,7 +532,7 @@ function ComposeDialogBody({
) : (
<span
title="Voice transcription isn't supported in this browser."
className="inline-flex items-center gap-1 text-[11px] text-muted-foreground"
className="inline-flex items-center gap-1 text-xs text-muted-foreground"
>
<MicOff className="size-3" aria-hidden />
Voice unavailable
@@ -547,10 +547,10 @@ function ComposeDialogBody({
onChange={(e) => setSummary(e.target.value)}
/>
{voice.isListening && voice.interim ? (
<p className="text-[11px] italic text-muted-foreground">{voice.interim}</p>
<p className="text-xs italic text-muted-foreground">{voice.interim}</p>
) : null}
{voice.error ? (
<p className="text-[11px] text-rose-700">Voice error: {voice.error}</p>
<p className="text-xs text-rose-700">Voice error: {voice.error}</p>
) : null}
</div>
@@ -589,7 +589,7 @@ function ComposeDialogBody({
onChange={setFollowUpAt}
className="max-w-xs"
/>
<p className="text-[11px] text-muted-foreground">
<p className="text-xs text-muted-foreground">
A reminder is created on this interest for the time above.
</p>
</div>

View File

@@ -414,7 +414,7 @@ function StatusBadge({ status }: { status: DocumentRow['status'] }) {
<Badge
variant="outline"
className={cn(
'border-transparent text-[10px] font-semibold uppercase tracking-wide',
'border-transparent text-xs font-semibold uppercase tracking-wide',
STATUS_TONES[status],
)}
>

View File

@@ -276,7 +276,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
)}
{(interest.activeReminderCount ?? 0) > 0 ? (
<span
className="inline-flex items-center gap-1 rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-[11px] font-medium text-amber-800"
className="inline-flex items-center gap-1 rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-800"
title={`${interest.activeReminderCount} pending reminder${
interest.activeReminderCount === 1 ? '' : 's'
}`}

View File

@@ -402,7 +402,7 @@ function ActiveEoiCard({
local preference here so the UI matches what was sent. */}
<span
className={cn(
'inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide',
'inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-xs font-medium uppercase tracking-wide',
signingOrder === 'SEQUENTIAL'
? 'border-indigo-200 bg-indigo-50 text-indigo-800'
: 'border-sky-200 bg-sky-50 text-sky-800',
@@ -665,7 +665,7 @@ function StatusBadge({ status }: { status: DocumentRow['status'] }) {
<Badge
variant="outline"
className={cn(
'border-transparent text-[10px] font-semibold uppercase tracking-wide',
'border-transparent text-xs font-semibold uppercase tracking-wide',
STATUS_TONES[status],
)}
>

View File

@@ -355,7 +355,7 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }:
requestClose();
}}
>
<SheetContent className="w-full sm:max-w-2xl overflow-y-auto">
<SheetContent className="w-full sm:max-w-2xl lg:max-w-4xl overflow-y-auto">
<SheetHeader>
<SheetTitle>{isEdit ? 'Edit Interest' : 'New Interest'}</SheetTitle>
</SheetHeader>
@@ -533,7 +533,7 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }:
/>
<span className="flex-1">{option.label}</span>
{isPrimary && (
<span className="ml-2 rounded bg-primary/15 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-primary">
<span className="ml-2 rounded bg-primary/15 px-1.5 py-0.5 text-xs font-medium uppercase tracking-wide text-primary">
primary
</span>
)}
@@ -935,7 +935,7 @@ function DimensionInput({
}}
/>
{altValue ? (
<p className="text-[11px] leading-tight text-muted-foreground"> {altValue}</p>
<p className="text-xs leading-tight text-muted-foreground"> {altValue}</p>
) : null}
</div>
);

View File

@@ -410,7 +410,7 @@ function StatusBadge({ status }: { status: DocumentRow['status'] }) {
<Badge
variant="outline"
className={cn(
'border-transparent text-[10px] font-semibold uppercase tracking-wide',
'border-transparent text-xs font-semibold uppercase tracking-wide',
STATUS_TONES[status],
)}
>

View File

@@ -363,7 +363,7 @@ function MilestoneAdvanceButton({
onChange={setDate}
placeholder="Pick a date"
/>
<p className="text-[11px] text-muted-foreground">
<p className="text-xs text-muted-foreground">
Defaults to today - back-date if the event happened earlier.
</p>
</div>
@@ -429,7 +429,7 @@ function MilestoneBackfillButton({
onChange={setDate}
placeholder="Pick a date"
/>
<p className="text-[11px] text-muted-foreground">
<p className="text-xs text-muted-foreground">
Records the date the milestone happened. Does not change the deal&apos;s pipeline stage.
</p>
</div>
@@ -491,14 +491,14 @@ function MilestoneSection({
<Icon className={cn('size-4', isActive ? 'text-brand-600' : 'text-muted-foreground')} />
<h3 className="text-sm font-semibold tracking-tight text-foreground">{title}</h3>
{isActive ? (
<span className="inline-flex items-center gap-1 rounded-full bg-brand-600 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.08em] text-white shadow-sm">
<span className="inline-flex items-center gap-1 rounded-full bg-brand-600 px-2 py-0.5 text-xs font-semibold uppercase tracking-[0.08em] text-white shadow-sm">
<span className="size-1.5 rounded-full bg-white/90" aria-hidden />
Next step
</span>
) : null}
</div>
{status ? (
<span className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
{humanizeStatus(status)}
</span>
) : null}
@@ -1004,7 +1004,7 @@ function OverviewTab({
disabled={stageMutation.isPending}
onConfirm={(date) => advance('deposit_paid', date)}
/>
<span className="text-[11px] text-muted-foreground">
<span className="text-xs text-muted-foreground">
Or record a payment in the Payments section.
</span>
</div>
@@ -1119,7 +1119,7 @@ function OverviewTab({
gates the actual stage move). */}
{pastMilestones.length > 0 && (
<div className="rounded-lg border bg-muted/20">
<div className="flex items-center gap-2 border-b px-4 py-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
<div className="flex items-center gap-2 border-b px-4 py-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
<span>Past</span>
</div>
<Accordion type="multiple" className="px-4">
@@ -1129,7 +1129,7 @@ function OverviewTab({
<div className="flex flex-1 items-center gap-2 text-left text-muted-foreground">
<CheckCircle2 className="size-3 shrink-0 text-emerald-600" aria-hidden />
<span className="font-medium text-foreground">{m.title}</span>
<span className="text-[10px]">·</span>
<span className="text-xs">·</span>
<span className="truncate text-xs">{m.pastSummary}</span>
</div>
</AccordionTrigger>
@@ -1406,7 +1406,7 @@ function OverviewTab({
a context hint. */}
<span
className={cn(
'inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-[10px] font-medium',
'inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-xs font-medium',
STAGE_BADGE[interest.pipelineStage as PipelineStage] ??
'bg-muted text-muted-foreground',
)}

View File

@@ -131,7 +131,7 @@ export function InterestTimeline({ interestId }: InterestTimelineProps) {
<p className="text-sm">
{event.description}
{isAuto ? (
<span className="ml-2 inline-flex items-center gap-1 rounded-full bg-muted px-1.5 py-0.5 align-middle text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
<span className="ml-2 inline-flex items-center gap-1 rounded-full bg-muted px-1.5 py-0.5 align-middle text-xs font-medium uppercase tracking-wide text-muted-foreground">
<Bot className="h-3 w-3" aria-hidden />
Auto
</span>

View File

@@ -378,7 +378,7 @@ function LinkedBerthRowItem({
<HelpCircle className="h-3.5 w-3.5" aria-hidden />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs text-[11px] leading-snug">
<TooltipContent side="top" className="max-w-xs text-xs leading-snug">
Mark this berth as one your client is actively considering. When on, the berth
appears as <strong>Under Offer</strong> on the public map and counts toward the
recommender&apos;s &quot;heat&quot; score. Turn off if the link is legal/EOI-only.
@@ -413,7 +413,7 @@ function LinkedBerthRowItem({
<HelpCircle className="h-3.5 w-3.5" aria-hidden />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs text-[11px] leading-snug">
<TooltipContent side="top" className="max-w-xs text-xs leading-snug">
Include this berth in the EOI&apos;s signed berth range. When on, the berth is
covered by the same signature and shows up in the EOI&apos;s{' '}
<strong>Berth Range</strong> form field (e.g. &quot;A1-A3, B5-B7&quot;). Turn off
@@ -786,7 +786,7 @@ function BerthSection({
) : null}
</h4>
</div>
<p className="text-[11px] text-muted-foreground">{hint}</p>
<p className="text-xs text-muted-foreground">{hint}</p>
</div>
{count === 0 && emptyText ? (
<p className="rounded-md border border-dashed bg-muted/30 px-3 py-2 text-xs text-muted-foreground">

View File

@@ -44,7 +44,7 @@ export function MultiEoiChip({ interestId }: { interestId: string }) {
return (
<span
title={`This interest has ${inflight.length} in-flight EOI documents - review on the EOI tab.`}
className="inline-flex items-center gap-1 rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-[11px] font-medium text-amber-800"
className="inline-flex items-center gap-1 rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-800"
>
<FileSignature className="size-3" aria-hidden />
{inflight.length} EOIs

View File

@@ -1,8 +1,8 @@
'use client';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Plus, Trash2, Receipt } from 'lucide-react';
import { ChevronDown, ChevronRight, Plus, Trash2, Receipt } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { DatePicker } from '@/components/ui/date-picker';
@@ -77,6 +77,40 @@ function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString();
}
/**
* Per-interest collapse state. Persisted to localStorage so a rep
* who collapses the section on one deal sees it stay collapsed when
* they navigate away and back. Default collapsed (deposits are a
* reference history once the rep stops actively recording them); the
* "add a deposit" flow auto-expands so the new row is visible.
*/
function usePaymentsCollapsed(interestId: string): readonly [boolean, (v: boolean) => void] {
const storageKey = `pn-crm.payments-collapsed.v1.${interestId}`;
const [collapsed, setCollapsedState] = useState<boolean>(true);
// Canonical client-only hydration pattern: server renders the default
// (true), then on mount we read localStorage and reconcile. Initializing
// useState() lazily from localStorage would risk a hydration mismatch
// since the server has no access to the user's browser storage.
useEffect(() => {
try {
const raw = window.localStorage.getItem(storageKey);
// eslint-disable-next-line react-hooks/set-state-in-effect
if (raw !== null) setCollapsedState(raw === '1');
} catch {
// ignore — localStorage may be unavailable in private mode
}
}, [storageKey]);
function setCollapsed(v: boolean): void {
setCollapsedState(v);
try {
window.localStorage.setItem(storageKey, v ? '1' : '0');
} catch {
// ignore
}
}
return [collapsed, setCollapsed] as const;
}
export function PaymentsSection({
interestId,
depositExpectedAmount,
@@ -88,6 +122,7 @@ export function PaymentsSection({
}) {
const queryClient = useQueryClient();
const [recordOpen, setRecordOpen] = useState(false);
const [collapsed, setCollapsed] = usePaymentsCollapsed(interestId);
const { data, isLoading } = useQuery<PaymentsResponse>({
queryKey: ['interest-payments', interestId],
@@ -122,12 +157,65 @@ export function PaymentsSection({
? Math.max(0, expectedAmount - runningTotal)
: null;
// Collapsed summary bar — drops the full section to one line so the
// milestone strip above gets the rep's primary visual focus. Click
// anywhere on the bar (or the chevron) to expand inline.
if (collapsed) {
const summaryText =
payments.length === 0
? 'Not received yet'
: `${formatMoney(total?.total ?? '0', expectedCurrency)} received · ${payments.length} payment${payments.length === 1 ? '' : 's'}`;
return (
<>
<button
type="button"
onClick={() => setCollapsed(false)}
className="group flex w-full items-center justify-between gap-3 rounded-lg border bg-card/40 px-4 py-3 text-left transition-colors hover:bg-card/60"
aria-expanded={false}
aria-controls={`payments-section-${interestId}`}
>
<span className="flex items-center gap-2">
<ChevronRight
className="size-4 text-muted-foreground transition-transform group-hover:translate-x-0.5"
aria-hidden
/>
<span className="text-sm font-semibold">Payments</span>
<span className="text-xs text-muted-foreground">· {summaryText}</span>
</span>
<span className="text-xs text-muted-foreground">Expand</span>
</button>
<RecordPaymentSheet
open={recordOpen}
onOpenChange={setRecordOpen}
interestId={interestId}
defaultCurrency={expectedCurrency}
onRecorded={() => setCollapsed(false)}
/>
</>
);
}
return (
<section className="rounded-lg border bg-card/40 p-4 space-y-3">
<section
id={`payments-section-${interestId}`}
className="rounded-lg border bg-card/40 p-4 space-y-3"
>
<div className="flex items-center justify-between gap-3">
<div>
<h3 className="text-sm font-semibold">Payments</h3>
<p className="text-xs text-muted-foreground">
<div className="flex-1">
<button
type="button"
onClick={() => setCollapsed(true)}
className="group inline-flex items-center gap-2 text-left"
aria-expanded={true}
aria-controls={`payments-section-${interestId}`}
>
<ChevronDown
className="size-4 text-muted-foreground transition-transform group-hover:translate-y-0.5"
aria-hidden
/>
<h3 className="text-sm font-semibold">Payments</h3>
</button>
<p className="mt-0.5 text-xs text-muted-foreground">
Records that money was received or refunded. No invoices are issued - the bank handles
that.
</p>
@@ -169,7 +257,7 @@ export function PaymentsSection({
<li key={p.id} className="flex items-center justify-between gap-3 px-3 py-2">
<div className="flex items-center gap-2.5">
<span
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium ${
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium ${
TYPE_TINT[p.paymentType] ?? TYPE_TINT.other
}`}
>
@@ -213,6 +301,7 @@ export function PaymentsSection({
onOpenChange={setRecordOpen}
interestId={interestId}
defaultCurrency={expectedCurrency}
onRecorded={() => setCollapsed(false)}
/>
</section>
);
@@ -223,11 +312,13 @@ function RecordPaymentSheet({
onOpenChange,
interestId,
defaultCurrency,
onRecorded,
}: {
open: boolean;
onOpenChange: (v: boolean) => void;
interestId: string;
defaultCurrency: string;
onRecorded?: () => void;
}) {
const queryClient = useQueryClient();
const [paymentType, setPaymentType] = useState<string>('deposit');
@@ -257,7 +348,7 @@ function RecordPaymentSheet({
queryClient.invalidateQueries({ queryKey: ['interest-payments', interestId] });
queryClient.invalidateQueries({ queryKey: ['interests', interestId] });
onOpenChange(false);
// Reset form for next use
onRecorded?.();
setAmount('');
setNotes('');
setAcknowledgedNoReceipt(false);

View File

@@ -188,7 +188,7 @@ export function QualificationChecklist({
</span>
{c.autoSatisfied && (
<span
className="rounded bg-emerald-100 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200"
className="rounded bg-emerald-100 px-1.5 py-0.5 text-xs font-medium uppercase tracking-wide text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200"
title="System-derived from data on this interest"
>
Auto

View File

@@ -174,7 +174,7 @@ export function SupplementalInfoRequestButton({ interestId, eoiStatus }: Props)
submitted. Hidden when the list is empty. */}
{tokens.length > 0 ? (
<div className="space-y-1 border-t pt-2">
<div className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Issuance history
</div>
<ul className="divide-y">