chore(copy): em-dash sweep across user-facing JSX text + bump lint to error

Replaced 174 em-dashes (—) with " - " (space-hyphen-space) across 49
files in src/components + src/app. The em-dash reads as a tell-tale
"AI-generated" marker per the user's design feedback; hyphens with
spaces preserve the connector semantics without the AI tint.

Touched only lines outside pure-comment context (// /* * */). Code
comments, JSDoc, audit-log strings, structured logging strings, and
templates outside the lint scope retain their em-dashes for now —
they're not user-visible.

Also captured two remaining cases that used the `—` HTML entity
instead of the literal character (system-monitoring-dashboard,
interest-stage-picker) — replaced with a plain hyphen.

Bumped the existing `no-restricted-syntax` rule from `warn` → `error`
in eslint.config.mjs scoped to src/components/**/*.tsx +
src/app/**/*.tsx. New code reintroducing em-dashes in JSX text now
fails the lint gate.

Verified: tsc clean, vitest 1448/1448, eslint 0 em-dash warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 20:02:58 +02:00
parent 292a8b5e4a
commit f0dbefcac2
59 changed files with 213 additions and 205 deletions

View File

@@ -57,7 +57,7 @@ export function DealPulseChip({ interest }: { interest: DealHealthInput }) {
<PopoverContent side="bottom" align="start" className="w-80 p-4 space-y-3">
<div>
<p className="text-sm font-semibold">
Deal pulse {label} ({health.score} / 100)
Deal pulse - {label} ({health.score} / 100)
</p>
<p className="mt-0.5 text-xs text-muted-foreground">
How likely this deal is to keep moving forward, scored from 0 to 100.
@@ -70,7 +70,7 @@ export function DealPulseChip({ interest }: { interest: DealHealthInput }) {
</p>
{health.signals.length === 0 ? (
<p className="mt-1 text-xs text-muted-foreground">
Nothing notable yet the score is sitting at the baseline (50). Log a contact,
Nothing notable yet - the score is sitting at the baseline (50). Log a contact,
progress the stage, or send a signing request and you&apos;ll see the dial move.
</p>
) : (

View File

@@ -467,7 +467,7 @@ export function InlineStagePicker({
<AlertDialogDescription>
This interest has {linkedBerthCount} linked{' '}
{linkedBerthCount === 1 ? 'berth' : 'berths'}. Going back to{' '}
<strong>New Enquiry</strong> usually means restarting the lead keeping the berth
<strong>New Enquiry</strong> usually means restarting the lead - keeping the berth
links would leave them showing as under offer on the public map for a deal that&apos;s
no longer in progress.
</AlertDialogDescription>

View File

@@ -431,14 +431,14 @@ function ComposeDialogBody({
<SheetTitle>{isEdit ? 'Edit contact log entry' : 'Log a contact'}</SheetTitle>
<SheetDescription>
Record the channel, the direction, and what was discussed. Optionally schedule a
follow-up a reminder will be created automatically.
follow-up - a reminder will be created automatically.
</SheetDescription>
</SheetHeader>
<div className="space-y-3 py-1">
{/* Quick-template buttons. Tap one to seed the channel + direction
+ a starter summary so the rep can focus on the substance.
Hidden when editing templates are a fresh-entry affordance. */}
Hidden when editing - templates are a fresh-entry affordance. */}
{!isEdit ? (
<div className="flex flex-wrap gap-1.5">
{(Object.keys(TEMPLATE_SEEDS) as Template[]).map((t) => {

View File

@@ -162,7 +162,7 @@ export function InterestContractTab({ interestId, clientId: _clientId }: Interes
{/* Reuses the external-EOI upload dialog. The endpoint
`/api/v1/interests/{id}/external-eoi` is EOI-specific today
for contract paper-uploads we'll need the equivalent
- for contract paper-uploads we'll need the equivalent
contract endpoint (deferred to a follow-up; the dialog UI
is the pattern we'll clone). For now the flow is documented
as 'coming soon' rather than misrouting through EOI. */}
@@ -174,7 +174,7 @@ export function InterestContractTab({ interestId, clientId: _clientId }: Interes
/>
)}
{/* Phase 4 upload-for-Documenso-signing dialog. Multi-step
{/* Phase 4 - upload-for-Documenso-signing dialog. Multi-step
(file → recipients → fields → send) backed by the Phase 3
service. Auto-detect runs after the file lands; rep can
tweak placements before sending. */}
@@ -187,7 +187,7 @@ export function InterestContractTab({ interestId, clientId: _clientId }: Interes
/>
)}
{/* "Mark as signed externally" flips the contract doc-status
{/* "Mark as signed externally" - flips the contract doc-status
to 'signed' without uploading a file. Used when the rep is
keeping the canonical copy elsewhere and just wants the CRM
state to reflect the close. */}
@@ -299,7 +299,7 @@ function ActiveContractCard({
</div>
) : signers.length === 0 ? (
<p className="text-sm text-muted-foreground italic">
The signing service hasn&apos;t reported signers yet check back in a moment.
The signing service hasn&apos;t reported signers yet - check back in a moment.
</p>
) : (
<SigningProgress documentId={doc.id} signers={signers} />

View File

@@ -143,7 +143,7 @@ export function InterestEoiTab({ interestId, clientId }: InterestEoiTabProps) {
/>
)}
{/* History strip completed + cancelled EOIs from earlier in the
{/* History strip - completed + cancelled EOIs from earlier in the
deal's life. Quiet and skimmable; the active document above
carries the day-to-day attention. */}
{completedDocs.length > 0 && (
@@ -347,7 +347,7 @@ function ActiveEoiCard({
Created {new Date(doc.createdAt).toLocaleDateString()} ·{' '}
{totalCount > 0 ? `${signedCount} of ${totalCount} signed` : 'No signers loaded'}
</span>
{/* Signing-order badge tells the team whether recipients
{/* Signing-order badge - tells the team whether recipients
must sign in order or can sign concurrently. Drives off
the per-port setting; for v2 templates the template's
stored order wins server-side and we still surface our
@@ -361,7 +361,7 @@ function ActiveEoiCard({
)}
title={
signingOrder === 'SEQUENTIAL'
? 'Signers receive the invite chain one at a time each must sign before the next is emailed.'
? 'Signers receive the invite chain one at a time - each must sign before the next is emailed.'
: 'All signers receive the invite at once and can sign in any order.'
}
>
@@ -386,7 +386,7 @@ function ActiveEoiCard({
</Link>
</Button>
)}
{/* Remind all hides once every signer is signed no-one to nudge. */}
{/* Remind all hides once every signer is signed - no-one to nudge. */}
{!effectivelyCompleted && (
<Button
variant="outline"
@@ -416,7 +416,7 @@ function ActiveEoiCard({
</div>
) : signers.length === 0 ? (
<p className="text-sm text-muted-foreground italic">
The signing service hasn&apos;t reported signers yet check back in a moment.
The signing service hasn&apos;t reported signers yet - check back in a moment.
</p>
) : (
<SigningProgress documentId={doc.id} signers={signers} />
@@ -442,7 +442,7 @@ function ActiveEoiCard({
{/* Footer hides once every signer is signed: Cancel + Remind reminder
stop making sense, and the rep's natural next action is to view
the signed PDF (rendered above) or open the linked document
detail page. Upload-paper-signed-copy stays available useful
detail page. Upload-paper-signed-copy stays available - useful
for in-person sign-out workflows even after the digital flow. */}
{!effectivelyCompleted ? (
<footer className="mt-3 flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground">
@@ -461,7 +461,7 @@ function ActiveEoiCard({
<Upload />
Upload paper-signed copy
</Button>
{/* Regenerate is only safe when no one has signed yet once
{/* Regenerate is only safe when no one has signed yet - once
signatures are on the doc, the rep must go through the
cancel-with-notify path so collaborators learn about the
discard. */}
@@ -474,7 +474,7 @@ function ActiveEoiCard({
const ok = await confirm({
title: 'Regenerate this EOI?',
description:
'The current envelope will be voided silently no recipients will be notified and the generate dialog will re-open so you can rebuild.',
'The current envelope will be voided silently - no recipients will be notified - and the generate dialog will re-open so you can rebuild.',
confirmLabel: 'Regenerate',
});
if (ok) {
@@ -551,7 +551,7 @@ function SignedPdfPreview({ fileId }: { fileId: string }) {
if (isError || !data?.data.url) {
return (
<p className="text-xs italic text-muted-foreground">
Preview unavailable use the Download button to grab the signed PDF.
Preview unavailable - use the Download button to grab the signed PDF.
</p>
);
}

View File

@@ -165,7 +165,7 @@ export function InterestReservationTab({
{/* Reuses the external-EOI upload dialog. The endpoint
`/api/v1/interests/{id}/external-eoi` is EOI-specific today
for reservation paper-uploads we'll need the equivalent
- for reservation paper-uploads we'll need the equivalent
reservation endpoint (deferred to a follow-up; the dialog UI
is the pattern we'll clone). For now the flow is documented
as 'coming soon' rather than misrouting through EOI. */}
@@ -177,7 +177,7 @@ export function InterestReservationTab({
/>
)}
{/* Phase 4 upload-for-Documenso-signing dialog. */}
{/* Phase 4 - upload-for-Documenso-signing dialog. */}
{uploadForSigningOpen && (
<UploadForSigningDialog
open={uploadForSigningOpen}
@@ -295,7 +295,7 @@ function ActiveReservationCard({
</div>
) : signers.length === 0 ? (
<p className="text-sm text-muted-foreground italic">
The signing service hasn&apos;t reported signers yet check back in a moment.
The signing service hasn&apos;t reported signers yet - check back in a moment.
</p>
) : (
<SigningProgress documentId={doc.id} signers={signers} />

View File

@@ -119,7 +119,7 @@ export function InterestStagePicker({
<AlertTriangle className="h-4 w-4 mt-0.5 shrink-0" aria-hidden />
{canOverride ? (
<span>
This is not a normal forward transition. Override is enabled supply a reason
This is not a normal forward transition. Override is enabled - supply a reason
below explaining the manual stage change. Recorded in the audit log.
</span>
) : (
@@ -138,7 +138,7 @@ export function InterestStagePicker({
checked={override}
onChange={(e) => setOverride(e.target.checked)}
/>
Force-override (skip transition rules) &mdash; requires a reason
Force-override (skip transition rules) - requires a reason
</label>
)}

View File

@@ -329,7 +329,7 @@ function MilestoneAdvanceButton({
placeholder="Pick a date"
/>
<p className="text-[11px] text-muted-foreground">
Defaults to today back-date if the event happened earlier.
Defaults to today - back-date if the event happened earlier.
</p>
</div>
<div className="flex justify-end gap-2">
@@ -962,11 +962,11 @@ function OverviewTab({
return (
<div className="space-y-6">
{/* Skip-ahead nudge informational only; fires when the deal jumped
{/* Skip-ahead nudge - informational only; fires when the deal jumped
past a milestone without stamping the matching date. */}
<SkipAheadBanner interest={interest} />
{/* Conflict callout fires when a linked berth is sold or already
{/* Conflict callout - fires when a linked berth is sold or already
under offer to another active deal. Doesn't block the rep; just
surfaces the situation so they treat the deal as a backup. */}
<InterestBerthStatusBanner
@@ -976,22 +976,22 @@ function OverviewTab({
archivedAt={null}
/>
{/* Qualification checklist surfaces the port's per-port criteria so
{/* Qualification checklist - surfaces the port's per-port criteria so
the rep can mark each one confirmed before the deal advances out
of 'enquiry'. Hidden when the port has no enabled criteria. */}
<QualificationChecklist interestId={interestId} currentStage={interest.pipelineStage} />
{/* Payments bank-issued invoices live elsewhere; this is the
{/* Payments - bank-issued invoices live elsewhere; this is the
internal audit record of money received against the deal. The
running deposit total here drives the auto-advance into the
deposit_paid stage server-side. Hidden before the reservation
stage: no deposit is expected yet, so the empty card is just
noise the next-milestone card carries the actionable copy
noise - the next-milestone card carries the actionable copy
instead. Render order: deprioritized below the milestone strip
so the rep's eye lands on the active step first. */}
{/* Pre-reservation: the dedicated "Next step" guidance card was
removed in favour of a brighter NEXT STEP pill on the active
MilestoneSection below (it already owns the workflow actions
MilestoneSection below (it already owns the workflow actions -
two surfaces was redundant). Nurturing keeps a slim helper
since no milestone is naturally "current" while a deal is
paused. */}
@@ -1005,7 +1005,7 @@ function OverviewTab({
</div>
) : null}
{/* Sales-process milestones phase-aware so the user only sees
{/* Sales-process milestones - phase-aware so the user only sees
what's actionable now. Past milestones collapse into a tight
history strip; the current milestone gets the full card; future
milestones are hidden behind a toggle so reps can still
@@ -1097,7 +1097,7 @@ function OverviewTab({
</dl>
</div>
{/* Contact client's primary email + phone (from the linked client
{/* Contact - client's primary email + phone (from the linked client
record) AND the first/last-contact activity dates from the
contact log. Phone is rendered via libphonenumber-js's
international formatter so `+33633219796` reads as
@@ -1125,7 +1125,7 @@ function OverviewTab({
}}
/>
) : (
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground"> - </span>
)}
</EditableRow>
<EditableRow label="Phone">
@@ -1150,7 +1150,7 @@ function OverviewTab({
}}
/>
) : (
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground"> - </span>
)}
</EditableRow>
{interest.dateFirstContact || interest.dateLastContact ? (
@@ -1160,7 +1160,7 @@ function OverviewTab({
</>
) : (
<p className="mt-1 text-xs text-muted-foreground italic">
No contact activity logged yet log a call, email, or meeting from the Contact log
No contact activity logged yet - log a call, email, or meeting from the Contact log
tab to start tracking.
</p>
)}
@@ -1170,7 +1170,7 @@ function OverviewTab({
</dl>
</div>
{/* Berth requirements desired length / width / draft. Editable
{/* Berth requirements - desired length / width / draft. Editable
inline so reps can capture or correct a buyer's needs without
leaving the Overview tab. These values drive the auto-tick on
the "Dimensions confirmed" qualification row + the
@@ -1183,7 +1183,7 @@ function OverviewTab({
value={interest.desiredLengthFt ?? null}
onSave={save('desiredLengthFt')}
placeholder="e.g. 60"
emptyText=""
emptyText=" - "
/>
</EditableRow>
<EditableRow label="Desired width (ft)">
@@ -1191,7 +1191,7 @@ function OverviewTab({
value={interest.desiredWidthFt ?? null}
onSave={save('desiredWidthFt')}
placeholder="e.g. 25"
emptyText=""
emptyText=" - "
/>
</EditableRow>
<EditableRow label="Desired draft (ft)">
@@ -1199,7 +1199,7 @@ function OverviewTab({
value={interest.desiredDraftFt ?? null}
onSave={save('desiredDraftFt')}
placeholder="e.g. 6"
emptyText=""
emptyText=" - "
/>
</EditableRow>
</dl>
@@ -1215,7 +1215,7 @@ function OverviewTab({
{/* Most-recent threaded note teaser. Saves a click into the Notes
tab when the rep just wants to peek at "what was discussed last."
Always rendered now that the redundant `interests.notes` blob is
gone falls back to an empty-state prompt so reps still have an
gone - falls back to an empty-state prompt so reps still have an
obvious entry point to the Notes tab from Overview. */}
<div className="space-y-1 md:col-span-2">
<div className="mb-2 flex items-center justify-between">
@@ -1271,7 +1271,7 @@ function OverviewTab({
what's already linked before browsing more options. Each row exposes
per-berth role-flag toggles and the EOI bypass control (only visible
once the parent interest's primary EOI is signed). */}
{/* Won-status wrap-up checklist only renders when this interest's
{/* Won-status wrap-up checklist - only renders when this interest's
outcome is `won`. Surfaces upload slots for the manual paperwork
that didn't flow through the EOI->Contract chain automatically. */}
<WonStatusPanel interestId={interestId} outcome={interest.outcome ?? null} />
@@ -1298,7 +1298,7 @@ function OverviewTab({
{confirmDialog}
{/* Mounted at the Overview level so the EOI milestone's "Generate EOI"
footer button can launch the dialog without leaving the tab. Same
dialog component the dedicated EOI tab uses single source of
dialog component the dedicated EOI tab uses - single source of
truth for the editing/confirmation flow. */}
<EoiGenerateDialog
interestId={interestId}

View File

@@ -127,7 +127,7 @@ function formatDimensions(
const SPECIFIC_CONSEQUENCE_ON =
'This berth will show as “Under Offer” on the public-facing marina map.';
const SPECIFIC_CONSEQUENCE_OFF =
'This berth stays marked “Available” on the public map the link is internal only.';
'This berth stays marked “Available” on the public map - the link is internal only.';
// ─── Hooks ──────────────────────────────────────────────────────────────────
@@ -349,7 +349,7 @@ function LinkedBerthRowItem({
<div className="mt-3 grid grid-cols-1 gap-3 border-t pt-3 sm:grid-cols-2">
<div className="space-y-1">
{/* Switch sits next to its label (gap-2.5) instead of being
flexed to the far right via justify-between when the
flexed to the far right via justify-between - when the
column is wide, justify-between created a confusing visual
gulf between the action and what it controls. */}
<div className="flex items-center gap-2.5">
@@ -477,7 +477,7 @@ function LinkedBerthRowItem({
<DialogHeader>
<DialogTitle>Remove berth {row.mooringNumber} from interest?</DialogTitle>
<DialogDescription>
The berth itself isn&apos;t deleted only its link to this interest.
The berth itself isn&apos;t deleted - only its link to this interest.
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2 sm:gap-2">
@@ -707,7 +707,7 @@ export function LinkedBerthsList({ interestId }: LinkedBerthsListProps) {
<>
<BerthSection
title="Deal berth"
hint="The one berth this interest is anchored to drives templates, the EOI primary slot, and the public-map status. Promote any other berth to take its place."
hint="The one berth this interest is anchored to - drives templates, the EOI primary slot, and the public-map status. Promote any other berth to take its place."
emptyText="No deal berth selected. Pick one of the linked berths below as the primary."
count={dealBerth ? 1 : 0}
>

View File

@@ -128,7 +128,7 @@ export function PaymentsSection({
<div>
<h3 className="text-sm font-semibold">Payments</h3>
<p className="text-xs text-muted-foreground">
Records that money was received or refunded. No invoices are issued the bank handles
Records that money was received or refunded. No invoices are issued - the bank handles
that.
</p>
</div>
@@ -274,8 +274,8 @@ function RecordPaymentSheet({
<SheetHeader>
<SheetTitle>Record payment</SheetTitle>
<SheetDescription>
Capture that money was received (or refunded). Reps don&apos;t issue invoices the bank
does that so this is just an audit record.
Capture that money was received (or refunded). Reps don&apos;t issue invoices - the bank
does that - so this is just an audit record.
</SheetDescription>
</SheetHeader>

View File

@@ -169,7 +169,7 @@ export function PipelineBoard({ filters }: PipelineBoardProps = {}) {
{allData?.truncated ? (
<div className="mb-3 rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-xs text-amber-900">
Showing the {allData.total.toLocaleString()} most-recently-updated interests. Older active
deals aren&apos;t on the board archive completed work to keep the kanban readable.
deals aren&apos;t on the board - archive completed work to keep the kanban readable.
</div>
) : null}
<div className="flex gap-3 overflow-x-auto pb-4">

View File

@@ -212,7 +212,7 @@ export function QualificationChecklist({
{showPromoteHint ? (
<div className="flex items-center justify-between rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2">
<p className="text-xs text-emerald-800">
All criteria confirmed this lead is ready to qualify.
All criteria confirmed - this lead is ready to qualify.
</p>
<Button
type="button"

View File

@@ -97,11 +97,11 @@ export function WonStatusPanel({ interestId, outcome }: WonStatusPanelProps) {
<CardHeader className="gap-1">
<CardTitle className="flex items-center gap-2 text-base text-emerald-900">
<Trophy className="size-4" aria-hidden />
Won wrap-up checklist
Won - wrap-up checklist
</CardTitle>
<p className="text-xs text-emerald-800/80">
Upload anything that didn&apos;t flow through the system automatically. Reservations,
deposit invoicing, and client billing are handled outside the CRM this checklist is for
deposit invoicing, and client billing are handled outside the CRM - this checklist is for
the paperwork that lives on the deal itself.
</p>
</CardHeader>