diff --git a/src/app/(dashboard)/[portSlug]/admin/documenso/page.tsx b/src/app/(dashboard)/[portSlug]/admin/documenso/page.tsx
index e0ba4d7f..ba2b06d1 100644
--- a/src/app/(dashboard)/[portSlug]/admin/documenso/page.tsx
+++ b/src/app/(dashboard)/[portSlug]/admin/documenso/page.tsx
@@ -1,9 +1,5 @@
import { CheckCircle2, Info } from 'lucide-react';
-import {
- SettingsFormCard,
- type SettingFieldDef,
-} from '@/components/admin/shared/settings-form-card';
import { RegistryDrivenForm } from '@/components/admin/shared/registry-driven-form';
import { DocumensoTestButton } from '@/components/admin/documenso/documenso-test-button';
import { EmbeddedSigningCard } from '@/components/admin/documenso/embedded-signing-card';
@@ -11,57 +7,11 @@ import { TemplateSyncButton } from '@/components/admin/documenso/template-sync-b
import { PageHeader } from '@/components/shared/page-header';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-// API_FIELDS removed — replaced by
-// which adds the new webhook-secret field + AES encrypts the API key at rest.
-
-const CONTRACT_RESERVATION_FIELDS: SettingFieldDef[] = [
- {
- key: 'documenso_contract_template_id',
- label: 'Contract Documenso template ID (optional)',
- description:
- 'Numeric template ID for sales contract generation. Leave blank to use the per-interest upload-and-place-fields flow instead (the typical path for contracts, since they are usually drafted custom per client).',
- type: 'string',
- placeholder: '',
- defaultValue: '',
- },
- {
- 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.',
- type: 'string',
- placeholder: '',
- defaultValue: '',
- },
-];
-
-// Embedded signing field config + Test + Setup help all live inside
-// `` (imported above). Kept out of the field list
-// here so the admin page reads as a flat sequence of cards.
-
-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.',
- type: 'select',
- options: [
- { value: 'PARALLEL', label: 'PARALLEL - all signers invited at once' },
- { value: 'SEQUENTIAL', label: 'SEQUENTIAL - one at a time in order' },
- ],
- defaultValue: 'PARALLEL',
- },
- {
- key: 'documenso_redirect_url',
- label: 'Post-signing redirect URL',
- description:
- "URL Documenso redirects the signer to after they complete signing. Typically the marketing site's success page so signers land on a branded thank-you rather than Documenso's own page. Leave blank to use Documenso's default. v1 and v2 both honour this. Example: https://portnimara.com/sign/success",
- type: 'string',
- placeholder: 'https://portnimara.com/sign/success',
- defaultValue: '',
- },
-];
+// All field arrays removed — every Documenso setting now flows through
+// `RegistryDrivenForm`, which surfaces the env-fallback / port / global
+// source badge on each field. The settings themselves live in
+// `src/lib/settings/registry.ts` under sections `documenso.api` /
+// `.signers` / `.templates` / `.behavior`.
export default function DocumensoSettingsPage() {
return (
@@ -235,10 +185,10 @@ export default function DocumensoSettingsPage() {
extra={}
/>
- }
/>
-
-
);
diff --git a/src/components/clients/client-pipeline-summary.tsx b/src/components/clients/client-pipeline-summary.tsx
index 3df8a95e..be82bd89 100644
--- a/src/components/clients/client-pipeline-summary.tsx
+++ b/src/components/clients/client-pipeline-summary.tsx
@@ -15,6 +15,7 @@ import {
STAGE_BADGE,
STAGE_DOT,
STAGE_LABELS,
+ STAGE_SHORT_LABELS,
safeStage,
type PipelineStage,
} from '@/components/clients/pipeline-constants';
@@ -54,29 +55,54 @@ export function StageStepper({
// micro-dots that vanish under cramped layouts.
const height = size === 'xs' ? 'h-1' : 'h-1.5';
return (
-
+ {/* Stage-name row below the bar — surfaces all reached stage names
+ inline (compact short-labels) so the bar isn't a mystery without
+ hovering. Future stages render in muted text so the rep can still
+ see the ladder ahead. The `xs` size variant hides this row to
+ keep the cramped table-cell footprint intact. */}
+ {size !== 'xs' ? (
+
+ // Larger bottom spacing (pb-1 + mb-4) gives the empty-state row enough
+ // breathing room above the "Add a watcher…" select — the prior `mb-3`
+ // alone left the two lines stacked tight against each other.
+
+ No one is watching this document yet.
+
) : (
{watchers.map((w) => {
diff --git a/src/components/inbox/inbox-page-shell.tsx b/src/components/inbox/inbox-page-shell.tsx
index 14ee3895..61826c35 100644
--- a/src/components/inbox/inbox-page-shell.tsx
+++ b/src/components/inbox/inbox-page-shell.tsx
@@ -11,7 +11,7 @@ import { useAlertCount } from '@/components/alerts/use-alerts';
/**
* Merged "Inbox" surface — replaces the previously-separate /alerts and
- * /reminders pages. Two stacked sections (Alerts first, Reminders second)
+ * /reminders pages. Two stacked sections (Reminders first, Alerts second)
* preserve the source distinction (system-flagged vs user-set) while
* giving reps a single "things demanding my attention" surface.
*
diff --git a/src/components/invoices/upload-receipts-guide.tsx b/src/components/invoices/upload-receipts-guide.tsx
index fcdee7cb..0b37a40c 100644
--- a/src/components/invoices/upload-receipts-guide.tsx
+++ b/src/components/invoices/upload-receipts-guide.tsx
@@ -26,9 +26,9 @@ export function UploadReceiptsGuide({ portSlug }: Props) {
return (
@@ -41,27 +41,22 @@ export function UploadReceiptsGuide({ portSlug }: Props) {
}
/>
- {/* What it does, in plain English */}
+ {/* What it does */}
-
What does it actually do?
+
What it does
- You paid out of pocket for something the marina needs (fuel, hardware, a part run,
- lunch with a broker). Snap a photo of the receipt and this tool turns it into a
- reimbursement request. It pulls out the vendor, the date, the total, and the currency,
- drops them into the expense form, and queues the whole thing for the parent
- company's finance team to approve and pay you back.
+ Photograph the receipt; the scanner extracts the vendor, date, total, and currency
+ into a pre-filled expense record. Finance reviews and reimburses from the parent
+ company.
- The behind-the-scenes part is called OCR (short for “optical character
- recognition”). Think of it as a fancy phone camera that knows how to read
- printed text. Combined with a bit of AI to figure out which number is the total and
- which is the tax, it turns a paper receipt into a ready-to-save expense in about five
- seconds. No typing. No spreadsheets. No chasing finance for the form.
+ Extraction uses on-device OCR with AI-assisted field detection. Typical turnaround
+ from photograph to saved record is under ten seconds.
@@ -72,12 +67,12 @@ export function UploadReceiptsGuide({ portSlug }: Props) {
}
- title="Add the scanner to your phone"
- description="One-time setup. After this, the scanner opens like a normal app from your home screen."
+ title="Install the scanner"
+ description="One-time setup. The scanner then opens from the home screen like any native app."
>
}
- label="iPhone or iPad (Safari)"
+ label="iPhone / iPad (Safari)"
steps={[
<>
Open{' '}
@@ -85,20 +80,20 @@ export function UploadReceiptsGuide({ portSlug }: Props) {
href={scannerUrl as never}
className="text-brand underline-offset-2 hover:underline"
>
- this link
+ the scanner
{' '}
- in Safari on your phone.
+ in Safari.
>,
- 'Tap the Share button at the bottom of the screen (the square with the arrow pointing up).',
+ 'Tap the Share icon.',
<>
- Scroll down and tap Add to Home Screen.
+ Select Add to Home Screen.
>,
- 'Confirm the name "Scanner" and tap Add. The icon now sits on your home screen.',
+ 'Confirm to install.',
]}
/>
}
- label="Android phone (Chrome)"
+ label="Android (Chrome)"
steps={[
<>
Open{' '}
@@ -106,16 +101,15 @@ export function UploadReceiptsGuide({ portSlug }: Props) {
href={scannerUrl as never}
className="text-brand underline-offset-2 hover:underline"
>
- this link
+ the scanner
{' '}
- in Chrome on your phone.
+ in Chrome.
>,
- 'Tap the three-dot menu in the top-right corner.',
+ 'Open the three-dot menu.',
<>
- Tap Install app (older versions of Chrome say{' '}
- Add to Home screen).
+ Select Install app.
>,
- 'Confirm to install. The icon now sits on your home screen.',
+ 'Confirm to install.',
]}
/>
@@ -123,63 +117,54 @@ export function UploadReceiptsGuide({ portSlug }: Props) {
}
- title="Snap a photo of a receipt"
- description="Open the scanner from your home screen and follow the prompts. The whole thing takes about ten seconds."
+ title="Capture a receipt"
+ description="Launch the scanner and follow the on-screen prompts."
>
- Tap the camera tile. Your phone
- opens its camera. Hold the receipt flat, get the whole thing in the frame, and snap.
+ Tap the camera tile. Centre the
+ receipt flat and within the frame; capture.
- Wait a few seconds. The system
- reads the receipt and fills in the merchant, date, total, and currency for you. A
- loading spinner shows while this happens.
+ Review the extracted fields.{' '}
+ Vendor, date, total, and currency populate automatically. Correct any field by tapping
+ it; category is the field most often set manually.
- Glance over the numbers. Most of
- the time everything is correct. If something looks off (wrong total, wrong category),
- tap the field and fix it. The category is the field you most often need to set
- yourself.
-
-
- Tap Save. The receipt becomes a
- pending expense ready for reimbursement. The parent company's finance team will
- review it on the{' '}
+ Save. The record lands as a
+ pending expense for finance review on the{' '}
Expenses page
- {' '}
- and approve it for payback. You can check the status of any expense you submitted from
- there too.
+
+ . Submission status is visible from the same view.
- {/* Tips */}
+ {/* Best practices */}
-
Tips for the best results
+
Best practices
- Get the whole receipt in the frame. If the edges are cut off, the total or date might be
- missed and finance might bounce it back to you.
+ Frame the entire receipt. Cropped edges can drop the total or date and trigger a finance
+ review.
-
Hold the camera steady. Blurry photos are harder to read. Retake if needed.
+
Hold steady; bright, even lighting yields the most reliable extraction.
- Receipts in foreign currencies are fine. The scanner picks up the currency code if it is
- printed on the slip. The parent company handles the conversion when they reimburse you.
-
-
If the camera looks dim, just turn on a light. Bright, even lighting works best.
-
- Add a quick note in the description if the expense needs context (who you met, what the
- part was for, etc.). Saves finance from having to ask.
+ Foreign currencies are supported when the currency code is printed on the slip;
+ conversion is handled at reimbursement.
- Lost the home-screen icon? Open this page on your phone again and tap the{' '}
- Open scanner button at the top.
+ Add context in the description field where relevant (counterparty, purpose) to
+ accelerate finance approval.
+
+
+ Lost the home-screen shortcut? Reopen this page on the device and select{' '}
+ Open scanner.
@@ -241,7 +226,7 @@ function PlatformBlock({
- Done. The scanner now opens from your home screen like a normal app.
+ The scanner now launches from the home screen.
);
diff --git a/src/components/residential/residential-interests-list.tsx b/src/components/residential/residential-interests-list.tsx
index cf5f0d39..550ae65f 100644
--- a/src/components/residential/residential-interests-list.tsx
+++ b/src/components/residential/residential-interests-list.tsx
@@ -2,7 +2,7 @@
import { useState } from 'react';
import Link from 'next/link';
-import { useParams } from 'next/navigation';
+import { useParams, useRouter } from 'next/navigation';
import { useQuery } from '@tanstack/react-query';
import { Input } from '@/components/ui/input';
@@ -46,6 +46,7 @@ const STAGE_LABELS: Record = {
export function ResidentialInterestsList() {
const params = useParams<{ portSlug: string }>();
+ const router = useRouter();
const portSlug = params?.portSlug ?? '';
const [search, setSearch] = useState('');
const [stage, setStage] = useState('all');
@@ -125,12 +126,21 @@ export function ResidentialInterestsList() {
router.push(`/${portSlug}/residential/interests/${i.id}` as never)}
>
e.stopPropagation()}
>
{STAGE_LABELS[i.pipelineStage] ?? i.pipelineStage}
diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx
index 903737e3..3a7da416 100644
--- a/src/components/ui/command.tsx
+++ b/src/components/ui/command.tsx
@@ -64,7 +64,16 @@ const CommandList = React.forwardRef<
// event ourselves so the list scrolls regardless of focus state.
` for non-Popover hosts; the static max-h still applies.
+ className={cn(
+ 'max-h-[min(300px,var(--radix-popover-content-available-height,300px))] overflow-y-auto overflow-x-hidden overscroll-contain',
+ className,
+ )}
onWheel={(event) => {
onWheel?.(event);
if (event.defaultPrevented) return;
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
index 469a16cf..6e019183 100644
--- a/src/components/ui/dropdown-menu.tsx
+++ b/src/components/ui/dropdown-menu.tsx
@@ -63,10 +63,13 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
- // Cap at 24rem (384px) so long menus don't visually stretch
- // edge-to-edge of the viewport — internal scroll handles
- // overflow. Consumers can override via the `className` prop.
- 'z-50 max-h-96 min-w-32 overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
+ // Cap at the smaller of 24rem (384px) and the radix-reported
+ // available height under/above the trigger, so long menus don't
+ // visually stretch past the viewport edge on small screens —
+ // internal scroll handles overflow. The CSS variable is set by
+ // Radix on `[data-side]` collision detection. Consumers can
+ // override via the `className` prop.
+ 'z-50 max-h-[min(24rem,var(--radix-dropdown-menu-content-available-height,24rem))] min-w-32 overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)',
className,
)}
diff --git a/src/components/website-analytics/pageviews-chart.tsx b/src/components/website-analytics/pageviews-chart.tsx
index a776db94..543d6c23 100644
--- a/src/components/website-analytics/pageviews-chart.tsx
+++ b/src/components/website-analytics/pageviews-chart.tsx
@@ -68,6 +68,11 @@ export function PageviewsChart({ data }: Props) {
fontSize={11}
tick={{ fill: 'hsl(var(--muted-foreground))' }}
tickFormatter={formatXTick}
+ // Anchor first + last ticks then let Recharts thin out the middle —
+ // multi-week ranges previously crowded every day-bucket label onto
+ // the axis. minTickGap enforces ~52px between rendered ticks.
+ interval="preserveStartEnd"
+ minTickGap={52}
/>