Skip to content

Commit fdb58f8

Browse files
fix: tabs in blogs & hydration error fixes (#832)
* fix: tabs in blog posts * fix: hydration errors * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent e9aa470 commit fdb58f8

File tree

7 files changed

+164
-76
lines changed

7 files changed

+164
-76
lines changed

src/components/DocFeedbackProvider.tsx

Lines changed: 65 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export function DocFeedbackProvider({
4646
const [blockMarkdowns, setBlockMarkdowns] = React.useState<
4747
Map<string, string>
4848
>(new Map())
49+
const [blockElements, setBlockElements] = React.useState<
50+
Map<string, HTMLElement>
51+
>(new Map())
4952
const [hoveredBlockId, setHoveredBlockId] = React.useState<string | null>(
5053
null,
5154
)
@@ -87,15 +90,28 @@ export function DocFeedbackProvider({
8790
const selectorMap = new Map<string, string>()
8891
const hashMap = new Map<string, string>()
8992
const markdownMap = new Map<string, string>()
93+
const elementMap = new Map<string, HTMLElement>()
9094
const listeners = new Map<
9195
HTMLElement,
9296
{ enter: (e: MouseEvent) => void; leave: (e: MouseEvent) => void }
9397
>()
98+
const findClosestBlock = (target: HTMLElement): HTMLElement | null => {
99+
let closestBlock: HTMLElement | null = null
100+
101+
for (const candidate of blocks) {
102+
if (!candidate.contains(target)) continue
103+
if (!closestBlock || closestBlock.contains(candidate)) {
104+
closestBlock = candidate
105+
}
106+
}
107+
108+
return closestBlock
109+
}
94110

95111
Promise.all(
96112
blocks.map(async (block, index) => {
97113
const blockId = `block-${index}`
98-
block.setAttribute('data-block-id', blockId)
114+
elementMap.set(blockId, block)
99115

100116
const identifier = await getBlockIdentifier(block)
101117
selectorMap.set(blockId, identifier.selector)
@@ -106,8 +122,10 @@ export function DocFeedbackProvider({
106122
const handleMouseEnter = (e: MouseEvent) => {
107123
// Only handle hover if this is the most specific block being hovered
108124
// (prevent parent blocks from showing hover when child blocks are hovered)
109-
const target = e.target as HTMLElement
110-
const closestBlock = target.closest('[data-block-id]')
125+
const target = e.target
126+
if (!(target instanceof HTMLElement)) return
127+
128+
const closestBlock = findClosestBlock(target)
111129
if (closestBlock === block) {
112130
setHoveredBlockId(blockId)
113131
block.style.backgroundColor = 'rgba(59, 130, 246, 0.05)' // blue with low opacity
@@ -118,10 +136,15 @@ export function DocFeedbackProvider({
118136
const handleMouseLeave = (e: MouseEvent) => {
119137
// Only clear hover if we're actually leaving this block
120138
// (not just entering a child element)
121-
const relatedTarget = e.relatedTarget as HTMLElement
139+
const relatedTarget =
140+
e.relatedTarget instanceof HTMLElement ? e.relatedTarget : null
141+
const closestBlock = relatedTarget
142+
? findClosestBlock(relatedTarget)
143+
: null
122144
if (
145+
!relatedTarget ||
123146
!block.contains(relatedTarget) ||
124-
relatedTarget?.closest('[data-block-id]') !== block
147+
closestBlock !== block
125148
) {
126149
setHoveredBlockId((current) =>
127150
current === blockId ? null : current,
@@ -147,13 +170,14 @@ export function DocFeedbackProvider({
147170
setBlockSelectors(new Map(selectorMap))
148171
setBlockContentHashes(new Map(hashMap))
149172
setBlockMarkdowns(new Map(markdownMap))
173+
setBlockElements(new Map(elementMap))
150174

151175
// Visual indicators will be updated by the separate effect below
152176
})
153177

154178
return () => {
179+
setBlockElements(new Map())
155180
blocks.forEach((block) => {
156-
block.removeAttribute('data-block-id')
157181
block.style.backgroundColor = ''
158182
block.style.borderRight = ''
159183
block.style.paddingRight = ''
@@ -173,9 +197,7 @@ export function DocFeedbackProvider({
173197
if (!user || blockSelectors.size === 0) return
174198

175199
blockSelectors.forEach((selector, blockId) => {
176-
const block = document.querySelector(
177-
`[data-block-id="${blockId}"]`,
178-
) as HTMLElement
200+
const block = blockElements.get(blockId)
179201
if (!block) return
180202

181203
const hasNote = userNotes.some((n) => n.blockSelector === selector)
@@ -196,7 +218,7 @@ export function DocFeedbackProvider({
196218
block.style.paddingRight = ''
197219
}
198220
})
199-
}, [user, userNotes, userImprovements, blockSelectors])
221+
}, [user, userNotes, userImprovements, blockSelectors, blockElements])
200222

201223
const handleCloseCreating = React.useCallback(() => {
202224
setCreatingState(null)
@@ -250,6 +272,8 @@ export function DocFeedbackProvider({
250272
{Array.from(blockSelectors.keys()).map((blockId) => {
251273
const selector = blockSelectors.get(blockId)
252274
if (!selector) return null
275+
const block = blockElements.get(blockId)
276+
if (!block) return null
253277

254278
// Check if this block has a note or improvement (only for logged-in users)
255279
const note = user
@@ -264,6 +288,7 @@ export function DocFeedbackProvider({
264288
<BlockButton
265289
key={blockId}
266290
blockId={blockId}
291+
block={block}
267292
isHovered={isHovered}
268293
hasNote={!!note}
269294
hasImprovement={!!improvement}
@@ -286,8 +311,10 @@ export function DocFeedbackProvider({
286311
)?.[0]
287312

288313
if (!blockId) return null
314+
const block = blockElements.get(blockId)
315+
if (!block) return null
289316

290-
return <NotePortal key={note.id} blockId={blockId} note={note} />
317+
return <NotePortal key={note.id} block={block} note={note} />
291318
})}
292319

293320
{/* Render improvements inline */}
@@ -298,20 +325,19 @@ export function DocFeedbackProvider({
298325
)?.[0]
299326

300327
if (!blockId) return null
328+
const block = blockElements.get(blockId)
329+
if (!block) return null
301330

302331
return (
303-
<NotePortal
304-
key={improvement.id}
305-
blockId={blockId}
306-
note={improvement}
307-
/>
332+
<NotePortal key={improvement.id} block={block} note={improvement} />
308333
)
309334
})}
310335

311336
{/* Render creating interface */}
312337
{creatingState && (
313338
<CreatingFeedbackPortal
314339
blockId={creatingState.blockId}
340+
block={blockElements.get(creatingState.blockId)}
315341
type={creatingState.type}
316342
blockSelector={blockSelectors.get(creatingState.blockId) || ''}
317343
blockContentHash={blockContentHashes.get(creatingState.blockId) || ''}
@@ -329,6 +355,7 @@ export function DocFeedbackProvider({
329355
// Component to render floating button for a block
330356
function BlockButton({
331357
blockId,
358+
block,
332359
isHovered,
333360
hasNote,
334361
hasImprovement,
@@ -339,6 +366,7 @@ function BlockButton({
339366
onShowNote,
340367
}: {
341368
blockId: string
369+
block: HTMLElement
342370
isHovered: boolean
343371
hasNote: boolean
344372
hasImprovement: boolean
@@ -356,12 +384,6 @@ function BlockButton({
356384

357385
if (!mounted) return null
358386

359-
// Find the block element
360-
const block = document.querySelector(
361-
`[data-block-id="${blockId}"]`,
362-
) as HTMLElement
363-
if (!block) return null
364-
365387
// Don't show button if block is inside an editor or note portal
366388
if (block.closest('[data-editor-portal], [data-note-portal]')) return null
367389

@@ -372,9 +394,9 @@ function BlockButton({
372394
if (!isHovered && !isMenuOpen) return null
373395

374396
// Create portal container for button positioned at top-right of block
375-
let portalContainer = document.querySelector(
397+
let portalContainer = block.querySelector<HTMLElement>(
376398
`[data-button-portal="${blockId}"]`,
377-
) as HTMLElement
399+
)
378400

379401
if (!portalContainer) {
380402
portalContainer = document.createElement('div')
@@ -407,7 +429,13 @@ function BlockButton({
407429
}
408430

409431
// Component to render note after a block
410-
function NotePortal({ blockId, note }: { blockId: string; note: DocFeedback }) {
432+
function NotePortal({
433+
block,
434+
note,
435+
}: {
436+
block: HTMLElement
437+
note: DocFeedback
438+
}) {
411439
const [mounted, setMounted] = React.useState(false)
412440

413441
React.useEffect(() => {
@@ -416,24 +444,21 @@ function NotePortal({ blockId, note }: { blockId: string; note: DocFeedback }) {
416444

417445
if (!mounted) return null
418446

419-
// Find the block element
420-
const block = document.querySelector(`[data-block-id="${blockId}"]`)
421-
if (!block) return null
422-
423447
// Don't show note if block is inside an editor portal
424448
if (block.closest('[data-editor-portal]')) return null
425449

426450
// Find the actual insertion point - if block is inside an anchor-heading, insert after the anchor
427-
let insertionPoint = block as HTMLElement
451+
let insertionPoint = block
428452
const anchorParent = block.parentElement
429453
if (anchorParent?.classList.contains('anchor-heading')) {
430454
insertionPoint = anchorParent
431455
}
432456

433457
// Create portal container after the insertion point
434-
let portalContainer = insertionPoint.parentElement?.querySelector(
435-
`[data-note-portal="${note.id}"]`,
436-
) as HTMLElement
458+
let portalContainer =
459+
insertionPoint.parentElement?.querySelector<HTMLElement>(
460+
`[data-note-portal="${note.id}"]`,
461+
)
437462

438463
if (!portalContainer) {
439464
portalContainer = document.createElement('div')
@@ -454,6 +479,7 @@ function NotePortal({ blockId, note }: { blockId: string; note: DocFeedback }) {
454479
// Component to render creating feedback interface after a block
455480
function CreatingFeedbackPortal({
456481
blockId,
482+
block,
457483
type,
458484
blockSelector,
459485
blockContentHash,
@@ -464,6 +490,7 @@ function CreatingFeedbackPortal({
464490
onClose,
465491
}: {
466492
blockId: string
493+
block?: HTMLElement
467494
type: 'note' | 'improvement'
468495
blockSelector: string
469496
blockContentHash?: string
@@ -480,22 +507,20 @@ function CreatingFeedbackPortal({
480507
}, [])
481508

482509
if (!mounted) return null
483-
484-
// Find the block element
485-
const block = document.querySelector(`[data-block-id="${blockId}"]`)
486510
if (!block) return null
487511

488512
// Find the actual insertion point - if block is inside an anchor-heading, insert after the anchor
489-
let insertionPoint = block as HTMLElement
513+
let insertionPoint = block
490514
const anchorParent = block.parentElement
491515
if (anchorParent?.classList.contains('anchor-heading')) {
492516
insertionPoint = anchorParent
493517
}
494518

495519
// Create portal container after the insertion point
496-
let portalContainer = insertionPoint.parentElement?.querySelector(
497-
`[data-creating-portal="${blockId}"]`,
498-
) as HTMLElement
520+
let portalContainer =
521+
insertionPoint.parentElement?.querySelector<HTMLElement>(
522+
`[data-creating-portal="${blockId}"]`,
523+
)
499524

500525
if (!portalContainer) {
501526
portalContainer = document.createElement('div')

src/components/ThemeProvider.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,19 @@ const ThemeContext = createContext<ThemeContextProps | undefined>(undefined)
7878
type ThemeProviderProps = {
7979
children: ReactNode
8080
}
81-
const getResolvedThemeFromDOM = createIsomorphicFn()
82-
.server((): ResolvedTheme => 'light')
83-
.client((): ResolvedTheme => {
84-
return document.documentElement.classList.contains('dark')
85-
? 'dark'
86-
: 'light'
87-
})
8881

8982
export function ThemeProvider({ children }: ThemeProviderProps) {
90-
const [themeMode, setThemeMode] = useState<ThemeMode>(getStoredThemeMode)
91-
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(
92-
getResolvedThemeFromDOM,
93-
)
83+
const [themeMode, setThemeMode] = useState<ThemeMode>('auto')
84+
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>('light')
85+
86+
useEffect(() => {
87+
const storedThemeMode = getStoredThemeMode()
88+
setThemeMode(storedThemeMode)
89+
updateThemeClass(storedThemeMode)
90+
setResolvedTheme(
91+
storedThemeMode === 'auto' ? getSystemTheme() : storedThemeMode,
92+
)
93+
}, [])
9494

9595
// Listen for system theme changes when in auto mode
9696
useEffect(() => {

src/components/markdown/MdComponents.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type MdCommentComponentProps = {
2727
'data-component'?: string
2828
'data-files-meta'?: string
2929
'data-package-manager-meta'?: string
30+
preserveTabPanels?: boolean
3031
children?: React.ReactNode
3132
}
3233

@@ -48,11 +49,25 @@ function isMdFrameworkPanelElement(
4849
)
4950
}
5051

52+
function renderPanelChildren(
53+
panels: Array<React.ReactElement<MdTabPanelProps>>,
54+
preserveTabPanels: boolean,
55+
) {
56+
return panels.map((panel, index) => {
57+
if (!preserveTabPanels) {
58+
return panel.props.children
59+
}
60+
61+
return <React.Fragment key={index}>{panel.props.children}</React.Fragment>
62+
})
63+
}
64+
5165
export function MdCommentComponent({
5266
'data-attributes': rawAttributes,
5367
'data-component': componentName,
5468
'data-files-meta': filesMeta,
5569
'data-package-manager-meta': packageManagerMeta,
70+
preserveTabPanels = false,
5671
children,
5772
}: MdCommentComponentProps) {
5873
const parsedAttributes = parseJson(rawAttributes)
@@ -122,7 +137,7 @@ export function MdCommentComponent({
122137

123138
return (
124139
<FileTabs tabs={tabs}>
125-
{panels.map((panel) => panel.props.children)}
140+
{renderPanelChildren(panels, preserveTabPanels)}
126141
</FileTabs>
127142
)
128143
}
@@ -137,7 +152,7 @@ export function MdCommentComponent({
137152
}
138153

139154
return (
140-
<Tabs tabs={tabs}>{panels.map((panel) => panel.props.children)}</Tabs>
155+
<Tabs tabs={tabs}>{renderPanelChildren(panels, preserveTabPanels)}</Tabs>
141156
)
142157
}
143158

src/styles/app.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,19 @@ button {
195195
@apply opacity-50;
196196
}
197197

198+
.anchor-heading-link {
199+
text-decoration: none !important;
200+
@apply ml-2 inline-block opacity-0 transition duration-100;
201+
}
202+
203+
:hover > .anchor-heading-link {
204+
@apply opacity-50;
205+
}
206+
207+
.anchor-heading-link:focus {
208+
@apply opacity-75;
209+
}
210+
198211
:has(+ .anchor-heading) {
199212
margin-bottom: 0 !important;
200213
}

0 commit comments

Comments
 (0)