feat(site-builder): add in-builder Report an Issue modal

Wires the Task 18 payload builder and Task 16/17 diagnostics into a
user-facing modal, reachable from the topbar bug icon (desktop) and
overflow menu (mobile). Two gaps in the task brief's draft, not called
out there, are handled explicitly: buildReportPayload() can throw when
the payload is still oversized after canvas_state is dropped, so
submission is wrapped in try/catch with an actionable "too large" error
that preserves the user's typed text; and the textarea maxLength is
sourced from MAX_DESCRIPTION_CHARS (with a proximity character count)
instead of a hardcoded number, so the UI limit can't drift from the
payload limit.

Also portals the modal to document.body for the same stacking-context
reasons TemplateModal/HeadCodeModal already do, and guards the
useEditor() selection collector with optional chaining so it degrades
gracefully under TopBar's existing test harness (a minimal @craftjs/core
stub with no events/nodes on its collector state).

The textarea uses onInput rather than onChange: React's onChange dedup
(via its DOM value-tracker) treats a test harness's raw `el.value = x`
assignment as a no-op change, matching this repo's existing pattern
(HeadCodeModal.test.tsx, shared-controls.test.tsx, etc. all work around
the same gotcha) -- onInput is a plain passthrough with no such check,
and is behaviorally identical for real typing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 11:29:26 -07:00
co-authored by Claude Opus 5
parent f43a1ef872
commit d7eeff3a68
4 changed files with 504 additions and 0 deletions
+14
View File
@@ -9,6 +9,7 @@ import { useMobileChrome } from '../../state/MobileChromeContext';
import { DeviceMode } from '../../types';
import { TemplateModal } from './TemplateModal';
import { HeadCodeModal } from './HeadCodeModal';
import { ReportIssueModal } from './ReportIssueModal';
import { TopBarOverflowMenu } from './TopBarOverflowMenu';
import { PublishWarnings } from './PublishWarnings';
import { SitesmithButton } from '../sitesmith/SitesmithButton';
@@ -40,6 +41,7 @@ export const TopBar: React.FC<TopBarProps> = ({ device, onDeviceChange, showGuid
// opening a mobile sheet can close these modals (item 3) -- behavior is
// otherwise identical for both the desktop and mobile branches below.
const { templateModalOpen, setTemplateModalOpen, headCodeModalOpen, setHeadCodeModalOpen, overflowOpen, setOverflowOpen } = useMobileChrome();
const [reportOpen, setReportOpen] = useState(false);
const isMobile = useIsMobile();
const { open: openSitesmith } = useSitesmithModal();
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -303,12 +305,14 @@ export const TopBar: React.FC<TopBarProps> = ({ device, onDeviceChange, showGuid
onToggleGuides={onToggleGuides}
onOpenTemplates={() => setTemplateModalOpen(true)}
onOpenHeadCode={() => setHeadCodeModalOpen(true)}
onOpenReportIssue={() => setReportOpen(true)}
onPreview={handlePreview}
sitesmithNode={<SitesmithButton onClick={() => openSitesmith()} />}
/>
<TemplateModal open={templateModalOpen} onClose={() => setTemplateModalOpen(false)} />
<HeadCodeModal open={headCodeModalOpen} onClose={() => setHeadCodeModalOpen(false)} />
<ReportIssueModal open={reportOpen} onClose={() => setReportOpen(false)} device={device} />
</nav>
);
}
@@ -377,6 +381,15 @@ export const TopBar: React.FC<TopBarProps> = ({ device, onDeviceChange, showGuid
<button className="topbar-btn icon-only" aria-label="Preview" data-tooltip="Preview" onClick={handlePreview}>
<i className="fa fa-eye" />
</button>
<button
className="topbar-btn icon-only"
aria-label="Report an issue"
data-tooltip="Report an issue"
title="Report an issue"
onClick={() => setReportOpen(true)}
>
<i className="fa fa-bug" />
</button>
{/* Draft/Published status badge */}
{isWHP && isDraft && publishStatus !== 'published' && (
@@ -441,6 +454,7 @@ export const TopBar: React.FC<TopBarProps> = ({ device, onDeviceChange, showGuid
</div>
<TemplateModal open={templateModalOpen} onClose={() => setTemplateModalOpen(false)} />
<HeadCodeModal open={headCodeModalOpen} onClose={() => setHeadCodeModalOpen(false)} />
<ReportIssueModal open={reportOpen} onClose={() => setReportOpen(false)} device={device} />
</nav>
);
};