Files
dsh-context/src/client/components/dateRange.tsx
T
bowenliang123 7cb0a9fa46 refactor(comments): trim client comments to the value the code cannot carry
Apply the repo's comments guideline to src/client/**: delete comments that
restate JSX, Tailwind utilities or standard React behaviour, narrate history,
duplicate docs/, or describe what the next line does; keep only container-query
and containment traps, portal requirements, reference-stability rules with a
real re-render consequence, CSS<->TS constant parities, harness seat and payload
contracts. Align the page's name with the shipped UI label ("Context Insights",
not "Context Dashboard"), unify drifted phrasings, drop the last non-English
fragments, and rewrap survivors onto fewer, fuller lines within the 140-col limit.

Comment lines 5,183 -> 1,822. No code change: comment-stripped content is
identical per file; the client bundle differs only by dead Tailwind utilities
that existed because deleted comments contained those words.
2026-10-10 00:26:03 +08:00

251 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The Insights page's date-range picker: one pill carries both ends of the LIVE window, so the picker and the preset
* group never disagree. Two clicks make the range in any order — the pair is SORTED, so every reader downstream (the
* scope window, the ledger-day predicate, the chip) only ever sees `from <= to`.
*
* The panel portals to <body> and fixes off its trigger's rect: the page is the `lc-ov` query container, whose scroller
* would clip an in-tree popover. Any scroll, resize, outside press, or Escape closes it rather than float a detached anchor.
*/
import { useEffect, useLayoutEffect, useRef, useState, type ReactElement } from 'react'
import { createPortal } from 'react-dom'
import { MONTH_KEY_RE, monthGridOf, shiftDayKey, shiftMonthKey } from '../../shared/days'
import { orderedDayRange, type DayRange } from '../overview'
import type { ViewKit } from '../viewkit'
import { useEscapeClose } from './escapeClose'
import { IconChevronDown } from '../primitives'
export interface DateRangeProps {
/** The scope's day span: the picked range, else the preset's own window read as days (its open end is today);
* null when the scope is unbounded. */
value: DayRange | null
picked: boolean
onChange: (range: DayRange | null) => void
today: string
locale: string
}
/** Trigger-to-panel distance and the viewport margin the panel clamps inside, in px. */
const GAP = 6
const MARGIN = 8
const CELL_ID = 'lc-dp-day'
function dayStepOf(key: string): number | null {
if (key === 'ArrowLeft') return -1
if (key === 'ArrowRight') return 1
if (key === 'ArrowUp') return -7
if (key === 'ArrowDown') return 7
return null
}
export function monthLabelOf(month: string, locale: string): string {
if (!MONTH_KEY_RE.test(month)) return month
const date = new Date(Number(month.slice(0, 4)), Number(month.slice(5, 7)) - 1, 1)
return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'long' }).format(date)
}
export function makeDateRange(kit: ViewKit): (props: DateRangeProps) => ReactElement {
const { t } = kit
return function DateRange(props: DateRangeProps): ReactElement {
const [open, setOpen] = useState(false)
const [month, setMonth] = useState('')
const [cursor, setCursor] = useState('')
const [anchor, setAnchor] = useState<string | null>(null)
const [hover, setHover] = useState<string | null>(null)
const [spot, setSpot] = useState<{ left: number; top: number } | null>(null)
const rootRef = useRef<HTMLDivElement | null>(null)
const panelRef = useRef<HTMLDivElement | null>(null)
const close = (): void => { setOpen(false) }
useEscapeClose(open, close)
// Outside press closes; scroll and resize retract the panel instead of leaving it off its moved anchor.
useEffect(() => {
if (!open) return undefined
const onDown = (ev: MouseEvent): void => {
const target = ev.target
if (!(target instanceof Node)) return
if (rootRef.current?.contains(target) === true || panelRef.current?.contains(target) === true) return
close()
}
window.addEventListener('mousedown', onDown)
window.addEventListener('scroll', close, true)
window.addEventListener('resize', close)
return () => {
window.removeEventListener('mousedown', onDown)
window.removeEventListener('scroll', close, true)
window.removeEventListener('resize', close)
}
}, [open])
// Re-seat the panel after every commit; the comparison makes a re-measure landing on the same spot a no-op, so the
// every-render measurement cannot loop. Object.is, not ===: an unmeasurable anchor reads NaN, and NaN === NaN is
// false — every re-measure would then mint a fresh object and spin the tree.
useLayoutEffect(() => {
const anchorEl = rootRef.current
const panelEl = panelRef.current
if (!open || anchorEl === null || panelEl === null) return
const rect = anchorEl.getBoundingClientRect()
const left = Math.max(MARGIN, Math.min(rect.right - panelEl.offsetWidth, window.innerWidth - panelEl.offsetWidth - MARGIN))
const below = rect.bottom + GAP
const top = below + panelEl.offsetHeight > window.innerHeight - MARGIN
? Math.max(MARGIN, rect.top - panelEl.offsetHeight - GAP)
: below
setSpot(current => current !== null && Object.is(current.left, left) && Object.is(current.top, top) ? current : { left, top })
})
const draft = anchor === null ? props.value : orderedDayRange(anchor, hover ?? anchor)
const grid = open ? monthGridOf(month) : null
const show = (): void => {
const home = props.today.slice(0, 7)
setMonth(home)
setCursor(`${home}-01`)
setAnchor(null)
setHover(null)
setOpen(true)
}
const pick = (key: string): void => {
if (anchor === null) {
setAnchor(key)
setHover(null)
return
}
const next = orderedDayRange(anchor, key)
setAnchor(null)
setHover(null)
setOpen(false)
props.onChange(next)
}
const stepMonth = (delta: number): void => {
const next = shiftMonthKey(month, delta)
if (next === null) return
setMonth(next)
setCursor(`${next}-01`)
}
const onKeyDown = (ev: React.KeyboardEvent): void => {
if (ev.key === 'Enter' || ev.key === ' ') {
ev.preventDefault()
pick(cursor)
return
}
const step = dayStepOf(ev.key)
if (step === null) return
ev.preventDefault()
const next = shiftDayKey(cursor, step)
if (next !== null && next <= props.today && next.slice(0, 7) === month) setCursor(next)
}
const shown = spot !== null && open ? spot : null
// A first click with no hover yet has chosen only the start, so the end keeps its placeholder rather than echoing it.
const from = draft?.from
const to = anchor !== null && hover === null ? undefined : draft?.to
return (
<div className="lc-dp" ref={rootRef}>
<button
type="button"
className={'lc-dp-trigger' + (props.picked ? ' lc-dp-trigger-on' : '')}
aria-haspopup="dialog"
aria-expanded={grid !== null}
aria-label={t('ov.date.open')}
onClick={() => { if (open) { close() } else { show() } }}
>
<span className={'lc-dp-half' + (from === undefined ? ' lc-dp-half-empty' : '')}>{from ?? t('ov.date.from')}</span>
<span className="lc-dp-sep">→</span>
<span className={'lc-dp-half' + (to === undefined ? ' lc-dp-half-empty' : '')}>{to ?? t('ov.date.to')}</span>
<IconChevronDown size={12} />
</button>
{grid === null ? null : createPortal(
<div
ref={panelRef}
className="lc-tip lc-dp-panel"
role="dialog"
aria-label={t('ov.date.label')}
style={shown === null
? { left: 0, top: 0, visibility: 'hidden' }
: { left: shown.left, top: shown.top }}
>
<div className="lc-dp-nav">
<button type="button" className="lc-dp-navbtn" aria-label={t('ov.date.prev')} onClick={() => { stepMonth(-1) }}>‹</button>
<span className="lc-dp-month">{monthLabelOf(month, props.locale)}</span>
<button
type="button"
className="lc-dp-navbtn"
aria-label={t('ov.date.next')}
disabled={month >= props.today.slice(0, 7)}
onClick={() => { stepMonth(1) }}
>›</button>
</div>
<div className="lc-dp-wds" aria-hidden="true">
{[0, 1, 2, 3, 4, 5, 6].map(d => <span key={d} className="lc-dp-wd">{t(`ov.date.wd.${String(d)}`)}</span>)}
</div>
<div
className="lc-dp-grid"
role="grid"
tabIndex={0}
aria-label={t('ov.date.pick')}
aria-activedescendant={CELL_ID}
onKeyDown={onKeyDown}
>
{grid.map((week, w) => (
<div className="lc-dp-week" role="row" key={w}>
{week.map((key, d) => {
if (key === null) return <span className="lc-dp-cell lc-dp-blank" key={d} />
const future = key > props.today
const edge = draft !== null && (key === draft.from || key === draft.to)
const inRange = draft !== null && key >= draft.from && key <= draft.to
const className = 'lc-dp-cell'
+ (inRange ? ' lc-dp-in' : '')
+ (edge ? ' lc-dp-edge' : '')
+ (key === cursor ? ' lc-dp-cursor' : '')
+ (key === props.today ? ' lc-dp-today' : '')
+ (future ? ' lc-dp-future' : '')
const day = String(Number(key.slice(8, 10)))
if (future) {
// The keyboard cursor never lands here, so this cell carries no activedescendant id.
return (
<span className={className} role="gridcell" aria-disabled="true" aria-label={key} key={d}>
{day}
</span>
)
}
return (
<span
className={className}
role="gridcell"
id={key === cursor ? CELL_ID : undefined}
aria-label={key}
aria-selected={edge}
key={d}
onMouseEnter={() => { if (anchor !== null) setHover(key) }}
onClick={() => { pick(key) }}
>
{day}
</span>
)
})}
</div>
))}
</div>
<div className="lc-dp-foot">
<span className="lc-dp-hint">{anchor === null ? t('ov.date.hint') : t('ov.date.hintEnd')}</span>
{props.picked && (
<button
type="button"
className="lc-dp-clear"
onClick={() => { props.onChange(null) }}
>{t('ov.date.clear')}</button>
)}
</div>
</div>,
document.body,
)}
</div>
)
}
}