mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-31 19:40:54 +00:00
The desktop app and the macOS menu bar can now run in English, French, Japanese, Korean, Simplified Chinese and Traditional Chinese. A Language setting in the desktop Settings picks the language or follows the system, and the choice is shared so the menu bar switches with it, relaunching to pick up the language with a retry so a switch never leaves it down. Every user-facing string in the desktop renderer goes through a local lookup table, 1,249 keys per language, no framework and no network. The menu bar gains four .lproj to join English and Simplified Chinese, 626 keys each. A completeness gate fails the build if any language is missing a key, in both the desktop catalogs and the menu bar coverage suite. Fonts fall back per language so Japanese, Korean and both Chinese render their correct glyph forms, and dates and numbers format for the locale with values unchanged. English output is byte-identical. Strings produced by the CLI engine stay English until the CLI is localized, a separate effort. Validated live in all six languages: no missing text, no clipping, correct fonts, numbers unchanged, and the menu bar switching for every language.
86 lines
3.4 KiB
TypeScript
86 lines
3.4 KiB
TypeScript
import { useLayoutEffect, useState, type ReactNode, type RefObject } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
|
|
const EDGE = 8
|
|
const OFFSET = 6
|
|
const MAX_HEIGHT = 320
|
|
const MAX_WIDTH = 320
|
|
// Below this the menu would show barely two rows: use the space above instead.
|
|
const MIN_BELOW = 160
|
|
|
|
/**
|
|
* Menu or popover rendered into document.body and positioned against its
|
|
* trigger. The card anatomy clips its inner surface to keep the rounded
|
|
* corners, which cut off any menu drawn inside it; a portal puts every anchored
|
|
* surface above that clip. Opens under the trigger's left edge and flips above
|
|
* it when the space below is too short to be usable.
|
|
*/
|
|
export function AnchoredSurface({
|
|
anchor,
|
|
surfaceRef,
|
|
matchWidth = false,
|
|
className,
|
|
children,
|
|
...rest
|
|
}: {
|
|
anchor: RefObject<HTMLElement | null>
|
|
surfaceRef: RefObject<HTMLDivElement | null>
|
|
/** Take the trigger's width (capped), rather than sizing to the content. */
|
|
matchWidth?: boolean
|
|
className?: string
|
|
children: ReactNode
|
|
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'children' | 'className'>) {
|
|
const [box, setBox] = useState<React.CSSProperties | null>(null)
|
|
|
|
useLayoutEffect(() => {
|
|
// The surface's natural width, captured once before we constrain it. The
|
|
// selected row is bold (plus an icon and padding), so it is wider than the
|
|
// trigger measured at regular weight; matching only the trigger would clip
|
|
// it. Read before the first setBox, so it reflects unconstrained content.
|
|
let contentWidth = 0
|
|
const place = () => {
|
|
const trigger = anchor.current
|
|
const surface = surfaceRef.current
|
|
if (!trigger || !surface) return
|
|
if (contentWidth === 0) contentWidth = Math.ceil(surface.getBoundingClientRect().width)
|
|
const rect = trigger.getBoundingClientRect()
|
|
const below = window.innerHeight - rect.bottom - OFFSET - EDGE
|
|
const above = rect.top - OFFSET - EDGE
|
|
const flip = below < MIN_BELOW && above > below
|
|
const room = Math.max(0, flip ? above : below)
|
|
const maxHeight = Math.min(MAX_HEIGHT, room)
|
|
const width = matchWidth ? Math.min(Math.max(rect.width, 160, contentWidth), MAX_WIDTH) : surface.offsetWidth
|
|
setBox({
|
|
position: 'fixed',
|
|
top: flip ? Math.max(EDGE, rect.top - OFFSET - Math.min(surface.offsetHeight, maxHeight)) : rect.bottom + OFFSET,
|
|
left: Math.max(EDGE, Math.min(rect.left, window.innerWidth - width - EDGE)),
|
|
maxHeight,
|
|
// `.pop-menu` is content-box, so an inline width renders 10px wider
|
|
// than the figure the clamp above used. Border-box makes them agree and
|
|
// keeps the edge gutter intact.
|
|
...(matchWidth ? { width, boxSizing: 'border-box' as const } : {}),
|
|
})
|
|
}
|
|
place()
|
|
window.addEventListener('resize', place)
|
|
window.addEventListener('scroll', place, true)
|
|
return () => {
|
|
window.removeEventListener('resize', place)
|
|
window.removeEventListener('scroll', place, true)
|
|
}
|
|
}, [anchor, matchWidth, surfaceRef])
|
|
|
|
return createPortal(
|
|
<div
|
|
{...rest}
|
|
ref={surfaceRef}
|
|
className={className}
|
|
// Measured on the first layout pass; keep it off-screen until then so the
|
|
// unplaced frame never flashes at the top-left corner.
|
|
style={box ?? { position: 'fixed', top: -9999, left: -9999, maxHeight: MAX_HEIGHT }}
|
|
>
|
|
{children}
|
|
</div>,
|
|
document.body,
|
|
)
|
|
}
|