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

@@ -44,7 +44,10 @@ const eslintConfig = [
files: ['src/components/**/*.tsx', 'src/app/**/*.tsx'],
rules: {
'no-restricted-syntax': [
'warn',
// Bumped from warn → error after the 2026-05-21 sweep cleared
// the existing 108 instances. New code reintroducing em-dashes
// now fails the lint gate.
'error',
{
selector: "JSXText[value=/\\u2014/]",
message:

View File

@@ -91,7 +91,7 @@ export default function SetupPage() {
password: data.password,
},
});
toast.success('Administrator account created sign in to continue.');
toast.success('Administrator account created - sign in to continue.');
router.replace('/login');
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to create administrator account');
@@ -114,7 +114,7 @@ export default function SetupPage() {
<div className="text-center space-y-1">
<h1 className="text-xl font-semibold">Welcome to {appName}</h1>
<p className="text-sm text-muted-foreground">
No administrator account exists yet. Create one to get started you&rsquo;ll be the
No administrator account exists yet. Create one to get started - you&rsquo;ll be the
super-administrator for this installation.
</p>
</div>

View File

@@ -28,7 +28,7 @@ const CONTRACT_RESERVATION_FIELDS: SettingFieldDef[] = [
key: 'documenso_reservation_template_id',
label: 'Reservation agreement Documenso template ID (optional)',
description:
'Numeric template ID for reservation agreements. Same logic leave blank to upload per interest.',
'Numeric template ID for reservation agreements. Same logic - leave blank to upload per interest.',
type: 'string',
placeholder: '',
defaultValue: '',
@@ -44,11 +44,11 @@ const V2_FEATURE_FIELDS: SettingFieldDef[] = [
key: 'documenso_signing_order',
label: 'Signing order',
description:
'Whether all signers receive the invitation at once (PARALLEL anyone can sign first) or only the next pending signer gets the email once the previous one finishes (SEQUENTIAL). Applied at envelope-create time on both v1 and v2: v1 honours meta.signingOrder on /templates/{id}/generate-document; v2 honours it via /envelope/update right after /template/use.',
'Whether all signers receive the invitation at once (PARALLEL - anyone can sign first) or only the next pending signer gets the email once the previous one finishes (SEQUENTIAL). Applied at envelope-create time on both v1 and v2: v1 honours meta.signingOrder on /templates/{id}/generate-document; v2 honours it via /envelope/update right after /template/use.',
type: 'select',
options: [
{ value: 'PARALLEL', label: 'PARALLEL all signers invited at once' },
{ value: 'SEQUENTIAL', label: 'SEQUENTIAL one at a time in order' },
{ value: 'PARALLEL', label: 'PARALLEL - all signers invited at once' },
{ value: 'SEQUENTIAL', label: 'SEQUENTIAL - one at a time in order' },
],
defaultValue: 'PARALLEL',
},
@@ -75,14 +75,14 @@ export default function DocumensoSettingsPage() {
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Info className="h-4 w-4" aria-hidden="true" />
v1 vs v2 what changes when you flip the API version
v1 vs v2 - what changes when you flip the API version
</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-sm">
<p className="text-muted-foreground">
The CRM supports both Documenso 1.13.x (v1) and 2.x (v2). v1 is the default for
backwards compatibility. v2 is recommended for new ports and unlocks the features below.
Switching versions does <strong>not</strong> require any code changes version-aware
Switching versions does <strong>not</strong> require any code changes - version-aware
client methods pick the right endpoint per port. Switch, save, then run the
test-connection button to confirm the chosen instance is actually on the matching
Documenso version.
@@ -111,7 +111,7 @@ export default function DocumensoSettingsPage() {
/>
<span>
<strong>Percent-based field coordinates.</strong> No page-dimension lookup needed
coordinates are portable across page sizes. v1 requires us to assume A4 for
- coordinates are portable across page sizes. v1 requires us to assume A4 for
auto-placed fields.
</span>
</li>
@@ -122,7 +122,7 @@ export default function DocumensoSettingsPage() {
/>
<span>
<strong>Richer field metadata.</strong> TEXT labels &amp; required flags, NUMBER
min/max + format, CHECKBOX/DROPDOWN/RADIO option lists with defaults all ignored
min/max + format, CHECKBOX/DROPDOWN/RADIO option lists with defaults - all ignored
by v1, surfaced by v2 in the signing UI.
</span>
</li>
@@ -134,7 +134,7 @@ export default function DocumensoSettingsPage() {
<span>
<strong>v2-flavoured webhook events.</strong> <code>RECIPIENT_VIEWED</code>,{' '}
<code>RECIPIENT_SIGNED</code>, <code>DOCUMENT_RECIPIENT_COMPLETED</code>,{' '}
<code>DOCUMENT_DECLINED</code>, <code>DOCUMENT_REMINDER_SENT</code> all routed
<code>DOCUMENT_DECLINED</code>, <code>DOCUMENT_REMINDER_SENT</code> - all routed
through the same dedup + audit pipeline as v1 events.
</span>
</li>
@@ -147,9 +147,9 @@ export default function DocumensoSettingsPage() {
<strong>Envelope CRUD endpoints.</strong> <code>GET</code>, <code>DELETE</code>,
<code>POST /envelope/create</code> (multipart),{' '}
<code>POST /envelope/distribute</code>, <code>POST /envelope/redistribute</code>,{' '}
<code>GET /envelope/{'{id}'}/download</code> all routed through{' '}
<code>GET /envelope/{'{id}'}/download</code> - all routed through{' '}
<code>/api/v2/envelope/...</code> when v2 is selected. The template-generate path
is intentionally still v1 (relies on Documenso 2.x&apos;s backward-compat window
is intentionally still v1 (relies on Documenso 2.x&apos;s backward-compat window -
see the deferred-roadmap below).
</span>
</li>
@@ -160,7 +160,7 @@ export default function DocumensoSettingsPage() {
/>
<span>
<strong>One-call send.</strong> v2&apos;s <code>/envelope/distribute</code>{' '}
returns per-recipient <code>signingUrl</code> in the same response v1 requires a
returns per-recipient <code>signingUrl</code> in the same response - v1 requires a
separate GET to fetch them. Faster send flow on the rep side.
</span>
</li>
@@ -186,7 +186,7 @@ export default function DocumensoSettingsPage() {
behaviour&quot; card; Documenso redirects the signer to that URL after they
complete signing. Use to land clients on the marketing site&apos;s success page or
back in the portal instead of Documenso&apos;s default thank-you page. (v1 honours
this too listed here because the admin setting was added with the v2 work.)
this too - listed here because the admin setting was added with the v2 work.)
</span>
</li>
</ul>
@@ -201,7 +201,7 @@ export default function DocumensoSettingsPage() {
<strong>
Single-shot <code>/template/use</code>
</strong>{' '}
with v2 <code>prefillFields</code> by ID current EOI flow uses{' '}
with v2 <code>prefillFields</code> by ID - current EOI flow uses{' '}
<code>/api/v1/templates/{'{id}'}/generate-document</code> with{' '}
<code>formValues</code> keyed by name. v2 instances accept both during their
backward-compat window; full migration requires per-template field-ID capture in
@@ -211,17 +211,17 @@ export default function DocumensoSettingsPage() {
<strong>
Update envelope metadata after creation (<code>/envelope/update</code>)
</strong>{' '}
change title / subject / redirectUrl on a doc already in DRAFT/PENDING without
- change title / subject / redirectUrl on a doc already in DRAFT/PENDING without
re-generating.
</li>
<li>
<strong>Non-SIGNER recipient roles (CC / VIEWER)</strong> APPROVER role is already
<strong>Non-SIGNER recipient roles (CC / VIEWER)</strong> - APPROVER role is already
used by the EOI template; CC + VIEWER not yet exposed in the recipient builder.
Useful for sales managers who want a copy without a signature slot.
</li>
</ul>
<p className="mt-2 text-xs text-muted-foreground">
Sequential signing and post-signing redirect URL <strong>are now wired</strong> see
Sequential signing and post-signing redirect URL <strong>are now wired</strong> - see
the new &quot;v2 signing behaviour&quot; card below to configure them.
</p>
</div>
@@ -244,13 +244,13 @@ export default function DocumensoSettingsPage() {
<RegistryDrivenForm
sections={['documenso.signers']}
title="Signers (developer + approver)"
description="Identity bound to the developer (signing order 2) and approver (signing order 3) slots in your Documenso templates. Leave name + email blank to fall through to whatever you set on the Documenso template itself; set them here to override the template's stored values at send time. Recipient IDs are populated automatically by 'Sync from Documenso' below. Linking a CRM user is optional when set, the platform fires an in-CRM notification for that user when it's their turn to sign."
description="Identity bound to the developer (signing order 2) and approver (signing order 3) slots in your Documenso templates. Leave name + email blank to fall through to whatever you set on the Documenso template itself; set them here to override the template's stored values at send time. Recipient IDs are populated automatically by 'Sync from Documenso' below. Linking a CRM user is optional - when set, the platform fires an in-CRM notification for that user when it's their turn to sign."
/>
<RegistryDrivenForm
sections={['documenso.templates']}
title="EOI generation"
description="Default pathway, template, and email behaviour when an interest's EOI is generated. Recipient + field discovery happens via 'Sync from Documenso' below that also populates the template ID for you."
description="Default pathway, template, and email behaviour when an interest's EOI is generated. Recipient + field discovery happens via 'Sync from Documenso' below - that also populates the template ID for you."
extra={<TemplateSyncButton />}
/>

View File

@@ -69,7 +69,7 @@ export default function ErrorCodeReferencePage() {
</h1>
<p className="text-muted-foreground text-sm mt-1">
Every error code the platform can return, with its HTTP status and the plain-language
message a user sees. Codes are stable identifiers once shipped, they never get
message a user sees. Codes are stable identifiers - once shipped, they never get
renamed.
</p>
</div>

View File

@@ -30,34 +30,34 @@ const TRIGGERS: Array<{
{
key: 'eoi_sent',
label: 'EOI sent',
description: 'Rep generates an EOI for signing moves the deal to "EOI" stage.',
description: 'Rep generates an EOI for signing - moves the deal to "EOI" stage.',
defaultMode: 'auto',
},
{
key: 'eoi_signed',
label: 'EOI signed (all parties)',
description:
'All signatories complete the EOI moves the deal to "Reservation" stage. Conventional CRM behaviour.',
'All signatories complete the EOI - moves the deal to "Reservation" stage. Conventional CRM behaviour.',
defaultMode: 'auto',
},
{
key: 'reservation_signed',
label: 'Reservation agreement signed',
description:
'Reservation paperwork signed by all parties keeps the deal at "Reservation" with sub-status signed.',
'Reservation paperwork signed by all parties - keeps the deal at "Reservation" with sub-status signed.',
defaultMode: 'auto',
},
{
key: 'deposit_received',
label: 'Deposit received in full',
description:
'Deposit total reaches the expected amount moves the deal to "Deposit Paid" stage.',
'Deposit total reaches the expected amount - moves the deal to "Deposit Paid" stage.',
defaultMode: 'auto',
},
{
key: 'contract_signed',
label: 'Sales contract signed',
description: 'Final contract signed by all parties moves the deal to "Contract" stage.',
description: 'Final contract signed by all parties - moves the deal to "Contract" stage.',
defaultMode: 'auto',
},
];
@@ -166,7 +166,7 @@ export default function PipelineRulesPage() {
>
<p className="text-sm font-semibold">Custom</p>
<p className="text-xs text-muted-foreground">
Mix and match the per-trigger toggles below override the preset.
Mix and match - the per-trigger toggles below override the preset.
</p>
</div>
</div>

View File

@@ -24,7 +24,7 @@ export default function PulseAdminPage() {
<p className="text-muted-foreground">
Every interest row carries a small coloured chip in the detail header. It scores the
deal from 0100 using rule-based signals (no AI). Click the chip on any interest to see
the per-signal breakdown every +N or -N traces back to a dated event on the deal.
the per-signal breakdown - every +N or -N traces back to a dated event on the deal.
</p>
<p className="text-muted-foreground">
Positive signals (recent EOI sent, deposit received, contract signed) push the score up.

View File

@@ -206,14 +206,14 @@ export default function ScanReceiptPage() {
)}
{uploadMutation.isError && (
<span className="text-destructive">
Receipt upload failed save will still create the expense without an image.
Receipt upload failed - save will still create the expense without an image.
</span>
)}
</div>
</div>
) : (
<div className="grid gap-2 sm:grid-cols-2">
{/* Camera button available on mobile devices that surface the
{/* Camera button - available on mobile devices that surface the
built-in capture flow when an `image/*` input has the
`capture` attribute. Hidden on desktop where it's a no-op. */}
<Button
@@ -225,7 +225,7 @@ export default function ScanReceiptPage() {
<Camera className="mr-2 h-5 w-5" />
Take photo
</Button>
{/* File picker works on every platform. Phrased so the copy
{/* File picker - works on every platform. Phrased so the copy
fits both mobile (library/files) and desktop (drag and drop). */}
<Button
type="button"
@@ -243,7 +243,7 @@ export default function ScanReceiptPage() {
</p>
</div>
)}
{/* `image/*` is the broadest accept includes HEIC on iOS,
{/* `image/*` is the broadest accept - includes HEIC on iOS,
JPEG/PNG/WebP everywhere. The capture attribute on the second
input invokes the native camera flow on mobile. */}
<input
@@ -272,7 +272,7 @@ export default function ScanReceiptPage() {
{scanMutation.isError && (
<div className="mt-4 rounded-md border border-amber-300 bg-amber-50 p-3 text-xs text-amber-900 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-200">
<span className="font-medium">Couldn&apos;t read this receipt automatically.</span>{' '}
You can still fill in the details manually below the receipt image will save with
You can still fill in the details manually below - the receipt image will save with
the expense.
</div>
)}

View File

@@ -25,7 +25,7 @@ export default async function PortalProfilePage() {
<span className="font-medium">{session.email}</span>
</div>
<p className="text-xs text-gray-400 pt-1">
To update name, phone, or address, please contact your port team they keep the records
To update name, phone, or address, please contact your port team - they keep the records
authoritative.
</p>
</div>

View File

@@ -1,7 +1,7 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Deal Pulse & Heat Port Nimara CRM',
title: 'Deal Pulse & Heat - Port Nimara CRM',
description:
'How the deal pulse chip + heat score work: signals, calibration, and what to do when a deal goes cold.',
};
@@ -35,7 +35,7 @@ export default function DealPulseDocsPage() {
<p>
The colored chip on each interest is a fast read of{' '}
<strong>how hot the deal is right now</strong> based on what&apos;s been happening on it
lately not a prediction, not an AI score, just a mechanical rollup of recent activity.
lately - not a prediction, not an AI score, just a mechanical rollup of recent activity.
</p>
</section>
@@ -53,7 +53,7 @@ export default function DealPulseDocsPage() {
<dt className="font-semibold text-amber-900">Warm</dt>
<dd className="text-amber-900/90">
Activity in the last 1430 days. The deal isn&apos;t neglected but the cadence has
slowed usually means a follow-up reminder is the right next action.
slowed - usually means a follow-up reminder is the right next action.
</dd>
</div>
<div className="rounded-md border bg-slate-100 p-3">
@@ -86,7 +86,7 @@ export default function DealPulseDocsPage() {
</li>
<li>
<strong>Time at current stage.</strong> Stagnation drags the score down even if other
signals look good a deal stuck at Reservation for six weeks should not read hot.
signals look good - a deal stuck at Reservation for six weeks should not read hot.
</li>
</ul>
<p className="text-muted-foreground">
@@ -128,7 +128,7 @@ export default function DealPulseDocsPage() {
Can I override the chip on a specific deal?
</summary>
<p className="mt-2 text-muted-foreground">
Not directly the chip is a read-only summary. To change it, change the inputs: log a
Not directly - the chip is a read-only summary. To change it, change the inputs: log a
contact, advance a stage, or close the deal.
</p>
</details>

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>
);

View File

@@ -43,7 +43,7 @@ export function ActiveInterestsPopover({ berthId, portSlug, count }: Props) {
// inside the conditionally-rendered PopoverContent.
});
if (count === 0) return <span className="text-muted-foreground"></span>;
if (count === 0) return <span className="text-muted-foreground"> - </span>;
return (
<Popover>

View File

@@ -43,7 +43,7 @@ export function BerthDealDocumentsTab({ berthId }: { berthId: string }) {
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
EOIs, contracts, and other deal documents attached to interests currently linked to this
berth. Read-only to send, sign, or edit, open the document on the linked interest&apos;s
berth. Read-only - to send, sign, or edit, open the document on the linked interest&apos;s
page.
</p>
<Card>

View File

@@ -263,7 +263,7 @@ export function BerthDetailHeader({ berth }: BerthDetailHeaderProps) {
title/area block sits side-by-side with the action buttons. */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
<div className="flex-1 min-w-0 flex items-center gap-3 flex-wrap">
{/* Compact mooring chip the mooring number sits inside a
{/* Compact mooring chip - the mooring number sits inside a
rounded plate tinted by the mooring-letter palette (same
colour used for the row-accent in the berth list). The
redundant "B Dock" tag from the previous design is replaced
@@ -367,7 +367,7 @@ function InterestLinkPicker({
</span>
</span>
) : (
' No interest '
' - No interest - '
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" aria-hidden />
</Button>
@@ -386,7 +386,7 @@ function InterestLinkPicker({
}}
className="text-muted-foreground"
>
No interest
- No interest -
</CommandItem>
</CommandGroup>
<CommandGroup heading="Most recent first">

View File

@@ -245,7 +245,7 @@ function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Pro
</div>
</div>
<p className="text-xs text-muted-foreground">
This action is reversible restore individually from each archived client.
This action is reversible - restore individually from each archived client.
</p>
</div>
)}

View File

@@ -175,7 +175,7 @@ function BulkHardDeleteDialogBody({ onOpenChange, clientIds, onDeleted }: Props)
{stage === 'partial' && (
<div className="space-y-3 text-sm">
<div className="rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-900">
{partialDeleted} of {clientIds.length} permanently deleted. {skipped.length} skipped
{partialDeleted} of {clientIds.length} permanently deleted. {skipped.length} skipped -
see below.
</div>
<div className="rounded-md border max-h-60 overflow-y-auto">

View File

@@ -278,7 +278,7 @@ function SmartArchiveDialogBody({
<DialogHeader>
<DialogTitle>Archive {clientName}</DialogTitle>
<DialogDescription>
Archive is reversible the client can be restored from the archived list. Decide what
Archive is reversible - the client can be restored from the archived list. Decide what
should happen to the relationships below before continuing.
</DialogDescription>
</DialogHeader>
@@ -314,7 +314,7 @@ function SmartArchiveDialogBody({
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-amber-900 flex items-center gap-2">
<AlertTriangle className="h-4 w-4" aria-hidden />
Late-stage interest confirmation required
Late-stage interest - confirmation required
</CardTitle>
</CardHeader>
<CardContent className="text-xs text-amber-900">
@@ -569,12 +569,12 @@ function SmartArchiveDialogBody({
</CardTitle>
</CardHeader>
<CardContent className="text-xs text-muted-foreground space-y-1">
<p>EOI documents retained for audit (always)</p>
{dossier.hasPortalUser && <p>Portal user deactivated (login revoked)</p>}
<p>EOI documents - retained for audit (always)</p>
{dossier.hasPortalUser && <p>Portal user - deactivated (login revoked)</p>}
{dossier.companies.length > 0 && (
<p>Company memberships end-dated to today (history preserved)</p>
<p>Company memberships - end-dated to today (history preserved)</p>
)}
<p>Notes, contacts, tags, addresses survive on the archived client</p>
<p>Notes, contacts, tags, addresses - survive on the archived client</p>
</CardContent>
</Card>

View File

@@ -163,7 +163,7 @@ function SmartRestoreDialogBody({ open, onOpenChange, clientId, clientName, onSu
<div key={r.id} className="flex items-start gap-2">
<span className="mt-0.5">{iconFor(r.kind)}</span>
<span>
<span className="font-medium">{r.label}</span> {r.reason}
<span className="font-medium">{r.label}</span> - {r.reason}
</span>
</div>
))}
@@ -217,7 +217,7 @@ function SmartRestoreDialogBody({ open, onOpenChange, clientId, clientName, onSu
<div key={r.id} className="flex items-start gap-2">
<span className="mt-0.5">{iconFor(r.kind)}</span>
<span>
<span className="font-medium">{r.label}</span> {r.reason}.{' '}
<span className="font-medium">{r.label}</span> - {r.reason}.{' '}
<span className="italic">{r.lockReason}</span>
</span>
</div>

View File

@@ -49,7 +49,7 @@ export function CustomizeWidgetsMenu() {
<DialogHeader>
<DialogTitle>Customize dashboard</DialogTitle>
<DialogDescription>
Pick which analytics cards appear on your dashboard. Hidden cards leave no empty space
Pick which analytics cards appear on your dashboard. Hidden cards leave no empty space -
the layout reflows to fill the available width.
</DialogDescription>
</DialogHeader>

View File

@@ -98,10 +98,10 @@ export function PipelineValueTile() {
<PopoverContent align="start" className="w-80 text-xs leading-relaxed">
<p className="font-semibold text-foreground">How the weighted forecast works</p>
<p className="mt-2 text-muted-foreground">
Each pipeline stage has a close-probability how likely a deal at that stage is to
Each pipeline stage has a close-probability - how likely a deal at that stage is to
actually close. Multiplying the berth price by the stage weight gives an{' '}
<strong>expected</strong> value for that deal. Summing across every active deal
yields the weighted forecast a defensible &ldquo;what will likely land&rdquo;
yields the weighted forecast - a defensible &ldquo;what will likely land&rdquo;
number, vs the gross which assumes every deal closes at full value.
</p>
<div className="mt-3 grid grid-cols-[1fr_auto] gap-x-3 gap-y-1 rounded-md bg-muted/50 p-2.5 text-[11px]">
@@ -205,7 +205,7 @@ export function PipelineValueTile() {
{s.dealsMissingPrice > 0 ? (
<p
className="mt-0.5 inline-flex items-center gap-1 text-[10px] font-medium text-warning"
title={`${s.dealsMissingPrice} of ${s.count} ${s.count === 1 ? 'deal has' : 'deals have'} a berth with no price set gross is undercounted here.`}
title={`${s.dealsMissingPrice} of ${s.count} ${s.count === 1 ? 'deal has' : 'deals have'} a berth with no price set - gross is undercounted here.`}
>
<AlertTriangle className="size-3" aria-hidden />
{s.dealsMissingPrice === s.count

View File

@@ -231,10 +231,10 @@ export function CreateDocumentWizard({ portSlug }: CreateDocumentWizardProps) {
</SelectTrigger>
<SelectContent>
<SelectItem value="documenso-template">
Generated EOI rendered + signed externally
Generated EOI - rendered + signed externally
</SelectItem>
<SelectItem value="inapp">
Manual EOI rendered in CRM, sent for e-signature
Manual EOI - rendered in CRM, sent for e-signature
</SelectItem>
</SelectContent>
</Select>

View File

@@ -133,7 +133,7 @@ export function EoiCancelDialog({ documentId, signers, open, onOpenChange }: Eoi
))}
</ul>
<p className="text-xs italic text-muted-foreground">
Leave all unchecked to cancel silently no emails will be sent.
Leave all unchecked to cancel silently - no emails will be sent.
</p>
</div>
)}

View File

@@ -262,7 +262,7 @@ export function EoiGenerateDialog({
const persistMissingFields = async (): Promise<void> => {
if (!clientId) {
toastError(new Error('Client ID missing refresh the page.'));
toastError(new Error('Client ID missing - refresh the page.'));
return;
}
setFixSaving(true);
@@ -363,7 +363,7 @@ export function EoiGenerateDialog({
{
key: 'dimensions',
label: `Dimensions (L × W × D, ${effectiveDimensionUnit})`,
value: ctx.yacht ? dimensionsForRender.map((v) => v ?? '').join(' × ') : null,
value: ctx.yacht ? dimensionsForRender.map((v) => v ?? ' - ').join(' × ') : null,
},
{
key: 'berth',
@@ -481,7 +481,7 @@ export function EoiGenerateDialog({
Generate Expression of Interest
</SheetTitle>
<SheetDescription>
Review the values that will be auto-filled. Edit anything inline changes save back to
Review the values that will be auto-filled. Edit anything inline - changes save back to
the client / interest record automatically. The EOI is generated once everything looks
right.
</SheetDescription>
@@ -497,7 +497,7 @@ export function EoiGenerateDialog({
</SelectTrigger>
<SelectContent>
<SelectItem value={DOCUMENSO_TEMPLATE_VALUE}>
Standard EOI sent for e-signature (recommended)
Standard EOI - sent for e-signature (recommended)
</SelectItem>
{inAppTemplates.map((t) => (
<SelectItem key={t.id} value={t.id}>
@@ -555,7 +555,7 @@ export function EoiGenerateDialog({
<div className="space-y-1 border-t pt-2">
<div className="flex items-center justify-between">
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
Optional (Section 3 left blank if absent)
Optional (Section 3 - left blank if absent)
</p>
{ctx.yacht ? (
<div className="inline-flex rounded-md border bg-muted/30 p-0.5 text-[11px]">
@@ -662,7 +662,7 @@ export function EoiGenerateDialog({
Missing required client details
</p>
<p className="text-[11px] text-amber-800/80">
Fill the fields below they&apos;ll be saved to the client&apos;s record before
Fill the fields below - they&apos;ll be saved to the client&apos;s record before
the EOI renders.
</p>
</div>
@@ -804,7 +804,7 @@ export function EoiGenerateDialog({
</SheetFooter>
</SheetContent>
{/* Phase 3c nested yacht-spawn Sheet. Pre-selects the linked
{/* Phase 3c - nested yacht-spawn Sheet. Pre-selects the linked
client as owner so the rep only types the yacht-specific
fields. After save, PATCH the interest with the new yachtId so
the EOI's yacht block populates without a manual re-link. */}
@@ -917,7 +917,9 @@ function PreviewRow({
)
) : (
<>
<span className="flex-1">{value ?? (missing ? 'Missing — required' : 'Not set')}</span>
<span className="flex-1">
{value ?? (missing ? 'Missing - required' : 'Not set')}
</span>
{edit ? (
<button
type="button"
@@ -1004,7 +1006,7 @@ function OverridableContactField({
)}
>
<span className="flex-1">
{effective ?? (missing ? 'Missing required' : 'Not set')}
{effective ?? (missing ? 'Missing - required' : 'Not set')}
{override?.value != null ? (
<span className="ml-1 inline-flex items-center rounded bg-amber-100 px-1 text-[10px] font-medium text-amber-800">
[EOI]
@@ -1265,7 +1267,7 @@ function OverridableAddressField({
)}
>
<span className="flex-1">
{effectiveSummary ?? (missing ? 'Missing required' : 'Not set')}
{effectiveSummary ?? (missing ? 'Missing - required' : 'Not set')}
{override ? (
<span className="ml-1 inline-flex items-center rounded bg-amber-100 px-1 text-[10px] font-medium text-amber-800">
[EOI]

View File

@@ -73,7 +73,7 @@ export function NewDocumentMenu({
<div className="flex flex-col">
<span>Upload file</span>
<span className="text-xs text-muted-foreground">
Drop or browse stored in the current folder
Drop or browse - stored in the current folder
</span>
</div>
</DropdownMenuItem>
@@ -83,7 +83,7 @@ export function NewDocumentMenu({
<div className="flex flex-col">
<span>Generate document for signing</span>
<span className="text-xs text-muted-foreground">
EOI, contract, or custom sent for e-signature
EOI, contract, or custom - sent for e-signature
</span>
</div>
</Link>

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>

View File

@@ -250,7 +250,7 @@ function ReminderFormBody({
<SheetHeader>
<SheetTitle>{isEdit ? 'Edit reminder' : 'New reminder'}</SheetTitle>
<p className="text-sm text-muted-foreground mt-2">
Reminders are personal nudges a follow-up call, a note to yourself, or something a
Reminders are personal nudges - a follow-up call, a note to yourself, or something a
teammate needs to action by a date. They show up in your dashboard, the daily digest
email, and on whichever client / interest / berth you link them to.
</p>
@@ -282,7 +282,7 @@ function ReminderFormBody({
/>
</div>
{/* 2fr/1fr split the datetime-local control needs more room
{/* 2fr/1fr split - the datetime-local control needs more room
for "MM/DD/YYYY HH:MM AM" than a 4-item priority Select. */}
<div className="grid grid-cols-[2fr_1fr] gap-4">
<div className="space-y-2">

View File

@@ -236,7 +236,7 @@ export function MobileSearchOverlay({ open, onOpenChange }: MobileSearchOverlayP
without this, the console throws an a11y violation. */}
<VaulDrawer.Title className="sr-only">Search</VaulDrawer.Title>
{/* Drag handle Vaul reads this as a swipe target. Centered grip
{/* Drag handle - Vaul reads this as a swipe target. Centered grip
+ a small label below feels iOS-native. */}
<div className="flex flex-col items-center pt-2.5 pb-1.5">
<div className="h-1.5 w-12 rounded-full bg-muted" aria-hidden />
@@ -374,7 +374,7 @@ function EmptyHint() {
<Search className="size-7" aria-hidden />
</div>
<p className="text-sm text-muted-foreground">
Search clients, yachts, interests, berths, invoices, documents paste a UUID or invoice
Search clients, yachts, interests, berths, invoices, documents - paste a UUID or invoice
number to jump directly.
</p>
</div>

View File

@@ -274,7 +274,7 @@ function SendDocumentDialogInner({
dangerouslySetInnerHTML={{ __html: previewHtml }}
/>
<p className="text-xs text-muted-foreground">
The PDF file is added by the system after the body your text won&rsquo;t override
The PDF file is added by the system after the body - your text won&rsquo;t override
it.
</p>
</div>

View File

@@ -104,11 +104,11 @@ export function WebsiteAnalyticsShell() {
<NotConfiguredEmptyState portSlug={portSlug} />
) : (
<>
{/* Realtime panel collapsible "what's happening RIGHT NOW"
{/* Realtime panel - collapsible "what's happening RIGHT NOW"
strip at the very top. Polling only fires while expanded. */}
<RealtimePanel />
{/* Live indicator + KPI tiles mirrors Umami's overview row. */}
{/* Live indicator + KPI tiles - mirrors Umami's overview row. */}
<div className="grid gap-3 grid-cols-2 sm:gap-4 md:grid-cols-3 lg:grid-cols-6">
<ActiveVisitorsBadge value={active.data?.data?.visitors} loading={active.isLoading} />
<KpiPair
@@ -117,7 +117,7 @@ export function WebsiteAnalyticsShell() {
value={stats.data?.data?.visitors}
prev={stats.data?.data?.comparison?.visitors}
accent="teal"
tooltip="Unique people who visited the site at least once. Counted by anonymous device fingerprint one person across two devices counts as two."
tooltip="Unique people who visited the site at least once. Counted by anonymous device fingerprint - one person across two devices counts as two."
/>
<KpiPair
label="Visits"
@@ -175,7 +175,7 @@ export function WebsiteAnalyticsShell() {
browsing five pages in one sitting still counts as 1 session.
</p>
<p className="mt-2 text-muted-foreground">
Pages-per-session (pageviews ÷ sessions) is a rough engagement signal higher
Pages-per-session (pageviews ÷ sessions) is a rough engagement signal - higher
means people are exploring deeper.
</p>
</PopoverContent>
@@ -257,14 +257,14 @@ export function WebsiteAnalyticsShell() {
/>
</div>
{/* Engagement heatmap full-width so the 7×24 grid has room
{/* Engagement heatmap - full-width so the 7×24 grid has room
to breathe and cells are large enough to hover comfortably. */}
<WeeklyHeatmap range={range} />
{/* Recent sessions */}
<SessionsList range={range} />
{/* World heatmap visitor counts per country (full-width, bottom of page) */}
{/* World heatmap - visitor counts per country (full-width, bottom of page) */}
<VisitorWorldMap
rows={allCountries.data?.data ?? null}
loading={allCountries.isLoading}
@@ -405,7 +405,7 @@ function BounceRateTile({
delta={delta}
deltaSuffix="%"
lowerIsBetter
tooltip="Share of visits that ended without a second pageview i.e. someone landed, didn't click anything, and left. Lower is generally better."
tooltip="Share of visits that ended without a second pageview - i.e. someone landed, didn't click anything, and left. Lower is generally better."
/>
);
}

View File

@@ -59,7 +59,7 @@ export function WeeklyHeatmap({ range }: Props) {
<PopoverContent align="start" className="w-80 text-xs leading-relaxed">
<p className="font-semibold text-foreground">When is your audience active?</p>
<p className="mt-2 text-muted-foreground">
Each cell is one hour of one day the columns are{' '}
Each cell is one hour of one day - the columns are{' '}
<strong>hours of the day in UTC</strong> (0 = midnight, 23 = 11 PM) and the rows are
days of the week. Darker blue means more sessions started during that hour across
the whole selected period. Hover any cell for the exact session count.
@@ -126,7 +126,7 @@ export function WeeklyHeatmap({ range }: Props) {
{/* Legend + floating value indicator */}
<div className="mt-4 flex items-center justify-between gap-4 text-[11px] text-muted-foreground">
<span>
Hour of day (UTC) colour intensity scaled to peak ({max.toLocaleString()}{' '}
Hour of day (UTC) - colour intensity scaled to peak ({max.toLocaleString()}{' '}
sessions)
</span>
<div className="flex items-center gap-1">
@@ -147,7 +147,7 @@ export function WeeklyHeatmap({ range }: Props) {
</div>
</div>
{/* Hover tooltip single element re-positioned via the
{/* Hover tooltip - single element re-positioned via the
hovered cell's data, much cheaper than mounting 168
Radix Tooltips. */}
{hover ? (

View File

@@ -275,7 +275,7 @@ export function YachtForm({
<Separator />
{/* Dimensions auto-convert ft ↔ m. Whichever unit the operator
{/* Dimensions - auto-convert ft ↔ m. Whichever unit the operator
types into, the other unit gets recomputed in place. We round
the converted value to keep the input clean (2 decimal places),
and skip the recompute when the user is mid-edit on the same
@@ -285,7 +285,7 @@ export function YachtForm({
Dimensions
</h3>
<p className="text-xs text-muted-foreground -mt-2">
Type a value in either ft or m the other unit auto-fills.
Type a value in either ft or m - the other unit auto-fills.
</p>
<div className="grid grid-cols-3 gap-4">
<DimensionPair