+
+ {editing ? (
+
- ) : null}
-
- ) : (
-
+ )}
+
+
)
}
diff --git a/src/components/campaign/shared/CampaignCellEditOverlay.tsx b/src/components/campaign/shared/CampaignCellEditOverlay.tsx
index 64ad6f50..9bb86bc6 100644
--- a/src/components/campaign/shared/CampaignCellEditOverlay.tsx
+++ b/src/components/campaign/shared/CampaignCellEditOverlay.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useLayoutEffect, useRef, useState, type ReactNode } from 'react'
+import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react'
import { createPortal } from 'react-dom'
import { CampaignHoverTooltip } from '@/components/campaign/shared/CampaignHoverTooltip'
@@ -110,6 +110,18 @@ export const CampaignCellEditOverlay = ({
const sharedSheet = useCampaignListSheet()
const wasOpenRef = useRef(open)
+ // Miss #52: without the provider every opened cell keeps its own Drawer
+ // root. The fallback below stays for prod resilience, but in dev the
+ // missing provider is called out the moment a sheet cell mounts.
+ useEffect(() => {
+ if (variant !== 'sheet' || sharedSheet) return
+ if (process.env.NODE_ENV !== 'development') return
+ console.error(
+ 'CampaignCellEditOverlay: variant="sheet" sem CampaignListSheetProvider — ' +
+ 'cada célula aberta mantém um Drawer próprio (miss #52). Envolva a lista no provider.',
+ )
+ }, [variant, sharedSheet])
+
/**
* The region is mounted from the first open onwards and never unmounted
* again — a live region has to exist BEFORE its text changes or most screen
diff --git a/tests/unit/sharedSheetHostConventions.unit.spec.ts b/tests/unit/sharedSheetHostConventions.unit.spec.ts
new file mode 100644
index 00000000..de42e059
--- /dev/null
+++ b/tests/unit/sharedSheetHostConventions.unit.spec.ts
@@ -0,0 +1,41 @@
+import { readdirSync, readFileSync } from 'node:fs'
+import { relative, resolve } from 'node:path'
+import { describe, expect, it } from 'vitest'
+
+// Miss #52 (2026-07-30): the municipalities list hydrated ~125 idle Drawer
+// roots (25 rows × 5 sheet controls) and prod tripped React #130 — CSS
+// `display:none` does not remove hydration cost. The fix pattern is ONE
+// shared Drawer per list surface via `CampaignListSheetProvider`, with cell
+// bodies portaled into it. The provider reached the municípios mobile cards
+// and this pass (2026-07-31) the three chip-relation lists (lideranças,
+// dobradinhas, assessores) — this guard keeps the next chip-cell table from
+// mounting sheet overlays providerless.
+describe('shared sheet host for chip-relation cells (miss #52)', () => {
+ const componentRoots = ['src/components', 'src/app'] as const
+ const chipCellUsage = /
{
+ const offenders: string[] = []
+
+ for (const root of componentRoots) {
+ const files = readdirSync(resolve(process.cwd(), root), {
+ recursive: true,
+ withFileTypes: true,
+ })
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.tsx'))
+ .map((entry) => resolve(entry.parentPath, entry.name))
+
+ for (const file of files) {
+ const source = readFileSync(file, 'utf8')
+ if (!chipCellUsage.test(source)) continue
+ if (source.includes('CampaignListSheetProvider')) continue
+ offenders.push(relative(process.cwd(), file))
+ }
+ }
+
+ expect(
+ offenders,
+ 'chip-relation cells open sheet overlays on coarse pointers — wrap the table in CampaignListSheetProvider (one shared Drawer per list)',
+ ).toEqual([])
+ })
+})