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 ( -
- {PIPELINE_STAGES.map((stage, i) => { - const isReached = i <= idx; - const isCurrent = i === idx; - return ( -
0 ? 'border-l border-card' : '', - )} - /> - ); - })} +
+
+ {PIPELINE_STAGES.map((stage, i) => { + const isReached = i <= idx; + const isCurrent = i === idx; + return ( +
0 ? 'border-l border-card' : '', + )} + /> + ); + })} +
+ {/* 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' ? ( +
+ {PIPELINE_STAGES.map((stage, i) => { + const isReached = i <= idx; + return ( +
+ {STAGE_SHORT_LABELS[stage]} +
+ ); + })} +
+ ) : null}
); } diff --git a/src/components/clients/pipeline-constants.ts b/src/components/clients/pipeline-constants.ts index baafc706..5939afbf 100644 --- a/src/components/clients/pipeline-constants.ts +++ b/src/components/clients/pipeline-constants.ts @@ -2,6 +2,7 @@ export { PIPELINE_STAGES, STAGE_LABELS, + STAGE_SHORT_LABELS, STAGE_BADGE, STAGE_DOT, STAGE_WEIGHTS, diff --git a/src/components/documents/document-detail.tsx b/src/components/documents/document-detail.tsx index aab13941..40db1123 100644 --- a/src/components/documents/document-detail.tsx +++ b/src/components/documents/document-detail.tsx @@ -584,7 +584,12 @@ function WatchersCard({ documentId, watchers }: { documentId: string; watchers:

{watchers.length === 0 ? ( -

No one is watching this document yet.

+ // 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." >
    1. - 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.
    2. - 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.
    3. - 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. -
    4. -
    5. - 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} />