Compare commits
2 Commits
9240acddb6
...
ba6028c2ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba6028c2ea | ||
|
|
93776944b9 |
@@ -83,6 +83,20 @@ export function BarChart({
|
|||||||
setTooltip({ x: mx, y: my, label: catLabel, values })
|
setTooltip({ x: mx, y: my, label: catLabel, values })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showBarTooltip(e: React.MouseEvent<SVGRectElement>, cat: string) {
|
||||||
|
const rect = e.currentTarget.closest('svg')!.getBoundingClientRect()
|
||||||
|
handleMouseEnter(
|
||||||
|
cat,
|
||||||
|
e.clientX - rect.left,
|
||||||
|
e.clientY - rect.top,
|
||||||
|
series.map((ss, ssi) => ({
|
||||||
|
series: ss.label,
|
||||||
|
value: ss.data.find((d) => d.x === cat)?.y ?? 0,
|
||||||
|
color: ss.color ?? CHART_COLORS[ssi % CHART_COLORS.length],
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.wrapper} ${className ?? ''}`}>
|
<div className={`${styles.wrapper} ${className ?? ''}`}>
|
||||||
{yLabel && <div className={styles.yLabel}>{yLabel}</div>}
|
{yLabel && <div className={styles.yLabel}>{yLabel}</div>}
|
||||||
@@ -138,19 +152,7 @@ export function BarChart({
|
|||||||
height={barH}
|
height={barH}
|
||||||
fill={color}
|
fill={color}
|
||||||
className={styles.bar}
|
className={styles.bar}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => showBarTooltip(e, cat)}
|
||||||
const rect = e.currentTarget.closest('svg')!.getBoundingClientRect()
|
|
||||||
handleMouseEnter(
|
|
||||||
cat,
|
|
||||||
e.clientX - rect.left,
|
|
||||||
e.clientY - rect.top,
|
|
||||||
series.map((ss, ssi) => ({
|
|
||||||
series: ss.label,
|
|
||||||
value: ss.data.find((d) => d.x === cat)?.y ?? 0,
|
|
||||||
color: ss.color ?? CHART_COLORS[ssi % CHART_COLORS.length],
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -184,19 +186,7 @@ export function BarChart({
|
|||||||
height={barH}
|
height={barH}
|
||||||
fill={color}
|
fill={color}
|
||||||
className={styles.bar}
|
className={styles.bar}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => showBarTooltip(e, cat)}
|
||||||
const svgEl = e.currentTarget.closest('svg')!.getBoundingClientRect()
|
|
||||||
handleMouseEnter(
|
|
||||||
cat,
|
|
||||||
e.clientX - svgEl.left,
|
|
||||||
e.clientY - svgEl.top,
|
|
||||||
series.map((ss, ssi) => ({
|
|
||||||
series: ss.label,
|
|
||||||
value: ss.data.find((d) => d.x === cat)?.y ?? 0,
|
|
||||||
color: ss.color ?? CHART_COLORS[ssi % CHART_COLORS.length],
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -186,6 +186,11 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
|
|||||||
setScopeFilters((prev) => prev.filter((_, i) => i !== idx))
|
setScopeFilters((prev) => prev.filter((_, i) => i !== idx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleExpanded(e: React.MouseEvent, id: string) {
|
||||||
|
e.stopPropagation()
|
||||||
|
setExpandedId((prev) => (prev === id ? null : id))
|
||||||
|
}
|
||||||
|
|
||||||
if (!open) return null
|
if (!open) return null
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
@@ -328,10 +333,7 @@ export function CommandPalette({ open, onClose, onSelect, data, onOpen, onQueryC
|
|||||||
{result.expandedContent && (
|
{result.expandedContent && (
|
||||||
<button
|
<button
|
||||||
className={styles.expandBtn}
|
className={styles.expandBtn}
|
||||||
onClick={(e) => {
|
onClick={(e) => toggleExpanded(e, result.id)}
|
||||||
e.stopPropagation()
|
|
||||||
setExpandedId((prev) => (prev === result.id ? null : result.id))
|
|
||||||
}}
|
|
||||||
aria-expanded={isExpanded}
|
aria-expanded={isExpanded}
|
||||||
aria-label="Toggle detail"
|
aria-label="Toggle detail"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
|||||||
const [toasts, setToasts] = useState<ToastItem[]>([])
|
const [toasts, setToasts] = useState<ToastItem[]>([])
|
||||||
const timersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map())
|
const timersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map())
|
||||||
|
|
||||||
|
const removeToast = useCallback((id: string) => {
|
||||||
|
setToasts((prev) => prev.filter((t) => t.id !== id))
|
||||||
|
}, [])
|
||||||
|
|
||||||
const dismiss = useCallback((id: string) => {
|
const dismiss = useCallback((id: string) => {
|
||||||
// Clear auto-dismiss timer if running
|
// Clear auto-dismiss timer if running
|
||||||
const timer = timersRef.current.get(id)
|
const timer = timersRef.current.get(id)
|
||||||
@@ -71,10 +75,8 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Remove after animation completes
|
// Remove after animation completes
|
||||||
setTimeout(() => {
|
setTimeout(() => removeToast(id), EXIT_ANIMATION_MS)
|
||||||
setToasts((prev) => prev.filter((t) => t.id !== id))
|
}, [removeToast])
|
||||||
}, EXIT_ANIMATION_MS)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const toast = useCallback(
|
const toast = useCallback(
|
||||||
(options: ToastOptions): string => {
|
(options: ToastOptions): string => {
|
||||||
|
|||||||
@@ -31,6 +31,52 @@ function flattenVisibleNodes(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Keyboard nav helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function handleArrowDown(visibleNodes: FlatNode[], currentIndex: number, focusNode: (id: string) => void) {
|
||||||
|
const next = visibleNodes[currentIndex + 1]
|
||||||
|
if (next) focusNode(next.node.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArrowUp(visibleNodes: FlatNode[], currentIndex: number, focusNode: (id: string) => void) {
|
||||||
|
const prev = visibleNodes[currentIndex - 1]
|
||||||
|
if (prev) focusNode(prev.node.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArrowRight(
|
||||||
|
current: FlatNode | undefined,
|
||||||
|
currentIndex: number,
|
||||||
|
expandedSet: Set<string>,
|
||||||
|
visibleNodes: FlatNode[],
|
||||||
|
handleToggle: (id: string) => void,
|
||||||
|
focusNode: (id: string) => void,
|
||||||
|
) {
|
||||||
|
if (!current) return
|
||||||
|
const hasChildren = current.node.children && current.node.children.length > 0
|
||||||
|
if (!hasChildren) return
|
||||||
|
if (!expandedSet.has(current.node.id)) {
|
||||||
|
handleToggle(current.node.id)
|
||||||
|
} else {
|
||||||
|
const next = visibleNodes[currentIndex + 1]
|
||||||
|
if (next) focusNode(next.node.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArrowLeft(
|
||||||
|
current: FlatNode | undefined,
|
||||||
|
expandedSet: Set<string>,
|
||||||
|
handleToggle: (id: string) => void,
|
||||||
|
focusNode: (id: string) => void,
|
||||||
|
) {
|
||||||
|
if (!current) return
|
||||||
|
const hasChildren = current.node.children && current.node.children.length > 0
|
||||||
|
if (hasChildren && expandedSet.has(current.node.id)) {
|
||||||
|
handleToggle(current.node.id)
|
||||||
|
} else if (current.parentId !== null) {
|
||||||
|
focusNode(current.parentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface TreeViewProps {
|
interface TreeViewProps {
|
||||||
nodes: TreeNode[]
|
nodes: TreeNode[]
|
||||||
onSelect?: (id: string) => void
|
onSelect?: (id: string) => void
|
||||||
@@ -105,68 +151,13 @@ export function TreeView({
|
|||||||
const current = visibleNodes[currentIndex]
|
const current = visibleNodes[currentIndex]
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'ArrowDown': {
|
case 'ArrowDown': { e.preventDefault(); handleArrowDown(visibleNodes, currentIndex, focusNode); break }
|
||||||
e.preventDefault()
|
case 'ArrowUp': { e.preventDefault(); handleArrowUp(visibleNodes, currentIndex, focusNode); break }
|
||||||
const next = visibleNodes[currentIndex + 1]
|
case 'ArrowRight': { e.preventDefault(); handleArrowRight(current, currentIndex, expandedSet, visibleNodes, handleToggle, focusNode); break }
|
||||||
if (next) focusNode(next.node.id)
|
case 'ArrowLeft': { e.preventDefault(); handleArrowLeft(current, expandedSet, handleToggle, focusNode); break }
|
||||||
break
|
case 'Enter': { e.preventDefault(); if (current) onSelect?.(current.node.id); break }
|
||||||
}
|
case 'Home': { e.preventDefault(); if (visibleNodes.length > 0) focusNode(visibleNodes[0].node.id); break }
|
||||||
case 'ArrowUp': {
|
case 'End': { e.preventDefault(); if (visibleNodes.length > 0) focusNode(visibleNodes[visibleNodes.length - 1].node.id); break }
|
||||||
e.preventDefault()
|
|
||||||
const prev = visibleNodes[currentIndex - 1]
|
|
||||||
if (prev) focusNode(prev.node.id)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'ArrowRight': {
|
|
||||||
e.preventDefault()
|
|
||||||
if (!current) break
|
|
||||||
const hasChildren = current.node.children && current.node.children.length > 0
|
|
||||||
if (hasChildren) {
|
|
||||||
if (!expandedSet.has(current.node.id)) {
|
|
||||||
// Expand it
|
|
||||||
handleToggle(current.node.id)
|
|
||||||
} else {
|
|
||||||
// Move to first child (it will be the next visible node)
|
|
||||||
const next = visibleNodes[currentIndex + 1]
|
|
||||||
if (next) focusNode(next.node.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'ArrowLeft': {
|
|
||||||
e.preventDefault()
|
|
||||||
if (!current) break
|
|
||||||
const hasChildren = current.node.children && current.node.children.length > 0
|
|
||||||
if (hasChildren && expandedSet.has(current.node.id)) {
|
|
||||||
// Collapse
|
|
||||||
handleToggle(current.node.id)
|
|
||||||
} else if (current.parentId !== null) {
|
|
||||||
// Move to parent
|
|
||||||
focusNode(current.parentId)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'Enter': {
|
|
||||||
e.preventDefault()
|
|
||||||
if (current) {
|
|
||||||
onSelect?.(current.node.id)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'Home': {
|
|
||||||
e.preventDefault()
|
|
||||||
if (visibleNodes.length > 0) {
|
|
||||||
focusNode(visibleNodes[0].node.id)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'End': {
|
|
||||||
e.preventDefault()
|
|
||||||
if (visibleNodes.length > 0) {
|
|
||||||
focusNode(visibleNodes[visibleNodes.length - 1].node.id)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|||||||
@@ -124,6 +124,52 @@ function filterNodes(
|
|||||||
return { filtered: walk(nodes), matchedParentIds }
|
return { filtered: walk(nodes), matchedParentIds }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Keyboard nav helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function handleArrowDown(visibleNodes: FlatNode[], currentIndex: number, focusNode: (id: string) => void) {
|
||||||
|
const next = visibleNodes[currentIndex + 1]
|
||||||
|
if (next) focusNode(next.node.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArrowUp(visibleNodes: FlatNode[], currentIndex: number, focusNode: (id: string) => void) {
|
||||||
|
const prev = visibleNodes[currentIndex - 1]
|
||||||
|
if (prev) focusNode(prev.node.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArrowRight(
|
||||||
|
current: FlatNode | undefined,
|
||||||
|
currentIndex: number,
|
||||||
|
expandedSet: Set<string>,
|
||||||
|
visibleNodes: FlatNode[],
|
||||||
|
handleToggle: (id: string) => void,
|
||||||
|
focusNode: (id: string) => void,
|
||||||
|
) {
|
||||||
|
if (!current) return
|
||||||
|
const hasChildren = current.node.children && current.node.children.length > 0
|
||||||
|
if (!hasChildren) return
|
||||||
|
if (!expandedSet.has(current.node.id)) {
|
||||||
|
handleToggle(current.node.id)
|
||||||
|
} else {
|
||||||
|
const next = visibleNodes[currentIndex + 1]
|
||||||
|
if (next) focusNode(next.node.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArrowLeft(
|
||||||
|
current: FlatNode | undefined,
|
||||||
|
expandedSet: Set<string>,
|
||||||
|
handleToggle: (id: string) => void,
|
||||||
|
focusNode: (id: string) => void,
|
||||||
|
) {
|
||||||
|
if (!current) return
|
||||||
|
const hasChildren = current.node.children && current.node.children.length > 0
|
||||||
|
if (hasChildren && expandedSet.has(current.node.id)) {
|
||||||
|
handleToggle(current.node.id)
|
||||||
|
} else if (current.parentId !== null) {
|
||||||
|
focusNode(current.parentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── SidebarTree ──────────────────────────────────────────────────────────────
|
// ── SidebarTree ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function SidebarTree({
|
export function SidebarTree({
|
||||||
@@ -222,64 +268,13 @@ export function SidebarTree({
|
|||||||
const current = visibleNodes[currentIndex]
|
const current = visibleNodes[currentIndex]
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'ArrowDown': {
|
case 'ArrowDown': { e.preventDefault(); handleArrowDown(visibleNodes, currentIndex, focusNode); break }
|
||||||
e.preventDefault()
|
case 'ArrowUp': { e.preventDefault(); handleArrowUp(visibleNodes, currentIndex, focusNode); break }
|
||||||
const next = visibleNodes[currentIndex + 1]
|
case 'ArrowRight': { e.preventDefault(); handleArrowRight(current, currentIndex, expandedSet, visibleNodes, handleToggle, focusNode); break }
|
||||||
if (next) focusNode(next.node.id)
|
case 'ArrowLeft': { e.preventDefault(); handleArrowLeft(current, expandedSet, handleToggle, focusNode); break }
|
||||||
break
|
case 'Enter': { e.preventDefault(); if (current?.node.path) navigate(current.node.path); break }
|
||||||
}
|
case 'Home': { e.preventDefault(); if (visibleNodes.length > 0) focusNode(visibleNodes[0].node.id); break }
|
||||||
case 'ArrowUp': {
|
case 'End': { e.preventDefault(); if (visibleNodes.length > 0) focusNode(visibleNodes[visibleNodes.length - 1].node.id); break }
|
||||||
e.preventDefault()
|
|
||||||
const prev = visibleNodes[currentIndex - 1]
|
|
||||||
if (prev) focusNode(prev.node.id)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'ArrowRight': {
|
|
||||||
e.preventDefault()
|
|
||||||
if (!current) break
|
|
||||||
const hasChildren = current.node.children && current.node.children.length > 0
|
|
||||||
if (hasChildren) {
|
|
||||||
if (!expandedSet.has(current.node.id)) {
|
|
||||||
handleToggle(current.node.id)
|
|
||||||
} else {
|
|
||||||
const next = visibleNodes[currentIndex + 1]
|
|
||||||
if (next) focusNode(next.node.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'ArrowLeft': {
|
|
||||||
e.preventDefault()
|
|
||||||
if (!current) break
|
|
||||||
const hasChildren = current.node.children && current.node.children.length > 0
|
|
||||||
if (hasChildren && expandedSet.has(current.node.id)) {
|
|
||||||
handleToggle(current.node.id)
|
|
||||||
} else if (current.parentId !== null) {
|
|
||||||
focusNode(current.parentId)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'Enter': {
|
|
||||||
e.preventDefault()
|
|
||||||
if (current?.node.path) {
|
|
||||||
navigate(current.node.path)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'Home': {
|
|
||||||
e.preventDefault()
|
|
||||||
if (visibleNodes.length > 0) {
|
|
||||||
focusNode(visibleNodes[0].node.id)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'End': {
|
|
||||||
e.preventDefault()
|
|
||||||
if (visibleNodes.length > 0) {
|
|
||||||
focusNode(visibleNodes[visibleNodes.length - 1].node.id)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|||||||
Reference in New Issue
Block a user