+ {/* Rating buttons — dynamic per-tenant colour via RatingSegment's
+ per-option `color` override (wins over the tone token, applies
+ regardless of selection — same pattern as the former inline style). */}
+
{/* Exit batch mode */}
-
+
Exit
-
+
);
}
diff --git a/app/components/editor/BreadcrumbDropdown.tsx b/app/components/editor/BreadcrumbDropdown.tsx
index 3ab10092d..3990b3cd1 100644
--- a/app/components/editor/BreadcrumbDropdown.tsx
+++ b/app/components/editor/BreadcrumbDropdown.tsx
@@ -1,5 +1,5 @@
import { useRef, useState } from "react";
-import { Popover } from "@core/shared-ui";
+import { Button, Popover } from "@core/shared-ui";
/**
* Commercial PCA Phase U (Batch C2b) — a flat unit row as it arrives from
@@ -56,15 +56,16 @@ export function BreadcrumbDropdown({ units, activeUnitId, onSelect }: Breadcrumb
return (
<>
- setOpen((v) => !v)}
aria-haspopup="listbox"
aria-expanded={open}
aria-label={`Inspection scope: ${activeLabel}. Switch scope`}
title="Switch inspection scope"
- className="inline-flex items-center gap-1.5 h-7 px-2.5 rounded-ih-button bg-ih-bg-muted text-ih-fg-2 text-[12px] font-bold hover:bg-ih-border max-w-[200px]"
+ className="border-none bg-ih-bg-muted hover:bg-ih-border max-w-[200px]"
>
+
setOpen(false)} anchorRef={anchorRef} align="left">
diff --git a/app/components/editor/BurstCamera.tsx b/app/components/editor/BurstCamera.tsx
index 6406dd6f1..bbb51d8f4 100644
--- a/app/components/editor/BurstCamera.tsx
+++ b/app/components/editor/BurstCamera.tsx
@@ -1,5 +1,5 @@
import { useState, useRef, useCallback, useEffect } from "react";
-import { Icon } from "@core/shared-ui";
+import { Icon, IconButton, Button } from "@core/shared-ui";
interface Capture {
id: string;
@@ -129,15 +129,17 @@ export function BurstCamera({ open, onClose, onCommit }: BurstCameraProps) {
{/* Top chrome */}
{/* ds-allow: fixed-dark camera overlay chrome (light-on-dark) */}
-
+ {/* ds-allow: over-video control — stays light-on-dark regardless of theme (legible over arbitrary camera footage) */}
+
-
+
{captures.length > 0 && (
{captures.length} captured
)}
-
+ {/* ds-allow: over-video control — stays light-on-dark regardless of theme (legible over arbitrary camera footage) */}
+
-
+
Full PCA adds the ASTM E2018 transmittal, two cost tables, reviewer sign-off and photo appendix.
diff --git a/app/components/editor/CostItemsPanel.tsx b/app/components/editor/CostItemsPanel.tsx
index 2ce573785..1c6bb3371 100644
--- a/app/components/editor/CostItemsPanel.tsx
+++ b/app/components/editor/CostItemsPanel.tsx
@@ -1,4 +1,5 @@
import { useRef, useState } from "react";
+import { Button } from "@core/shared-ui";
import { formatDollars } from "~/lib/money";
import { MoneyInput } from "~/components/MoneyInput";
import { CostExportButtons } from "~/components/CostExportButtons";
@@ -188,13 +189,9 @@ export function CostItemsPanel({
{/* Export the current Opinion of Cost — only worth offering once at
least one line exists (empty exports are just a header row). */}
{rows.length > 0 && }
-
+
+ Add cost item
-
+
@@ -440,14 +437,9 @@ function CostItemRow({
)}
-
+
Remove
-
+
);
diff --git a/app/components/editor/CustomDefectForm.tsx b/app/components/editor/CustomDefectForm.tsx
index 859fde0e4..04c31fcbf 100644
--- a/app/components/editor/CustomDefectForm.tsx
+++ b/app/components/editor/CustomDefectForm.tsx
@@ -1,3 +1,4 @@
+import { Button } from "@core/shared-ui";
import type { CustomDefectCategory } from "../../lib/custom-defects";
export interface CustomDefectFormProps {
@@ -70,21 +71,21 @@ export function CustomDefectForm({
)}
-
Cancel
-
-
+
Add defect
-
+
);
diff --git a/app/components/editor/EditorHeader.tsx b/app/components/editor/EditorHeader.tsx
index 8e230f6b6..1908cac66 100644
--- a/app/components/editor/EditorHeader.tsx
+++ b/app/components/editor/EditorHeader.tsx
@@ -1,6 +1,6 @@
import type { ReactNode } from "react";
import type { useInspectionState } from "~/hooks/useInspection";
-import { Icon } from "@core/shared-ui";
+import { Button, IconButton, Icon } from "@core/shared-ui";
import type { ColorScheme } from "~/lib/ui-prefs";
import { ProgressStripText } from "~/components/editor/ProgressStripText";
import { TemplateMenu } from "~/components/editor/TemplateMenu";
@@ -191,24 +191,25 @@ export function EditorHeader({
{/* #181 — Version history (only when collab editing is enabled) */}
{collabEditing && (
- onOpenVersionHistory?.()}
- className="flex w-9 h-9 rounded-md items-center justify-center text-ih-fg-3 hover:bg-ih-bg-muted"
title="Version history"
>
-
+
)}
{/* Right zone: theme + settings + preview + preview PDF + sign now + publish */}
{/* Theme cycle: light → dark → field → auto */}
- setColorScheme(scheme === 'light' ? 'dark' : scheme === 'dark' ? 'field' : scheme === 'field' ? 'auto' : 'light')}
- className="w-9 h-9 rounded-md hidden xl:flex items-center justify-center text-ih-fg-3 hover:bg-ih-bg-muted"
+ className="hidden xl:flex"
title={`Theme: ${scheme}${scheme === 'field' ? ' (high-contrast outdoor)' : ''}`}
>
{scheme === 'dark' ? (
@@ -220,12 +221,12 @@ export function EditorHeader({
) : (
)}
-
+
{/* Settings button */}
- state.setSettingsOpen(true)}
- className="w-9 h-9 rounded-md flex items-center justify-center text-ih-fg-3 hover:bg-ih-bg-muted"
title="Inspection settings"
>
-
+
{/* Template menu [config] — global template actions consolidated here
(swap template · save as new · update source), replacing the buttons
@@ -262,63 +263,75 @@ export function EditorHeader({
{/* Preview full report — opens the whole report (all sections) in a new tab.
Owner preview works on drafts (tokenless via the report-view loader). */}
{tenantSlug && (
- window.open(`/report-view/${tenantSlug}/${state.inspection.id}`, "_blank", "noopener")}
- className="hidden 2xl:inline-flex h-9 px-3 rounded-md border border-ih-border text-[12px] font-bold text-ih-fg-2 hover:bg-ih-bg-muted items-center gap-1.5"
+ className="hidden 2xl:inline-flex"
title="Preview the full report (all sections) in a new tab"
+ icon={
+
+
+
+
+ }
>
-
-
-
-
Preview
-
+
)}
{/* Preview PDF — opens the real server-rendered PDF deliverable (the exact
client deliverable) in a new tab. Owner on-demand render works pre-publish
on drafts via the owner/JWT-authed /api/inspections/:id/pdf endpoint. */}
- window.open(`/api/inspections/${state.inspection.id}/pdf?type=full`, "_blank", "noopener")}
- className="hidden xl:inline-flex h-9 px-3 rounded-md border border-ih-border text-[12px] font-bold text-ih-fg-2 hover:bg-ih-bg-muted items-center gap-1.5"
+ className="hidden xl:inline-flex"
title="Preview the real server-rendered PDF (the exact client deliverable) in a new tab"
+ icon={
+
+
+
+ }
>
-
-
-
Preview PDF
-
+
{/* Sign now button */}
- setSignModalOpen(true)}
- className="hidden xl:inline-flex h-9 px-3 rounded-md border border-ih-border text-[12px] font-bold text-ih-fg-2 hover:bg-ih-bg-muted items-center gap-1.5"
+ className="hidden xl:inline-flex"
title="Sign this inspection now"
+ icon={}
>
-
Sign now
-
+
{/* Publish button */}
-
+
+
+ }
>
-
-
-
Publish
-
+
diff --git a/app/components/editor/FooterBar.tsx b/app/components/editor/FooterBar.tsx
index 70dc77061..701ed5748 100644
--- a/app/components/editor/FooterBar.tsx
+++ b/app/components/editor/FooterBar.tsx
@@ -1,4 +1,5 @@
import { useState } from "react";
+import { Button } from "@core/shared-ui";
import type { PresenceUser, PresenceStatus } from "~/hooks/usePresence";
interface FooterBarProps {
@@ -30,13 +31,14 @@ export function FooterBar({ connected = false, status, roster = [] }: FooterBarP
return (
)}
diff --git a/app/components/portal/sections/ReportView.tsx b/app/components/portal/sections/ReportView.tsx
index f871f3824..083674721 100644
--- a/app/components/portal/sections/ReportView.tsx
+++ b/app/components/portal/sections/ReportView.tsx
@@ -30,7 +30,6 @@ import { ReportRepairPanel } from "./report/ReportRepairPanel";
import { BuildingProfile } from "./report/BuildingProfile";
import { PcaSkeleton } from "./report/PcaSkeleton";
import { ReportToc } from "./report/ReportToc";
-import { PagedTocInjection } from "./report/PagedTocInjection";
import { PerUnitReportBlock } from "./report/PerUnitReportBlock";
import { CostTables } from "./report/CostTables";
import { WordExportButton } from "./report/WordExportButton";
@@ -131,7 +130,7 @@ export function reportViewProps(
reportTheme: data.reportTheme,
initialFilter: data.initialFilter ?? "all",
printMode: data.printMode ?? false,
- pagedToc: data.pagedToc ?? false,
+ tocPages: data.tocPages,
isPublished: data.isPublished ?? false,
signature: data.signature ?? null,
verification: data.verification ?? null,
@@ -202,10 +201,6 @@ function mediaTileKey(photo: ReportPhoto, idx: number): string {
}
}
-/* ------------------------------------------------------------------ */
-/* Gated Paged.js TOC page-number injection (Commercial PCA Phase O 7-9) */
-/* ------------------------------------------------------------------ */
-
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
@@ -517,18 +512,11 @@ export function ReportView(props: ReportViewProps) {
{/* Commercial PCA Phase O — reserved TOC slot: after the cover/header
front matter, before the PcaSkeleton body. `?? []` guards the
inline-Hub mount that may pass a partial payload during
- transition; ReportToc itself renders nothing when empty. */}
-
- {/* Commercial PCA Phase O Tasks 7–9 (GATED, opt-in). Injected ONLY when
- the render URL carried `?print=1&pagedtoc=1`. When the gate is off —
- the DEFAULT, incl. the current production generatePdfFromUrl which
- does NOT send the param — none of this renders and the report is
- byte-for-byte unchanged (no Paged.js script, no target-counter CSS,
- no re-pagination). Client layer only: this proves Paged.js fills the
- TOC page numbers via `target-counter` in a real browser. The CF-side
- readiness wait + @page/pdfOptions reconciliation are still unverified;
- see scripts/spike/pagedjs-cf-spike.md. */}
- {data.pagedToc === true && }
+ transition; ReportToc itself renders nothing when empty.
+ Task 19a — `tocPages` (undefined on the web + PDF pass 1) fills the
+ reserved page-ref slot with real page numbers on pass 2, resolved
+ server-side by extractAnchorPages against the pass-1 render. */}
+ caller).
- * When the gate is off nothing here renders and the report is byte-identical to
- * the pre-Paged.js output — so the current production PDF (which never sends the
- * param) is completely untouched.
- *
- * Readiness signal: the vendored `paged.polyfill.js` auto-runs on document-ready
- * and reads `window.PagedConfig`. We set `PagedConfig.after(done)` — Paged.js
- * invokes it AFTER the preview flow finishes re-paginating — to raise
- * `window.__pagedReady = true` and stamp `data-paged-done="1"` on . The CF
- * follow-up will wait on that signal before capturing the PDF (the spike's
- * Step-3 risk: `quickAction('pdf')` may not expose an in-page wait hook — see
- * scripts/spike/pagedjs-cf-spike.md). The inline PagedConfig script MUST
- * precede the polyfill
-
-
-
-