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

@@ -354,7 +354,7 @@ export function AuditLogList() {
row.original.ipAddress ? (
<code className="text-xs text-muted-foreground">{row.original.ipAddress}</code>
) : (
<span className="text-xs text-muted-foreground"></span>
<span className="text-xs text-muted-foreground"> - </span>
),
size: 130,
},
@@ -457,7 +457,7 @@ export function AuditLogList() {
<SelectItem value="job_failed">Job failed</SelectItem>
<SelectItem value="cron_run">Cron run</SelectItem>
{/* L-AU02: actions that fire in the code but were missing from
the dropdown reps couldn't filter on them. */}
the dropdown - reps couldn't filter on them. */}
<SelectItem value="password_change">Password change</SelectItem>
<SelectItem value="portal_invite">Portal invite</SelectItem>
<SelectItem value="portal_activate">Portal activate</SelectItem>
@@ -585,7 +585,7 @@ export function AuditLogList() {
{dateRangeInvalid && (
<p className="mt-2 text-xs text-destructive">
From date must be on or before To date date filter ignored.
From date must be on or before To date - date filter ignored.
</p>
)}
@@ -642,7 +642,7 @@ export function AuditLogList() {
<>
<SheetHeader>
<SheetTitle>
{detailEntry.action.replace(/_/g, ' ')} {detailEntry.entityType}
{detailEntry.action.replace(/_/g, ' ')} - {detailEntry.entityType}
</SheetTitle>
<SheetDescription>
{formatDate(detailEntry.createdAt, 'datetime.medium')}

View File

@@ -121,7 +121,7 @@ export function BackupAdminPanel() {
</CardHeader>
<CardContent className="text-xs text-muted-foreground">
Backups land at <code>backups/&lt;id&gt;.dump</code> via{' '}
<code>getStorageBackend().put()</code>. Restore is intentionally not exposed in the UI
<code>getStorageBackend().put()</code>. Restore is intentionally not exposed in the UI -
download the .dump file and run <code>pg_restore</code> manually.
</CardContent>
</Card>

View File

@@ -192,7 +192,7 @@ export function BulkAddBerthsWizard() {
return (
<Card>
<CardHeader>
<CardTitle>Step 1 Sequence</CardTitle>
<CardTitle>Step 1 - Sequence</CardTitle>
<CardDescription>
Pick the dock letter and the mooring-number range. Tenure + status apply to every row;
everything else (dimensions, pricing, pontoon) is filled per row in Step 2.
@@ -265,7 +265,7 @@ export function BulkAddBerthsWizard() {
return (
<Card>
<CardHeader>
<CardTitle>Step 2 Fill in each row</CardTitle>
<CardTitle>Step 2 - Fill in each row</CardTitle>
<CardDescription>
Per-row dimensions, pricing, pontoon. Use the &ldquo;Apply to all&rdquo; inputs in the
header to copy a value down every row at once.
@@ -435,7 +435,7 @@ export function BulkAddBerthsWizard() {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__"></SelectItem>
<SelectItem value="__none__"> - </SelectItem>
{sidePontoonOptions.filter(Boolean).map((p) => (
<SelectItem key={p} value={p}>
{p}

View File

@@ -171,7 +171,7 @@ export function CustomFieldsManager() {
the form <code className="rounded bg-amber-100 px-1">{`{{custom.fieldName}}`}</code> now
expand in EOI/contract/email templates for client/interest/berth contexts. They still
don&rsquo;t plug into the global search index, the berth recommender, or the entity-diff
audit log use them for rep-only annotations and template-merge values, but anything
audit log - use them for rep-only annotations and template-merge values, but anything
load-bearing for the deal flow still needs a first-class column.
</span>
</WarningCallout>

View File

@@ -72,7 +72,7 @@ export function EmbeddedSigningCard() {
};
setResult({ ...res.data, at: new Date() });
if (res.data.ok) toast.success('Embedded signing host reachable.');
else toast.error('Embedded signing host probe failed see card.');
else toast.error('Embedded signing host probe failed - see card.');
} catch (err) {
toastError(err);
setResult({
@@ -200,7 +200,7 @@ export function EmbeddedSigningCard() {
<p className="text-muted-foreground">
The marketing site needs to handle <code>/sign/[role]/[token]</code> by forwarding
to the underlying Documenso signing URL (or embedding it in an iframe). Role is one
of <code>client</code> / <code>developer</code> / <code>approver</code> useful for
of <code>client</code> / <code>developer</code> / <code>approver</code> - useful for
tracking which slot the signer is in.
</p>
<p className="mt-1 text-muted-foreground">Minimum Next.js example:</p>
@@ -228,7 +228,7 @@ export default function SignPage({ params }) {
<p className="text-muted-foreground">
Use the Test connection button to verify <code>/</code> and{' '}
<code>/sign/success</code> return 2xx. If either fails, the marketing site
isn&apos;t ready fix the route before flipping live or signers will land on a 404
isn&apos;t ready - fix the route before flipping live or signers will land on a 404
page from outbound emails.
</p>
</section>

View File

@@ -106,7 +106,7 @@ export function TemplateSyncButton() {
onSuccess: (result) => {
setLastResult(result);
toast.success(
`Synced "${result.title}" ${result.recipients.length} recipients, ${result.fieldCount} fields cached`,
`Synced "${result.title}" - ${result.recipients.length} recipients, ${result.fieldCount} fields cached`,
);
void queryClient.invalidateQueries({ queryKey: ['settings', 'resolved'] });
void queryClient.invalidateQueries({
@@ -218,7 +218,7 @@ export function TemplateSyncButton() {
<div className="font-medium text-muted-foreground">Template-level settings</div>
<p className="text-[11px] text-muted-foreground">
Read from the template itself on Documenso. These values are bound to the
template, so every envelope generated from it inherits them {' '}
template, so every envelope generated from it inherits them -{' '}
<code>/template/use</code> does <strong>not</strong> accept overrides for these.
Change them in Documenso&apos;s template editor.
</p>
@@ -236,7 +236,7 @@ export function TemplateSyncButton() {
</span>
{lastResult.templateMeta.distributionMethod === 'EMAIL' && (
<span className="ml-1 rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-medium text-amber-900 dark:bg-amber-950 dark:text-amber-200">
Documenso will email recipients directly the CRM&apos;s branded email
Documenso will email recipients directly - the CRM&apos;s branded email
is in addition. Set to NONE on the template to let the CRM be the sole
sender.
</span>
@@ -256,7 +256,7 @@ export function TemplateSyncButton() {
Fields: {lastResult.fieldCount} cached for <code>prefillFields</code>
{lastResult.fieldCount === 0 && (
<span className="ml-1 font-normal text-muted-foreground">
that&apos;s fine if your template is a fillable PDF (AcroForm). The CRM will
- that&apos;s fine if your template is a fillable PDF (AcroForm). The CRM will
fill it via <code>formValues</code>-by-name instead, same as on v1.{' '}
<code>prefillFields</code> is only needed if you placed field overlays directly in
the Documenso template editor.
@@ -314,7 +314,7 @@ export function TemplateSyncButton() {
</div>
<p className="pt-0.5 text-[11px] text-muted-foreground">
These are the fillable fields actually in the PDF binary on Documenso. The CRM
fills them by name at send time this is the same mechanism the prod v1 server
fills them by name at send time - this is the same mechanism the prod v1 server
uses.
</p>
{lastResult.acroForm.map((report) => (
@@ -427,7 +427,7 @@ export function TemplateSyncButton() {
{sync.isError && !lastResult && (
<div className="rounded-md border border-destructive/40 bg-destructive/5 p-3 text-xs">
<div className="flex items-center gap-2 font-medium text-destructive">
<XCircle className="size-3" /> Sync failed check the Documenso credentials above and
<XCircle className="size-3" /> Sync failed - check the Documenso credentials above and
confirm the template exists on the configured instance.
</div>
</div>

View File

@@ -155,7 +155,7 @@ export function TemplateForm({ open, onOpenChange, template, onSuccess }: Templa
<p className="text-xs text-muted-foreground">
Paste or edit TipTap JSON. Use{' '}
<code className="rounded bg-muted px-1 text-xs">{'{{scope.field}}'}</code> tokens for
dynamic content see the list below.
dynamic content - see the list below.
</p>
<textarea
id="template-content"

View File

@@ -122,7 +122,7 @@ export function EmailRoutingCard() {
{!isSalesAvailable ? (
<WarningCallout>
<p className="text-sm">
Sales sender is disabled configure SMTP credentials in the &quot;Sales send-from
Sales sender is disabled - configure SMTP credentials in the &quot;Sales send-from
account&quot; card below to enable the <code>sales</code> option.
</p>
</WarningCallout>

View File

@@ -348,7 +348,7 @@ function CreateCriterionDialog({
<DialogTitle>Add qualification criterion</DialogTitle>
<DialogDescription>
The <strong>key</strong> is a stable identifier code references (lowercase alphanumeric
+ underscores). It can&apos;t be changed once created per-interest state rows
+ underscores). It can&apos;t be changed once created - per-interest state rows
reference it.
</DialogDescription>
</DialogHeader>

View File

@@ -188,8 +188,8 @@ export function ResidentialStagesAdmin() {
</SelectTrigger>
<SelectContent>
<SelectItem value="none">In-progress</SelectItem>
<SelectItem value="won">Closed won</SelectItem>
<SelectItem value="lost">Closed lost</SelectItem>
<SelectItem value="won">Closed - won</SelectItem>
<SelectItem value="lost">Closed - lost</SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -83,7 +83,7 @@ export function RoleList() {
{/* Display-normalize: snake_case → "Snake Case" so admin-
created roles with arbitrary keys still read cleanly.
The underlying name is stored verbatim and is what code
checks against display is purely cosmetic. */}
checks against - display is purely cosmetic. */}
<span className="font-medium">{formatRole(row.original.name)}</span>
{row.original.isSystem && (
<Badge variant="outline" className="text-xs">
@@ -248,14 +248,14 @@ export function RoleList() {
onSuccess={fetchRoles}
/>
{/* Permissions inspector opens when admin clicks the count
{/* Permissions inspector - opens when admin clicks the count
badge in the table. Lists granted vs denied per resource so
they can spot gaps before opening the editor. */}
<Dialog open={!!viewingPermissions} onOpenChange={(o) => !o && setViewingPermissions(null)}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
Permissions {viewingPermissions ? formatRole(viewingPermissions.name) : ''}
Permissions - {viewingPermissions ? formatRole(viewingPermissions.name) : ''}
</DialogTitle>
<DialogDescription>
Granted vs total per resource. Click Edit to change.

View File

@@ -159,7 +159,7 @@ export function SalesEmailConfigCard() {
message: res.data.error ?? 'Unknown error',
at: new Date(),
});
toast.error('SMTP test failed see card for details.');
toast.error('SMTP test failed - see card for details.');
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
@@ -221,7 +221,7 @@ export function SalesEmailConfigCard() {
<CardTitle>Sales send-from account</CardTitle>
<CardDescription>
SMTP credentials for human-touch outbound (brochures + per-berth PDFs). IMAP creds
enable the bounce monitor leave blank to disable bounce-rejection banners. Passwords
enable the bounce monitor - leave blank to disable bounce-rejection banners. Passwords
are encrypted at rest and never returned by the API.
</CardDescription>
</CardHeader>
@@ -272,7 +272,7 @@ export function SalesEmailConfigCard() {
/>
</Field>
<Field
label={`SMTP password ${smtpPassSet ? '(stored leave blank to keep)' : ''}`}
label={`SMTP password ${smtpPassSet ? '(stored - leave blank to keep)' : ''}`}
id="sef-smtp-pass"
>
<Input
@@ -322,7 +322,7 @@ export function SalesEmailConfigCard() {
/>
</Field>
<Field
label={`IMAP password ${imapPassSet ? '(stored leave blank to keep)' : ''}`}
label={`IMAP password ${imapPassSet ? '(stored - leave blank to keep)' : ''}`}
id="sef-imap-pass"
>
<Input

View File

@@ -94,7 +94,7 @@ const KNOWN_SETTINGS: Array<{
key: 'default_new_interest_owner',
label: 'Default New-Interest Owner',
description:
'User ID to auto-assign as the deal owner when a new interest is created. Stored as { "userId": "..." }. Leave blank to have new interests unassigned by default the rep can pick an owner from the interest detail header.',
'User ID to auto-assign as the deal owner when a new interest is created. Stored as { "userId": "..." }. Leave blank to have new interests unassigned by default - the rep can pick an owner from the interest detail header.',
type: 'json',
defaultValue: { userId: null },
},
@@ -136,7 +136,7 @@ const KNOWN_SETTINGS: Array<{
// ─── Berth recommender (src/lib/services/berth-recommender.service.ts) ──────
{
key: 'recommender_max_oversize_pct',
label: 'Recommender max oversize %',
label: 'Recommender - max oversize %',
description:
'Cap on how much larger a berth can be than the desired length/width/draft before it stops being suggested. Default 30.',
type: 'number',
@@ -144,35 +144,35 @@ const KNOWN_SETTINGS: Array<{
},
{
key: 'recommender_top_n_default',
label: 'Recommender default result count',
label: 'Recommender - default result count',
description: 'Default number of berth recommendations returned per request. Default 8.',
type: 'number',
defaultValue: 8,
},
{
key: 'fallthrough_policy',
label: 'Recommender fall-through policy',
label: 'Recommender - fall-through policy',
description: 'How berths re-enter the recommender after a lost deal.',
type: 'select',
defaultValue: 'immediate_with_heat',
options: [
{
value: 'immediate_with_heat',
label: 'Immediate (with heat boost) surface again right away',
label: 'Immediate (with heat boost) - surface again right away',
},
{
value: 'cooldown',
label: 'Cooldown wait N days (see below)',
label: 'Cooldown - wait N days (see below)',
},
{
value: 'never_auto_recommend',
label: 'Never only re-surface via manual rep search',
label: 'Never - only re-surface via manual rep search',
},
],
},
{
key: 'fallthrough_cooldown_days',
label: 'Recommender fall-through cooldown (days)',
label: 'Recommender - fall-through cooldown (days)',
description:
'Days a berth stays out of the recommender after a lost deal when the policy is `cooldown`. Default 30.',
type: 'number',
@@ -180,14 +180,14 @@ const KNOWN_SETTINGS: Array<{
},
{
key: 'heat_weight_recency',
label: 'Heat weight recency',
label: 'Heat weight - recency',
description: 'Weight given to how recently the prior interest fell through. Default 30.',
type: 'number',
defaultValue: 30,
},
{
key: 'heat_weight_furthest_stage',
label: 'Heat weight furthest stage',
label: 'Heat weight - furthest stage',
description:
'Weight given to how close the prior interest got to closing before falling through. Default 40.',
type: 'number',
@@ -195,7 +195,7 @@ const KNOWN_SETTINGS: Array<{
},
{
key: 'heat_weight_interest_count',
label: 'Heat weight historical interest count',
label: 'Heat weight - historical interest count',
description:
'Weight given to how often this berth has attracted interest historically. Default 15.',
type: 'number',
@@ -203,7 +203,7 @@ const KNOWN_SETTINGS: Array<{
},
{
key: 'heat_weight_eoi_count',
label: 'Heat weight historical EOI count',
label: 'Heat weight - historical EOI count',
description:
'Weight given to how often interest in this berth has reached EOI signing. Default 15.',
type: 'number',
@@ -211,7 +211,7 @@ const KNOWN_SETTINGS: Array<{
},
{
key: 'tier_ladder_hide_late_stage',
label: 'Recommender hide late-stage tier',
label: 'Recommender - hide late-stage tier',
description:
'Hide berths whose only active interests are late-stage (close to closing) from recommendations.',
type: 'boolean',
@@ -219,7 +219,7 @@ const KNOWN_SETTINGS: Array<{
},
{
key: 'documents_show_expired_tab',
label: 'Documents show Expired tab',
label: 'Documents - show Expired tab',
description:
'When off, the Expired tab on the documents hub is hidden. Use this when expired EOIs are noise that distracts reps from active deals.',
type: 'boolean',
@@ -227,12 +227,15 @@ const KNOWN_SETTINGS: Array<{
},
{
key: 'berths_default_currency',
label: 'Berths default currency',
label: 'Berths - default currency',
description:
'Currency applied to newly-created berths when none is specified on the form. Existing berths keep their per-row currency. Defaults to USD.',
type: 'select',
defaultValue: 'USD',
options: SUPPORTED_CURRENCIES.map((c) => ({ value: c.code, label: `${c.code}${c.label}` })),
options: SUPPORTED_CURRENCIES.map((c) => ({
value: c.code,
label: `${c.code} - ${c.label}`,
})),
},
];
@@ -350,7 +353,7 @@ export function SettingsManager() {
</CardContent>
</Card>
{/* String + Select Settings both render in the same card.
{/* String + Select Settings - both render in the same card.
'select' settings get a Select dropdown bound to setting.options;
'string' settings get a free-text Input. */}
{KNOWN_SETTINGS.some((s) => s.type === 'string' || s.type === 'select') && (
@@ -526,7 +529,7 @@ export function SettingsManager() {
this for one-off feature flags, integration secrets, or experimental tunables that the
platform reads at runtime via{' '}
<code className="text-xs">getSystemSetting(portId, key)</code>. Values can be JSON
objects, plain strings, numbers, or booleans. Most reps will never need this section
objects, plain strings, numbers, or booleans. Most reps will never need this section -
touch only if you know which key affects what.
</CardDescription>
</CardHeader>

View File

@@ -616,7 +616,7 @@ function UserSelectInput({
<SelectValue placeholder={isLoading ? 'Loading users…' : placeholder} />
</SelectTrigger>
<SelectContent>
<SelectItem value={NONE}> No CRM user linked </SelectItem>
<SelectItem value={NONE}> - No CRM user linked - </SelectItem>
{(data?.data ?? []).map((u) => (
<SelectItem key={u.id} value={u.id}>
{u.name || u.email} {u.name ? `· ${u.email}` : ''}

View File

@@ -84,7 +84,7 @@ const S3_FIELDS: SettingFieldDef[] = [
// (`pnpm tsx scripts/encrypt-plaintext-credentials.ts`) this field is
// empty and the encrypted form takes over.
key: 'storage_s3_access_key',
label: 'S3 access key (legacy plaintext deprecated)',
label: 'S3 access key (legacy plaintext - deprecated)',
description:
'Deprecated. Use the AES-encrypted access key field below instead. After running the migration script, this row is removed and only the encrypted form is used.',
type: 'string',
@@ -222,13 +222,13 @@ export function StorageAdminPanel() {
description="Where the CRM stores per-berth PDFs, brochures, GDPR exports, profile photos, and other binary files."
/>
{/* AES-encrypted access key write path. The legacy plaintext access
{/* AES-encrypted access key - write path. The legacy plaintext access
key field below is read-only deprecation; new writes should go
through this card. After running the encrypt-plaintext-credentials
migration script, the legacy field becomes empty. */}
<RegistryDrivenForm
title="S3 access key (encrypted)"
description="AES-encrypted at rest. Type your access key here it replaces the deprecated plaintext field below and fixes audit finding S-23."
description="AES-encrypted at rest. Type your access key here - it replaces the deprecated plaintext field below and fixes audit finding S-23."
sections={['storage.s3']}
/>
@@ -254,7 +254,7 @@ export function StorageAdminPanel() {
<div className="rounded-md border p-3 text-sm">
{testResult.ok ? (
<div className="flex items-center gap-2 text-emerald-600">
<CheckCircle2 className="h-4 w-4" aria-hidden /> Connection OK round-trip
<CheckCircle2 className="h-4 w-4" aria-hidden /> Connection OK - round-trip
succeeded.
</div>
) : (
@@ -271,7 +271,7 @@ export function StorageAdminPanel() {
<SettingsFormCard
title="Filesystem configuration"
description="Used when the active backend is filesystem. Only single-node deployments multi-node servers must use S3."
description="Used when the active backend is filesystem. Only single-node deployments - multi-node servers must use S3."
fields={FS_FIELDS}
/>
@@ -335,7 +335,7 @@ export function StorageAdminPanel() {
<p className="text-xs text-muted-foreground">
<strong>Switch + migrate</strong> copies every existing file to the new backend then
flips the pointer atomically. Reversible with a follow-up reverse-migration.{' '}
<strong>Switch only</strong> flips the pointer immediately old files become
<strong>Switch only</strong> flips the pointer immediately - old files become
inaccessible until you migrate them or revert the backend.
</p>
</div>
@@ -400,7 +400,7 @@ export function StorageAdminPanel() {
{s.fileCount} existing file
{s.fileCount === 1 ? '' : 's'} on <code className="text-xs">{s.backend}</code> will
not be reachable from the CRM after the switch unless you migrate them later. This is
rarely the right choice prefer Switch + migrate.
rarely the right choice - prefer Switch + migrate.
</WarningCallout>
)}
<DialogFooter>

View File

@@ -173,7 +173,7 @@ function RecentErrorsPanel() {
<div className="space-y-0.5 min-w-0">
<p className="font-medium truncate">{error.message}</p>
<p className="text-xs text-muted-foreground">
{error.source === 'queue' ? 'Queue' : 'Audit'} &mdash;{' '}
{error.source === 'queue' ? 'Queue' : 'Audit'} -{' '}
{new Date(error.timestamp).toLocaleString()}
</p>
</div>

View File

@@ -221,7 +221,7 @@ function UserFormBody({ open, onOpenChange, user, onSuccess }: UserFormProps) {
required
/>
<p className="text-xs text-muted-foreground">
How this user appears across the app usually their full name, but they can pick
How this user appears across the app - usually their full name, but they can pick
a nickname.
</p>
</div>
@@ -238,7 +238,7 @@ function UserFormBody({ open, onOpenChange, user, onSuccess }: UserFormProps) {
/>
{isEdit && email.trim().toLowerCase() !== originalEmail.toLowerCase() ? (
<p className="text-xs text-amber-600">
You&apos;ll be asked to confirm the original address will receive an automated
You&apos;ll be asked to confirm - the original address will receive an automated
notice that you, the admin, changed their sign-in email.
</p>
) : isEdit ? (

View File

@@ -210,7 +210,7 @@ export function UserPermissionMatrix({ userId }: UserPermissionMatrixProps) {
if (isSuperAdmin) {
return (
<div className="rounded-md border bg-muted/30 p-4 text-sm text-muted-foreground">
Super-admin users bypass per-port permission checks. Overrides don&apos;t apply here
Super-admin users bypass per-port permission checks. Overrides don&apos;t apply here -
revoke the super-admin flag on the Profile tab first.
</div>
);